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.settingslib.bluetooth;
18
19import android.bluetooth.BluetoothClass;
20import android.bluetooth.BluetoothDevice;
21import android.bluetooth.BluetoothProfile;
22import android.bluetooth.BluetoothUuid;
23import android.content.Context;
24import android.content.SharedPreferences;
25import android.os.ParcelUuid;
26import android.os.SystemClock;
27import android.text.TextUtils;
28import android.util.Log;
29import android.bluetooth.BluetoothAdapter;
30
31import com.android.settingslib.R;
32
33import java.util.ArrayList;
34import java.util.Collection;
35import java.util.Collections;
36import java.util.HashMap;
37import java.util.List;
38
39/**
40 * CachedBluetoothDevice represents a remote Bluetooth device. It contains
41 * attributes of the device (such as the address, name, RSSI, etc.) and
42 * functionality that can be performed on the device (connect, pair, disconnect,
43 * etc.).
44 */
45public final class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> {
46    private static final String TAG = "CachedBluetoothDevice";
47    private static final boolean DEBUG = Utils.V;
48
49    private final Context mContext;
50    private final LocalBluetoothAdapter mLocalAdapter;
51    private final LocalBluetoothProfileManager mProfileManager;
52    private final BluetoothDevice mDevice;
53    private String mName;
54    private short mRssi;
55    private BluetoothClass mBtClass;
56    private HashMap<LocalBluetoothProfile, Integer> mProfileConnectionState;
57
58    private final List<LocalBluetoothProfile> mProfiles =
59            new ArrayList<LocalBluetoothProfile>();
60
61    // List of profiles that were previously in mProfiles, but have been removed
62    private final List<LocalBluetoothProfile> mRemovedProfiles =
63            new ArrayList<LocalBluetoothProfile>();
64
65    // Device supports PANU but not NAP: remove PanProfile after device disconnects from NAP
66    private boolean mLocalNapRoleConnected;
67
68    private boolean mVisible;
69
70    private int mMessageRejectionCount;
71
72    private final Collection<Callback> mCallbacks = new ArrayList<Callback>();
73
74    // Following constants indicate the user's choices of Phone book/message access settings
75    // User hasn't made any choice or settings app has wiped out the memory
76    public final static int ACCESS_UNKNOWN = 0;
77    // User has accepted the connection and let Settings app remember the decision
78    public final static int ACCESS_ALLOWED = 1;
79    // User has rejected the connection and let Settings app remember the decision
80    public final static int ACCESS_REJECTED = 2;
81
82    // How many times user should reject the connection to make the choice persist.
83    private final static int MESSAGE_REJECTION_COUNT_LIMIT_TO_PERSIST = 2;
84
85    private final static String MESSAGE_REJECTION_COUNT_PREFS_NAME = "bluetooth_message_reject";
86
87    /**
88     * When we connect to multiple profiles, we only want to display a single
89     * error even if they all fail. This tracks that state.
90     */
91    private boolean mIsConnectingErrorPossible;
92
93    /**
94     * Last time a bt profile auto-connect was attempted.
95     * If an ACTION_UUID intent comes in within
96     * MAX_UUID_DELAY_FOR_AUTO_CONNECT milliseconds, we will try auto-connect
97     * again with the new UUIDs
98     */
99    private long mConnectAttempted;
100
101    // See mConnectAttempted
102    private static final long MAX_UUID_DELAY_FOR_AUTO_CONNECT = 5000;
103    private static final long MAX_HOGP_DELAY_FOR_AUTO_CONNECT = 30000;
104
105    /** Auto-connect after pairing only if locally initiated. */
106    private boolean mConnectAfterPairing;
107
108    /**
109     * Describes the current device and profile for logging.
110     *
111     * @param profile Profile to describe
112     * @return Description of the device and profile
113     */
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    public void disconnect() {
173        for (LocalBluetoothProfile profile : mProfiles) {
174            disconnect(profile);
175        }
176        // Disconnect  PBAP server in case its connected
177        // This is to ensure all the profiles are disconnected as some CK/Hs do not
178        // disconnect  PBAP connection when HF connection is brought down
179        PbapServerProfile PbapProfile = mProfileManager.getPbapProfile();
180        if (PbapProfile.getConnectionStatus(mDevice) == BluetoothProfile.STATE_CONNECTED)
181        {
182            PbapProfile.disconnect(mDevice);
183        }
184    }
185
186    public void disconnect(LocalBluetoothProfile profile) {
187        if (profile.disconnect(mDevice)) {
188            if (Utils.D) {
189                Log.d(TAG, "Command sent successfully:DISCONNECT " + describe(profile));
190            }
191        }
192    }
193
194    public 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    public void connectProfile(LocalBluetoothProfile profile) {
263        mConnectAttempted = SystemClock.elapsedRealtime();
264        // Reset the only-show-one-error-dialog tracking variable
265        mIsConnectingErrorPossible = true;
266        connectInt(profile);
267        // Refresh the UI based on profile.connect() call
268        refresh();
269    }
270
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    public boolean startPairing() {
294        // Pairing is unreliable while scanning, so cancel discovery
295        if (mLocalAdapter.isDiscovering()) {
296            mLocalAdapter.cancelDiscovery();
297        }
298
299        if (!mDevice.createBond()) {
300            return false;
301        }
302
303        mConnectAfterPairing = true;  // auto-connect after pairing
304        return true;
305    }
306
307    /**
308     * Return true if user initiated pairing on this device. The message text is
309     * slightly different for local vs. remote initiated pairing dialogs.
310     */
311    boolean isUserInitiatedPairing() {
312        return mConnectAfterPairing;
313    }
314
315    public void unpair() {
316        int state = getBondState();
317
318        if (state == BluetoothDevice.BOND_BONDING) {
319            mDevice.cancelBondProcess();
320        }
321
322        if (state != BluetoothDevice.BOND_NONE) {
323            final BluetoothDevice dev = mDevice;
324            if (dev != null) {
325                final boolean successful = dev.removeBond();
326                if (successful) {
327                    if (Utils.D) {
328                        Log.d(TAG, "Command sent successfully:REMOVE_BOND " + describe(null));
329                    }
330                } else if (Utils.V) {
331                    Log.v(TAG, "Framework rejected command immediately:REMOVE_BOND " +
332                            describe(null));
333                }
334            }
335        }
336    }
337
338    public int getProfileConnectionState(LocalBluetoothProfile profile) {
339        if (mProfileConnectionState == null ||
340                mProfileConnectionState.get(profile) == null) {
341            // If cache is empty make the binder call to get the state
342            int state = profile.getConnectionStatus(mDevice);
343            mProfileConnectionState.put(profile, state);
344        }
345        return mProfileConnectionState.get(profile);
346    }
347
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    public BluetoothDevice getDevice() {
372        return mDevice;
373    }
374
375    public 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    public 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    public boolean isVisible() {
422        return mVisible;
423    }
424
425    public void setVisible(boolean visible) {
426        if (mVisible != visible) {
427            mVisible = visible;
428            dispatchAttributesChanged();
429        }
430    }
431
432    public 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    public boolean isConnected() {
449        for (LocalBluetoothProfile profile : mProfiles) {
450            int status = getProfileConnectionState(profile);
451            if (status == BluetoothProfile.STATE_CONNECTED) {
452                return true;
453            }
454        }
455
456        return false;
457    }
458
459    public boolean isConnectedProfile(LocalBluetoothProfile profile) {
460        int status = getProfileConnectionState(profile);
461        return status == BluetoothProfile.STATE_CONNECTED;
462
463    }
464
465    public boolean isBusy() {
466        for (LocalBluetoothProfile profile : mProfiles) {
467            int status = getProfileConnectionState(profile);
468            if (status == BluetoothProfile.STATE_CONNECTING
469                    || status == BluetoothProfile.STATE_DISCONNECTING) {
470                return true;
471            }
472        }
473        return getBondState() == BluetoothDevice.BOND_BONDING;
474    }
475
476    /**
477     * Fetches a new value for the cached BT class.
478     */
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        /**
491         * Now we know if the device supports PBAP, update permissions...
492         */
493        processPhonebookAccess();
494
495        mProfileManager.updateProfiles(uuids, localUuids, mProfiles, mRemovedProfiles,
496                                       mLocalNapRoleConnected, mDevice);
497
498        if (DEBUG) {
499            Log.e(TAG, "updating profiles for " + mDevice.getAliasName());
500            BluetoothClass bluetoothClass = mDevice.getBluetoothClass();
501
502            if (bluetoothClass != null) Log.v(TAG, "Class: " + bluetoothClass.toString());
503            Log.v(TAG, "UUID:");
504            for (ParcelUuid uuid : uuids) {
505                Log.v(TAG, "  " + uuid);
506            }
507        }
508        return true;
509    }
510
511    /**
512     * Refreshes the UI for the BT class, including fetching the latest value
513     * for the class.
514     */
515    void refreshBtClass() {
516        fetchBtClass();
517        dispatchAttributesChanged();
518    }
519
520    /**
521     * Refreshes the UI when framework alerts us of a UUID change.
522     */
523    void onUuidChanged() {
524        updateProfiles();
525        ParcelUuid[] uuids = mDevice.getUuids();
526
527        long timeout = MAX_UUID_DELAY_FOR_AUTO_CONNECT;
528        if (BluetoothUuid.isUuidPresent(uuids, BluetoothUuid.Hogp)) {
529            timeout = MAX_HOGP_DELAY_FOR_AUTO_CONNECT;
530        }
531
532        if (DEBUG) {
533            Log.d(TAG, "onUuidChanged: Time since last connect"
534                    + (SystemClock.elapsedRealtime() - mConnectAttempted));
535        }
536
537        /*
538         * If a connect was attempted earlier without any UUID, we will do the connect now.
539         * Otherwise, allow the connect on UUID change.
540         */
541        if (!mProfiles.isEmpty()
542                && ((mConnectAttempted + timeout) > SystemClock.elapsedRealtime())) {
543            connectWithoutResettingTimer(false);
544        }
545
546        dispatchAttributesChanged();
547    }
548
549    void onBondingStateChanged(int bondState) {
550        if (bondState == BluetoothDevice.BOND_NONE) {
551            mProfiles.clear();
552            mConnectAfterPairing = false;  // cancel auto-connect
553            setPhonebookPermissionChoice(ACCESS_UNKNOWN);
554            setMessagePermissionChoice(ACCESS_UNKNOWN);
555            setSimPermissionChoice(ACCESS_UNKNOWN);
556            mMessageRejectionCount = 0;
557            saveMessageRejectionCount();
558        }
559
560        refresh();
561
562        if (bondState == BluetoothDevice.BOND_BONDED) {
563            if (mDevice.isBluetoothDock()) {
564                onBondingDockConnect();
565            } else if (mConnectAfterPairing) {
566                connect(false);
567            }
568            mConnectAfterPairing = false;
569        }
570    }
571
572    void setBtClass(BluetoothClass btClass) {
573        if (btClass != null && mBtClass != btClass) {
574            mBtClass = btClass;
575            dispatchAttributesChanged();
576        }
577    }
578
579    public BluetoothClass getBtClass() {
580        return mBtClass;
581    }
582
583    public List<LocalBluetoothProfile> getProfiles() {
584        return Collections.unmodifiableList(mProfiles);
585    }
586
587    public List<LocalBluetoothProfile> getConnectableProfiles() {
588        List<LocalBluetoothProfile> connectableProfiles =
589                new ArrayList<LocalBluetoothProfile>();
590        for (LocalBluetoothProfile profile : mProfiles) {
591            if (profile.isConnectable()) {
592                connectableProfiles.add(profile);
593            }
594        }
595        return connectableProfiles;
596    }
597
598    public List<LocalBluetoothProfile> getRemovedProfiles() {
599        return mRemovedProfiles;
600    }
601
602    public void registerCallback(Callback callback) {
603        synchronized (mCallbacks) {
604            mCallbacks.add(callback);
605        }
606    }
607
608    public void unregisterCallback(Callback callback) {
609        synchronized (mCallbacks) {
610            mCallbacks.remove(callback);
611        }
612    }
613
614    private void dispatchAttributesChanged() {
615        synchronized (mCallbacks) {
616            for (Callback callback : mCallbacks) {
617                callback.onDeviceAttributesChanged();
618            }
619        }
620    }
621
622    @Override
623    public String toString() {
624        return mDevice.toString();
625    }
626
627    @Override
628    public boolean equals(Object o) {
629        if ((o == null) || !(o instanceof CachedBluetoothDevice)) {
630            return false;
631        }
632        return mDevice.equals(((CachedBluetoothDevice) o).mDevice);
633    }
634
635    @Override
636    public int hashCode() {
637        return mDevice.getAddress().hashCode();
638    }
639
640    // This comparison uses non-final fields so the sort order may change
641    // when device attributes change (such as bonding state). Settings
642    // will completely refresh the device list when this happens.
643    public int compareTo(CachedBluetoothDevice another) {
644        // Connected above not connected
645        int comparison = (another.isConnected() ? 1 : 0) - (isConnected() ? 1 : 0);
646        if (comparison != 0) return comparison;
647
648        // Paired above not paired
649        comparison = (another.getBondState() == BluetoothDevice.BOND_BONDED ? 1 : 0) -
650            (getBondState() == BluetoothDevice.BOND_BONDED ? 1 : 0);
651        if (comparison != 0) return comparison;
652
653        // Visible above not visible
654        comparison = (another.mVisible ? 1 : 0) - (mVisible ? 1 : 0);
655        if (comparison != 0) return comparison;
656
657        // Stronger signal above weaker signal
658        comparison = another.mRssi - mRssi;
659        if (comparison != 0) return comparison;
660
661        // Fallback on name
662        return mName.compareTo(another.mName);
663    }
664
665    public interface Callback {
666        void onDeviceAttributesChanged();
667    }
668
669    public int getPhonebookPermissionChoice() {
670        int permission = mDevice.getPhonebookAccessPermission();
671        if (permission == BluetoothDevice.ACCESS_ALLOWED) {
672            return ACCESS_ALLOWED;
673        } else if (permission == BluetoothDevice.ACCESS_REJECTED) {
674            return ACCESS_REJECTED;
675        }
676        return ACCESS_UNKNOWN;
677    }
678
679    public void setPhonebookPermissionChoice(int permissionChoice) {
680        int permission = BluetoothDevice.ACCESS_UNKNOWN;
681        if (permissionChoice == ACCESS_ALLOWED) {
682            permission = BluetoothDevice.ACCESS_ALLOWED;
683        } else if (permissionChoice == ACCESS_REJECTED) {
684            permission = BluetoothDevice.ACCESS_REJECTED;
685        }
686        mDevice.setPhonebookAccessPermission(permission);
687    }
688
689    // Migrates data from old data store (in Settings app's shared preferences) to new (in Bluetooth
690    // app's shared preferences).
691    private void migratePhonebookPermissionChoice() {
692        SharedPreferences preferences = mContext.getSharedPreferences(
693                "bluetooth_phonebook_permission", Context.MODE_PRIVATE);
694        if (!preferences.contains(mDevice.getAddress())) {
695            return;
696        }
697
698        if (mDevice.getPhonebookAccessPermission() == BluetoothDevice.ACCESS_UNKNOWN) {
699            int oldPermission = preferences.getInt(mDevice.getAddress(), ACCESS_UNKNOWN);
700            if (oldPermission == ACCESS_ALLOWED) {
701                mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_ALLOWED);
702            } else if (oldPermission == ACCESS_REJECTED) {
703                mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_REJECTED);
704            }
705        }
706
707        SharedPreferences.Editor editor = preferences.edit();
708        editor.remove(mDevice.getAddress());
709        editor.commit();
710    }
711
712    public int getMessagePermissionChoice() {
713        int permission = mDevice.getMessageAccessPermission();
714        if (permission == BluetoothDevice.ACCESS_ALLOWED) {
715            return ACCESS_ALLOWED;
716        } else if (permission == BluetoothDevice.ACCESS_REJECTED) {
717            return ACCESS_REJECTED;
718        }
719        return ACCESS_UNKNOWN;
720    }
721
722    public void setMessagePermissionChoice(int permissionChoice) {
723        int permission = BluetoothDevice.ACCESS_UNKNOWN;
724        if (permissionChoice == ACCESS_ALLOWED) {
725            permission = BluetoothDevice.ACCESS_ALLOWED;
726        } else if (permissionChoice == ACCESS_REJECTED) {
727            permission = BluetoothDevice.ACCESS_REJECTED;
728        }
729        mDevice.setMessageAccessPermission(permission);
730    }
731
732    public int getSimPermissionChoice() {
733        int permission = mDevice.getSimAccessPermission();
734        if (permission == BluetoothDevice.ACCESS_ALLOWED) {
735            return ACCESS_ALLOWED;
736        } else if (permission == BluetoothDevice.ACCESS_REJECTED) {
737            return ACCESS_REJECTED;
738        }
739        return ACCESS_UNKNOWN;
740    }
741
742    void setSimPermissionChoice(int permissionChoice) {
743        int permission = BluetoothDevice.ACCESS_UNKNOWN;
744        if (permissionChoice == ACCESS_ALLOWED) {
745            permission = BluetoothDevice.ACCESS_ALLOWED;
746        } else if (permissionChoice == ACCESS_REJECTED) {
747            permission = BluetoothDevice.ACCESS_REJECTED;
748        }
749        mDevice.setSimAccessPermission(permission);
750    }
751
752    // Migrates data from old data store (in Settings app's shared preferences) to new (in Bluetooth
753    // app's shared preferences).
754    private void migrateMessagePermissionChoice() {
755        SharedPreferences preferences = mContext.getSharedPreferences(
756                "bluetooth_message_permission", Context.MODE_PRIVATE);
757        if (!preferences.contains(mDevice.getAddress())) {
758            return;
759        }
760
761        if (mDevice.getMessageAccessPermission() == BluetoothDevice.ACCESS_UNKNOWN) {
762            int oldPermission = preferences.getInt(mDevice.getAddress(), ACCESS_UNKNOWN);
763            if (oldPermission == ACCESS_ALLOWED) {
764                mDevice.setMessageAccessPermission(BluetoothDevice.ACCESS_ALLOWED);
765            } else if (oldPermission == ACCESS_REJECTED) {
766                mDevice.setMessageAccessPermission(BluetoothDevice.ACCESS_REJECTED);
767            }
768        }
769
770        SharedPreferences.Editor editor = preferences.edit();
771        editor.remove(mDevice.getAddress());
772        editor.commit();
773    }
774
775    /**
776     * @return Whether this rejection should persist.
777     */
778    public boolean checkAndIncreaseMessageRejectionCount() {
779        if (mMessageRejectionCount < MESSAGE_REJECTION_COUNT_LIMIT_TO_PERSIST) {
780            mMessageRejectionCount++;
781            saveMessageRejectionCount();
782        }
783        return mMessageRejectionCount >= MESSAGE_REJECTION_COUNT_LIMIT_TO_PERSIST;
784    }
785
786    private void fetchMessageRejectionCount() {
787        SharedPreferences preference = mContext.getSharedPreferences(
788                MESSAGE_REJECTION_COUNT_PREFS_NAME, Context.MODE_PRIVATE);
789        mMessageRejectionCount = preference.getInt(mDevice.getAddress(), 0);
790    }
791
792    private void saveMessageRejectionCount() {
793        SharedPreferences.Editor editor = mContext.getSharedPreferences(
794                MESSAGE_REJECTION_COUNT_PREFS_NAME, Context.MODE_PRIVATE).edit();
795        if (mMessageRejectionCount == 0) {
796            editor.remove(mDevice.getAddress());
797        } else {
798            editor.putInt(mDevice.getAddress(), mMessageRejectionCount);
799        }
800        editor.commit();
801    }
802
803    private void processPhonebookAccess() {
804        if (mDevice.getBondState() != BluetoothDevice.BOND_BONDED) return;
805
806        ParcelUuid[] uuids = mDevice.getUuids();
807        if (BluetoothUuid.containsAnyUuid(uuids, PbapServerProfile.PBAB_CLIENT_UUIDS)) {
808            // The pairing dialog now warns of phone-book access for paired devices.
809            // No separate prompt is displayed after pairing.
810            if (getPhonebookPermissionChoice() == CachedBluetoothDevice.ACCESS_UNKNOWN) {
811                if (mDevice.getBluetoothClass().getDeviceClass()
812                        == BluetoothClass.Device.AUDIO_VIDEO_HANDSFREE) {
813                    setPhonebookPermissionChoice(CachedBluetoothDevice.ACCESS_ALLOWED);
814                } else {
815                    setPhonebookPermissionChoice(CachedBluetoothDevice.ACCESS_REJECTED);
816                }
817            }
818        }
819    }
820
821    public int getMaxConnectionState() {
822        int maxState = BluetoothProfile.STATE_DISCONNECTED;
823        for (LocalBluetoothProfile profile : getProfiles()) {
824            int connectionStatus = getProfileConnectionState(profile);
825            if (connectionStatus > maxState) {
826                maxState = connectionStatus;
827            }
828        }
829        return maxState;
830    }
831
832    /**
833     * @return resource for string that discribes the connection state of this device.
834     */
835    public int getConnectionSummary() {
836        boolean profileConnected = false;       // at least one profile is connected
837        boolean a2dpNotConnected = false;       // A2DP is preferred but not connected
838        boolean hfpNotConnected = false;    // HFP is preferred but not connected
839
840        for (LocalBluetoothProfile profile : getProfiles()) {
841            int connectionStatus = getProfileConnectionState(profile);
842
843            switch (connectionStatus) {
844                case BluetoothProfile.STATE_CONNECTING:
845                case BluetoothProfile.STATE_DISCONNECTING:
846                    return Utils.getConnectionStateSummary(connectionStatus);
847
848                case BluetoothProfile.STATE_CONNECTED:
849                    profileConnected = true;
850                    break;
851
852                case BluetoothProfile.STATE_DISCONNECTED:
853                    if (profile.isProfileReady()) {
854                        if ((profile instanceof A2dpProfile) ||
855                            (profile instanceof A2dpSinkProfile)){
856                            a2dpNotConnected = true;
857                        } else if ((profile instanceof HeadsetProfile) ||
858                                   (profile instanceof HfpClientProfile)) {
859                            hfpNotConnected = true;
860                        }
861                    }
862                    break;
863            }
864        }
865
866        if (profileConnected) {
867            if (a2dpNotConnected && hfpNotConnected) {
868                return R.string.bluetooth_connected_no_headset_no_a2dp;
869            } else if (a2dpNotConnected) {
870                return R.string.bluetooth_connected_no_a2dp;
871            } else if (hfpNotConnected) {
872                return R.string.bluetooth_connected_no_headset;
873            } else {
874                return R.string.bluetooth_connected;
875            }
876        }
877
878        return getBondState() == BluetoothDevice.BOND_BONDING ? R.string.bluetooth_pairing : 0;
879    }
880}
881