• 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.settings.bluetooth;
18 
19 import android.bluetooth.BluetoothClass;
20 import android.bluetooth.BluetoothDevice;
21 import android.bluetooth.BluetoothProfile;
22 import android.content.Context;
23 import android.content.SharedPreferences;
24 import android.os.ParcelUuid;
25 import android.os.SystemClock;
26 import android.text.TextUtils;
27 import android.util.Log;
28 import android.bluetooth.BluetoothAdapter;
29 
30 import java.util.ArrayList;
31 import java.util.Collection;
32 import java.util.Collections;
33 import java.util.HashMap;
34 import java.util.List;
35 
36 /**
37  * CachedBluetoothDevice represents a remote Bluetooth device. It contains
38  * attributes of the device (such as the address, name, RSSI, etc.) and
39  * functionality that can be performed on the device (connect, pair, disconnect,
40  * etc.).
41  */
42 final class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> {
43     private static final String TAG = "CachedBluetoothDevice";
44     private static final boolean DEBUG = Utils.V;
45 
46     private final Context mContext;
47     private final LocalBluetoothAdapter mLocalAdapter;
48     private final LocalBluetoothProfileManager mProfileManager;
49     private final BluetoothDevice mDevice;
50     private String mName;
51     private short mRssi;
52     private BluetoothClass mBtClass;
53     private HashMap<LocalBluetoothProfile, Integer> mProfileConnectionState;
54 
55     private final List<LocalBluetoothProfile> mProfiles =
56             new ArrayList<LocalBluetoothProfile>();
57 
58     // List of profiles that were previously in mProfiles, but have been removed
59     private final List<LocalBluetoothProfile> mRemovedProfiles =
60             new ArrayList<LocalBluetoothProfile>();
61 
62     // Device supports PANU but not NAP: remove PanProfile after device disconnects from NAP
63     private boolean mLocalNapRoleConnected;
64 
65     private boolean mVisible;
66 
67     private int mPhonebookPermissionChoice;
68 
69     private int mMessagePermissionChoice;
70 
71     private int mMessageRejectionCount;
72 
73     private final Collection<Callback> mCallbacks = new ArrayList<Callback>();
74 
75     // Following constants indicate the user's choices of Phone book/message access settings
76     // User hasn't made any choice or settings app has wiped out the memory
77     public final static int ACCESS_UNKNOWN = 0;
78     // User has accepted the connection and let Settings app remember the decision
79     public final static int ACCESS_ALLOWED = 1;
80     // User has rejected the connection and let Settings app remember the decision
81     public final static int ACCESS_REJECTED = 2;
82 
83     // How many times user should reject the connection to make the choice persist.
84     private final static int MESSAGE_REJECTION_COUNT_LIMIT_TO_PERSIST = 2;
85 
86     private final static String MESSAGE_REJECTION_COUNT_PREFS_NAME = "bluetooth_message_reject";
87 
88     /**
89      * When we connect to multiple profiles, we only want to display a single
90      * error even if they all fail. This tracks that state.
91      */
92     private boolean mIsConnectingErrorPossible;
93 
94     /**
95      * Last time a bt profile auto-connect was attempted.
96      * If an ACTION_UUID intent comes in within
97      * MAX_UUID_DELAY_FOR_AUTO_CONNECT milliseconds, we will try auto-connect
98      * again with the new UUIDs
99      */
100     private long mConnectAttempted;
101 
102     // See mConnectAttempted
103     private static final long MAX_UUID_DELAY_FOR_AUTO_CONNECT = 5000;
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     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     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     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     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     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     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     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     BluetoothDevice getDevice() {
372         return mDevice;
373     }
374 
getName()375     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     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     boolean isVisible() {
422         return mVisible;
423     }
424 
setVisible(boolean visible)425     void setVisible(boolean visible) {
426         if (mVisible != visible) {
427             mVisible = visible;
428             dispatchAttributesChanged();
429         }
430     }
431 
getBondState()432     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     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     boolean isConnectedProfile(LocalBluetoothProfile profile) {
460         int status = getProfileConnectionState(profile);
461         return status == BluetoothProfile.STATE_CONNECTED;
462 
463     }
464 
isBusy()465     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         mProfileManager.updateProfiles(uuids, localUuids, mProfiles, mRemovedProfiles,
491                                        mLocalNapRoleConnected, mDevice);
492 
493         if (DEBUG) {
494             Log.e(TAG, "updating profiles for " + mDevice.getAliasName());
495             BluetoothClass bluetoothClass = mDevice.getBluetoothClass();
496 
497             if (bluetoothClass != null) Log.v(TAG, "Class: " + bluetoothClass.toString());
498             Log.v(TAG, "UUID:");
499             for (ParcelUuid uuid : uuids) {
500                 Log.v(TAG, "  " + uuid);
501             }
502         }
503         return true;
504     }
505 
506     /**
507      * Refreshes the UI for the BT class, including fetching the latest value
508      * for the class.
509      */
refreshBtClass()510     void refreshBtClass() {
511         fetchBtClass();
512         dispatchAttributesChanged();
513     }
514 
515     /**
516      * Refreshes the UI when framework alerts us of a UUID change.
517      */
onUuidChanged()518     void onUuidChanged() {
519         updateProfiles();
520 
521         if (DEBUG) {
522             Log.e(TAG, "onUuidChanged: Time since last connect"
523                     + (SystemClock.elapsedRealtime() - mConnectAttempted));
524         }
525 
526         /*
527          * If a connect was attempted earlier without any UUID, we will do the
528          * connect now.
529          */
530         if (!mProfiles.isEmpty()
531                 && (mConnectAttempted + MAX_UUID_DELAY_FOR_AUTO_CONNECT) > SystemClock
532                         .elapsedRealtime()) {
533             connectWithoutResettingTimer(false);
534         }
535         dispatchAttributesChanged();
536     }
537 
onBondingStateChanged(int bondState)538     void onBondingStateChanged(int bondState) {
539         if (bondState == BluetoothDevice.BOND_NONE) {
540             mProfiles.clear();
541             mConnectAfterPairing = false;  // cancel auto-connect
542             setPhonebookPermissionChoice(ACCESS_UNKNOWN);
543             setMessagePermissionChoice(ACCESS_UNKNOWN);
544             mMessageRejectionCount = 0;
545             saveMessageRejectionCount();
546         }
547 
548         refresh();
549 
550         if (bondState == BluetoothDevice.BOND_BONDED) {
551             if (mDevice.isBluetoothDock()) {
552                 onBondingDockConnect();
553             } else if (mConnectAfterPairing) {
554                 connect(false);
555             }
556             mConnectAfterPairing = false;
557         }
558     }
559 
setBtClass(BluetoothClass btClass)560     void setBtClass(BluetoothClass btClass) {
561         if (btClass != null && mBtClass != btClass) {
562             mBtClass = btClass;
563             dispatchAttributesChanged();
564         }
565     }
566 
getBtClass()567     BluetoothClass getBtClass() {
568         return mBtClass;
569     }
570 
getProfiles()571     List<LocalBluetoothProfile> getProfiles() {
572         return Collections.unmodifiableList(mProfiles);
573     }
574 
getConnectableProfiles()575     List<LocalBluetoothProfile> getConnectableProfiles() {
576         List<LocalBluetoothProfile> connectableProfiles =
577                 new ArrayList<LocalBluetoothProfile>();
578         for (LocalBluetoothProfile profile : mProfiles) {
579             if (profile.isConnectable()) {
580                 connectableProfiles.add(profile);
581             }
582         }
583         return connectableProfiles;
584     }
585 
getRemovedProfiles()586     List<LocalBluetoothProfile> getRemovedProfiles() {
587         return mRemovedProfiles;
588     }
589 
registerCallback(Callback callback)590     void registerCallback(Callback callback) {
591         synchronized (mCallbacks) {
592             mCallbacks.add(callback);
593         }
594     }
595 
unregisterCallback(Callback callback)596     void unregisterCallback(Callback callback) {
597         synchronized (mCallbacks) {
598             mCallbacks.remove(callback);
599         }
600     }
601 
dispatchAttributesChanged()602     private void dispatchAttributesChanged() {
603         synchronized (mCallbacks) {
604             for (Callback callback : mCallbacks) {
605                 callback.onDeviceAttributesChanged();
606             }
607         }
608     }
609 
610     @Override
toString()611     public String toString() {
612         return mDevice.toString();
613     }
614 
615     @Override
equals(Object o)616     public boolean equals(Object o) {
617         if ((o == null) || !(o instanceof CachedBluetoothDevice)) {
618             return false;
619         }
620         return mDevice.equals(((CachedBluetoothDevice) o).mDevice);
621     }
622 
623     @Override
hashCode()624     public int hashCode() {
625         return mDevice.getAddress().hashCode();
626     }
627 
628     // This comparison uses non-final fields so the sort order may change
629     // when device attributes change (such as bonding state). Settings
630     // will completely refresh the device list when this happens.
compareTo(CachedBluetoothDevice another)631     public int compareTo(CachedBluetoothDevice another) {
632         // Connected above not connected
633         int comparison = (another.isConnected() ? 1 : 0) - (isConnected() ? 1 : 0);
634         if (comparison != 0) return comparison;
635 
636         // Paired above not paired
637         comparison = (another.getBondState() == BluetoothDevice.BOND_BONDED ? 1 : 0) -
638             (getBondState() == BluetoothDevice.BOND_BONDED ? 1 : 0);
639         if (comparison != 0) return comparison;
640 
641         // Visible above not visible
642         comparison = (another.mVisible ? 1 : 0) - (mVisible ? 1 : 0);
643         if (comparison != 0) return comparison;
644 
645         // Stronger signal above weaker signal
646         comparison = another.mRssi - mRssi;
647         if (comparison != 0) return comparison;
648 
649         // Fallback on name
650         return mName.compareTo(another.mName);
651     }
652 
653     public interface Callback {
onDeviceAttributesChanged()654         void onDeviceAttributesChanged();
655     }
656 
getPhonebookPermissionChoice()657     int getPhonebookPermissionChoice() {
658         int permission = mDevice.getPhonebookAccessPermission();
659         if (permission == BluetoothDevice.ACCESS_ALLOWED) {
660             return ACCESS_ALLOWED;
661         } else if (permission == BluetoothDevice.ACCESS_REJECTED) {
662             return ACCESS_REJECTED;
663         }
664         return ACCESS_UNKNOWN;
665     }
666 
setPhonebookPermissionChoice(int permissionChoice)667     void setPhonebookPermissionChoice(int permissionChoice) {
668         int permission = BluetoothDevice.ACCESS_UNKNOWN;
669         if (permissionChoice == ACCESS_ALLOWED) {
670             permission = BluetoothDevice.ACCESS_ALLOWED;
671         } else if (permissionChoice == ACCESS_REJECTED) {
672             permission = BluetoothDevice.ACCESS_REJECTED;
673         }
674         mDevice.setPhonebookAccessPermission(permission);
675     }
676 
677     // Migrates data from old data store (in Settings app's shared preferences) to new (in Bluetooth
678     // app's shared preferences).
migratePhonebookPermissionChoice()679     private void migratePhonebookPermissionChoice() {
680         SharedPreferences preferences = mContext.getSharedPreferences(
681                 "bluetooth_phonebook_permission", Context.MODE_PRIVATE);
682         if (!preferences.contains(mDevice.getAddress())) {
683             return;
684         }
685 
686         if (mDevice.getPhonebookAccessPermission() == BluetoothDevice.ACCESS_UNKNOWN) {
687             int oldPermission = preferences.getInt(mDevice.getAddress(), ACCESS_UNKNOWN);
688             if (oldPermission == ACCESS_ALLOWED) {
689                 mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_ALLOWED);
690             } else if (oldPermission == ACCESS_REJECTED) {
691                 mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_REJECTED);
692             }
693         }
694 
695         SharedPreferences.Editor editor = preferences.edit();
696         editor.remove(mDevice.getAddress());
697         editor.commit();
698     }
699 
getMessagePermissionChoice()700     int getMessagePermissionChoice() {
701         int permission = mDevice.getMessageAccessPermission();
702         if (permission == BluetoothDevice.ACCESS_ALLOWED) {
703             return ACCESS_ALLOWED;
704         } else if (permission == BluetoothDevice.ACCESS_REJECTED) {
705             return ACCESS_REJECTED;
706         }
707         return ACCESS_UNKNOWN;
708     }
709 
setMessagePermissionChoice(int permissionChoice)710     void setMessagePermissionChoice(int permissionChoice) {
711         int permission = BluetoothDevice.ACCESS_UNKNOWN;
712         if (permissionChoice == ACCESS_ALLOWED) {
713             permission = BluetoothDevice.ACCESS_ALLOWED;
714         } else if (permissionChoice == ACCESS_REJECTED) {
715             permission = BluetoothDevice.ACCESS_REJECTED;
716         }
717         mDevice.setMessageAccessPermission(permission);
718     }
719 
720     // Migrates data from old data store (in Settings app's shared preferences) to new (in Bluetooth
721     // app's shared preferences).
migrateMessagePermissionChoice()722     private void migrateMessagePermissionChoice() {
723         SharedPreferences preferences = mContext.getSharedPreferences(
724                 "bluetooth_message_permission", Context.MODE_PRIVATE);
725         if (!preferences.contains(mDevice.getAddress())) {
726             return;
727         }
728 
729         if (mDevice.getMessageAccessPermission() == BluetoothDevice.ACCESS_UNKNOWN) {
730             int oldPermission = preferences.getInt(mDevice.getAddress(), ACCESS_UNKNOWN);
731             if (oldPermission == ACCESS_ALLOWED) {
732                 mDevice.setMessageAccessPermission(BluetoothDevice.ACCESS_ALLOWED);
733             } else if (oldPermission == ACCESS_REJECTED) {
734                 mDevice.setMessageAccessPermission(BluetoothDevice.ACCESS_REJECTED);
735             }
736         }
737 
738         SharedPreferences.Editor editor = preferences.edit();
739         editor.remove(mDevice.getAddress());
740         editor.commit();
741     }
742 
743     /**
744      * @return Whether this rejection should persist.
745      */
checkAndIncreaseMessageRejectionCount()746     boolean checkAndIncreaseMessageRejectionCount() {
747         if (mMessageRejectionCount < MESSAGE_REJECTION_COUNT_LIMIT_TO_PERSIST) {
748             mMessageRejectionCount++;
749             saveMessageRejectionCount();
750         }
751         return mMessageRejectionCount >= MESSAGE_REJECTION_COUNT_LIMIT_TO_PERSIST;
752     }
753 
fetchMessageRejectionCount()754     private void fetchMessageRejectionCount() {
755         SharedPreferences preference = mContext.getSharedPreferences(
756                 MESSAGE_REJECTION_COUNT_PREFS_NAME, Context.MODE_PRIVATE);
757         mMessageRejectionCount = preference.getInt(mDevice.getAddress(), 0);
758     }
759 
saveMessageRejectionCount()760     private void saveMessageRejectionCount() {
761         SharedPreferences.Editor editor = mContext.getSharedPreferences(
762                 MESSAGE_REJECTION_COUNT_PREFS_NAME, Context.MODE_PRIVATE).edit();
763         if (mMessageRejectionCount == 0) {
764             editor.remove(mDevice.getAddress());
765         } else {
766             editor.putInt(mDevice.getAddress(), mMessageRejectionCount);
767         }
768         editor.commit();
769     }
770 }
771