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