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
17package com.android.settings.bluetooth;
18
19import android.bluetooth.BluetoothClass;
20import android.bluetooth.BluetoothDevice;
21import android.bluetooth.BluetoothProfile;
22import android.content.Context;
23import android.content.SharedPreferences;
24import android.os.ParcelUuid;
25import android.os.SystemClock;
26import android.text.TextUtils;
27import android.util.Log;
28import android.bluetooth.BluetoothAdapter;
29
30import java.util.ArrayList;
31import java.util.Collection;
32import java.util.Collections;
33import java.util.HashMap;
34import 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 */
42final 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     */
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
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
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
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
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
194    void connect(boolean connectAllProfiles) {
195        if (!ensurePaired()) {
196            return;
197        }
198
199        mConnectAttempted = SystemClock.elapsedRealtime();
200        connectWithoutResettingTimer(connectAllProfiles);
201    }
202
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
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
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     */
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
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
284    private boolean ensurePaired() {
285        if (getBondState() == BluetoothDevice.BOND_NONE) {
286            startPairing();
287            return false;
288        } else {
289            return true;
290        }
291    }
292
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     */
311    boolean isUserInitiatedPairing() {
312        return mConnectAfterPairing;
313    }
314
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
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
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?
359    private void fillData() {
360        fetchName();
361        fetchBtClass();
362        updateProfiles();
363        migratePhonebookPermissionChoice();
364        migrateMessagePermissionChoice();
365        fetchMessageRejectionCount();
366
367        mVisible = false;
368        dispatchAttributesChanged();
369    }
370
371    BluetoothDevice getDevice() {
372        return mDevice;
373    }
374
375    String getName() {
376        return mName;
377    }
378
379    /**
380     * Populate name from BluetoothDevice.ACTION_FOUND intent
381     */
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     */
395    void setName(String name) {
396        if (!mName.equals(name)) {
397            mName = name;
398            mDevice.setAlias(name);
399            dispatchAttributesChanged();
400        }
401    }
402
403    void refreshName() {
404        fetchName();
405        dispatchAttributesChanged();
406    }
407
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
417    void refresh() {
418        dispatchAttributesChanged();
419    }
420
421    boolean isVisible() {
422        return mVisible;
423    }
424
425    void setVisible(boolean visible) {
426        if (mVisible != visible) {
427            mVisible = visible;
428            dispatchAttributesChanged();
429        }
430    }
431
432    int getBondState() {
433        return mDevice.getBondState();
434    }
435
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     */
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
459    boolean isConnectedProfile(LocalBluetoothProfile profile) {
460        int status = getProfileConnectionState(profile);
461        return status == BluetoothProfile.STATE_CONNECTED;
462
463    }
464
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     */
479    private void fetchBtClass() {
480        mBtClass = mDevice.getBluetoothClass();
481    }
482
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     */
510    void refreshBtClass() {
511        fetchBtClass();
512        dispatchAttributesChanged();
513    }
514
515    /**
516     * Refreshes the UI when framework alerts us of a UUID change.
517     */
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
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
560    void setBtClass(BluetoothClass btClass) {
561        if (btClass != null && mBtClass != btClass) {
562            mBtClass = btClass;
563            dispatchAttributesChanged();
564        }
565    }
566
567    BluetoothClass getBtClass() {
568        return mBtClass;
569    }
570
571    List<LocalBluetoothProfile> getProfiles() {
572        return Collections.unmodifiableList(mProfiles);
573    }
574
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
586    List<LocalBluetoothProfile> getRemovedProfiles() {
587        return mRemovedProfiles;
588    }
589
590    void registerCallback(Callback callback) {
591        synchronized (mCallbacks) {
592            mCallbacks.add(callback);
593        }
594    }
595
596    void unregisterCallback(Callback callback) {
597        synchronized (mCallbacks) {
598            mCallbacks.remove(callback);
599        }
600    }
601
602    private void dispatchAttributesChanged() {
603        synchronized (mCallbacks) {
604            for (Callback callback : mCallbacks) {
605                callback.onDeviceAttributesChanged();
606            }
607        }
608    }
609
610    @Override
611    public String toString() {
612        return mDevice.toString();
613    }
614
615    @Override
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
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.
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 {
654        void onDeviceAttributesChanged();
655    }
656
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
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).
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
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
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).
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     */
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
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
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