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.settings.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 java.util.ArrayList;
32 import java.util.Collection;
33 import java.util.Collections;
34 import java.util.HashMap;
35 import java.util.List;
36 
37 /**
38  * CachedBluetoothDevice represents a remote Bluetooth device. It contains
39  * attributes of the device (such as the address, name, RSSI, etc.) and
40  * functionality that can be performed on the device (connect, pair, disconnect,
41  * etc.).
42  */
43 final class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> {
44     private static final String TAG = "CachedBluetoothDevice";
45     private static final boolean DEBUG = Utils.V;
46 
47     private final Context mContext;
48     private final LocalBluetoothAdapter mLocalAdapter;
49     private final LocalBluetoothProfileManager mProfileManager;
50     private final BluetoothDevice mDevice;
51     private String mName;
52     private short mRssi;
53     private BluetoothClass mBtClass;
54     private HashMap<LocalBluetoothProfile, Integer> mProfileConnectionState;
55 
56     private final List<LocalBluetoothProfile> mProfiles =
57             new ArrayList<LocalBluetoothProfile>();
58 
59     // List of profiles that were previously in mProfiles, but have been removed
60     private final List<LocalBluetoothProfile> mRemovedProfiles =
61             new ArrayList<LocalBluetoothProfile>();
62 
63     // Device supports PANU but not NAP: remove PanProfile after device disconnects from NAP
64     private boolean mLocalNapRoleConnected;
65 
66     private boolean mVisible;
67 
68     private int mPhonebookPermissionChoice;
69 
70     private int mMessagePermissionChoice;
71 
72     private int mMessageRejectionCount;
73 
74     private final Collection<Callback> mCallbacks = new ArrayList<Callback>();
75 
76     // Following constants indicate the user's choices of Phone book/message access settings
77     // User hasn't made any choice or settings app has wiped out the memory
78     public final static int ACCESS_UNKNOWN = 0;
79     // User has accepted the connection and let Settings app remember the decision
80     public final static int ACCESS_ALLOWED = 1;
81     // User has rejected the connection and let Settings app remember the decision
82     public final static int ACCESS_REJECTED = 2;
83 
84     // How many times user should reject the connection to make the choice persist.
85     private final static int MESSAGE_REJECTION_COUNT_LIMIT_TO_PERSIST = 2;
86 
87     private final static String MESSAGE_REJECTION_COUNT_PREFS_NAME = "bluetooth_message_reject";
88 
89     /**
90      * When we connect to multiple profiles, we only want to display a single
91      * error even if they all fail. This tracks that state.
92      */
93     private boolean mIsConnectingErrorPossible;
94 
95     /**
96      * Last time a bt profile auto-connect was attempted.
97      * If an ACTION_UUID intent comes in within
98      * MAX_UUID_DELAY_FOR_AUTO_CONNECT milliseconds, we will try auto-connect
99      * again with the new UUIDs
100      */
101     private long mConnectAttempted;
102 
103     // See mConnectAttempted
104     private static final long MAX_UUID_DELAY_FOR_AUTO_CONNECT = 5000;
105 
106     /** Auto-connect after pairing only if locally initiated. */
107     private boolean mConnectAfterPairing;
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     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     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     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     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     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         mConnectAfterPairing = true;  // auto-connect after pairing
305         return true;
306     }
307 
308     /**
309      * Return true if user initiated pairing on this device. The message text is
310      * slightly different for local vs. remote initiated pairing dialogs.
311      */
isUserInitiatedPairing()312     boolean isUserInitiatedPairing() {
313         return mConnectAfterPairing;
314     }
315 
unpair()316     void unpair() {
317         int state = getBondState();
318 
319         if (state == BluetoothDevice.BOND_BONDING) {
320             mDevice.cancelBondProcess();
321         }
322 
323         if (state != BluetoothDevice.BOND_NONE) {
324             final BluetoothDevice dev = mDevice;
325             if (dev != null) {
326                 final boolean successful = dev.removeBond();
327                 if (successful) {
328                     if (Utils.D) {
329                         Log.d(TAG, "Command sent successfully:REMOVE_BOND " + describe(null));
330                     }
331                 } else if (Utils.V) {
332                     Log.v(TAG, "Framework rejected command immediately:REMOVE_BOND " +
333                             describe(null));
334                 }
335             }
336         }
337     }
338 
getProfileConnectionState(LocalBluetoothProfile profile)339     int getProfileConnectionState(LocalBluetoothProfile profile) {
340         if (mProfileConnectionState == null ||
341                 mProfileConnectionState.get(profile) == null) {
342             // If cache is empty make the binder call to get the state
343             int state = profile.getConnectionStatus(mDevice);
344             mProfileConnectionState.put(profile, state);
345         }
346         return mProfileConnectionState.get(profile);
347     }
348 
clearProfileConnectionState()349     public void clearProfileConnectionState ()
350     {
351         if (Utils.D) {
352             Log.d(TAG," Clearing all connection state for dev:" + mDevice.getName());
353         }
354         for (LocalBluetoothProfile profile :getProfiles()) {
355             mProfileConnectionState.put(profile, BluetoothProfile.STATE_DISCONNECTED);
356         }
357     }
358 
359     // TODO: do any of these need to run async on a background thread?
fillData()360     private void fillData() {
361         fetchName();
362         fetchBtClass();
363         updateProfiles();
364         migratePhonebookPermissionChoice();
365         migrateMessagePermissionChoice();
366         fetchMessageRejectionCount();
367 
368         mVisible = false;
369         dispatchAttributesChanged();
370     }
371 
getDevice()372     BluetoothDevice getDevice() {
373         return mDevice;
374     }
375 
getName()376     String getName() {
377         return mName;
378     }
379 
380     /**
381      * Populate name from BluetoothDevice.ACTION_FOUND intent
382      */
setNewName(String name)383     void setNewName(String name) {
384         if (mName == null) {
385             mName = name;
386             if (mName == null || TextUtils.isEmpty(mName)) {
387                 mName = mDevice.getAddress();
388             }
389             dispatchAttributesChanged();
390         }
391     }
392 
393     /**
394      * user changes the device name
395      */
setName(String name)396     void setName(String name) {
397         if (!mName.equals(name)) {
398             mName = name;
399             mDevice.setAlias(name);
400             dispatchAttributesChanged();
401         }
402     }
403 
refreshName()404     void refreshName() {
405         fetchName();
406         dispatchAttributesChanged();
407     }
408 
fetchName()409     private void fetchName() {
410         mName = mDevice.getAliasName();
411 
412         if (TextUtils.isEmpty(mName)) {
413             mName = mDevice.getAddress();
414             if (DEBUG) Log.d(TAG, "Device has no name (yet), use address: " + mName);
415         }
416     }
417 
refresh()418     void refresh() {
419         dispatchAttributesChanged();
420     }
421 
isVisible()422     boolean isVisible() {
423         return mVisible;
424     }
425 
setVisible(boolean visible)426     void setVisible(boolean visible) {
427         if (mVisible != visible) {
428             mVisible = visible;
429             dispatchAttributesChanged();
430         }
431     }
432 
getBondState()433     int getBondState() {
434         return mDevice.getBondState();
435     }
436 
setRssi(short rssi)437     void setRssi(short rssi) {
438         if (mRssi != rssi) {
439             mRssi = rssi;
440             dispatchAttributesChanged();
441         }
442     }
443 
444     /**
445      * Checks whether we are connected to this device (any profile counts).
446      *
447      * @return Whether it is connected.
448      */
isConnected()449     boolean isConnected() {
450         for (LocalBluetoothProfile profile : mProfiles) {
451             int status = getProfileConnectionState(profile);
452             if (status == BluetoothProfile.STATE_CONNECTED) {
453                 return true;
454             }
455         }
456 
457         return false;
458     }
459 
isConnectedProfile(LocalBluetoothProfile profile)460     boolean isConnectedProfile(LocalBluetoothProfile profile) {
461         int status = getProfileConnectionState(profile);
462         return status == BluetoothProfile.STATE_CONNECTED;
463 
464     }
465 
isBusy()466     boolean isBusy() {
467         for (LocalBluetoothProfile profile : mProfiles) {
468             int status = getProfileConnectionState(profile);
469             if (status == BluetoothProfile.STATE_CONNECTING
470                     || status == BluetoothProfile.STATE_DISCONNECTING) {
471                 return true;
472             }
473         }
474         return getBondState() == BluetoothDevice.BOND_BONDING;
475     }
476 
477     /**
478      * Fetches a new value for the cached BT class.
479      */
fetchBtClass()480     private void fetchBtClass() {
481         mBtClass = mDevice.getBluetoothClass();
482     }
483 
updateProfiles()484     private boolean updateProfiles() {
485         ParcelUuid[] uuids = mDevice.getUuids();
486         if (uuids == null) return false;
487 
488         ParcelUuid[] localUuids = mLocalAdapter.getUuids();
489         if (localUuids == null) return false;
490 
491         /**
492          * Now we know if the device supports PBAP, update permissions...
493          */
494         processPhonebookAccess();
495 
496         mProfileManager.updateProfiles(uuids, localUuids, mProfiles, mRemovedProfiles,
497                                        mLocalNapRoleConnected, mDevice);
498 
499         if (DEBUG) {
500             Log.e(TAG, "updating profiles for " + mDevice.getAliasName());
501             BluetoothClass bluetoothClass = mDevice.getBluetoothClass();
502 
503             if (bluetoothClass != null) Log.v(TAG, "Class: " + bluetoothClass.toString());
504             Log.v(TAG, "UUID:");
505             for (ParcelUuid uuid : uuids) {
506                 Log.v(TAG, "  " + uuid);
507             }
508         }
509         return true;
510     }
511 
512     /**
513      * Refreshes the UI for the BT class, including fetching the latest value
514      * for the class.
515      */
refreshBtClass()516     void refreshBtClass() {
517         fetchBtClass();
518         dispatchAttributesChanged();
519     }
520 
521     /**
522      * Refreshes the UI when framework alerts us of a UUID change.
523      */
onUuidChanged()524     void onUuidChanged() {
525         updateProfiles();
526 
527         if (DEBUG) {
528             Log.e(TAG, "onUuidChanged: Time since last connect"
529                     + (SystemClock.elapsedRealtime() - mConnectAttempted));
530         }
531 
532         /*
533          * If a connect was attempted earlier without any UUID, we will do the
534          * connect now.
535          */
536         if (!mProfiles.isEmpty()
537                 && (mConnectAttempted + MAX_UUID_DELAY_FOR_AUTO_CONNECT) > SystemClock
538                         .elapsedRealtime()) {
539             connectWithoutResettingTimer(false);
540         }
541         dispatchAttributesChanged();
542     }
543 
onBondingStateChanged(int bondState)544     void onBondingStateChanged(int bondState) {
545         if (bondState == BluetoothDevice.BOND_NONE) {
546             mProfiles.clear();
547             mConnectAfterPairing = false;  // cancel auto-connect
548             setPhonebookPermissionChoice(ACCESS_UNKNOWN);
549             setMessagePermissionChoice(ACCESS_UNKNOWN);
550             mMessageRejectionCount = 0;
551             saveMessageRejectionCount();
552         }
553 
554         refresh();
555 
556         if (bondState == BluetoothDevice.BOND_BONDED) {
557             if (mDevice.isBluetoothDock()) {
558                 onBondingDockConnect();
559             } else if (mConnectAfterPairing) {
560                 connect(false);
561             }
562             mConnectAfterPairing = false;
563         }
564     }
565 
setBtClass(BluetoothClass btClass)566     void setBtClass(BluetoothClass btClass) {
567         if (btClass != null && mBtClass != btClass) {
568             mBtClass = btClass;
569             dispatchAttributesChanged();
570         }
571     }
572 
getBtClass()573     BluetoothClass getBtClass() {
574         return mBtClass;
575     }
576 
getProfiles()577     List<LocalBluetoothProfile> getProfiles() {
578         return Collections.unmodifiableList(mProfiles);
579     }
580 
getConnectableProfiles()581     List<LocalBluetoothProfile> getConnectableProfiles() {
582         List<LocalBluetoothProfile> connectableProfiles =
583                 new ArrayList<LocalBluetoothProfile>();
584         for (LocalBluetoothProfile profile : mProfiles) {
585             if (profile.isConnectable()) {
586                 connectableProfiles.add(profile);
587             }
588         }
589         return connectableProfiles;
590     }
591 
getRemovedProfiles()592     List<LocalBluetoothProfile> getRemovedProfiles() {
593         return mRemovedProfiles;
594     }
595 
registerCallback(Callback callback)596     void registerCallback(Callback callback) {
597         synchronized (mCallbacks) {
598             mCallbacks.add(callback);
599         }
600     }
601 
unregisterCallback(Callback callback)602     void unregisterCallback(Callback callback) {
603         synchronized (mCallbacks) {
604             mCallbacks.remove(callback);
605         }
606     }
607 
dispatchAttributesChanged()608     private void dispatchAttributesChanged() {
609         synchronized (mCallbacks) {
610             for (Callback callback : mCallbacks) {
611                 callback.onDeviceAttributesChanged();
612             }
613         }
614     }
615 
616     @Override
toString()617     public String toString() {
618         return mDevice.toString();
619     }
620 
621     @Override
equals(Object o)622     public boolean equals(Object o) {
623         if ((o == null) || !(o instanceof CachedBluetoothDevice)) {
624             return false;
625         }
626         return mDevice.equals(((CachedBluetoothDevice) o).mDevice);
627     }
628 
629     @Override
hashCode()630     public int hashCode() {
631         return mDevice.getAddress().hashCode();
632     }
633 
634     // This comparison uses non-final fields so the sort order may change
635     // when device attributes change (such as bonding state). Settings
636     // will completely refresh the device list when this happens.
compareTo(CachedBluetoothDevice another)637     public int compareTo(CachedBluetoothDevice another) {
638         // Connected above not connected
639         int comparison = (another.isConnected() ? 1 : 0) - (isConnected() ? 1 : 0);
640         if (comparison != 0) return comparison;
641 
642         // Paired above not paired
643         comparison = (another.getBondState() == BluetoothDevice.BOND_BONDED ? 1 : 0) -
644             (getBondState() == BluetoothDevice.BOND_BONDED ? 1 : 0);
645         if (comparison != 0) return comparison;
646 
647         // Visible above not visible
648         comparison = (another.mVisible ? 1 : 0) - (mVisible ? 1 : 0);
649         if (comparison != 0) return comparison;
650 
651         // Stronger signal above weaker signal
652         comparison = another.mRssi - mRssi;
653         if (comparison != 0) return comparison;
654 
655         // Fallback on name
656         return mName.compareTo(another.mName);
657     }
658 
659     public interface Callback {
onDeviceAttributesChanged()660         void onDeviceAttributesChanged();
661     }
662 
getPhonebookPermissionChoice()663     int getPhonebookPermissionChoice() {
664         int permission = mDevice.getPhonebookAccessPermission();
665         if (permission == BluetoothDevice.ACCESS_ALLOWED) {
666             return ACCESS_ALLOWED;
667         } else if (permission == BluetoothDevice.ACCESS_REJECTED) {
668             return ACCESS_REJECTED;
669         }
670         return ACCESS_UNKNOWN;
671     }
672 
setPhonebookPermissionChoice(int permissionChoice)673     void setPhonebookPermissionChoice(int permissionChoice) {
674         int permission = BluetoothDevice.ACCESS_UNKNOWN;
675         if (permissionChoice == ACCESS_ALLOWED) {
676             permission = BluetoothDevice.ACCESS_ALLOWED;
677         } else if (permissionChoice == ACCESS_REJECTED) {
678             permission = BluetoothDevice.ACCESS_REJECTED;
679         }
680         mDevice.setPhonebookAccessPermission(permission);
681     }
682 
683     // Migrates data from old data store (in Settings app's shared preferences) to new (in Bluetooth
684     // app's shared preferences).
migratePhonebookPermissionChoice()685     private void migratePhonebookPermissionChoice() {
686         SharedPreferences preferences = mContext.getSharedPreferences(
687                 "bluetooth_phonebook_permission", Context.MODE_PRIVATE);
688         if (!preferences.contains(mDevice.getAddress())) {
689             return;
690         }
691 
692         if (mDevice.getPhonebookAccessPermission() == BluetoothDevice.ACCESS_UNKNOWN) {
693             int oldPermission = preferences.getInt(mDevice.getAddress(), ACCESS_UNKNOWN);
694             if (oldPermission == ACCESS_ALLOWED) {
695                 mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_ALLOWED);
696             } else if (oldPermission == ACCESS_REJECTED) {
697                 mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_REJECTED);
698             }
699         }
700 
701         SharedPreferences.Editor editor = preferences.edit();
702         editor.remove(mDevice.getAddress());
703         editor.commit();
704     }
705 
getMessagePermissionChoice()706     int getMessagePermissionChoice() {
707         int permission = mDevice.getMessageAccessPermission();
708         if (permission == BluetoothDevice.ACCESS_ALLOWED) {
709             return ACCESS_ALLOWED;
710         } else if (permission == BluetoothDevice.ACCESS_REJECTED) {
711             return ACCESS_REJECTED;
712         }
713         return ACCESS_UNKNOWN;
714     }
715 
setMessagePermissionChoice(int permissionChoice)716     void setMessagePermissionChoice(int permissionChoice) {
717         int permission = BluetoothDevice.ACCESS_UNKNOWN;
718         if (permissionChoice == ACCESS_ALLOWED) {
719             permission = BluetoothDevice.ACCESS_ALLOWED;
720         } else if (permissionChoice == ACCESS_REJECTED) {
721             permission = BluetoothDevice.ACCESS_REJECTED;
722         }
723         mDevice.setMessageAccessPermission(permission);
724     }
725 
726     // Migrates data from old data store (in Settings app's shared preferences) to new (in Bluetooth
727     // app's shared preferences).
migrateMessagePermissionChoice()728     private void migrateMessagePermissionChoice() {
729         SharedPreferences preferences = mContext.getSharedPreferences(
730                 "bluetooth_message_permission", Context.MODE_PRIVATE);
731         if (!preferences.contains(mDevice.getAddress())) {
732             return;
733         }
734 
735         if (mDevice.getMessageAccessPermission() == BluetoothDevice.ACCESS_UNKNOWN) {
736             int oldPermission = preferences.getInt(mDevice.getAddress(), ACCESS_UNKNOWN);
737             if (oldPermission == ACCESS_ALLOWED) {
738                 mDevice.setMessageAccessPermission(BluetoothDevice.ACCESS_ALLOWED);
739             } else if (oldPermission == ACCESS_REJECTED) {
740                 mDevice.setMessageAccessPermission(BluetoothDevice.ACCESS_REJECTED);
741             }
742         }
743 
744         SharedPreferences.Editor editor = preferences.edit();
745         editor.remove(mDevice.getAddress());
746         editor.commit();
747     }
748 
749     /**
750      * @return Whether this rejection should persist.
751      */
checkAndIncreaseMessageRejectionCount()752     boolean checkAndIncreaseMessageRejectionCount() {
753         if (mMessageRejectionCount < MESSAGE_REJECTION_COUNT_LIMIT_TO_PERSIST) {
754             mMessageRejectionCount++;
755             saveMessageRejectionCount();
756         }
757         return mMessageRejectionCount >= MESSAGE_REJECTION_COUNT_LIMIT_TO_PERSIST;
758     }
759 
fetchMessageRejectionCount()760     private void fetchMessageRejectionCount() {
761         SharedPreferences preference = mContext.getSharedPreferences(
762                 MESSAGE_REJECTION_COUNT_PREFS_NAME, Context.MODE_PRIVATE);
763         mMessageRejectionCount = preference.getInt(mDevice.getAddress(), 0);
764     }
765 
saveMessageRejectionCount()766     private void saveMessageRejectionCount() {
767         SharedPreferences.Editor editor = mContext.getSharedPreferences(
768                 MESSAGE_REJECTION_COUNT_PREFS_NAME, Context.MODE_PRIVATE).edit();
769         if (mMessageRejectionCount == 0) {
770             editor.remove(mDevice.getAddress());
771         } else {
772             editor.putInt(mDevice.getAddress(), mMessageRejectionCount);
773         }
774         editor.commit();
775     }
776 
processPhonebookAccess()777     private void processPhonebookAccess() {
778         if (mDevice.getBondState() != BluetoothDevice.BOND_BONDED) return;
779 
780         ParcelUuid[] uuids = mDevice.getUuids();
781         if (BluetoothUuid.containsAnyUuid(uuids, PbapServerProfile.PBAB_CLIENT_UUIDS)) {
782             // The pairing dialog now warns of phone-book access for paired devices.
783             // No separate prompt is displayed after pairing.
784             setPhonebookPermissionChoice(CachedBluetoothDevice.ACCESS_ALLOWED);
785         }
786     }
787 }
788