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