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