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