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