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 mPhonebookRejectedTimes;
72
73    private int mMessageRejectedTimes;
74
75    private final Collection<Callback> mCallbacks = new ArrayList<Callback>();
76
77    // Following constants indicate the user's choices of Phone book/message access settings
78    // User hasn't made any choice or settings app has wiped out the memory
79    final static int ACCESS_UNKNOWN = 0;
80    // User has accepted the connection and let Settings app remember the decision
81    final static int ACCESS_ALLOWED = 1;
82    // User has rejected the connection and let Settings app remember the decision
83    final static int ACCESS_REJECTED = 2;
84
85    // how many times did User reject the connection to make the rejected persist.
86    final static int PERSIST_REJECTED_TIMES_LIMIT = 2;
87
88    private final static String PHONEBOOK_PREFS_NAME = "bluetooth_phonebook_permission";
89    private final static String MESSAGE_PREFS_NAME = "bluetooth_message_permission";
90    private final static String PHONEBOOK_REJECT_TIMES = "bluetooth_phonebook_reject";
91    private final static String MESSAGE_REJECT_TIMES = "bluetooth_message_reject";
92
93    /**
94     * When we connect to multiple profiles, we only want to display a single
95     * error even if they all fail. This tracks that state.
96     */
97    private boolean mIsConnectingErrorPossible;
98
99    /**
100     * Last time a bt profile auto-connect was attempted.
101     * If an ACTION_UUID intent comes in within
102     * MAX_UUID_DELAY_FOR_AUTO_CONNECT milliseconds, we will try auto-connect
103     * again with the new UUIDs
104     */
105    private long mConnectAttempted;
106
107    // See mConnectAttempted
108    private static final long MAX_UUID_DELAY_FOR_AUTO_CONNECT = 5000;
109
110    /** Auto-connect after pairing only if locally initiated. */
111    private boolean mConnectAfterPairing;
112
113    /**
114     * Describes the current device and profile for logging.
115     *
116     * @param profile Profile to describe
117     * @return Description of the device and profile
118     */
119    private String describe(LocalBluetoothProfile profile) {
120        StringBuilder sb = new StringBuilder();
121        sb.append("Address:").append(mDevice);
122        if (profile != null) {
123            sb.append(" Profile:").append(profile);
124        }
125
126        return sb.toString();
127    }
128
129    void onProfileStateChanged(LocalBluetoothProfile profile, int newProfileState) {
130        if (Utils.D) {
131            Log.d(TAG, "onProfileStateChanged: profile " + profile +
132                    " newProfileState " + newProfileState);
133        }
134        if (mLocalAdapter.getBluetoothState() == BluetoothAdapter.STATE_TURNING_OFF)
135        {
136            if (Utils.D) Log.d(TAG, " BT Turninig Off...Profile conn state change ignored...");
137            return;
138        }
139        mProfileConnectionState.put(profile, newProfileState);
140        if (newProfileState == BluetoothProfile.STATE_CONNECTED) {
141            if (!mProfiles.contains(profile)) {
142                mRemovedProfiles.remove(profile);
143                mProfiles.add(profile);
144                if (profile instanceof PanProfile &&
145                        ((PanProfile) profile).isLocalRoleNap(mDevice)) {
146                    // Device doesn't support NAP, so remove PanProfile on disconnect
147                    mLocalNapRoleConnected = true;
148                }
149            }
150            if (profile instanceof MapProfile) {
151                profile.setPreferred(mDevice, true);
152            }
153        } else if (profile instanceof MapProfile &&
154                newProfileState == BluetoothProfile.STATE_DISCONNECTED) {
155            if (mProfiles.contains(profile)) {
156                mRemovedProfiles.add(profile);
157                mProfiles.remove(profile);
158            }
159            profile.setPreferred(mDevice, false);
160        } else if (mLocalNapRoleConnected && profile instanceof PanProfile &&
161                ((PanProfile) profile).isLocalRoleNap(mDevice) &&
162                newProfileState == BluetoothProfile.STATE_DISCONNECTED) {
163            Log.d(TAG, "Removing PanProfile from device after NAP disconnect");
164            mProfiles.remove(profile);
165            mRemovedProfiles.add(profile);
166            mLocalNapRoleConnected = false;
167        }
168    }
169
170    CachedBluetoothDevice(Context context,
171                          LocalBluetoothAdapter adapter,
172                          LocalBluetoothProfileManager profileManager,
173                          BluetoothDevice device) {
174        mContext = context;
175        mLocalAdapter = adapter;
176        mProfileManager = profileManager;
177        mDevice = device;
178        mProfileConnectionState = new HashMap<LocalBluetoothProfile, Integer>();
179        fillData();
180    }
181
182    void disconnect() {
183        for (LocalBluetoothProfile profile : mProfiles) {
184            disconnect(profile);
185        }
186        // Disconnect  PBAP server in case its connected
187        // This is to ensure all the profiles are disconnected as some CK/Hs do not
188        // disconnect  PBAP connection when HF connection is brought down
189        PbapServerProfile PbapProfile = mProfileManager.getPbapProfile();
190        if (PbapProfile.getConnectionStatus(mDevice) == BluetoothProfile.STATE_CONNECTED)
191        {
192            PbapProfile.disconnect(mDevice);
193        }
194    }
195
196    void disconnect(LocalBluetoothProfile profile) {
197        if (profile.disconnect(mDevice)) {
198            if (Utils.D) {
199                Log.d(TAG, "Command sent successfully:DISCONNECT " + describe(profile));
200            }
201        }
202    }
203
204    void connect(boolean connectAllProfiles) {
205        if (!ensurePaired()) {
206            return;
207        }
208
209        mConnectAttempted = SystemClock.elapsedRealtime();
210        connectWithoutResettingTimer(connectAllProfiles);
211    }
212
213    void onBondingDockConnect() {
214        // Attempt to connect if UUIDs are available. Otherwise,
215        // we will connect when the ACTION_UUID intent arrives.
216        connect(false);
217    }
218
219    private void connectWithoutResettingTimer(boolean connectAllProfiles) {
220        // Try to initialize the profiles if they were not.
221        if (mProfiles.isEmpty()) {
222            // if mProfiles is empty, then do not invoke updateProfiles. This causes a race
223            // condition with carkits during pairing, wherein RemoteDevice.UUIDs have been updated
224            // from bluetooth stack but ACTION.uuid is not sent yet.
225            // Eventually ACTION.uuid will be received which shall trigger the connection of the
226            // various profiles
227            // If UUIDs are not available yet, connect will be happen
228            // upon arrival of the ACTION_UUID intent.
229            Log.d(TAG, "No profiles. Maybe we will connect later");
230            return;
231        }
232
233        // Reset the only-show-one-error-dialog tracking variable
234        mIsConnectingErrorPossible = true;
235
236        int preferredProfiles = 0;
237        for (LocalBluetoothProfile profile : mProfiles) {
238            if (connectAllProfiles ? profile.isConnectable() : profile.isAutoConnectable()) {
239                if (profile.isPreferred(mDevice)) {
240                    ++preferredProfiles;
241                    connectInt(profile);
242                }
243            }
244        }
245        if (DEBUG) Log.d(TAG, "Preferred profiles = " + preferredProfiles);
246
247        if (preferredProfiles == 0) {
248            connectAutoConnectableProfiles();
249        }
250    }
251
252    private void connectAutoConnectableProfiles() {
253        if (!ensurePaired()) {
254            return;
255        }
256        // Reset the only-show-one-error-dialog tracking variable
257        mIsConnectingErrorPossible = true;
258
259        for (LocalBluetoothProfile profile : mProfiles) {
260            if (profile.isAutoConnectable()) {
261                profile.setPreferred(mDevice, true);
262                connectInt(profile);
263            }
264        }
265    }
266
267    /**
268     * Connect this device to the specified profile.
269     *
270     * @param profile the profile to use with the remote device
271     */
272    void connectProfile(LocalBluetoothProfile profile) {
273        mConnectAttempted = SystemClock.elapsedRealtime();
274        // Reset the only-show-one-error-dialog tracking variable
275        mIsConnectingErrorPossible = true;
276        connectInt(profile);
277        // Refresh the UI based on profile.connect() call
278        refresh();
279    }
280
281    synchronized void connectInt(LocalBluetoothProfile profile) {
282        if (!ensurePaired()) {
283            return;
284        }
285        if (profile.connect(mDevice)) {
286            if (Utils.D) {
287                Log.d(TAG, "Command sent successfully:CONNECT " + describe(profile));
288            }
289            return;
290        }
291        Log.i(TAG, "Failed to connect " + profile.toString() + " to " + mName);
292    }
293
294    private boolean ensurePaired() {
295        if (getBondState() == BluetoothDevice.BOND_NONE) {
296            startPairing();
297            return false;
298        } else {
299            return true;
300        }
301    }
302
303    boolean startPairing() {
304        // Pairing is unreliable while scanning, so cancel discovery
305        if (mLocalAdapter.isDiscovering()) {
306            mLocalAdapter.cancelDiscovery();
307        }
308
309        if (!mDevice.createBond()) {
310            return false;
311        }
312
313        mConnectAfterPairing = true;  // auto-connect after pairing
314        return true;
315    }
316
317    /**
318     * Return true if user initiated pairing on this device. The message text is
319     * slightly different for local vs. remote initiated pairing dialogs.
320     */
321    boolean isUserInitiatedPairing() {
322        return mConnectAfterPairing;
323    }
324
325    void unpair() {
326        int state = getBondState();
327
328        if (state == BluetoothDevice.BOND_BONDING) {
329            mDevice.cancelBondProcess();
330        }
331
332        if (state != BluetoothDevice.BOND_NONE) {
333            final BluetoothDevice dev = mDevice;
334            if (dev != null) {
335                final boolean successful = dev.removeBond();
336                if (successful) {
337                    if (Utils.D) {
338                        Log.d(TAG, "Command sent successfully:REMOVE_BOND " + describe(null));
339                    }
340                } else if (Utils.V) {
341                    Log.v(TAG, "Framework rejected command immediately:REMOVE_BOND " +
342                            describe(null));
343                }
344            }
345        }
346    }
347
348    int getProfileConnectionState(LocalBluetoothProfile profile) {
349        if (mProfileConnectionState == null ||
350                mProfileConnectionState.get(profile) == null) {
351            // If cache is empty make the binder call to get the state
352            int state = profile.getConnectionStatus(mDevice);
353            mProfileConnectionState.put(profile, state);
354        }
355        return mProfileConnectionState.get(profile);
356    }
357
358    public void clearProfileConnectionState ()
359    {
360        if (Utils.D) {
361            Log.d(TAG," Clearing all connection state for dev:" + mDevice.getName());
362        }
363        for (LocalBluetoothProfile profile :getProfiles()) {
364            mProfileConnectionState.put(profile, BluetoothProfile.STATE_DISCONNECTED);
365        }
366    }
367
368    // TODO: do any of these need to run async on a background thread?
369    private void fillData() {
370        fetchName();
371        fetchBtClass();
372        updateProfiles();
373        fetchPhonebookPermissionChoice();
374        fetchMessagePermissionChoice();
375        fetchPhonebookRejectTimes();
376        fetchMessageRejectTimes();
377
378        mVisible = false;
379        dispatchAttributesChanged();
380    }
381
382    BluetoothDevice getDevice() {
383        return mDevice;
384    }
385
386    String getName() {
387        return mName;
388    }
389
390    void setName(String name) {
391        if (!mName.equals(name)) {
392            if (TextUtils.isEmpty(name)) {
393                // TODO: use friendly name for unknown device (bug 1181856)
394                mName = mDevice.getAddress();
395            } else {
396                mName = name;
397                mDevice.setAlias(name);
398            }
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            mPhonebookRejectedTimes = 0;
545            savePhonebookRejectTimes();
546            mMessageRejectedTimes = 0;
547            saveMessageRejectTimes();
548        }
549
550        refresh();
551
552        if (bondState == BluetoothDevice.BOND_BONDED) {
553            if (mDevice.isBluetoothDock()) {
554                onBondingDockConnect();
555            } else if (mConnectAfterPairing) {
556                connect(false);
557            }
558            mConnectAfterPairing = false;
559        }
560    }
561
562    void setBtClass(BluetoothClass btClass) {
563        if (btClass != null && mBtClass != btClass) {
564            mBtClass = btClass;
565            dispatchAttributesChanged();
566        }
567    }
568
569    BluetoothClass getBtClass() {
570        return mBtClass;
571    }
572
573    List<LocalBluetoothProfile> getProfiles() {
574        return Collections.unmodifiableList(mProfiles);
575    }
576
577    List<LocalBluetoothProfile> getConnectableProfiles() {
578        List<LocalBluetoothProfile> connectableProfiles =
579                new ArrayList<LocalBluetoothProfile>();
580        for (LocalBluetoothProfile profile : mProfiles) {
581            if (profile.isConnectable()) {
582                connectableProfiles.add(profile);
583            }
584        }
585        return connectableProfiles;
586    }
587
588    List<LocalBluetoothProfile> getRemovedProfiles() {
589        return mRemovedProfiles;
590    }
591
592    void registerCallback(Callback callback) {
593        synchronized (mCallbacks) {
594            mCallbacks.add(callback);
595        }
596    }
597
598    void unregisterCallback(Callback callback) {
599        synchronized (mCallbacks) {
600            mCallbacks.remove(callback);
601        }
602    }
603
604    private void dispatchAttributesChanged() {
605        synchronized (mCallbacks) {
606            for (Callback callback : mCallbacks) {
607                callback.onDeviceAttributesChanged();
608            }
609        }
610    }
611
612    @Override
613    public String toString() {
614        return mDevice.toString();
615    }
616
617    @Override
618    public boolean equals(Object o) {
619        if ((o == null) || !(o instanceof CachedBluetoothDevice)) {
620            return false;
621        }
622        return mDevice.equals(((CachedBluetoothDevice) o).mDevice);
623    }
624
625    @Override
626    public int hashCode() {
627        return mDevice.getAddress().hashCode();
628    }
629
630    // This comparison uses non-final fields so the sort order may change
631    // when device attributes change (such as bonding state). Settings
632    // will completely refresh the device list when this happens.
633    public int compareTo(CachedBluetoothDevice another) {
634        // Connected above not connected
635        int comparison = (another.isConnected() ? 1 : 0) - (isConnected() ? 1 : 0);
636        if (comparison != 0) return comparison;
637
638        // Paired above not paired
639        comparison = (another.getBondState() == BluetoothDevice.BOND_BONDED ? 1 : 0) -
640            (getBondState() == BluetoothDevice.BOND_BONDED ? 1 : 0);
641        if (comparison != 0) return comparison;
642
643        // Visible above not visible
644        comparison = (another.mVisible ? 1 : 0) - (mVisible ? 1 : 0);
645        if (comparison != 0) return comparison;
646
647        // Stronger signal above weaker signal
648        comparison = another.mRssi - mRssi;
649        if (comparison != 0) return comparison;
650
651        // Fallback on name
652        return mName.compareTo(another.mName);
653    }
654
655    public interface Callback {
656        void onDeviceAttributesChanged();
657    }
658
659    int getPhonebookPermissionChoice() {
660        return mPhonebookPermissionChoice;
661    }
662
663    void setPhonebookPermissionChoice(int permissionChoice) {
664        // if user reject it, only save it when reject exceed limit.
665        if (permissionChoice == ACCESS_REJECTED) {
666            mPhonebookRejectedTimes++;
667            savePhonebookRejectTimes();
668            if (mPhonebookRejectedTimes < PERSIST_REJECTED_TIMES_LIMIT) {
669                return;
670            }
671        }
672
673        mPhonebookPermissionChoice = permissionChoice;
674
675        SharedPreferences.Editor editor =
676            mContext.getSharedPreferences(PHONEBOOK_PREFS_NAME, Context.MODE_PRIVATE).edit();
677        if (permissionChoice == ACCESS_UNKNOWN) {
678            editor.remove(mDevice.getAddress());
679        } else {
680            editor.putInt(mDevice.getAddress(), permissionChoice);
681        }
682        editor.commit();
683    }
684
685    private void fetchPhonebookPermissionChoice() {
686        SharedPreferences preference = mContext.getSharedPreferences(PHONEBOOK_PREFS_NAME,
687                                                                     Context.MODE_PRIVATE);
688        mPhonebookPermissionChoice = preference.getInt(mDevice.getAddress(),
689                                                       ACCESS_UNKNOWN);
690    }
691
692    private void fetchPhonebookRejectTimes() {
693        SharedPreferences preference = mContext.getSharedPreferences(PHONEBOOK_REJECT_TIMES,
694                                                                     Context.MODE_PRIVATE);
695        mPhonebookRejectedTimes = preference.getInt(mDevice.getAddress(), 0);
696    }
697
698    private void savePhonebookRejectTimes() {
699        SharedPreferences.Editor editor =
700            mContext.getSharedPreferences(PHONEBOOK_REJECT_TIMES,
701                                          Context.MODE_PRIVATE).edit();
702        if (mPhonebookRejectedTimes == 0) {
703            editor.remove(mDevice.getAddress());
704        } else {
705            editor.putInt(mDevice.getAddress(), mPhonebookRejectedTimes);
706        }
707        editor.commit();
708    }
709
710    int getMessagePermissionChoice() {
711        return mMessagePermissionChoice;
712    }
713
714    void setMessagePermissionChoice(int permissionChoice) {
715        // if user reject it, only save it when reject exceed limit.
716        if (permissionChoice == ACCESS_REJECTED) {
717            mMessageRejectedTimes++;
718            saveMessageRejectTimes();
719            if (mMessageRejectedTimes < PERSIST_REJECTED_TIMES_LIMIT) {
720                return;
721            }
722        }
723
724        mMessagePermissionChoice = permissionChoice;
725
726        SharedPreferences.Editor editor =
727            mContext.getSharedPreferences(MESSAGE_PREFS_NAME, Context.MODE_PRIVATE).edit();
728        if (permissionChoice == ACCESS_UNKNOWN) {
729            editor.remove(mDevice.getAddress());
730        } else {
731            editor.putInt(mDevice.getAddress(), permissionChoice);
732        }
733        editor.commit();
734    }
735
736    private void fetchMessagePermissionChoice() {
737        SharedPreferences preference = mContext.getSharedPreferences(MESSAGE_PREFS_NAME,
738                                                                     Context.MODE_PRIVATE);
739        mMessagePermissionChoice = preference.getInt(mDevice.getAddress(),
740                                                       ACCESS_UNKNOWN);
741    }
742
743    private void fetchMessageRejectTimes() {
744        SharedPreferences preference = mContext.getSharedPreferences(MESSAGE_REJECT_TIMES,
745                                                                     Context.MODE_PRIVATE);
746        mMessageRejectedTimes = preference.getInt(mDevice.getAddress(), 0);
747    }
748
749    private void saveMessageRejectTimes() {
750        SharedPreferences.Editor editor =
751            mContext.getSharedPreferences(MESSAGE_REJECT_TIMES, Context.MODE_PRIVATE).edit();
752        if (mMessageRejectedTimes == 0) {
753            editor.remove(mDevice.getAddress());
754        } else {
755            editor.putInt(mDevice.getAddress(), mMessageRejectedTimes);
756        }
757        editor.commit();
758    }
759
760}
761