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