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