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 static com.android.settingslib.flags.Flags.enableSetPreferredTransportForLeAudioDevice;
20 
21 import android.annotation.CallbackExecutor;
22 import android.annotation.StringRes;
23 import android.bluetooth.BluetoothAdapter;
24 import android.bluetooth.BluetoothClass;
25 import android.bluetooth.BluetoothCsipSetCoordinator;
26 import android.bluetooth.BluetoothDevice;
27 import android.bluetooth.BluetoothHearingAid;
28 import android.bluetooth.BluetoothProfile;
29 import android.bluetooth.BluetoothUuid;
30 import android.content.Context;
31 import android.content.SharedPreferences;
32 import android.content.res.Resources;
33 import android.graphics.drawable.BitmapDrawable;
34 import android.graphics.drawable.Drawable;
35 import android.net.Uri;
36 import android.os.Handler;
37 import android.os.Looper;
38 import android.os.Message;
39 import android.os.ParcelUuid;
40 import android.os.SystemClock;
41 import android.provider.Settings;
42 import android.text.SpannableStringBuilder;
43 import android.text.TextUtils;
44 import android.text.style.ForegroundColorSpan;
45 import android.util.Log;
46 import android.util.LruCache;
47 import android.util.Pair;
48 
49 import androidx.annotation.NonNull;
50 import androidx.annotation.Nullable;
51 import androidx.annotation.VisibleForTesting;
52 
53 import com.android.internal.util.ArrayUtils;
54 import com.android.settingslib.R;
55 import com.android.settingslib.Utils;
56 import com.android.settingslib.media.flags.Flags;
57 import com.android.settingslib.utils.ThreadUtils;
58 import com.android.settingslib.widget.AdaptiveOutlineDrawable;
59 
60 import com.google.common.util.concurrent.FutureCallback;
61 import com.google.common.util.concurrent.Futures;
62 import com.google.common.util.concurrent.ListenableFuture;
63 
64 import java.sql.Timestamp;
65 import java.util.ArrayList;
66 import java.util.Collection;
67 import java.util.HashSet;
68 import java.util.List;
69 import java.util.Map;
70 import java.util.Objects;
71 import java.util.Optional;
72 import java.util.Set;
73 import java.util.concurrent.ConcurrentHashMap;
74 import java.util.concurrent.CopyOnWriteArrayList;
75 import java.util.concurrent.Executor;
76 import java.util.stream.Stream;
77 
78 /**
79  * CachedBluetoothDevice represents a remote Bluetooth device. It contains
80  * attributes of the device (such as the address, name, RSSI, etc.) and
81  * functionality that can be performed on the device (connect, pair, disconnect,
82  * etc.).
83  */
84 public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> {
85     private static final String TAG = "CachedBluetoothDevice";
86 
87     // See mConnectAttempted
88     private static final long MAX_UUID_DELAY_FOR_AUTO_CONNECT = 5000;
89     // Some Hearing Aids (especially the 2nd device) needs more time to do service discovery
90     private static final long MAX_HEARING_AIDS_DELAY_FOR_AUTO_CONNECT = 15000;
91     private static final long MAX_HOGP_DELAY_FOR_AUTO_CONNECT = 30000;
92     private static final long MAX_LEAUDIO_DELAY_FOR_AUTO_CONNECT = 30000;
93     private static final long MAX_MEDIA_PROFILE_CONNECT_DELAY = 60000;
94 
95     private static final int DEFAULT_LOW_BATTERY_THRESHOLD = 20;
96 
97     // To be used instead of a resource id to indicate that low battery states should not be
98     // changed to a different color.
99     private static final int SUMMARY_NO_COLOR_FOR_LOW_BATTERY = 0;
100 
101     private final Context mContext;
102     private final BluetoothAdapter mLocalAdapter;
103     private final LocalBluetoothProfileManager mProfileManager;
104     private final Object mProfileLock = new Object();
105     BluetoothDevice mDevice;
106     private HearingAidInfo mHearingAidInfo;
107     private int mGroupId;
108     private Timestamp mBondTimestamp;
109     private LocalBluetoothManager mBluetoothManager;
110 
111     // Need this since there is no method for getting RSSI
112     short mRssi;
113 
114     // mProfiles and mRemovedProfiles does not do swap() between main and sub device. It is
115     // because current sub device is only for HearingAid and its profile is the same.
116     private final Collection<LocalBluetoothProfile> mProfiles = new CopyOnWriteArrayList<>();
117 
118     // List of profiles that were previously in mProfiles, but have been removed
119     private final Collection<LocalBluetoothProfile> mRemovedProfiles = new CopyOnWriteArrayList<>();
120 
121     // Device supports PANU but not NAP: remove PanProfile after device disconnects from NAP
122     private boolean mLocalNapRoleConnected;
123 
124     boolean mJustDiscovered;
125 
126     boolean mIsCoordinatedSetMember = false;
127 
128     private final Collection<Callback> mCallbacks = new CopyOnWriteArrayList<>();
129 
130     private final Map<Callback, Executor> mCallbackExecutorMap = new ConcurrentHashMap<>();
131 
132     /**
133      * Last time a bt profile auto-connect was attempted.
134      * If an ACTION_UUID intent comes in within
135      * MAX_UUID_DELAY_FOR_AUTO_CONNECT milliseconds, we will try auto-connect
136      * again with the new UUIDs
137      */
138     private long mConnectAttempted;
139 
140     // Active device state
141     private boolean mIsActiveDeviceA2dp = false;
142     private boolean mIsActiveDeviceHeadset = false;
143     private boolean mIsActiveDeviceHearingAid = false;
144     private boolean mIsActiveDeviceLeAudio = false;
145     // Media profile connect state
146     private boolean mIsA2dpProfileConnectedFail = false;
147     private boolean mIsHeadsetProfileConnectedFail = false;
148     private boolean mIsHearingAidProfileConnectedFail = false;
149     private boolean mIsLeAudioProfileConnectedFail = false;
150     private boolean mUnpairing;
151 
152     // Group second device for Hearing Aid
153     private CachedBluetoothDevice mSubDevice;
154     // Group member devices for the coordinated set
155     private Set<CachedBluetoothDevice> mMemberDevices = new HashSet<CachedBluetoothDevice>();
156     @VisibleForTesting
157     LruCache<String, BitmapDrawable> mDrawableCache;
158 
159     private final Handler mHandler = new Handler(Looper.getMainLooper()) {
160         @Override
161         public void handleMessage(Message msg) {
162             switch (msg.what) {
163                 case BluetoothProfile.A2DP:
164                     mIsA2dpProfileConnectedFail = true;
165                     break;
166                 case BluetoothProfile.HEADSET:
167                     mIsHeadsetProfileConnectedFail = true;
168                     break;
169                 case BluetoothProfile.HEARING_AID:
170                     mIsHearingAidProfileConnectedFail = true;
171                     break;
172                 case BluetoothProfile.LE_AUDIO:
173                     mIsLeAudioProfileConnectedFail = true;
174                     break;
175                 default:
176                     Log.w(TAG, "handleMessage(): unknown message : " + msg.what);
177                     break;
178             }
179             Log.w(TAG, "Connect to profile : " + msg.what + " timeout, show error message !");
180             refresh();
181         }
182     };
183 
CachedBluetoothDevice(Context context, LocalBluetoothProfileManager profileManager, BluetoothDevice device)184     CachedBluetoothDevice(Context context, LocalBluetoothProfileManager profileManager,
185             BluetoothDevice device) {
186         mContext = context;
187         mLocalAdapter = BluetoothAdapter.getDefaultAdapter();
188         mProfileManager = profileManager;
189         mDevice = device;
190         fillData();
191         mGroupId = BluetoothCsipSetCoordinator.GROUP_ID_INVALID;
192         initDrawableCache();
193         mUnpairing = false;
194     }
195 
196     /** Clears any pending messages in the message queue. */
release()197     public void release() {
198         mHandler.removeCallbacksAndMessages(null);
199     }
200 
initDrawableCache()201     private void initDrawableCache() {
202         int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
203         int cacheSize = maxMemory / 8;
204 
205         mDrawableCache = new LruCache<String, BitmapDrawable>(cacheSize) {
206             @Override
207             protected int sizeOf(String key, BitmapDrawable bitmap) {
208                 return bitmap.getBitmap().getByteCount() / 1024;
209             }
210         };
211     }
212 
213     /**
214      * Describes the current device and profile for logging.
215      *
216      * @param profile Profile to describe
217      * @return Description of the device and profile
218      */
describe(LocalBluetoothProfile profile)219     private String describe(LocalBluetoothProfile profile) {
220         StringBuilder sb = new StringBuilder();
221         sb.append("Address:").append(mDevice);
222         if (profile != null) {
223             sb.append(" Profile:").append(profile);
224         }
225 
226         return sb.toString();
227     }
228 
onProfileStateChanged(LocalBluetoothProfile profile, int newProfileState)229     void onProfileStateChanged(LocalBluetoothProfile profile, int newProfileState) {
230         if (BluetoothUtils.D) {
231             Log.d(TAG, "onProfileStateChanged: profile " + profile + ", device "
232                     + mDevice.getAnonymizedAddress() + ", newProfileState " + newProfileState);
233         }
234         if (mLocalAdapter.getState() == BluetoothAdapter.STATE_TURNING_OFF)
235         {
236             if (BluetoothUtils.D) {
237                 Log.d(TAG, " BT Turninig Off...Profile conn state change ignored...");
238             }
239             return;
240         }
241 
242         synchronized (mProfileLock) {
243             if (profile instanceof A2dpProfile || profile instanceof HeadsetProfile
244                     || profile instanceof HearingAidProfile || profile instanceof LeAudioProfile) {
245                 setProfileConnectedStatus(profile.getProfileId(), false);
246                 switch (newProfileState) {
247                     case BluetoothProfile.STATE_CONNECTED:
248                         mHandler.removeMessages(profile.getProfileId());
249                         break;
250                     case BluetoothProfile.STATE_CONNECTING:
251                         mHandler.sendEmptyMessageDelayed(profile.getProfileId(),
252                                 MAX_MEDIA_PROFILE_CONNECT_DELAY);
253                         break;
254                     case BluetoothProfile.STATE_DISCONNECTING:
255                         if (mHandler.hasMessages(profile.getProfileId())) {
256                             mHandler.removeMessages(profile.getProfileId());
257                         }
258                         break;
259                     case BluetoothProfile.STATE_DISCONNECTED:
260                         if (mHandler.hasMessages(profile.getProfileId())) {
261                             mHandler.removeMessages(profile.getProfileId());
262                             if (profile.getConnectionPolicy(mDevice) >
263                                 BluetoothProfile.CONNECTION_POLICY_FORBIDDEN) {
264                                 /*
265                                  * If we received state DISCONNECTED and previous state was
266                                  * CONNECTING and connection policy is FORBIDDEN or UNKNOWN
267                                  * then it's not really a failure to connect.
268                                  *
269                                  * Connection profile is considered as failed when connection
270                                  * policy indicates that profile should be connected
271                                  * but it got disconnected.
272                                  */
273                                 Log.w(TAG, "onProfileStateChanged(): Failed to connect profile");
274                                 setProfileConnectedStatus(profile.getProfileId(), true);
275                             }
276                         }
277                         break;
278                     default:
279                         Log.w(TAG, "onProfileStateChanged(): unknown profile state : "
280                                 + newProfileState);
281                         break;
282                 }
283             }
284 
285             if (newProfileState == BluetoothProfile.STATE_CONNECTED) {
286                 if (profile instanceof MapProfile) {
287                     profile.setEnabled(mDevice, true);
288                 }
289                 if (!mProfiles.contains(profile)) {
290                     mRemovedProfiles.remove(profile);
291                     mProfiles.add(profile);
292                     if (profile instanceof PanProfile
293                             && ((PanProfile) profile).isLocalRoleNap(mDevice)) {
294                         // Device doesn't support NAP, so remove PanProfile on disconnect
295                         mLocalNapRoleConnected = true;
296                     }
297                 }
298                 if (enableSetPreferredTransportForLeAudioDevice()
299                         && profile instanceof HidProfile) {
300                     updatePreferredTransport();
301                 }
302             } else if (profile instanceof MapProfile
303                     && newProfileState == BluetoothProfile.STATE_DISCONNECTED) {
304                 profile.setEnabled(mDevice, false);
305             } else if (mLocalNapRoleConnected && profile instanceof PanProfile
306                     && ((PanProfile) profile).isLocalRoleNap(mDevice)
307                     && newProfileState == BluetoothProfile.STATE_DISCONNECTED) {
308                 Log.d(TAG, "Removing PanProfile from device after NAP disconnect");
309                 mProfiles.remove(profile);
310                 mRemovedProfiles.add(profile);
311                 mLocalNapRoleConnected = false;
312             }
313 
314             if (enableSetPreferredTransportForLeAudioDevice()
315                     && profile instanceof LeAudioProfile) {
316                 updatePreferredTransport();
317             }
318 
319             HearingAidStatsLogUtils.updateHistoryIfNeeded(mContext, this, profile, newProfileState);
320         }
321 
322         fetchActiveDevices();
323     }
324 
updatePreferredTransport()325     private void updatePreferredTransport() {
326         if (mProfiles.stream().noneMatch(p -> p instanceof LeAudioProfile)
327                 || mProfiles.stream().noneMatch(p -> p instanceof HidProfile)) {
328             return;
329         }
330         // Both LeAudioProfile and HidProfile are connectable.
331         if (!mProfileManager
332                 .getHidProfile()
333                 .setPreferredTransport(
334                         mDevice,
335                         mProfileManager.getLeAudioProfile().isEnabled(mDevice)
336                                 ? BluetoothDevice.TRANSPORT_LE
337                                 : BluetoothDevice.TRANSPORT_BREDR)) {
338             Log.w(TAG, "Fail to set preferred transport");
339         }
340     }
341 
342     @VisibleForTesting
setProfileConnectedStatus(int profileId, boolean isFailed)343     void setProfileConnectedStatus(int profileId, boolean isFailed) {
344         switch (profileId) {
345             case BluetoothProfile.A2DP:
346                 mIsA2dpProfileConnectedFail = isFailed;
347                 break;
348             case BluetoothProfile.HEADSET:
349                 mIsHeadsetProfileConnectedFail = isFailed;
350                 break;
351             case BluetoothProfile.HEARING_AID:
352                 mIsHearingAidProfileConnectedFail = isFailed;
353                 break;
354             case BluetoothProfile.LE_AUDIO:
355                 mIsLeAudioProfileConnectedFail = isFailed;
356                 break;
357             default:
358                 Log.w(TAG, "setProfileConnectedStatus(): unknown profile id : " + profileId);
359                 break;
360         }
361     }
362 
disconnect()363     public void disconnect() {
364         synchronized (mProfileLock) {
365             if (getGroupId() != BluetoothCsipSetCoordinator.GROUP_ID_INVALID) {
366                 for (CachedBluetoothDevice member : getMemberDevice()) {
367                     Log.d(TAG, "Disconnect the member:" + member);
368                     member.disconnect();
369                 }
370             }
371             Log.d(TAG, "Disconnect " + this);
372             mDevice.disconnect();
373         }
374         // Disconnect  PBAP server in case its connected
375         // This is to ensure all the profiles are disconnected as some CK/Hs do not
376         // disconnect  PBAP connection when HF connection is brought down
377         PbapServerProfile PbapProfile = mProfileManager.getPbapProfile();
378         if (PbapProfile != null && isConnectedProfile(PbapProfile))
379         {
380             PbapProfile.setEnabled(mDevice, false);
381         }
382     }
383 
disconnect(LocalBluetoothProfile profile)384     public void disconnect(LocalBluetoothProfile profile) {
385         if (profile.setEnabled(mDevice, false)) {
386             if (BluetoothUtils.D) {
387                 Log.d(TAG, "Command sent successfully:DISCONNECT " + describe(profile));
388             }
389         }
390     }
391 
392     /**
393      * Connect this device.
394      *
395      * @param connectAllProfiles {@code true} to connect all profile, {@code false} otherwise.
396      *
397      * @deprecated use {@link #connect()} instead.
398      */
399     @Deprecated
connect(boolean connectAllProfiles)400     public void connect(boolean connectAllProfiles) {
401         connect();
402     }
403 
404     /**
405      * Connect this device.
406      */
connect()407     public void connect() {
408         if (!ensurePaired()) {
409             return;
410         }
411 
412         mConnectAttempted = SystemClock.elapsedRealtime();
413         connectDevice();
414     }
415 
setHearingAidInfo(HearingAidInfo hearingAidInfo)416     public void setHearingAidInfo(HearingAidInfo hearingAidInfo) {
417         mHearingAidInfo = hearingAidInfo;
418         dispatchAttributesChanged();
419     }
420 
getHearingAidInfo()421     public HearingAidInfo getHearingAidInfo() {
422         return mHearingAidInfo;
423     }
424 
425     /**
426      * @return {@code true} if {@code cachedBluetoothDevice} is hearing aid device
427      */
isHearingAidDevice()428     public boolean isHearingAidDevice() {
429         return mHearingAidInfo != null;
430     }
431 
getDeviceSide()432     public int getDeviceSide() {
433         return mHearingAidInfo != null
434                 ? mHearingAidInfo.getSide() : HearingAidInfo.DeviceSide.SIDE_INVALID;
435     }
436 
getDeviceMode()437     public int getDeviceMode() {
438         return mHearingAidInfo != null
439                 ? mHearingAidInfo.getMode() : HearingAidInfo.DeviceMode.MODE_INVALID;
440     }
441 
getHiSyncId()442     public long getHiSyncId() {
443         return mHearingAidInfo != null
444                 ? mHearingAidInfo.getHiSyncId() : BluetoothHearingAid.HI_SYNC_ID_INVALID;
445     }
446 
447     /**
448      * Mark the discovered device as member of coordinated set.
449      *
450      * @param isCoordinatedSetMember {@code true}, if the device is a member of a coordinated set.
451      */
setIsCoordinatedSetMember(boolean isCoordinatedSetMember)452     public void setIsCoordinatedSetMember(boolean isCoordinatedSetMember) {
453         mIsCoordinatedSetMember = isCoordinatedSetMember;
454     }
455 
456     /**
457      * Check if the device is a CSIP member device.
458      *
459      * @return {@code true}, if this device supports CSIP, otherwise returns {@code false}.
460      */
isCoordinatedSetMemberDevice()461     public boolean isCoordinatedSetMemberDevice() {
462         return mIsCoordinatedSetMember;
463     }
464 
465     /**
466     * Get the coordinated set group id.
467     *
468     * @return the group id.
469     */
getGroupId()470     public int getGroupId() {
471         return mGroupId;
472     }
473 
474     /**
475     * Set the coordinated set group id.
476     *
477     * @param id the group id from the CSIP.
478     */
setGroupId(int id)479     public void setGroupId(int id) {
480         Log.d(TAG, this.getDevice().getAnonymizedAddress() + " set GroupId " + id);
481         mGroupId = id;
482     }
483 
onBondingDockConnect()484     void onBondingDockConnect() {
485         // Attempt to connect if UUIDs are available. Otherwise,
486         // we will connect when the ACTION_UUID intent arrives.
487         connect();
488     }
489 
connectDevice()490     private void connectDevice() {
491         synchronized (mProfileLock) {
492             // Try to initialize the profiles if they were not.
493             if (mProfiles.isEmpty()) {
494                 // if mProfiles is empty, then do not invoke updateProfiles. This causes a race
495                 // condition with carkits during pairing, wherein RemoteDevice.UUIDs have been
496                 // updated from bluetooth stack but ACTION.uuid is not sent yet.
497                 // Eventually ACTION.uuid will be received which shall trigger the connection of the
498                 // various profiles
499                 // If UUIDs are not available yet, connect will be happen
500                 // upon arrival of the ACTION_UUID intent.
501                 Log.d(TAG, "No profiles. Maybe we will connect later for device " + mDevice);
502                 return;
503             }
504             Log.d(TAG, "connect " + this);
505             mDevice.connect();
506             if (getGroupId() != BluetoothCsipSetCoordinator.GROUP_ID_INVALID) {
507                 for (CachedBluetoothDevice member : getMemberDevice()) {
508                     Log.d(TAG, "connect the member:" + member);
509                     member.connect();
510                 }
511             }
512         }
513     }
514 
515     /**
516      * Connect this device to the specified profile.
517      *
518      * @param profile the profile to use with the remote device
519      */
connectProfile(LocalBluetoothProfile profile)520     public void connectProfile(LocalBluetoothProfile profile) {
521         mConnectAttempted = SystemClock.elapsedRealtime();
522         connectInt(profile);
523         // Refresh the UI based on profile.connect() call
524         refresh();
525     }
526 
connectInt(LocalBluetoothProfile profile)527     synchronized void connectInt(LocalBluetoothProfile profile) {
528         if (!ensurePaired()) {
529             return;
530         }
531         if (profile.setEnabled(mDevice, true)) {
532             if (BluetoothUtils.D) {
533                 Log.d(TAG, "Command sent successfully:CONNECT " + describe(profile));
534             }
535             return;
536         }
537         Log.i(TAG, "Failed to connect " + profile.toString() + " to " + getName());
538     }
539 
ensurePaired()540     private boolean ensurePaired() {
541         if (getBondState() == BluetoothDevice.BOND_NONE) {
542             startPairing();
543             return false;
544         } else {
545             return true;
546         }
547     }
548 
startPairing()549     public boolean startPairing() {
550         // Pairing is unreliable while scanning, so cancel discovery
551         if (mLocalAdapter.isDiscovering()) {
552             mLocalAdapter.cancelDiscovery();
553         }
554 
555         if (!mDevice.createBond()) {
556             return false;
557         }
558 
559         return true;
560     }
561 
unpair()562     public void unpair() {
563         int state = getBondState();
564 
565         if (state == BluetoothDevice.BOND_BONDING) {
566             mDevice.cancelBondProcess();
567         }
568 
569         if (state != BluetoothDevice.BOND_NONE) {
570             final BluetoothDevice dev = mDevice;
571             if (dev != null) {
572                 mUnpairing = true;
573                 final boolean successful = dev.removeBond();
574                 if (successful) {
575                     releaseLruCache();
576                     if (BluetoothUtils.D) {
577                         Log.d(TAG, "Command sent successfully:REMOVE_BOND " + describe(null));
578                     }
579                 } else if (BluetoothUtils.V) {
580                     Log.v(TAG, "Framework rejected command immediately:REMOVE_BOND " +
581                         describe(null));
582                 }
583             }
584         }
585     }
586 
getProfileConnectionState(LocalBluetoothProfile profile)587     public int getProfileConnectionState(LocalBluetoothProfile profile) {
588         return profile != null
589                 ? profile.getConnectionStatus(mDevice)
590                 : BluetoothProfile.STATE_DISCONNECTED;
591     }
592 
593     // TODO: do any of these need to run async on a background thread?
fillData()594     void fillData() {
595         updateProfiles();
596         fetchActiveDevices();
597         migratePhonebookPermissionChoice();
598         migrateMessagePermissionChoice();
599 
600         dispatchAttributesChanged();
601     }
602 
getDevice()603     public BluetoothDevice getDevice() {
604         return mDevice;
605     }
606 
607     /**
608      * Convenience method that can be mocked - it lets tests avoid having to call getDevice() which
609      * causes problems in tests since BluetoothDevice is final and cannot be mocked.
610      * @return the address of this device
611      */
getAddress()612     public String getAddress() {
613         return mDevice.getAddress();
614     }
615 
616     /**
617      * Get identity address from remote device
618      * @return {@link BluetoothDevice#getIdentityAddress()} if
619      * {@link BluetoothDevice#getIdentityAddress()} is not null otherwise return
620      * {@link BluetoothDevice#getAddress()}
621      */
getIdentityAddress()622     public String getIdentityAddress() {
623         final String identityAddress = mDevice.getIdentityAddress();
624         return TextUtils.isEmpty(identityAddress) ? getAddress() : identityAddress;
625     }
626 
627     /**
628      * Get name from remote device
629      * @return {@link BluetoothDevice#getAlias()} if
630      * {@link BluetoothDevice#getAlias()} is not null otherwise return
631      * {@link BluetoothDevice#getAddress()}
632      */
getName()633     public String getName() {
634         final String aliasName = mDevice.getAlias();
635         return TextUtils.isEmpty(aliasName) ? getAddress() : aliasName;
636     }
637 
638     /**
639      * User changes the device name
640      * @param name new alias name to be set, should never be null
641      */
setName(String name)642     public void setName(String name) {
643         // Prevent getName() to be set to null if setName(null) is called
644         if (TextUtils.isEmpty(name) || TextUtils.equals(name, getName())) {
645             return;
646         }
647         mDevice.setAlias(name);
648         dispatchAttributesChanged();
649 
650         for (CachedBluetoothDevice cbd : mMemberDevices) {
651             cbd.setName(name);
652         }
653         if (mSubDevice != null) {
654             mSubDevice.setName(name);
655         }
656     }
657 
658     /**
659      * Set this device as active device
660      * @return true if at least one profile on this device is set to active, false otherwise
661      */
setActive()662     public boolean setActive() {
663         boolean result = false;
664         A2dpProfile a2dpProfile = mProfileManager.getA2dpProfile();
665         if (a2dpProfile != null && isConnectedProfile(a2dpProfile)) {
666             if (a2dpProfile.setActiveDevice(getDevice())) {
667                 Log.i(TAG, "OnPreferenceClickListener: A2DP active device=" + this);
668                 result = true;
669             }
670         }
671         HeadsetProfile headsetProfile = mProfileManager.getHeadsetProfile();
672         if ((headsetProfile != null) && isConnectedProfile(headsetProfile)) {
673             if (headsetProfile.setActiveDevice(getDevice())) {
674                 Log.i(TAG, "OnPreferenceClickListener: Headset active device=" + this);
675                 result = true;
676             }
677         }
678         HearingAidProfile hearingAidProfile = mProfileManager.getHearingAidProfile();
679         if ((hearingAidProfile != null) && isConnectedProfile(hearingAidProfile)) {
680             if (hearingAidProfile.setActiveDevice(getDevice())) {
681                 Log.i(TAG, "OnPreferenceClickListener: Hearing Aid active device=" + this);
682                 result = true;
683             }
684         }
685         LeAudioProfile leAudioProfile = mProfileManager.getLeAudioProfile();
686         if ((leAudioProfile != null) && isConnectedProfile(leAudioProfile)) {
687             if (leAudioProfile.setActiveDevice(getDevice())) {
688                 Log.i(TAG, "OnPreferenceClickListener: LeAudio active device=" + this);
689                 result = true;
690             }
691         }
692         return result;
693     }
694 
refreshName()695     void refreshName() {
696         if (BluetoothUtils.D) {
697             Log.d(TAG, "Device name: " + getName());
698         }
699         dispatchAttributesChanged();
700     }
701 
702     /**
703      * Checks if device has a human readable name besides MAC address
704      * @return true if device's alias name is not null nor empty, false otherwise
705      */
hasHumanReadableName()706     public boolean hasHumanReadableName() {
707         return !TextUtils.isEmpty(mDevice.getAlias());
708     }
709 
710     /**
711      * Get battery level from remote device
712      * @return battery level in percentage [0-100],
713      * {@link BluetoothDevice#BATTERY_LEVEL_BLUETOOTH_OFF}, or
714      * {@link BluetoothDevice#BATTERY_LEVEL_UNKNOWN}
715      */
getBatteryLevel()716     public int getBatteryLevel() {
717         return mDevice.getBatteryLevel();
718     }
719 
720     /**
721      * Get the lowest battery level from remote device and its member devices
722      * @return battery level in percentage [0-100] or
723      * {@link BluetoothDevice#BATTERY_LEVEL_UNKNOWN}
724      */
getMinBatteryLevelWithMemberDevices()725     public int getMinBatteryLevelWithMemberDevices() {
726         return Stream.concat(Stream.of(this), mMemberDevices.stream())
727                 .mapToInt(cachedDevice -> cachedDevice.getBatteryLevel())
728                 .filter(batteryLevel -> batteryLevel > BluetoothDevice.BATTERY_LEVEL_UNKNOWN)
729                 .min()
730                 .orElse(BluetoothDevice.BATTERY_LEVEL_UNKNOWN);
731     }
732 
733     /**
734      * Get the lowest battery level from remote device and its member devices if it's greater than
735      * BluetoothDevice.BATTERY_LEVEL_UNKNOWN.
736      *
737      * <p>Android framework should only set mBatteryLevel to valid range [0-100],
738      * BluetoothDevice.BATTERY_LEVEL_BLUETOOTH_OFF, or BluetoothDevice.BATTERY_LEVEL_UNKNOWN, any
739      * other value should be a framework bug. Thus assume here that if value is greater than
740      * BluetoothDevice.BATTERY_LEVEL_UNKNOWN, it must be valid
741      *
742      * @return battery level in String [0-100] or Null if this lower than
743      *     BluetoothDevice.BATTERY_LEVEL_UNKNOWN
744      */
745     @Nullable
getValidMinBatteryLevelWithMemberDevices()746     private String getValidMinBatteryLevelWithMemberDevices() {
747         final int batteryLevel = getMinBatteryLevelWithMemberDevices();
748         return batteryLevel > BluetoothDevice.BATTERY_LEVEL_UNKNOWN
749                 ? com.android.settingslib.Utils.formatPercentage(batteryLevel)
750                 : null;
751     }
752 
refresh()753     void refresh() {
754         ListenableFuture<Void> future = ThreadUtils.getBackgroundExecutor().submit(() -> {
755             if (BluetoothUtils.isAdvancedDetailsHeader(mDevice)) {
756                 Uri uri = BluetoothUtils.getUriMetaData(getDevice(),
757                         BluetoothDevice.METADATA_MAIN_ICON);
758                 if (uri != null && mDrawableCache.get(uri.toString()) == null) {
759                     mDrawableCache.put(uri.toString(),
760                             (BitmapDrawable) BluetoothUtils.getBtDrawableWithDescription(
761                                     mContext, this).first);
762                 }
763             }
764             return null;
765         });
766         Futures.addCallback(future, new FutureCallback<>() {
767             @Override
768             public void onSuccess(Void result) {
769                 dispatchAttributesChanged();
770             }
771 
772             @Override
773             public void onFailure(Throwable t) {}
774         }, mContext.getMainExecutor());
775     }
776 
setJustDiscovered(boolean justDiscovered)777     public void setJustDiscovered(boolean justDiscovered) {
778         if (mJustDiscovered != justDiscovered) {
779             mJustDiscovered = justDiscovered;
780             dispatchAttributesChanged();
781         }
782     }
783 
getBondState()784     public int getBondState() {
785         return mDevice.getBondState();
786     }
787 
788     /**
789      * Update the device status as active or non-active per Bluetooth profile.
790      *
791      * @param isActive true if the device is active
792      * @param bluetoothProfile the Bluetooth profile
793      */
onActiveDeviceChanged(boolean isActive, int bluetoothProfile)794     public void onActiveDeviceChanged(boolean isActive, int bluetoothProfile) {
795         if (BluetoothUtils.D) {
796             Log.d(TAG, "onActiveDeviceChanged: "
797                     + "profile " + BluetoothProfile.getProfileName(bluetoothProfile)
798                     + ", device " + mDevice.getAnonymizedAddress()
799                     + ", isActive " + isActive);
800         }
801         boolean changed = false;
802         switch (bluetoothProfile) {
803         case BluetoothProfile.A2DP:
804             changed = (mIsActiveDeviceA2dp != isActive);
805             mIsActiveDeviceA2dp = isActive;
806             break;
807         case BluetoothProfile.HEADSET:
808             changed = (mIsActiveDeviceHeadset != isActive);
809             mIsActiveDeviceHeadset = isActive;
810             break;
811         case BluetoothProfile.HEARING_AID:
812             changed = (mIsActiveDeviceHearingAid != isActive);
813             mIsActiveDeviceHearingAid = isActive;
814             break;
815         case BluetoothProfile.LE_AUDIO:
816             changed = (mIsActiveDeviceLeAudio != isActive);
817             mIsActiveDeviceLeAudio = isActive;
818             break;
819         default:
820             Log.w(TAG, "onActiveDeviceChanged: unknown profile " + bluetoothProfile +
821                     " isActive " + isActive);
822             break;
823         }
824         if (changed) {
825             dispatchAttributesChanged();
826         }
827     }
828 
829     /**
830      * Update the profile audio state.
831      */
onAudioModeChanged()832     void onAudioModeChanged() {
833         dispatchAttributesChanged();
834     }
835 
836     /**
837      * Notify that the audio category has changed.
838      */
onAudioDeviceCategoryChanged()839     public void onAudioDeviceCategoryChanged() {
840         dispatchAttributesChanged();
841     }
842 
843     /**
844      * Get the device status as active or non-active per Bluetooth profile.
845      *
846      * @param bluetoothProfile the Bluetooth profile
847      * @return true if the device is active
848      */
849     @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
isActiveDevice(int bluetoothProfile)850     public boolean isActiveDevice(int bluetoothProfile) {
851         switch (bluetoothProfile) {
852             case BluetoothProfile.A2DP:
853                 return mIsActiveDeviceA2dp;
854             case BluetoothProfile.HEADSET:
855                 return mIsActiveDeviceHeadset;
856             case BluetoothProfile.HEARING_AID:
857                 return mIsActiveDeviceHearingAid;
858             case BluetoothProfile.LE_AUDIO:
859                 return mIsActiveDeviceLeAudio;
860             default:
861                 Log.w(TAG, "getActiveDevice: unknown profile " + bluetoothProfile);
862                 break;
863         }
864         return false;
865     }
866 
setRssi(short rssi)867     void setRssi(short rssi) {
868         if (mRssi != rssi) {
869             mRssi = rssi;
870             dispatchAttributesChanged();
871         }
872     }
873 
874     /**
875      * Checks whether we are connected to this device (any profile counts).
876      *
877      * @return Whether it is connected.
878      */
isConnected()879     public boolean isConnected() {
880         synchronized (mProfileLock) {
881             for (LocalBluetoothProfile profile : mProfiles) {
882                 int status = getProfileConnectionState(profile);
883                 if (status == BluetoothProfile.STATE_CONNECTED) {
884                     return true;
885                 }
886             }
887 
888             return false;
889         }
890     }
891 
isConnectedProfile(LocalBluetoothProfile profile)892     public boolean isConnectedProfile(LocalBluetoothProfile profile) {
893         int status = getProfileConnectionState(profile);
894         return status == BluetoothProfile.STATE_CONNECTED;
895 
896     }
897 
isBusy()898     public boolean isBusy() {
899         synchronized (mProfileLock) {
900             for (LocalBluetoothProfile profile : mProfiles) {
901                 int status = getProfileConnectionState(profile);
902                 if (status == BluetoothProfile.STATE_CONNECTING
903                         || status == BluetoothProfile.STATE_DISCONNECTING) {
904                     return true;
905                 }
906             }
907             return getBondState() == BluetoothDevice.BOND_BONDING;
908         }
909     }
910 
updateProfiles()911     private boolean updateProfiles() {
912         ParcelUuid[] uuids = mDevice.getUuids();
913         if (uuids == null) return false;
914 
915         List<ParcelUuid> uuidsList = mLocalAdapter.getUuidsList();
916         ParcelUuid[] localUuids = new ParcelUuid[uuidsList.size()];
917         uuidsList.toArray(localUuids);
918 
919         /*
920          * Now we know if the device supports PBAP, update permissions...
921          */
922         processPhonebookAccess();
923 
924         synchronized (mProfileLock) {
925             mProfileManager.updateProfiles(uuids, localUuids, mProfiles, mRemovedProfiles,
926                     mLocalNapRoleConnected, mDevice);
927         }
928 
929         if (BluetoothUtils.D) {
930             Log.d(TAG, "updating profiles for " + mDevice.getAnonymizedAddress());
931             BluetoothClass bluetoothClass = mDevice.getBluetoothClass();
932 
933             if (bluetoothClass != null) Log.v(TAG, "Class: " + bluetoothClass.toString());
934             Log.v(TAG, "UUID:");
935             for (ParcelUuid uuid : uuids) {
936                 Log.v(TAG, "  " + uuid);
937             }
938         }
939         return true;
940     }
941 
fetchActiveDevices()942     private void fetchActiveDevices() {
943         A2dpProfile a2dpProfile = mProfileManager.getA2dpProfile();
944         if (a2dpProfile != null) {
945             mIsActiveDeviceA2dp = mDevice.equals(a2dpProfile.getActiveDevice());
946         }
947         HeadsetProfile headsetProfile = mProfileManager.getHeadsetProfile();
948         if (headsetProfile != null) {
949             mIsActiveDeviceHeadset = mDevice.equals(headsetProfile.getActiveDevice());
950         }
951         HearingAidProfile hearingAidProfile = mProfileManager.getHearingAidProfile();
952         if (hearingAidProfile != null) {
953             mIsActiveDeviceHearingAid = hearingAidProfile.getActiveDevices().contains(mDevice);
954         }
955         LeAudioProfile leAudio = mProfileManager.getLeAudioProfile();
956         if (leAudio != null) {
957             mIsActiveDeviceLeAudio = leAudio.getActiveDevices().contains(mDevice);
958         }
959     }
960 
961     /**
962      * Refreshes the UI when framework alerts us of a UUID change.
963      */
onUuidChanged()964     void onUuidChanged() {
965         updateProfiles();
966         ParcelUuid[] uuids = mDevice.getUuids();
967 
968         long timeout = MAX_UUID_DELAY_FOR_AUTO_CONNECT;
969         if (ArrayUtils.contains(uuids, BluetoothUuid.HOGP)) {
970             timeout = MAX_HOGP_DELAY_FOR_AUTO_CONNECT;
971         } else if (ArrayUtils.contains(uuids, BluetoothUuid.HEARING_AID)) {
972             timeout = MAX_HEARING_AIDS_DELAY_FOR_AUTO_CONNECT;
973         } else if (ArrayUtils.contains(uuids, BluetoothUuid.LE_AUDIO)) {
974             timeout = MAX_LEAUDIO_DELAY_FOR_AUTO_CONNECT;
975         }
976 
977         if (BluetoothUtils.D) {
978             Log.d(TAG, "onUuidChanged: Time since last connect="
979                     + (SystemClock.elapsedRealtime() - mConnectAttempted));
980         }
981 
982         /*
983          * If a connect was attempted earlier without any UUID, we will do the connect now.
984          * Otherwise, allow the connect on UUID change.
985          */
986         if ((mConnectAttempted + timeout) > SystemClock.elapsedRealtime()) {
987             Log.d(TAG, "onUuidChanged: triggering connectDevice");
988             connectDevice();
989         }
990 
991         dispatchAttributesChanged();
992     }
993 
onBondingStateChanged(int bondState)994     void onBondingStateChanged(int bondState) {
995         if (bondState == BluetoothDevice.BOND_NONE) {
996             synchronized (mProfileLock) {
997                 mProfiles.clear();
998             }
999             mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_UNKNOWN);
1000             mDevice.setMessageAccessPermission(BluetoothDevice.ACCESS_UNKNOWN);
1001             mDevice.setSimAccessPermission(BluetoothDevice.ACCESS_UNKNOWN);
1002 
1003             mBondTimestamp = null;
1004         }
1005 
1006         refresh();
1007 
1008         if (bondState == BluetoothDevice.BOND_BONDED) {
1009             mBondTimestamp = new Timestamp(System.currentTimeMillis());
1010 
1011             if (mDevice.isBondingInitiatedLocally()) {
1012                 connect();
1013             }
1014 
1015             // Saves this device as just bonded and checks if it's an hearing device after
1016             // profiles are connected. This is for judging whether to display the survey.
1017             HearingAidStatsLogUtils.addToJustBonded(getAddress());
1018         }
1019     }
1020 
getBondTimestamp()1021     public Timestamp getBondTimestamp() {
1022         return mBondTimestamp;
1023     }
1024 
getBtClass()1025     public BluetoothClass getBtClass() {
1026         return mDevice.getBluetoothClass();
1027     }
1028 
getProfiles()1029     public List<LocalBluetoothProfile> getProfiles() {
1030         return new ArrayList<>(mProfiles);
1031     }
1032 
getConnectableProfiles()1033     public List<LocalBluetoothProfile> getConnectableProfiles() {
1034         List<LocalBluetoothProfile> connectableProfiles =
1035                 new ArrayList<LocalBluetoothProfile>();
1036         synchronized (mProfileLock) {
1037             for (LocalBluetoothProfile profile : mProfiles) {
1038                 if (profile.accessProfileEnabled()) {
1039                     connectableProfiles.add(profile);
1040                 }
1041             }
1042         }
1043         return connectableProfiles;
1044     }
1045 
getRemovedProfiles()1046     public List<LocalBluetoothProfile> getRemovedProfiles() {
1047         return new ArrayList<>(mRemovedProfiles);
1048     }
1049 
1050     /**
1051      * @deprecated Use {@link #registerCallback(Executor, Callback)}.
1052      */
1053     @Deprecated
registerCallback(Callback callback)1054     public void registerCallback(Callback callback) {
1055         mCallbacks.add(callback);
1056     }
1057 
1058     /**
1059      * Registers a {@link Callback} that will be invoked when the bluetooth device attribute is
1060      * changed.
1061      *
1062      * @param executor an {@link Executor} to execute given callback
1063      * @param callback user implementation of the {@link Callback}
1064      */
registerCallback( @onNull @allbackExecutor Executor executor, @NonNull Callback callback)1065     public void registerCallback(
1066             @NonNull @CallbackExecutor Executor executor, @NonNull Callback callback) {
1067         Objects.requireNonNull(executor, "executor cannot be null");
1068         Objects.requireNonNull(callback, "callback cannot be null");
1069         mCallbackExecutorMap.put(callback, executor);
1070     }
1071 
unregisterCallback(Callback callback)1072     public void unregisterCallback(Callback callback) {
1073         mCallbacks.remove(callback);
1074         mCallbackExecutorMap.remove(callback);
1075     }
1076 
dispatchAttributesChanged()1077     void dispatchAttributesChanged() {
1078         for (Callback callback : mCallbacks) {
1079             callback.onDeviceAttributesChanged();
1080         }
1081         mCallbackExecutorMap.forEach((callback, executor) ->
1082                 executor.execute(callback::onDeviceAttributesChanged));
1083     }
1084 
1085     @Override
toString()1086     public String toString() {
1087         StringBuilder builder = new StringBuilder("CachedBluetoothDevice{");
1088         builder.append("anonymizedAddress=").append(mDevice.getAnonymizedAddress());
1089         builder.append(", name=").append(getName());
1090         builder.append(", groupId=").append(mGroupId);
1091         builder.append(", member=").append(mMemberDevices);
1092         if (isHearingAidDevice()) {
1093             builder.append(", hearingAidInfo=").append(mHearingAidInfo);
1094             builder.append(", subDevice=").append(mSubDevice);
1095         }
1096         builder.append("}");
1097         return builder.toString();
1098     }
1099 
1100     @Override
equals(Object o)1101     public boolean equals(Object o) {
1102         if ((o == null) || !(o instanceof CachedBluetoothDevice)) {
1103             return false;
1104         }
1105         return mDevice.equals(((CachedBluetoothDevice) o).mDevice);
1106     }
1107 
1108     @Override
hashCode()1109     public int hashCode() {
1110         return mDevice.getAddress().hashCode();
1111     }
1112 
1113     // This comparison uses non-final fields so the sort order may change
1114     // when device attributes change (such as bonding state). Settings
1115     // will completely refresh the device list when this happens.
compareTo(CachedBluetoothDevice another)1116     public int compareTo(CachedBluetoothDevice another) {
1117         // Connected above not connected
1118         int comparison = (another.isConnected() ? 1 : 0) - (isConnected() ? 1 : 0);
1119         if (comparison != 0) return comparison;
1120 
1121         // Paired above not paired
1122         comparison = (another.getBondState() == BluetoothDevice.BOND_BONDED ? 1 : 0) -
1123             (getBondState() == BluetoothDevice.BOND_BONDED ? 1 : 0);
1124         if (comparison != 0) return comparison;
1125 
1126         // Just discovered above discovered in the past
1127         comparison = (another.mJustDiscovered ? 1 : 0) - (mJustDiscovered ? 1 : 0);
1128         if (comparison != 0) return comparison;
1129 
1130         // Stronger signal above weaker signal
1131         comparison = another.mRssi - mRssi;
1132         if (comparison != 0) return comparison;
1133 
1134         // Fallback on name
1135         return getName().compareTo(another.getName());
1136     }
1137 
1138     public interface Callback {
onDeviceAttributesChanged()1139         void onDeviceAttributesChanged();
1140     }
1141 
1142     // Migrates data from old data store (in Settings app's shared preferences) to new (in Bluetooth
1143     // app's shared preferences).
migratePhonebookPermissionChoice()1144     private void migratePhonebookPermissionChoice() {
1145         SharedPreferences preferences = mContext.getSharedPreferences(
1146                 "bluetooth_phonebook_permission", Context.MODE_PRIVATE);
1147         if (!preferences.contains(mDevice.getAddress())) {
1148             return;
1149         }
1150 
1151         if (mDevice.getPhonebookAccessPermission() == BluetoothDevice.ACCESS_UNKNOWN) {
1152             int oldPermission =
1153                     preferences.getInt(mDevice.getAddress(), BluetoothDevice.ACCESS_UNKNOWN);
1154             if (oldPermission == BluetoothDevice.ACCESS_ALLOWED) {
1155                 mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_ALLOWED);
1156             } else if (oldPermission == BluetoothDevice.ACCESS_REJECTED) {
1157                 mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_REJECTED);
1158             }
1159         }
1160 
1161         SharedPreferences.Editor editor = preferences.edit();
1162         editor.remove(mDevice.getAddress());
1163         editor.commit();
1164     }
1165 
1166     // Migrates data from old data store (in Settings app's shared preferences) to new (in Bluetooth
1167     // app's shared preferences).
migrateMessagePermissionChoice()1168     private void migrateMessagePermissionChoice() {
1169         SharedPreferences preferences = mContext.getSharedPreferences(
1170                 "bluetooth_message_permission", Context.MODE_PRIVATE);
1171         if (!preferences.contains(mDevice.getAddress())) {
1172             return;
1173         }
1174 
1175         if (mDevice.getMessageAccessPermission() == BluetoothDevice.ACCESS_UNKNOWN) {
1176             int oldPermission =
1177                     preferences.getInt(mDevice.getAddress(), BluetoothDevice.ACCESS_UNKNOWN);
1178             if (oldPermission == BluetoothDevice.ACCESS_ALLOWED) {
1179                 mDevice.setMessageAccessPermission(BluetoothDevice.ACCESS_ALLOWED);
1180             } else if (oldPermission == BluetoothDevice.ACCESS_REJECTED) {
1181                 mDevice.setMessageAccessPermission(BluetoothDevice.ACCESS_REJECTED);
1182             }
1183         }
1184 
1185         SharedPreferences.Editor editor = preferences.edit();
1186         editor.remove(mDevice.getAddress());
1187         editor.commit();
1188     }
1189 
processPhonebookAccess()1190     private void processPhonebookAccess() {
1191         if (mDevice.getBondState() != BluetoothDevice.BOND_BONDED) return;
1192 
1193         ParcelUuid[] uuids = mDevice.getUuids();
1194         if (BluetoothUuid.containsAnyUuid(uuids, PbapServerProfile.PBAB_CLIENT_UUIDS)) {
1195             // The pairing dialog now warns of phone-book access for paired devices.
1196             // No separate prompt is displayed after pairing.
1197             mDevice.getPhonebookAccessPermission();
1198         }
1199     }
1200 
getMaxConnectionState()1201     public int getMaxConnectionState() {
1202         int maxState = BluetoothProfile.STATE_DISCONNECTED;
1203         synchronized (mProfileLock) {
1204             for (LocalBluetoothProfile profile : getProfiles()) {
1205                 int connectionStatus = getProfileConnectionState(profile);
1206                 if (connectionStatus > maxState) {
1207                     maxState = connectionStatus;
1208                 }
1209             }
1210         }
1211         return maxState;
1212     }
1213 
1214     /**
1215      * Return full summary that describes connection state of this device
1216      *
1217      * @see #getConnectionSummary(boolean shortSummary)
1218      */
getConnectionSummary()1219     public String getConnectionSummary() {
1220         return getConnectionSummary(false /* shortSummary */);
1221     }
1222 
1223     /**
1224      * Return summary that describes connection state of this device. Summary depends on: 1. Whether
1225      * device has battery info 2. Whether device is in active usage(or in phone call) 3. Whether
1226      * device is in audio sharing process
1227      *
1228      * @param shortSummary {@code true} if need to return short version summary
1229      */
getConnectionSummary(boolean shortSummary)1230     public String getConnectionSummary(boolean shortSummary) {
1231         CharSequence summary = null;
1232         if (BluetoothUtils.isAudioSharingEnabled()) {
1233             if (mBluetoothManager == null) {
1234                 mBluetoothManager = LocalBluetoothManager.getInstance(mContext, null);
1235             }
1236             if (BluetoothUtils.isBroadcasting(mBluetoothManager)) {
1237                 summary = getBroadcastConnectionSummary(shortSummary);
1238             }
1239         }
1240         if (summary == null) {
1241             summary =
1242                     getConnectionSummary(
1243                             shortSummary,
1244                             false /* isTvSummary */,
1245                             SUMMARY_NO_COLOR_FOR_LOW_BATTERY);
1246         }
1247         return summary != null ? summary.toString() : null;
1248     }
1249 
1250     /**
1251      * Returns the connection summary of this device during le audio sharing.
1252      *
1253      * @param shortSummary {@code true} if need to return short version summary
1254      */
1255     @Nullable
getBroadcastConnectionSummary(boolean shortSummary)1256     private String getBroadcastConnectionSummary(boolean shortSummary) {
1257         if (isProfileConnectedFail() && isConnected()) {
1258             return mContext.getString(R.string.profile_connect_timeout_subtext);
1259         }
1260 
1261         synchronized (mProfileLock) {
1262             for (LocalBluetoothProfile profile : getProfiles()) {
1263                 int connectionStatus = getProfileConnectionState(profile);
1264                 if (connectionStatus == BluetoothProfile.STATE_CONNECTING
1265                         || connectionStatus == BluetoothProfile.STATE_DISCONNECTING) {
1266                     return mContext.getString(
1267                             BluetoothUtils.getConnectionStateSummary(connectionStatus));
1268                 }
1269             }
1270         }
1271 
1272         int leftBattery =
1273                 BluetoothUtils.getIntMetaData(
1274                         mDevice, BluetoothDevice.METADATA_UNTETHERED_LEFT_BATTERY);
1275         int rightBattery =
1276                 BluetoothUtils.getIntMetaData(
1277                         mDevice, BluetoothDevice.METADATA_UNTETHERED_RIGHT_BATTERY);
1278         String batteryLevelPercentageString = getValidMinBatteryLevelWithMemberDevices();
1279 
1280         if (mBluetoothManager == null) {
1281             mBluetoothManager = LocalBluetoothManager.getInstance(mContext, null);
1282         }
1283         if (BluetoothUtils.hasConnectedBroadcastSource(this, mBluetoothManager)) {
1284             // Gets summary for the buds which are in the audio sharing.
1285             int groupId = BluetoothUtils.getGroupId(this);
1286             if (groupId != BluetoothCsipSetCoordinator.GROUP_ID_INVALID
1287                     && groupId
1288                             == Settings.Secure.getInt(
1289                                     mContext.getContentResolver(),
1290                                     "bluetooth_le_broadcast_fallback_active_group_id",
1291                                     BluetoothCsipSetCoordinator.GROUP_ID_INVALID)) {
1292                 // The buds are primary buds
1293                 return getSummaryWithBatteryInfo(
1294                         R.string.bluetooth_active_battery_level_untethered,
1295                         R.string.bluetooth_active_battery_level,
1296                         R.string.bluetooth_active_no_battery_level,
1297                         leftBattery,
1298                         rightBattery,
1299                         batteryLevelPercentageString,
1300                         shortSummary);
1301             } else {
1302                 // The buds are not primary buds
1303                 return getSummaryWithBatteryInfo(
1304                         R.string.bluetooth_active_media_only_battery_level_untethered,
1305                         R.string.bluetooth_active_media_only_battery_level,
1306                         R.string.bluetooth_active_media_only_no_battery_level,
1307                         leftBattery,
1308                         rightBattery,
1309                         batteryLevelPercentageString,
1310                         shortSummary);
1311             }
1312         } else {
1313             // Gets summary for the buds which are not in the audio sharing.
1314             if (getProfiles().stream()
1315                     .anyMatch(
1316                             profile ->
1317                                     profile instanceof LeAudioProfile
1318                                             && profile.isEnabled(getDevice()))) {
1319                 // The buds support le audio.
1320                 if (isConnected()) {
1321                     return getSummaryWithBatteryInfo(
1322                             R.string.bluetooth_battery_level_untethered_lea_support,
1323                             R.string.bluetooth_battery_level_lea_support,
1324                             R.string.bluetooth_no_battery_level_lea_support,
1325                             leftBattery,
1326                             rightBattery,
1327                             batteryLevelPercentageString,
1328                             shortSummary);
1329                 } else {
1330                     return mContext.getString(R.string.bluetooth_saved_device_lea_support);
1331                 }
1332             }
1333         }
1334         return null;
1335     }
1336 
1337     /**
1338      * Returns the summary with correct format depending the battery info.
1339      *
1340      * @param untetheredBatteryResId resource id for untethered device with battery info
1341      * @param batteryResId resource id for device with single battery info
1342      * @param noBatteryResId resource id for device with no battery info
1343      * @param shortSummary {@code true} if need to return short version summary
1344      */
getSummaryWithBatteryInfo( @tringRes int untetheredBatteryResId, @StringRes int batteryResId, @StringRes int noBatteryResId, int leftBattery, int rightBattery, String batteryLevelPercentageString, boolean shortSummary)1345     private String getSummaryWithBatteryInfo(
1346             @StringRes int untetheredBatteryResId,
1347             @StringRes int batteryResId,
1348             @StringRes int noBatteryResId,
1349             int leftBattery,
1350             int rightBattery,
1351             String batteryLevelPercentageString,
1352             boolean shortSummary) {
1353         if (isTwsBatteryAvailable(leftBattery, rightBattery) && !shortSummary) {
1354             return mContext.getString(
1355                     untetheredBatteryResId,
1356                     Utils.formatPercentage(leftBattery),
1357                     Utils.formatPercentage(rightBattery));
1358         } else if (batteryLevelPercentageString != null && !shortSummary) {
1359             return mContext.getString(batteryResId, batteryLevelPercentageString);
1360         } else {
1361             return mContext.getString(noBatteryResId);
1362         }
1363     }
1364 
1365     /**
1366      * Returns android tv string that describes the connection state of this device.
1367      */
getTvConnectionSummary()1368     public CharSequence getTvConnectionSummary() {
1369         return getTvConnectionSummary(SUMMARY_NO_COLOR_FOR_LOW_BATTERY);
1370     }
1371 
1372     /**
1373      * Returns android tv string that describes the connection state of this device, with low
1374      * battery states highlighted in color.
1375      *
1376      * @param lowBatteryColorRes - resource id for the color that should be used for the part of the
1377      *                           CharSequence that contains low battery information.
1378      */
getTvConnectionSummary(int lowBatteryColorRes)1379     public CharSequence getTvConnectionSummary(int lowBatteryColorRes) {
1380         return getConnectionSummary(false /* shortSummary */, true /* isTvSummary */,
1381                 lowBatteryColorRes);
1382     }
1383 
1384     /**
1385      * Return summary that describes connection state of this device. Summary depends on:
1386      * 1. Whether device has battery info
1387      * 2. Whether device is in active usage(or in phone call)
1388      *
1389      * @param shortSummary       {@code true} if need to return short version summary
1390      * @param isTvSummary        {@code true} if the summary should be TV specific
1391      * @param lowBatteryColorRes Resource id of the color to be used for low battery strings. Use
1392      *                           {@link SUMMARY_NO_COLOR_FOR_LOW_BATTERY} if no separate color
1393      *                           should be used.
1394      */
getConnectionSummary(boolean shortSummary, boolean isTvSummary, int lowBatteryColorRes)1395     private CharSequence getConnectionSummary(boolean shortSummary, boolean isTvSummary,
1396             int lowBatteryColorRes) {
1397         boolean profileConnected = false;    // Updated as long as BluetoothProfile is connected
1398         boolean a2dpConnected = true;        // A2DP is connected
1399         boolean hfpConnected = true;         // HFP is connected
1400         boolean hearingAidConnected = true;  // Hearing Aid is connected
1401         boolean leAudioConnected = true;        // LeAudio is connected
1402         int leftBattery = -1;
1403         int rightBattery = -1;
1404 
1405         if (isProfileConnectedFail() && isConnected()) {
1406             return mContext.getString(R.string.profile_connect_timeout_subtext);
1407         }
1408 
1409         synchronized (mProfileLock) {
1410             for (LocalBluetoothProfile profile : getProfiles()) {
1411                 int connectionStatus = getProfileConnectionState(profile);
1412 
1413                 switch (connectionStatus) {
1414                     case BluetoothProfile.STATE_CONNECTING:
1415                     case BluetoothProfile.STATE_DISCONNECTING:
1416                         return mContext.getString(
1417                                 BluetoothUtils.getConnectionStateSummary(connectionStatus));
1418 
1419                     case BluetoothProfile.STATE_CONNECTED:
1420                         profileConnected = true;
1421                         break;
1422 
1423                     case BluetoothProfile.STATE_DISCONNECTED:
1424                         if (profile.isProfileReady()) {
1425                             if (profile instanceof A2dpProfile
1426                                     || profile instanceof A2dpSinkProfile) {
1427                                 a2dpConnected = false;
1428                             } else if (profile instanceof HeadsetProfile
1429                                     || profile instanceof HfpClientProfile) {
1430                                 hfpConnected = false;
1431                             } else if (profile instanceof HearingAidProfile) {
1432                                 hearingAidConnected = false;
1433                             } else if (profile instanceof LeAudioProfile) {
1434                                 leAudioConnected = false;
1435                             }
1436                         }
1437                         break;
1438                 }
1439             }
1440         }
1441 
1442         String batteryLevelPercentageString = getValidMinBatteryLevelWithMemberDevices();
1443         int stringRes = R.string.bluetooth_pairing;
1444         //when profile is connected, information would be available
1445         if (profileConnected) {
1446             leftBattery = getLeftBatteryLevel();
1447             rightBattery = getRightBatteryLevel();
1448 
1449             // Set default string with battery level in device connected situation.
1450             if (isTwsBatteryAvailable(leftBattery, rightBattery)) {
1451                 stringRes = R.string.bluetooth_battery_level_untethered;
1452             } else if (batteryLevelPercentageString != null && !shortSummary) {
1453                 stringRes = R.string.bluetooth_battery_level;
1454             }
1455 
1456             // Set active string in following device connected situation, also show battery
1457             // information if they have.
1458             //    1. Hearing Aid device active.
1459             //    2. Headset device active with in-calling state.
1460             //    3. A2DP device active without in-calling state.
1461             //    4. Le Audio device active
1462             if (a2dpConnected || hfpConnected || hearingAidConnected || leAudioConnected) {
1463                 final boolean isOnCall = Utils.isAudioModeOngoingCall(mContext);
1464                 if ((mIsActiveDeviceHearingAid)
1465                         || (mIsActiveDeviceHeadset && isOnCall)
1466                         || (mIsActiveDeviceA2dp && !isOnCall)
1467                         || mIsActiveDeviceLeAudio) {
1468                     if (isTwsBatteryAvailable(leftBattery, rightBattery) && !shortSummary) {
1469                         stringRes = R.string.bluetooth_active_battery_level_untethered;
1470                     } else if (batteryLevelPercentageString != null && !shortSummary) {
1471                         stringRes = R.string.bluetooth_active_battery_level;
1472                     } else {
1473                         stringRes = R.string.bluetooth_active_no_battery_level;
1474                     }
1475                 }
1476 
1477                 // Try to show left/right information for hearing
1478                 // aids specifically.
1479                 boolean isActiveAshaHearingAid = mIsActiveDeviceHearingAid;
1480                 boolean isActiveLeAudioHearingAid = mIsActiveDeviceLeAudio
1481                         && isConnectedHapClientDevice();
1482                 if (isActiveAshaHearingAid || isActiveLeAudioHearingAid) {
1483                     stringRes = getHearingDeviceSummaryRes(leftBattery, rightBattery, shortSummary);
1484                 }
1485             }
1486         }
1487 
1488         if (stringRes == R.string.bluetooth_pairing
1489                 && getBondState() != BluetoothDevice.BOND_BONDING) {
1490             return null;
1491         }
1492 
1493         boolean summaryIncludesBatteryLevel = stringRes == R.string.bluetooth_battery_level
1494                 || stringRes == R.string.bluetooth_active_battery_level
1495                 || stringRes == R.string.bluetooth_active_battery_level_untethered
1496                 || stringRes == R.string.bluetooth_active_battery_level_untethered_left
1497                 || stringRes == R.string.bluetooth_active_battery_level_untethered_right
1498                 || stringRes == R.string.bluetooth_battery_level_untethered;
1499         if (isTvSummary && summaryIncludesBatteryLevel && Flags.enableTvMediaOutputDialog()) {
1500             return getTvBatterySummary(
1501                     getMinBatteryLevelWithMemberDevices(),
1502                     leftBattery,
1503                     rightBattery,
1504                     lowBatteryColorRes);
1505         }
1506 
1507         if (isTwsBatteryAvailable(leftBattery, rightBattery)) {
1508             return mContext.getString(stringRes, Utils.formatPercentage(leftBattery),
1509                     Utils.formatPercentage(rightBattery));
1510         } else if (leftBattery > BluetoothDevice.BATTERY_LEVEL_UNKNOWN
1511                 && !BluetoothUtils.getBooleanMetaData(mDevice,
1512                 BluetoothDevice.METADATA_IS_UNTETHERED_HEADSET)) {
1513             return mContext.getString(stringRes, Utils.formatPercentage(leftBattery));
1514         } else if (rightBattery > BluetoothDevice.BATTERY_LEVEL_UNKNOWN
1515                 && !BluetoothUtils.getBooleanMetaData(mDevice,
1516                 BluetoothDevice.METADATA_IS_UNTETHERED_HEADSET)) {
1517             return mContext.getString(stringRes, Utils.formatPercentage(rightBattery));
1518         } else {
1519             return mContext.getString(stringRes, batteryLevelPercentageString);
1520         }
1521     }
1522 
getTvBatterySummary(int mainBattery, int leftBattery, int rightBattery, int lowBatteryColorRes)1523     private CharSequence getTvBatterySummary(int mainBattery, int leftBattery, int rightBattery,
1524             int lowBatteryColorRes) {
1525         // Since there doesn't seem to be a way to use format strings to add the
1526         // percentages and also mark which part of the string is left and right to color
1527         // them, we are using one string resource per battery.
1528         Resources res = mContext.getResources();
1529         SpannableStringBuilder spannableBuilder = new SpannableStringBuilder();
1530         if (leftBattery >= 0 || rightBattery >= 0) {
1531             // Not switching the left and right for RTL to keep the left earbud always on
1532             // the left.
1533             if (leftBattery >= 0) {
1534                 String left = res.getString(
1535                         R.string.tv_bluetooth_battery_level_untethered_left,
1536                         Utils.formatPercentage(leftBattery));
1537                 addBatterySpan(spannableBuilder, left, isBatteryLow(leftBattery,
1538                                 BluetoothDevice.METADATA_UNTETHERED_LEFT_LOW_BATTERY_THRESHOLD),
1539                         lowBatteryColorRes);
1540             }
1541             if (rightBattery >= 0) {
1542                 if (spannableBuilder.length() > 0) {
1543                     spannableBuilder.append(" ");
1544                 }
1545                 String right = res.getString(
1546                         R.string.tv_bluetooth_battery_level_untethered_right,
1547                         Utils.formatPercentage(rightBattery));
1548                 addBatterySpan(spannableBuilder, right, isBatteryLow(rightBattery,
1549                                 BluetoothDevice.METADATA_UNTETHERED_RIGHT_LOW_BATTERY_THRESHOLD),
1550                         lowBatteryColorRes);
1551             }
1552         } else {
1553             addBatterySpan(spannableBuilder, res.getString(R.string.tv_bluetooth_battery_level,
1554                             Utils.formatPercentage(mainBattery)),
1555                     isBatteryLow(mainBattery, BluetoothDevice.METADATA_MAIN_LOW_BATTERY_THRESHOLD),
1556                     lowBatteryColorRes);
1557         }
1558         return spannableBuilder;
1559     }
1560 
getHearingDeviceSummaryRes(int leftBattery, int rightBattery, boolean shortSummary)1561     private int getHearingDeviceSummaryRes(int leftBattery, int rightBattery,
1562             boolean shortSummary) {
1563         boolean isLeftDeviceConnected = getConnectedHearingAidSide(
1564                 HearingAidInfo.DeviceSide.SIDE_LEFT).isPresent();
1565         boolean isRightDeviceConnected = getConnectedHearingAidSide(
1566                 HearingAidInfo.DeviceSide.SIDE_RIGHT).isPresent();
1567         boolean shouldShowLeftBattery =
1568                 !shortSummary && (leftBattery > BluetoothDevice.BATTERY_LEVEL_UNKNOWN);
1569         boolean shouldShowRightBattery =
1570                 !shortSummary && (rightBattery > BluetoothDevice.BATTERY_LEVEL_UNKNOWN);
1571 
1572         if (isLeftDeviceConnected && isRightDeviceConnected) {
1573             return (shouldShowLeftBattery && shouldShowRightBattery)
1574                     ? R.string.bluetooth_active_battery_level_untethered
1575                     : R.string.bluetooth_hearing_aid_left_and_right_active;
1576         }
1577         if (isLeftDeviceConnected) {
1578             return shouldShowLeftBattery
1579                     ? R.string.bluetooth_active_battery_level_untethered_left
1580                     : R.string.bluetooth_hearing_aid_left_active;
1581         }
1582         if (isRightDeviceConnected) {
1583             return shouldShowRightBattery
1584                     ? R.string.bluetooth_active_battery_level_untethered_right
1585                     : R.string.bluetooth_hearing_aid_right_active;
1586         }
1587 
1588         return R.string.bluetooth_active_no_battery_level;
1589     }
1590 
addBatterySpan(SpannableStringBuilder builder, String batteryString, boolean lowBattery, int lowBatteryColorRes)1591     private void addBatterySpan(SpannableStringBuilder builder,
1592             String batteryString, boolean lowBattery, int lowBatteryColorRes) {
1593         if (lowBattery && lowBatteryColorRes != SUMMARY_NO_COLOR_FOR_LOW_BATTERY) {
1594             builder.append(batteryString,
1595                     new ForegroundColorSpan(mContext.getResources().getColor(lowBatteryColorRes)),
1596                     0 /* flags */);
1597         } else {
1598             builder.append(batteryString);
1599         }
1600     }
1601 
isBatteryLow(int batteryLevel, int metadataKey)1602     private boolean isBatteryLow(int batteryLevel, int metadataKey) {
1603         int lowBatteryThreshold = BluetoothUtils.getIntMetaData(mDevice, metadataKey);
1604         if (lowBatteryThreshold <= 0) {
1605             lowBatteryThreshold = DEFAULT_LOW_BATTERY_THRESHOLD;
1606         }
1607         return batteryLevel <= lowBatteryThreshold;
1608     }
1609 
isTwsBatteryAvailable(int leftBattery, int rightBattery)1610     private boolean isTwsBatteryAvailable(int leftBattery, int rightBattery) {
1611         return leftBattery >= 0 && rightBattery >= 0;
1612     }
1613 
getConnectedHearingAidSide( @earingAidInfo.DeviceSide int side)1614     private Optional<CachedBluetoothDevice> getConnectedHearingAidSide(
1615             @HearingAidInfo.DeviceSide int side) {
1616         return Stream.concat(Stream.of(this, mSubDevice), mMemberDevices.stream())
1617                 .filter(Objects::nonNull)
1618                 .filter(device -> device.getDeviceSide() == side
1619                         || device.getDeviceSide() == HearingAidInfo.DeviceSide.SIDE_LEFT_AND_RIGHT)
1620                 .filter(device -> device.getDevice().isConnected())
1621                 // For hearing aids, we should expect only one device assign to one side, but if
1622                 // it happens, we don't care which one.
1623                 .findAny();
1624     }
1625 
getLeftBatteryLevel()1626     private int getLeftBatteryLevel() {
1627         int leftBattery = BluetoothDevice.BATTERY_LEVEL_UNKNOWN;
1628         if (BluetoothUtils.getBooleanMetaData(mDevice,
1629                 BluetoothDevice.METADATA_IS_UNTETHERED_HEADSET)) {
1630             leftBattery = BluetoothUtils.getIntMetaData(mDevice,
1631                     BluetoothDevice.METADATA_UNTETHERED_LEFT_BATTERY);
1632         }
1633 
1634         // Retrieve hearing aids (ASHA, HAP) individual side battery level
1635         if (leftBattery == BluetoothDevice.BATTERY_LEVEL_UNKNOWN) {
1636             leftBattery = getConnectedHearingAidSide(HearingAidInfo.DeviceSide.SIDE_LEFT)
1637                     .map(CachedBluetoothDevice::getBatteryLevel)
1638                     .filter(batteryLevel -> batteryLevel > BluetoothDevice.BATTERY_LEVEL_UNKNOWN)
1639                     .orElse(BluetoothDevice.BATTERY_LEVEL_UNKNOWN);
1640         }
1641 
1642         return leftBattery;
1643     }
1644 
getRightBatteryLevel()1645     private int getRightBatteryLevel() {
1646         int rightBattery = BluetoothDevice.BATTERY_LEVEL_UNKNOWN;
1647         if (BluetoothUtils.getBooleanMetaData(
1648                 mDevice, BluetoothDevice.METADATA_IS_UNTETHERED_HEADSET)) {
1649             rightBattery = BluetoothUtils.getIntMetaData(mDevice,
1650                     BluetoothDevice.METADATA_UNTETHERED_RIGHT_BATTERY);
1651         }
1652 
1653         // Retrieve hearing aids (ASHA, HAP) individual side battery level
1654         if (rightBattery == BluetoothDevice.BATTERY_LEVEL_UNKNOWN) {
1655             rightBattery = getConnectedHearingAidSide(HearingAidInfo.DeviceSide.SIDE_RIGHT)
1656                     .map(CachedBluetoothDevice::getBatteryLevel)
1657                     .filter(batteryLevel -> batteryLevel > BluetoothDevice.BATTERY_LEVEL_UNKNOWN)
1658                     .orElse(BluetoothDevice.BATTERY_LEVEL_UNKNOWN);
1659         }
1660 
1661         return rightBattery;
1662     }
1663 
isProfileConnectedFail()1664     private boolean isProfileConnectedFail() {
1665         Log.d(TAG, "anonymizedAddress=" + mDevice.getAnonymizedAddress()
1666                 + " mIsA2dpProfileConnectedFail=" + mIsA2dpProfileConnectedFail
1667                 + " mIsHearingAidProfileConnectedFail=" + mIsHearingAidProfileConnectedFail
1668                 + " mIsLeAudioProfileConnectedFail=" + mIsLeAudioProfileConnectedFail
1669                 + " mIsHeadsetProfileConnectedFail=" + mIsHeadsetProfileConnectedFail
1670                 + " isConnectedSapDevice()=" + isConnectedSapDevice());
1671 
1672         return mIsA2dpProfileConnectedFail || mIsHearingAidProfileConnectedFail
1673                 || (!isConnectedSapDevice() && mIsHeadsetProfileConnectedFail)
1674                 || mIsLeAudioProfileConnectedFail;
1675     }
1676 
1677     /**
1678      * See {@link #getCarConnectionSummary(boolean, boolean)}
1679      */
getCarConnectionSummary()1680     public String getCarConnectionSummary() {
1681         return getCarConnectionSummary(false /* shortSummary */);
1682     }
1683 
1684     /**
1685      * See {@link #getCarConnectionSummary(boolean, boolean)}
1686      */
getCarConnectionSummary(boolean shortSummary)1687     public String getCarConnectionSummary(boolean shortSummary) {
1688         return getCarConnectionSummary(shortSummary, true /* useDisconnectedString */);
1689     }
1690 
1691     /**
1692      * Returns android auto string that describes the connection state of this device.
1693      *
1694      * @param shortSummary {@code true} if need to return short version summary
1695      * @param useDisconnectedString {@code true} if need to return disconnected summary string
1696      */
getCarConnectionSummary(boolean shortSummary, boolean useDisconnectedString)1697     public String getCarConnectionSummary(boolean shortSummary, boolean useDisconnectedString) {
1698         boolean profileConnected = false;       // at least one profile is connected
1699         boolean a2dpNotConnected = false;       // A2DP is preferred but not connected
1700         boolean hfpNotConnected = false;        // HFP is preferred but not connected
1701         boolean hearingAidNotConnected = false; // Hearing Aid is preferred but not connected
1702         boolean leAudioNotConnected = false;       // LeAudio is preferred but not connected
1703 
1704         synchronized (mProfileLock) {
1705             for (LocalBluetoothProfile profile : getProfiles()) {
1706                 int connectionStatus = getProfileConnectionState(profile);
1707 
1708                 switch (connectionStatus) {
1709                     case BluetoothProfile.STATE_CONNECTING:
1710                     case BluetoothProfile.STATE_DISCONNECTING:
1711                         return mContext.getString(
1712                                 BluetoothUtils.getConnectionStateSummary(connectionStatus));
1713 
1714                     case BluetoothProfile.STATE_CONNECTED:
1715                         if (shortSummary) {
1716                             return mContext.getString(BluetoothUtils.getConnectionStateSummary(
1717                                     connectionStatus), /* formatArgs= */ "");
1718                         }
1719                         profileConnected = true;
1720                         break;
1721 
1722                     case BluetoothProfile.STATE_DISCONNECTED:
1723                         if (profile.isProfileReady()) {
1724                             if (profile instanceof A2dpProfile
1725                                     || profile instanceof A2dpSinkProfile) {
1726                                 a2dpNotConnected = true;
1727                             } else if (profile instanceof HeadsetProfile
1728                                     || profile instanceof HfpClientProfile) {
1729                                 hfpNotConnected = true;
1730                             } else if (profile instanceof HearingAidProfile) {
1731                                 hearingAidNotConnected = true;
1732                             } else if (profile instanceof  LeAudioProfile) {
1733                                 leAudioNotConnected = true;
1734                             }
1735                         }
1736                         break;
1737                 }
1738             }
1739         }
1740 
1741         String batteryLevelPercentageString = null;
1742         // Android framework should only set mBatteryLevel to valid range [0-100],
1743         // BluetoothDevice.BATTERY_LEVEL_BLUETOOTH_OFF, or BluetoothDevice.BATTERY_LEVEL_UNKNOWN,
1744         // any other value should be a framework bug. Thus assume here that if value is greater
1745         // than BluetoothDevice.BATTERY_LEVEL_UNKNOWN, it must be valid
1746         final int batteryLevel = getMinBatteryLevelWithMemberDevices();
1747         if (batteryLevel > BluetoothDevice.BATTERY_LEVEL_UNKNOWN) {
1748             // TODO: name com.android.settingslib.bluetooth.Utils something different
1749             batteryLevelPercentageString =
1750                     com.android.settingslib.Utils.formatPercentage(batteryLevel);
1751         }
1752 
1753         // Prepare the string for the Active Device summary
1754         String[] activeDeviceStringsArray = mContext.getResources().getStringArray(
1755                 R.array.bluetooth_audio_active_device_summaries);
1756         String activeDeviceString = activeDeviceStringsArray[0];  // Default value: not active
1757         if (mIsActiveDeviceA2dp && mIsActiveDeviceHeadset) {
1758             activeDeviceString = activeDeviceStringsArray[1];     // Active for Media and Phone
1759         } else {
1760             if (mIsActiveDeviceA2dp) {
1761                 activeDeviceString = activeDeviceStringsArray[2]; // Active for Media only
1762             }
1763             if (mIsActiveDeviceHeadset) {
1764                 activeDeviceString = activeDeviceStringsArray[3]; // Active for Phone only
1765             }
1766         }
1767         if (!hearingAidNotConnected && mIsActiveDeviceHearingAid) {
1768             activeDeviceString = activeDeviceStringsArray[1];
1769             return mContext.getString(R.string.bluetooth_connected, activeDeviceString);
1770         }
1771 
1772         if (!leAudioNotConnected && mIsActiveDeviceLeAudio) {
1773             activeDeviceString = activeDeviceStringsArray[1];
1774             return mContext.getString(R.string.bluetooth_connected, activeDeviceString);
1775         }
1776 
1777         if (profileConnected) {
1778             if (a2dpNotConnected && hfpNotConnected) {
1779                 if (batteryLevelPercentageString != null) {
1780                     return mContext.getString(
1781                             R.string.bluetooth_connected_no_headset_no_a2dp_battery_level,
1782                             batteryLevelPercentageString, activeDeviceString);
1783                 } else {
1784                     return mContext.getString(R.string.bluetooth_connected_no_headset_no_a2dp,
1785                             activeDeviceString);
1786                 }
1787 
1788             } else if (a2dpNotConnected) {
1789                 if (batteryLevelPercentageString != null) {
1790                     return mContext.getString(R.string.bluetooth_connected_no_a2dp_battery_level,
1791                             batteryLevelPercentageString, activeDeviceString);
1792                 } else {
1793                     return mContext.getString(R.string.bluetooth_connected_no_a2dp,
1794                             activeDeviceString);
1795                 }
1796 
1797             } else if (hfpNotConnected) {
1798                 if (batteryLevelPercentageString != null) {
1799                     return mContext.getString(R.string.bluetooth_connected_no_headset_battery_level,
1800                             batteryLevelPercentageString, activeDeviceString);
1801                 } else {
1802                     return mContext.getString(R.string.bluetooth_connected_no_headset,
1803                             activeDeviceString);
1804                 }
1805             } else {
1806                 if (batteryLevelPercentageString != null) {
1807                     return mContext.getString(R.string.bluetooth_connected_battery_level,
1808                             batteryLevelPercentageString, activeDeviceString);
1809                 } else {
1810                     return mContext.getString(R.string.bluetooth_connected, activeDeviceString);
1811                 }
1812             }
1813         }
1814 
1815         if (getBondState() == BluetoothDevice.BOND_BONDING) {
1816             return mContext.getString(R.string.bluetooth_pairing);
1817         }
1818         return useDisconnectedString ? mContext.getString(R.string.bluetooth_disconnected) : null;
1819     }
1820 
1821     /**
1822      * @return {@code true} if {@code cachedBluetoothDevice} is a2dp device
1823      */
isConnectedA2dpDevice()1824     public boolean isConnectedA2dpDevice() {
1825         A2dpProfile a2dpProfile = mProfileManager.getA2dpProfile();
1826         return a2dpProfile != null && a2dpProfile.getConnectionStatus(mDevice) ==
1827                 BluetoothProfile.STATE_CONNECTED;
1828     }
1829 
1830     /**
1831      * @return {@code true} if {@code cachedBluetoothDevice} is HFP device
1832      */
isConnectedHfpDevice()1833     public boolean isConnectedHfpDevice() {
1834         HeadsetProfile headsetProfile = mProfileManager.getHeadsetProfile();
1835         return headsetProfile != null && headsetProfile.getConnectionStatus(mDevice) ==
1836                 BluetoothProfile.STATE_CONNECTED;
1837     }
1838 
1839     /**
1840      * @return {@code true} if {@code cachedBluetoothDevice} is ASHA hearing aid device
1841      */
isConnectedAshaHearingAidDevice()1842     public boolean isConnectedAshaHearingAidDevice() {
1843         HearingAidProfile hearingAidProfile = mProfileManager.getHearingAidProfile();
1844         return hearingAidProfile != null && hearingAidProfile.getConnectionStatus(mDevice) ==
1845                 BluetoothProfile.STATE_CONNECTED;
1846     }
1847 
1848     /**
1849      * @return {@code true} if {@code cachedBluetoothDevice} is HAP device
1850      */
isConnectedHapClientDevice()1851     public boolean isConnectedHapClientDevice() {
1852         HapClientProfile hapClientProfile = mProfileManager.getHapClientProfile();
1853         return hapClientProfile != null && hapClientProfile.getConnectionStatus(mDevice)
1854                 == BluetoothProfile.STATE_CONNECTED;
1855     }
1856 
1857     /**
1858      * @return {@code true} if {@code cachedBluetoothDevice} is LeAudio hearing aid device
1859      */
isConnectedLeAudioHearingAidDevice()1860     public boolean isConnectedLeAudioHearingAidDevice() {
1861         return isConnectedHapClientDevice() && isConnectedLeAudioDevice();
1862     }
1863 
1864     /**
1865      * @return {@code true} if {@code cachedBluetoothDevice} is hearing aid device
1866      *
1867      * The device may be an ASHA hearing aid that supports {@link HearingAidProfile} or a LeAudio
1868      * hearing aid that supports {@link HapClientProfile} and {@link LeAudioProfile}.
1869      */
isConnectedHearingAidDevice()1870     public boolean isConnectedHearingAidDevice() {
1871         return isConnectedAshaHearingAidDevice() || isConnectedLeAudioHearingAidDevice();
1872     }
1873 
1874     /**
1875      * @return {@code true} if {@code cachedBluetoothDevice} is LeAudio device
1876      */
isConnectedLeAudioDevice()1877     public boolean isConnectedLeAudioDevice() {
1878         LeAudioProfile leAudio = mProfileManager.getLeAudioProfile();
1879         return leAudio != null && leAudio.getConnectionStatus(mDevice) ==
1880                 BluetoothProfile.STATE_CONNECTED;
1881     }
1882 
isConnectedSapDevice()1883     private boolean isConnectedSapDevice() {
1884         SapProfile sapProfile = mProfileManager.getSapProfile();
1885         return sapProfile != null && sapProfile.getConnectionStatus(mDevice)
1886                 == BluetoothProfile.STATE_CONNECTED;
1887     }
1888 
getSubDevice()1889     public CachedBluetoothDevice getSubDevice() {
1890         return mSubDevice;
1891     }
1892 
setSubDevice(CachedBluetoothDevice subDevice)1893     public void setSubDevice(CachedBluetoothDevice subDevice) {
1894         mSubDevice = subDevice;
1895     }
1896 
switchSubDeviceContent()1897     public void switchSubDeviceContent() {
1898         // Backup from main device
1899         BluetoothDevice tmpDevice = mDevice;
1900         final short tmpRssi = mRssi;
1901         final boolean tmpJustDiscovered = mJustDiscovered;
1902         final HearingAidInfo tmpHearingAidInfo = mHearingAidInfo;
1903         // Set main device from sub device
1904         release();
1905         mDevice = mSubDevice.mDevice;
1906         mRssi = mSubDevice.mRssi;
1907         mJustDiscovered = mSubDevice.mJustDiscovered;
1908         mHearingAidInfo = mSubDevice.mHearingAidInfo;
1909         // Set sub device from backup
1910         mSubDevice.release();
1911         mSubDevice.mDevice = tmpDevice;
1912         mSubDevice.mRssi = tmpRssi;
1913         mSubDevice.mJustDiscovered = tmpJustDiscovered;
1914         mSubDevice.mHearingAidInfo = tmpHearingAidInfo;
1915         fetchActiveDevices();
1916     }
1917 
1918     /**
1919      * @return a set of member devices that are in the same coordinated set with this device.
1920      */
getMemberDevice()1921     public Set<CachedBluetoothDevice> getMemberDevice() {
1922         return mMemberDevices;
1923     }
1924 
1925     /**
1926      * Store the member devices that are in the same coordinated set.
1927      */
addMemberDevice(CachedBluetoothDevice memberDevice)1928     public void addMemberDevice(CachedBluetoothDevice memberDevice) {
1929         Log.d(TAG, this + " addMemberDevice = " + memberDevice);
1930         mMemberDevices.add(memberDevice);
1931     }
1932 
1933     /**
1934      * Remove a device from the member device sets.
1935      */
removeMemberDevice(CachedBluetoothDevice memberDevice)1936     public void removeMemberDevice(CachedBluetoothDevice memberDevice) {
1937         memberDevice.release();
1938         mMemberDevices.remove(memberDevice);
1939     }
1940 
1941     /**
1942      * In order to show the preference for the whole group, we always set the main device as the
1943      * first connected device in the coordinated set, and then switch the content of the main
1944      * device and member devices.
1945      *
1946      * @param newMainDevice the new Main device which is from the previous main device's member
1947      *                      list.
1948      */
switchMemberDeviceContent(CachedBluetoothDevice newMainDevice)1949     public void switchMemberDeviceContent(CachedBluetoothDevice newMainDevice) {
1950         // Remove the sub device from mMemberDevices first to prevent hash mismatch problem due
1951         // to mDevice switch
1952         removeMemberDevice(newMainDevice);
1953 
1954         // Backup from current main device
1955         final BluetoothDevice tmpDevice = mDevice;
1956         final short tmpRssi = mRssi;
1957         final boolean tmpJustDiscovered = mJustDiscovered;
1958         final HearingAidInfo tmpHearingAidInfo = mHearingAidInfo;
1959 
1960         // Set main device from sub device
1961         release();
1962         mDevice = newMainDevice.mDevice;
1963         mRssi = newMainDevice.mRssi;
1964         mJustDiscovered = newMainDevice.mJustDiscovered;
1965         mHearingAidInfo = newMainDevice.mHearingAidInfo;
1966         fillData();
1967 
1968         // Set sub device from backup
1969         newMainDevice.release();
1970         newMainDevice.mDevice = tmpDevice;
1971         newMainDevice.mRssi = tmpRssi;
1972         newMainDevice.mJustDiscovered = tmpJustDiscovered;
1973         newMainDevice.mHearingAidInfo = tmpHearingAidInfo;
1974         newMainDevice.fillData();
1975 
1976         // Add the sub device back into mMemberDevices with correct hash
1977         addMemberDevice(newMainDevice);
1978     }
1979 
1980     /**
1981      * Get cached bluetooth icon with description
1982      */
getDrawableWithDescription()1983     public Pair<Drawable, String> getDrawableWithDescription() {
1984         Uri uri = BluetoothUtils.getUriMetaData(mDevice, BluetoothDevice.METADATA_MAIN_ICON);
1985         Pair<Drawable, String> pair = BluetoothUtils.getBtClassDrawableWithDescription(
1986                 mContext, this);
1987 
1988         if (BluetoothUtils.isAdvancedDetailsHeader(mDevice) && uri != null) {
1989             BitmapDrawable drawable = mDrawableCache.get(uri.toString());
1990             if (drawable != null) {
1991                 Resources resources = mContext.getResources();
1992                 return new Pair<>(new AdaptiveOutlineDrawable(
1993                         resources, drawable.getBitmap()), pair.second);
1994             }
1995 
1996             refresh();
1997         }
1998 
1999         return BluetoothUtils.getBtRainbowDrawableWithDescription(mContext, this);
2000     }
2001 
releaseLruCache()2002     void releaseLruCache() {
2003         mDrawableCache.evictAll();
2004     }
2005 
getUnpairing()2006     boolean getUnpairing() {
2007         return mUnpairing;
2008     }
2009 
2010     @VisibleForTesting
setLocalBluetoothManager(LocalBluetoothManager bluetoothManager)2011     void setLocalBluetoothManager(LocalBluetoothManager bluetoothManager) {
2012         mBluetoothManager = bluetoothManager;
2013     }
2014 }
2015