• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2008 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.settingslib.bluetooth;
18 
19 import android.bluetooth.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.EventLog;
31 import android.util.Log;
32 import android.bluetooth.BluetoothAdapter;
33 import android.support.annotation.VisibleForTesting;
34 
35 import com.android.settingslib.R;
36 
37 import java.util.ArrayList;
38 import java.util.Collection;
39 import java.util.Collections;
40 import java.util.HashMap;
41 import java.util.List;
42 
43 /**
44  * CachedBluetoothDevice represents a remote Bluetooth device. It contains
45  * attributes of the device (such as the address, name, RSSI, etc.) and
46  * functionality that can be performed on the device (connect, pair, disconnect,
47  * etc.).
48  */
49 public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> {
50     private static final String TAG = "CachedBluetoothDevice";
51     private static final boolean DEBUG = Utils.V;
52 
53     private final Context mContext;
54     private final LocalBluetoothAdapter mLocalAdapter;
55     private final LocalBluetoothProfileManager mProfileManager;
56     private final AudioManager mAudioManager;
57     private final BluetoothDevice mDevice;
58     //TODO: consider remove, BluetoothDevice.getName() is already cached
59     private String mName;
60     private long mHiSyncId;
61     // Need this since there is no method for getting RSSI
62     private short mRssi;
63     //TODO: consider remove, BluetoothDevice.getBluetoothClass() is already cached
64     private BluetoothClass mBtClass;
65     private HashMap<LocalBluetoothProfile, Integer> mProfileConnectionState;
66 
67     private final List<LocalBluetoothProfile> mProfiles =
68             new ArrayList<LocalBluetoothProfile>();
69 
70     // List of profiles that were previously in mProfiles, but have been removed
71     private final List<LocalBluetoothProfile> mRemovedProfiles =
72             new ArrayList<LocalBluetoothProfile>();
73 
74     // Device supports PANU but not NAP: remove PanProfile after device disconnects from NAP
75     private boolean mLocalNapRoleConnected;
76 
77     private boolean mJustDiscovered;
78 
79     private int mMessageRejectionCount;
80 
81     private final Collection<Callback> mCallbacks = new ArrayList<Callback>();
82 
83     // Following constants indicate the user's choices of Phone book/message access settings
84     // User hasn't made any choice or settings app has wiped out the memory
85     public final static int ACCESS_UNKNOWN = 0;
86     // User has accepted the connection and let Settings app remember the decision
87     public final static int ACCESS_ALLOWED = 1;
88     // User has rejected the connection and let Settings app remember the decision
89     public final static int ACCESS_REJECTED = 2;
90 
91     // How many times user should reject the connection to make the choice persist.
92     private final static int MESSAGE_REJECTION_COUNT_LIMIT_TO_PERSIST = 2;
93 
94     private final static String MESSAGE_REJECTION_COUNT_PREFS_NAME = "bluetooth_message_reject";
95 
96     /**
97      * When we connect to multiple profiles, we only want to display a single
98      * error even if they all fail. This tracks that state.
99      */
100     private boolean mIsConnectingErrorPossible;
101 
getHiSyncId()102     public long getHiSyncId() {
103         return mHiSyncId;
104     }
105 
setHiSyncId(long id)106     public void setHiSyncId(long id) {
107         if (Utils.D) {
108             Log.d(TAG, "setHiSyncId: mDevice " + mDevice + ", id " + id);
109         }
110         mHiSyncId = id;
111     }
112 
113     /**
114      * Last time a bt profile auto-connect was attempted.
115      * If an ACTION_UUID intent comes in within
116      * MAX_UUID_DELAY_FOR_AUTO_CONNECT milliseconds, we will try auto-connect
117      * again with the new UUIDs
118      */
119     private long mConnectAttempted;
120 
121     // See mConnectAttempted
122     private static final long MAX_UUID_DELAY_FOR_AUTO_CONNECT = 5000;
123     private static final long MAX_HOGP_DELAY_FOR_AUTO_CONNECT = 30000;
124 
125     // Active device state
126     private boolean mIsActiveDeviceA2dp = false;
127     private boolean mIsActiveDeviceHeadset = false;
128     private boolean mIsActiveDeviceHearingAid = false;
129     /**
130      * Describes the current device and profile for logging.
131      *
132      * @param profile Profile to describe
133      * @return Description of the device and profile
134      */
describe(LocalBluetoothProfile profile)135     private String describe(LocalBluetoothProfile profile) {
136         StringBuilder sb = new StringBuilder();
137         sb.append("Address:").append(mDevice);
138         if (profile != null) {
139             sb.append(" Profile:").append(profile);
140         }
141 
142         return sb.toString();
143     }
144 
onProfileStateChanged(LocalBluetoothProfile profile, int newProfileState)145     void onProfileStateChanged(LocalBluetoothProfile profile, int newProfileState) {
146         if (Utils.D) {
147             Log.d(TAG, "onProfileStateChanged: profile " + profile +
148                     " newProfileState " + newProfileState);
149         }
150         if (mLocalAdapter.getBluetoothState() == BluetoothAdapter.STATE_TURNING_OFF)
151         {
152             if (Utils.D) Log.d(TAG, " BT Turninig Off...Profile conn state change ignored...");
153             return;
154         }
155         mProfileConnectionState.put(profile, newProfileState);
156         if (newProfileState == BluetoothProfile.STATE_CONNECTED) {
157             if (profile instanceof MapProfile) {
158                 profile.setPreferred(mDevice, true);
159             }
160             if (!mProfiles.contains(profile)) {
161                 mRemovedProfiles.remove(profile);
162                 mProfiles.add(profile);
163                 if (profile instanceof PanProfile &&
164                         ((PanProfile) profile).isLocalRoleNap(mDevice)) {
165                     // Device doesn't support NAP, so remove PanProfile on disconnect
166                     mLocalNapRoleConnected = true;
167                 }
168             }
169         } else if (profile instanceof MapProfile &&
170                 newProfileState == BluetoothProfile.STATE_DISCONNECTED) {
171             profile.setPreferred(mDevice, false);
172         } else if (mLocalNapRoleConnected && profile instanceof PanProfile &&
173                 ((PanProfile) profile).isLocalRoleNap(mDevice) &&
174                 newProfileState == BluetoothProfile.STATE_DISCONNECTED) {
175             Log.d(TAG, "Removing PanProfile from device after NAP disconnect");
176             mProfiles.remove(profile);
177             mRemovedProfiles.add(profile);
178             mLocalNapRoleConnected = false;
179         }
180         fetchActiveDevices();
181     }
182 
CachedBluetoothDevice(Context context, LocalBluetoothAdapter adapter, LocalBluetoothProfileManager profileManager, BluetoothDevice device)183     CachedBluetoothDevice(Context context,
184                           LocalBluetoothAdapter adapter,
185                           LocalBluetoothProfileManager profileManager,
186                           BluetoothDevice device) {
187         mContext = context;
188         mLocalAdapter = adapter;
189         mProfileManager = profileManager;
190         mAudioManager = context.getSystemService(AudioManager.class);
191         mDevice = device;
192         mProfileConnectionState = new HashMap<LocalBluetoothProfile, Integer>();
193         fillData();
194         mHiSyncId = BluetoothHearingAid.HI_SYNC_ID_INVALID;
195     }
196 
disconnect()197     public void disconnect() {
198         for (LocalBluetoothProfile profile : mProfiles) {
199             disconnect(profile);
200         }
201         // Disconnect  PBAP server in case its connected
202         // This is to ensure all the profiles are disconnected as some CK/Hs do not
203         // disconnect  PBAP connection when HF connection is brought down
204         PbapServerProfile PbapProfile = mProfileManager.getPbapProfile();
205         if (PbapProfile.getConnectionStatus(mDevice) == BluetoothProfile.STATE_CONNECTED)
206         {
207             PbapProfile.disconnect(mDevice);
208         }
209     }
210 
disconnect(LocalBluetoothProfile profile)211     public void disconnect(LocalBluetoothProfile profile) {
212         if (profile.disconnect(mDevice)) {
213             if (Utils.D) {
214                 Log.d(TAG, "Command sent successfully:DISCONNECT " + describe(profile));
215             }
216         }
217     }
218 
connect(boolean connectAllProfiles)219     public void connect(boolean connectAllProfiles) {
220         if (!ensurePaired()) {
221             return;
222         }
223 
224         mConnectAttempted = SystemClock.elapsedRealtime();
225         connectWithoutResettingTimer(connectAllProfiles);
226     }
227 
onBondingDockConnect()228     void onBondingDockConnect() {
229         // Attempt to connect if UUIDs are available. Otherwise,
230         // we will connect when the ACTION_UUID intent arrives.
231         connect(false);
232     }
233 
connectWithoutResettingTimer(boolean connectAllProfiles)234     private void connectWithoutResettingTimer(boolean connectAllProfiles) {
235         // Try to initialize the profiles if they were not.
236         if (mProfiles.isEmpty()) {
237             // if mProfiles is empty, then do not invoke updateProfiles. This causes a race
238             // condition with carkits during pairing, wherein RemoteDevice.UUIDs have been updated
239             // from bluetooth stack but ACTION.uuid is not sent yet.
240             // Eventually ACTION.uuid will be received which shall trigger the connection of the
241             // various profiles
242             // If UUIDs are not available yet, connect will be happen
243             // upon arrival of the ACTION_UUID intent.
244             Log.d(TAG, "No profiles. Maybe we will connect later");
245             return;
246         }
247 
248         // Reset the only-show-one-error-dialog tracking variable
249         mIsConnectingErrorPossible = true;
250 
251         int preferredProfiles = 0;
252         for (LocalBluetoothProfile profile : mProfiles) {
253             if (connectAllProfiles ? profile.isConnectable() : profile.isAutoConnectable()) {
254                 if (profile.isPreferred(mDevice)) {
255                     ++preferredProfiles;
256                     connectInt(profile);
257                 }
258             }
259         }
260         if (DEBUG) Log.d(TAG, "Preferred profiles = " + preferredProfiles);
261 
262         if (preferredProfiles == 0) {
263             connectAutoConnectableProfiles();
264         }
265     }
266 
connectAutoConnectableProfiles()267     private void connectAutoConnectableProfiles() {
268         if (!ensurePaired()) {
269             return;
270         }
271         // Reset the only-show-one-error-dialog tracking variable
272         mIsConnectingErrorPossible = true;
273 
274         for (LocalBluetoothProfile profile : mProfiles) {
275             if (profile.isAutoConnectable()) {
276                 profile.setPreferred(mDevice, true);
277                 connectInt(profile);
278             }
279         }
280     }
281 
282     /**
283      * Connect this device to the specified profile.
284      *
285      * @param profile the profile to use with the remote device
286      */
connectProfile(LocalBluetoothProfile profile)287     public void connectProfile(LocalBluetoothProfile profile) {
288         mConnectAttempted = SystemClock.elapsedRealtime();
289         // Reset the only-show-one-error-dialog tracking variable
290         mIsConnectingErrorPossible = true;
291         connectInt(profile);
292         // Refresh the UI based on profile.connect() call
293         refresh();
294     }
295 
connectInt(LocalBluetoothProfile profile)296     synchronized void connectInt(LocalBluetoothProfile profile) {
297         if (!ensurePaired()) {
298             return;
299         }
300         if (profile.connect(mDevice)) {
301             if (Utils.D) {
302                 Log.d(TAG, "Command sent successfully:CONNECT " + describe(profile));
303             }
304             return;
305         }
306         Log.i(TAG, "Failed to connect " + profile.toString() + " to " + mName);
307     }
308 
ensurePaired()309     private boolean ensurePaired() {
310         if (getBondState() == BluetoothDevice.BOND_NONE) {
311             startPairing();
312             return false;
313         } else {
314             return true;
315         }
316     }
317 
startPairing()318     public boolean startPairing() {
319         // Pairing is unreliable while scanning, so cancel discovery
320         if (mLocalAdapter.isDiscovering()) {
321             mLocalAdapter.cancelDiscovery();
322         }
323 
324         if (!mDevice.createBond()) {
325             return false;
326         }
327 
328         return true;
329     }
330 
331     /**
332      * Return true if user initiated pairing on this device. The message text is
333      * slightly different for local vs. remote initiated pairing dialogs.
334      */
isUserInitiatedPairing()335     boolean isUserInitiatedPairing() {
336         return mDevice.isBondingInitiatedLocally();
337     }
338 
unpair()339     public void unpair() {
340         int state = getBondState();
341 
342         if (state == BluetoothDevice.BOND_BONDING) {
343             mDevice.cancelBondProcess();
344         }
345 
346         if (state != BluetoothDevice.BOND_NONE) {
347             final BluetoothDevice dev = mDevice;
348             if (dev != null) {
349                 final boolean successful = dev.removeBond();
350                 if (successful) {
351                     if (Utils.D) {
352                         Log.d(TAG, "Command sent successfully:REMOVE_BOND " + describe(null));
353                     }
354                 } else if (Utils.V) {
355                     Log.v(TAG, "Framework rejected command immediately:REMOVE_BOND " +
356                         describe(null));
357                 }
358             }
359         }
360     }
361 
getProfileConnectionState(LocalBluetoothProfile profile)362     public int getProfileConnectionState(LocalBluetoothProfile profile) {
363         if (mProfileConnectionState.get(profile) == null) {
364             // If cache is empty make the binder call to get the state
365             int state = profile.getConnectionStatus(mDevice);
366             mProfileConnectionState.put(profile, state);
367         }
368         return mProfileConnectionState.get(profile);
369     }
370 
clearProfileConnectionState()371     public void clearProfileConnectionState ()
372     {
373         if (Utils.D) {
374             Log.d(TAG," Clearing all connection state for dev:" + mDevice.getName());
375         }
376         for (LocalBluetoothProfile profile :getProfiles()) {
377             mProfileConnectionState.put(profile, BluetoothProfile.STATE_DISCONNECTED);
378         }
379     }
380 
381     // TODO: do any of these need to run async on a background thread?
fillData()382     private void fillData() {
383         fetchName();
384         fetchBtClass();
385         updateProfiles();
386         fetchActiveDevices();
387         migratePhonebookPermissionChoice();
388         migrateMessagePermissionChoice();
389         fetchMessageRejectionCount();
390 
391         dispatchAttributesChanged();
392     }
393 
getDevice()394     public BluetoothDevice getDevice() {
395         return mDevice;
396     }
397 
398     /**
399      * Convenience method that can be mocked - it lets tests avoid having to call getDevice() which
400      * causes problems in tests since BluetoothDevice is final and cannot be mocked.
401      * @return the address of this device
402      */
getAddress()403     public String getAddress() {
404         return mDevice.getAddress();
405     }
406 
getName()407     public String getName() {
408         return mName;
409     }
410 
411     /**
412      * Populate name from BluetoothDevice.ACTION_FOUND intent
413      */
setNewName(String name)414     void setNewName(String name) {
415         if (mName == null) {
416             mName = name;
417             if (mName == null || TextUtils.isEmpty(mName)) {
418                 mName = mDevice.getAddress();
419             }
420             dispatchAttributesChanged();
421         }
422     }
423 
424     /**
425      * User changes the device name
426      * @param name new alias name to be set, should never be null
427      */
setName(String name)428     public void setName(String name) {
429         // Prevent mName to be set to null if setName(null) is called
430         if (name != null && !TextUtils.equals(name, mName)) {
431             mName = name;
432             mDevice.setAlias(name);
433             dispatchAttributesChanged();
434         }
435     }
436 
437     /**
438      * Set this device as active device
439      * @return true if at least one profile on this device is set to active, false otherwise
440      */
setActive()441     public boolean setActive() {
442         boolean result = false;
443         A2dpProfile a2dpProfile = mProfileManager.getA2dpProfile();
444         if (a2dpProfile != null && isConnectedProfile(a2dpProfile)) {
445             if (a2dpProfile.setActiveDevice(getDevice())) {
446                 Log.i(TAG, "OnPreferenceClickListener: A2DP active device=" + this);
447                 result = true;
448             }
449         }
450         HeadsetProfile headsetProfile = mProfileManager.getHeadsetProfile();
451         if ((headsetProfile != null) && isConnectedProfile(headsetProfile)) {
452             if (headsetProfile.setActiveDevice(getDevice())) {
453                 Log.i(TAG, "OnPreferenceClickListener: Headset active device=" + this);
454                 result = true;
455             }
456         }
457         HearingAidProfile hearingAidProfile = mProfileManager.getHearingAidProfile();
458         if ((hearingAidProfile != null) && isConnectedProfile(hearingAidProfile)) {
459             if (hearingAidProfile.setActiveDevice(getDevice())) {
460                 Log.i(TAG, "OnPreferenceClickListener: Hearing Aid active device=" + this);
461                 result = true;
462             }
463         }
464         return result;
465     }
466 
refreshName()467     void refreshName() {
468         fetchName();
469         dispatchAttributesChanged();
470     }
471 
fetchName()472     private void fetchName() {
473         mName = mDevice.getAliasName();
474 
475         if (TextUtils.isEmpty(mName)) {
476             mName = mDevice.getAddress();
477             if (DEBUG) Log.d(TAG, "Device has no name (yet), use address: " + mName);
478         }
479     }
480 
481     /**
482      * Checks if device has a human readable name besides MAC address
483      * @return true if device's alias name is not null nor empty, false otherwise
484      */
hasHumanReadableName()485     public boolean hasHumanReadableName() {
486         return !TextUtils.isEmpty(mDevice.getAliasName());
487     }
488 
489     /**
490      * Get battery level from remote device
491      * @return battery level in percentage [0-100], or {@link BluetoothDevice#BATTERY_LEVEL_UNKNOWN}
492      */
getBatteryLevel()493     public int getBatteryLevel() {
494         return mDevice.getBatteryLevel();
495     }
496 
refresh()497     void refresh() {
498         dispatchAttributesChanged();
499     }
500 
setJustDiscovered(boolean justDiscovered)501     public void setJustDiscovered(boolean justDiscovered) {
502         if (mJustDiscovered != justDiscovered) {
503             mJustDiscovered = justDiscovered;
504             dispatchAttributesChanged();
505         }
506     }
507 
getBondState()508     public int getBondState() {
509         return mDevice.getBondState();
510     }
511 
512     /**
513      * Update the device status as active or non-active per Bluetooth profile.
514      *
515      * @param isActive true if the device is active
516      * @param bluetoothProfile the Bluetooth profile
517      */
onActiveDeviceChanged(boolean isActive, int bluetoothProfile)518     public void onActiveDeviceChanged(boolean isActive, int bluetoothProfile) {
519         boolean changed = false;
520         switch (bluetoothProfile) {
521         case BluetoothProfile.A2DP:
522             changed = (mIsActiveDeviceA2dp != isActive);
523             mIsActiveDeviceA2dp = isActive;
524             break;
525         case BluetoothProfile.HEADSET:
526             changed = (mIsActiveDeviceHeadset != isActive);
527             mIsActiveDeviceHeadset = isActive;
528             break;
529         case BluetoothProfile.HEARING_AID:
530             changed = (mIsActiveDeviceHearingAid != isActive);
531             mIsActiveDeviceHearingAid = isActive;
532             break;
533         default:
534             Log.w(TAG, "onActiveDeviceChanged: unknown profile " + bluetoothProfile +
535                     " isActive " + isActive);
536             break;
537         }
538         if (changed) {
539             dispatchAttributesChanged();
540         }
541     }
542 
543     /**
544      * Update the profile audio state.
545      */
onAudioModeChanged()546     void onAudioModeChanged() {
547         dispatchAttributesChanged();
548     }
549     /**
550      * Get the device status as active or non-active per Bluetooth profile.
551      *
552      * @param bluetoothProfile the Bluetooth profile
553      * @return true if the device is active
554      */
555     @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
isActiveDevice(int bluetoothProfile)556     public boolean isActiveDevice(int bluetoothProfile) {
557         switch (bluetoothProfile) {
558             case BluetoothProfile.A2DP:
559                 return mIsActiveDeviceA2dp;
560             case BluetoothProfile.HEADSET:
561                 return mIsActiveDeviceHeadset;
562             case BluetoothProfile.HEARING_AID:
563                 return mIsActiveDeviceHearingAid;
564             default:
565                 Log.w(TAG, "getActiveDevice: unknown profile " + bluetoothProfile);
566                 break;
567         }
568         return false;
569     }
570 
setRssi(short rssi)571     void setRssi(short rssi) {
572         if (mRssi != rssi) {
573             mRssi = rssi;
574             dispatchAttributesChanged();
575         }
576     }
577 
578     /**
579      * Checks whether we are connected to this device (any profile counts).
580      *
581      * @return Whether it is connected.
582      */
isConnected()583     public boolean isConnected() {
584         for (LocalBluetoothProfile profile : mProfiles) {
585             int status = getProfileConnectionState(profile);
586             if (status == BluetoothProfile.STATE_CONNECTED) {
587                 return true;
588             }
589         }
590 
591         return false;
592     }
593 
isConnectedProfile(LocalBluetoothProfile profile)594     public boolean isConnectedProfile(LocalBluetoothProfile profile) {
595         int status = getProfileConnectionState(profile);
596         return status == BluetoothProfile.STATE_CONNECTED;
597 
598     }
599 
isBusy()600     public boolean isBusy() {
601         for (LocalBluetoothProfile profile : mProfiles) {
602             int status = getProfileConnectionState(profile);
603             if (status == BluetoothProfile.STATE_CONNECTING
604                     || status == BluetoothProfile.STATE_DISCONNECTING) {
605                 return true;
606             }
607         }
608         return getBondState() == BluetoothDevice.BOND_BONDING;
609     }
610 
611     /**
612      * Fetches a new value for the cached BT class.
613      */
fetchBtClass()614     private void fetchBtClass() {
615         mBtClass = mDevice.getBluetoothClass();
616     }
617 
updateProfiles()618     private boolean updateProfiles() {
619         ParcelUuid[] uuids = mDevice.getUuids();
620         if (uuids == null) return false;
621 
622         ParcelUuid[] localUuids = mLocalAdapter.getUuids();
623         if (localUuids == null) return false;
624 
625         /*
626          * Now we know if the device supports PBAP, update permissions...
627          */
628         processPhonebookAccess();
629 
630         mProfileManager.updateProfiles(uuids, localUuids, mProfiles, mRemovedProfiles,
631                                        mLocalNapRoleConnected, mDevice);
632 
633         if (DEBUG) {
634             Log.e(TAG, "updating profiles for " + mDevice.getAliasName());
635             BluetoothClass bluetoothClass = mDevice.getBluetoothClass();
636 
637             if (bluetoothClass != null) Log.v(TAG, "Class: " + bluetoothClass.toString());
638             Log.v(TAG, "UUID:");
639             for (ParcelUuid uuid : uuids) {
640                 Log.v(TAG, "  " + uuid);
641             }
642         }
643         return true;
644     }
645 
fetchActiveDevices()646     private void fetchActiveDevices() {
647         A2dpProfile a2dpProfile = mProfileManager.getA2dpProfile();
648         if (a2dpProfile != null) {
649             mIsActiveDeviceA2dp = mDevice.equals(a2dpProfile.getActiveDevice());
650         }
651         HeadsetProfile headsetProfile = mProfileManager.getHeadsetProfile();
652         if (headsetProfile != null) {
653             mIsActiveDeviceHeadset = mDevice.equals(headsetProfile.getActiveDevice());
654         }
655         HearingAidProfile hearingAidProfile = mProfileManager.getHearingAidProfile();
656         if (hearingAidProfile != null) {
657             mIsActiveDeviceHearingAid = hearingAidProfile.getActiveDevices().contains(mDevice);
658         }
659     }
660 
661     /**
662      * Refreshes the UI for the BT class, including fetching the latest value
663      * for the class.
664      */
refreshBtClass()665     void refreshBtClass() {
666         fetchBtClass();
667         dispatchAttributesChanged();
668     }
669 
670     /**
671      * Refreshes the UI when framework alerts us of a UUID change.
672      */
onUuidChanged()673     void onUuidChanged() {
674         updateProfiles();
675         ParcelUuid[] uuids = mDevice.getUuids();
676 
677         long timeout = MAX_UUID_DELAY_FOR_AUTO_CONNECT;
678         if (BluetoothUuid.isUuidPresent(uuids, BluetoothUuid.Hogp)) {
679             timeout = MAX_HOGP_DELAY_FOR_AUTO_CONNECT;
680         }
681 
682         if (DEBUG) {
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             connectWithoutResettingTimer(false);
694         }
695 
696         dispatchAttributesChanged();
697     }
698 
onBondingStateChanged(int bondState)699     void onBondingStateChanged(int bondState) {
700         if (bondState == BluetoothDevice.BOND_NONE) {
701             mProfiles.clear();
702             setPhonebookPermissionChoice(ACCESS_UNKNOWN);
703             setMessagePermissionChoice(ACCESS_UNKNOWN);
704             setSimPermissionChoice(ACCESS_UNKNOWN);
705             mMessageRejectionCount = 0;
706             saveMessageRejectionCount();
707         }
708 
709         refresh();
710 
711         if (bondState == BluetoothDevice.BOND_BONDED) {
712             if (mDevice.isBluetoothDock()) {
713                 onBondingDockConnect();
714             } else if (mDevice.isBondingInitiatedLocally()) {
715                 connect(false);
716             }
717         }
718     }
719 
setBtClass(BluetoothClass btClass)720     void setBtClass(BluetoothClass btClass) {
721         if (btClass != null && mBtClass != btClass) {
722             mBtClass = btClass;
723             dispatchAttributesChanged();
724         }
725     }
726 
getBtClass()727     public BluetoothClass getBtClass() {
728         return mBtClass;
729     }
730 
getProfiles()731     public List<LocalBluetoothProfile> getProfiles() {
732         return Collections.unmodifiableList(mProfiles);
733     }
734 
getConnectableProfiles()735     public List<LocalBluetoothProfile> getConnectableProfiles() {
736         List<LocalBluetoothProfile> connectableProfiles =
737                 new ArrayList<LocalBluetoothProfile>();
738         for (LocalBluetoothProfile profile : mProfiles) {
739             if (profile.isConnectable()) {
740                 connectableProfiles.add(profile);
741             }
742         }
743         return connectableProfiles;
744     }
745 
getRemovedProfiles()746     public List<LocalBluetoothProfile> getRemovedProfiles() {
747         return mRemovedProfiles;
748     }
749 
registerCallback(Callback callback)750     public void registerCallback(Callback callback) {
751         synchronized (mCallbacks) {
752             mCallbacks.add(callback);
753         }
754     }
755 
unregisterCallback(Callback callback)756     public void unregisterCallback(Callback callback) {
757         synchronized (mCallbacks) {
758             mCallbacks.remove(callback);
759         }
760     }
761 
dispatchAttributesChanged()762     private void dispatchAttributesChanged() {
763         synchronized (mCallbacks) {
764             for (Callback callback : mCallbacks) {
765                 callback.onDeviceAttributesChanged();
766             }
767         }
768     }
769 
770     @Override
toString()771     public String toString() {
772         return mDevice.toString();
773     }
774 
775     @Override
equals(Object o)776     public boolean equals(Object o) {
777         if ((o == null) || !(o instanceof CachedBluetoothDevice)) {
778             return false;
779         }
780         return mDevice.equals(((CachedBluetoothDevice) o).mDevice);
781     }
782 
783     @Override
hashCode()784     public int hashCode() {
785         return mDevice.getAddress().hashCode();
786     }
787 
788     // This comparison uses non-final fields so the sort order may change
789     // when device attributes change (such as bonding state). Settings
790     // will completely refresh the device list when this happens.
compareTo(CachedBluetoothDevice another)791     public int compareTo(CachedBluetoothDevice another) {
792         // Connected above not connected
793         int comparison = (another.isConnected() ? 1 : 0) - (isConnected() ? 1 : 0);
794         if (comparison != 0) return comparison;
795 
796         // Paired above not paired
797         comparison = (another.getBondState() == BluetoothDevice.BOND_BONDED ? 1 : 0) -
798             (getBondState() == BluetoothDevice.BOND_BONDED ? 1 : 0);
799         if (comparison != 0) return comparison;
800 
801         // Just discovered above discovered in the past
802         comparison = (another.mJustDiscovered ? 1 : 0) - (mJustDiscovered ? 1 : 0);
803         if (comparison != 0) return comparison;
804 
805         // Stronger signal above weaker signal
806         comparison = another.mRssi - mRssi;
807         if (comparison != 0) return comparison;
808 
809         // Fallback on name
810         return mName.compareTo(another.mName);
811     }
812 
813     public interface Callback {
onDeviceAttributesChanged()814         void onDeviceAttributesChanged();
815     }
816 
getPhonebookPermissionChoice()817     public int getPhonebookPermissionChoice() {
818         int permission = mDevice.getPhonebookAccessPermission();
819         if (permission == BluetoothDevice.ACCESS_ALLOWED) {
820             return ACCESS_ALLOWED;
821         } else if (permission == BluetoothDevice.ACCESS_REJECTED) {
822             return ACCESS_REJECTED;
823         }
824         return ACCESS_UNKNOWN;
825     }
826 
setPhonebookPermissionChoice(int permissionChoice)827     public void setPhonebookPermissionChoice(int permissionChoice) {
828         int permission = BluetoothDevice.ACCESS_UNKNOWN;
829         if (permissionChoice == ACCESS_ALLOWED) {
830             permission = BluetoothDevice.ACCESS_ALLOWED;
831         } else if (permissionChoice == ACCESS_REJECTED) {
832             permission = BluetoothDevice.ACCESS_REJECTED;
833         }
834         mDevice.setPhonebookAccessPermission(permission);
835     }
836 
837     // Migrates data from old data store (in Settings app's shared preferences) to new (in Bluetooth
838     // app's shared preferences).
migratePhonebookPermissionChoice()839     private void migratePhonebookPermissionChoice() {
840         SharedPreferences preferences = mContext.getSharedPreferences(
841                 "bluetooth_phonebook_permission", Context.MODE_PRIVATE);
842         if (!preferences.contains(mDevice.getAddress())) {
843             return;
844         }
845 
846         if (mDevice.getPhonebookAccessPermission() == BluetoothDevice.ACCESS_UNKNOWN) {
847             int oldPermission = preferences.getInt(mDevice.getAddress(), ACCESS_UNKNOWN);
848             if (oldPermission == ACCESS_ALLOWED) {
849                 mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_ALLOWED);
850             } else if (oldPermission == ACCESS_REJECTED) {
851                 mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_REJECTED);
852             }
853         }
854 
855         SharedPreferences.Editor editor = preferences.edit();
856         editor.remove(mDevice.getAddress());
857         editor.commit();
858     }
859 
getMessagePermissionChoice()860     public int getMessagePermissionChoice() {
861         int permission = mDevice.getMessageAccessPermission();
862         if (permission == BluetoothDevice.ACCESS_ALLOWED) {
863             return ACCESS_ALLOWED;
864         } else if (permission == BluetoothDevice.ACCESS_REJECTED) {
865             return ACCESS_REJECTED;
866         }
867         return ACCESS_UNKNOWN;
868     }
869 
setMessagePermissionChoice(int permissionChoice)870     public void setMessagePermissionChoice(int permissionChoice) {
871         int permission = BluetoothDevice.ACCESS_UNKNOWN;
872         if (permissionChoice == ACCESS_ALLOWED) {
873             permission = BluetoothDevice.ACCESS_ALLOWED;
874         } else if (permissionChoice == ACCESS_REJECTED) {
875             permission = BluetoothDevice.ACCESS_REJECTED;
876         }
877         mDevice.setMessageAccessPermission(permission);
878     }
879 
getSimPermissionChoice()880     public int getSimPermissionChoice() {
881         int permission = mDevice.getSimAccessPermission();
882         if (permission == BluetoothDevice.ACCESS_ALLOWED) {
883             return ACCESS_ALLOWED;
884         } else if (permission == BluetoothDevice.ACCESS_REJECTED) {
885             return ACCESS_REJECTED;
886         }
887         return ACCESS_UNKNOWN;
888     }
889 
setSimPermissionChoice(int permissionChoice)890     void setSimPermissionChoice(int permissionChoice) {
891         int permission = BluetoothDevice.ACCESS_UNKNOWN;
892         if (permissionChoice == ACCESS_ALLOWED) {
893             permission = BluetoothDevice.ACCESS_ALLOWED;
894         } else if (permissionChoice == ACCESS_REJECTED) {
895             permission = BluetoothDevice.ACCESS_REJECTED;
896         }
897         mDevice.setSimAccessPermission(permission);
898     }
899 
900     // Migrates data from old data store (in Settings app's shared preferences) to new (in Bluetooth
901     // app's shared preferences).
migrateMessagePermissionChoice()902     private void migrateMessagePermissionChoice() {
903         SharedPreferences preferences = mContext.getSharedPreferences(
904                 "bluetooth_message_permission", Context.MODE_PRIVATE);
905         if (!preferences.contains(mDevice.getAddress())) {
906             return;
907         }
908 
909         if (mDevice.getMessageAccessPermission() == BluetoothDevice.ACCESS_UNKNOWN) {
910             int oldPermission = preferences.getInt(mDevice.getAddress(), ACCESS_UNKNOWN);
911             if (oldPermission == ACCESS_ALLOWED) {
912                 mDevice.setMessageAccessPermission(BluetoothDevice.ACCESS_ALLOWED);
913             } else if (oldPermission == ACCESS_REJECTED) {
914                 mDevice.setMessageAccessPermission(BluetoothDevice.ACCESS_REJECTED);
915             }
916         }
917 
918         SharedPreferences.Editor editor = preferences.edit();
919         editor.remove(mDevice.getAddress());
920         editor.commit();
921     }
922 
923     /**
924      * @return Whether this rejection should persist.
925      */
checkAndIncreaseMessageRejectionCount()926     public boolean checkAndIncreaseMessageRejectionCount() {
927         if (mMessageRejectionCount < MESSAGE_REJECTION_COUNT_LIMIT_TO_PERSIST) {
928             mMessageRejectionCount++;
929             saveMessageRejectionCount();
930         }
931         return mMessageRejectionCount >= MESSAGE_REJECTION_COUNT_LIMIT_TO_PERSIST;
932     }
933 
fetchMessageRejectionCount()934     private void fetchMessageRejectionCount() {
935         SharedPreferences preference = mContext.getSharedPreferences(
936                 MESSAGE_REJECTION_COUNT_PREFS_NAME, Context.MODE_PRIVATE);
937         mMessageRejectionCount = preference.getInt(mDevice.getAddress(), 0);
938     }
939 
saveMessageRejectionCount()940     private void saveMessageRejectionCount() {
941         SharedPreferences.Editor editor = mContext.getSharedPreferences(
942                 MESSAGE_REJECTION_COUNT_PREFS_NAME, Context.MODE_PRIVATE).edit();
943         if (mMessageRejectionCount == 0) {
944             editor.remove(mDevice.getAddress());
945         } else {
946             editor.putInt(mDevice.getAddress(), mMessageRejectionCount);
947         }
948         editor.commit();
949     }
950 
processPhonebookAccess()951     private void processPhonebookAccess() {
952         if (mDevice.getBondState() != BluetoothDevice.BOND_BONDED) return;
953 
954         ParcelUuid[] uuids = mDevice.getUuids();
955         if (BluetoothUuid.containsAnyUuid(uuids, PbapServerProfile.PBAB_CLIENT_UUIDS)) {
956             // The pairing dialog now warns of phone-book access for paired devices.
957             // No separate prompt is displayed after pairing.
958             if (getPhonebookPermissionChoice() == CachedBluetoothDevice.ACCESS_UNKNOWN) {
959                 if (mDevice.getBluetoothClass().getDeviceClass()
960                         == BluetoothClass.Device.AUDIO_VIDEO_HANDSFREE ||
961                     mDevice.getBluetoothClass().getDeviceClass()
962                         == BluetoothClass.Device.AUDIO_VIDEO_WEARABLE_HEADSET) {
963                     EventLog.writeEvent(0x534e4554, "138529441", -1, "");
964                 }
965                 setPhonebookPermissionChoice(CachedBluetoothDevice.ACCESS_REJECTED);
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