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