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 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    /**
106     * Describes the current device and profile for logging.
107     *
108     * @param profile Profile to describe
109     * @return Description of the device and profile
110     */
111    private String describe(LocalBluetoothProfile profile) {
112        StringBuilder sb = new StringBuilder();
113        sb.append("Address:").append(mDevice);
114        if (profile != null) {
115            sb.append(" Profile:").append(profile);
116        }
117
118        return sb.toString();
119    }
120
121    void onProfileStateChanged(LocalBluetoothProfile profile, int newProfileState) {
122        if (Utils.D) {
123            Log.d(TAG, "onProfileStateChanged: profile " + profile +
124                    " newProfileState " + newProfileState);
125        }
126        if (mLocalAdapter.getBluetoothState() == BluetoothAdapter.STATE_TURNING_OFF)
127        {
128            if (Utils.D) Log.d(TAG, " BT Turninig Off...Profile conn state change ignored...");
129            return;
130        }
131        mProfileConnectionState.put(profile, newProfileState);
132        if (newProfileState == BluetoothProfile.STATE_CONNECTED) {
133            if (profile instanceof MapProfile) {
134                profile.setPreferred(mDevice, true);
135            } else if (!mProfiles.contains(profile)) {
136                mRemovedProfiles.remove(profile);
137                mProfiles.add(profile);
138                if (profile instanceof PanProfile &&
139                        ((PanProfile) profile).isLocalRoleNap(mDevice)) {
140                    // Device doesn't support NAP, so remove PanProfile on disconnect
141                    mLocalNapRoleConnected = true;
142                }
143            }
144        } else if (profile instanceof MapProfile &&
145                newProfileState == BluetoothProfile.STATE_DISCONNECTED) {
146            profile.setPreferred(mDevice, false);
147        } else if (mLocalNapRoleConnected && profile instanceof PanProfile &&
148                ((PanProfile) profile).isLocalRoleNap(mDevice) &&
149                newProfileState == BluetoothProfile.STATE_DISCONNECTED) {
150            Log.d(TAG, "Removing PanProfile from device after NAP disconnect");
151            mProfiles.remove(profile);
152            mRemovedProfiles.add(profile);
153            mLocalNapRoleConnected = false;
154        }
155    }
156
157    CachedBluetoothDevice(Context context,
158                          LocalBluetoothAdapter adapter,
159                          LocalBluetoothProfileManager profileManager,
160                          BluetoothDevice device) {
161        mContext = context;
162        mLocalAdapter = adapter;
163        mProfileManager = profileManager;
164        mDevice = device;
165        mProfileConnectionState = new HashMap<LocalBluetoothProfile, Integer>();
166        fillData();
167    }
168
169    public void disconnect() {
170        for (LocalBluetoothProfile profile : mProfiles) {
171            disconnect(profile);
172        }
173        // Disconnect  PBAP server in case its connected
174        // This is to ensure all the profiles are disconnected as some CK/Hs do not
175        // disconnect  PBAP connection when HF connection is brought down
176        PbapServerProfile PbapProfile = mProfileManager.getPbapProfile();
177        if (PbapProfile.getConnectionStatus(mDevice) == BluetoothProfile.STATE_CONNECTED)
178        {
179            PbapProfile.disconnect(mDevice);
180        }
181    }
182
183    public void disconnect(LocalBluetoothProfile profile) {
184        if (profile.disconnect(mDevice)) {
185            if (Utils.D) {
186                Log.d(TAG, "Command sent successfully:DISCONNECT " + describe(profile));
187            }
188        }
189    }
190
191    public void connect(boolean connectAllProfiles) {
192        if (!ensurePaired()) {
193            return;
194        }
195
196        mConnectAttempted = SystemClock.elapsedRealtime();
197        connectWithoutResettingTimer(connectAllProfiles);
198    }
199
200    void onBondingDockConnect() {
201        // Attempt to connect if UUIDs are available. Otherwise,
202        // we will connect when the ACTION_UUID intent arrives.
203        connect(false);
204    }
205
206    private void connectWithoutResettingTimer(boolean connectAllProfiles) {
207        // Try to initialize the profiles if they were not.
208        if (mProfiles.isEmpty()) {
209            // if mProfiles is empty, then do not invoke updateProfiles. This causes a race
210            // condition with carkits during pairing, wherein RemoteDevice.UUIDs have been updated
211            // from bluetooth stack but ACTION.uuid is not sent yet.
212            // Eventually ACTION.uuid will be received which shall trigger the connection of the
213            // various profiles
214            // If UUIDs are not available yet, connect will be happen
215            // upon arrival of the ACTION_UUID intent.
216            Log.d(TAG, "No profiles. Maybe we will connect later");
217            return;
218        }
219
220        // Reset the only-show-one-error-dialog tracking variable
221        mIsConnectingErrorPossible = true;
222
223        int preferredProfiles = 0;
224        for (LocalBluetoothProfile profile : mProfiles) {
225            if (connectAllProfiles ? profile.isConnectable() : profile.isAutoConnectable()) {
226                if (profile.isPreferred(mDevice)) {
227                    ++preferredProfiles;
228                    connectInt(profile);
229                }
230            }
231        }
232        if (DEBUG) Log.d(TAG, "Preferred profiles = " + preferredProfiles);
233
234        if (preferredProfiles == 0) {
235            connectAutoConnectableProfiles();
236        }
237    }
238
239    private void connectAutoConnectableProfiles() {
240        if (!ensurePaired()) {
241            return;
242        }
243        // Reset the only-show-one-error-dialog tracking variable
244        mIsConnectingErrorPossible = true;
245
246        for (LocalBluetoothProfile profile : mProfiles) {
247            if (profile.isAutoConnectable()) {
248                profile.setPreferred(mDevice, true);
249                connectInt(profile);
250            }
251        }
252    }
253
254    /**
255     * Connect this device to the specified profile.
256     *
257     * @param profile the profile to use with the remote device
258     */
259    public void connectProfile(LocalBluetoothProfile profile) {
260        mConnectAttempted = SystemClock.elapsedRealtime();
261        // Reset the only-show-one-error-dialog tracking variable
262        mIsConnectingErrorPossible = true;
263        connectInt(profile);
264        // Refresh the UI based on profile.connect() call
265        refresh();
266    }
267
268    synchronized void connectInt(LocalBluetoothProfile profile) {
269        if (!ensurePaired()) {
270            return;
271        }
272        if (profile.connect(mDevice)) {
273            if (Utils.D) {
274                Log.d(TAG, "Command sent successfully:CONNECT " + describe(profile));
275            }
276            return;
277        }
278        Log.i(TAG, "Failed to connect " + profile.toString() + " to " + mName);
279    }
280
281    private boolean ensurePaired() {
282        if (getBondState() == BluetoothDevice.BOND_NONE) {
283            startPairing();
284            return false;
285        } else {
286            return true;
287        }
288    }
289
290    public boolean startPairing() {
291        // Pairing is unreliable while scanning, so cancel discovery
292        if (mLocalAdapter.isDiscovering()) {
293            mLocalAdapter.cancelDiscovery();
294        }
295
296        if (!mDevice.createBond()) {
297            return false;
298        }
299
300        return true;
301    }
302
303    /**
304     * Return true if user initiated pairing on this device. The message text is
305     * slightly different for local vs. remote initiated pairing dialogs.
306     */
307    boolean isUserInitiatedPairing() {
308        return mDevice.isBondingInitiatedLocally();
309    }
310
311    public void unpair() {
312        int state = getBondState();
313
314        if (state == BluetoothDevice.BOND_BONDING) {
315            mDevice.cancelBondProcess();
316        }
317
318        if (state != BluetoothDevice.BOND_NONE) {
319            final BluetoothDevice dev = mDevice;
320            if (dev != null) {
321                final boolean successful = dev.removeBond();
322                if (successful) {
323                    if (Utils.D) {
324                        Log.d(TAG, "Command sent successfully:REMOVE_BOND " + describe(null));
325                    }
326                } else if (Utils.V) {
327                    Log.v(TAG, "Framework rejected command immediately:REMOVE_BOND " +
328                            describe(null));
329                }
330            }
331        }
332    }
333
334    public int getProfileConnectionState(LocalBluetoothProfile profile) {
335        if (mProfileConnectionState == null ||
336                mProfileConnectionState.get(profile) == null) {
337            // If cache is empty make the binder call to get the state
338            int state = profile.getConnectionStatus(mDevice);
339            mProfileConnectionState.put(profile, state);
340        }
341        return mProfileConnectionState.get(profile);
342    }
343
344    public void clearProfileConnectionState ()
345    {
346        if (Utils.D) {
347            Log.d(TAG," Clearing all connection state for dev:" + mDevice.getName());
348        }
349        for (LocalBluetoothProfile profile :getProfiles()) {
350            mProfileConnectionState.put(profile, BluetoothProfile.STATE_DISCONNECTED);
351        }
352    }
353
354    // TODO: do any of these need to run async on a background thread?
355    private void fillData() {
356        fetchName();
357        fetchBtClass();
358        updateProfiles();
359        migratePhonebookPermissionChoice();
360        migrateMessagePermissionChoice();
361        fetchMessageRejectionCount();
362
363        mVisible = false;
364        dispatchAttributesChanged();
365    }
366
367    public BluetoothDevice getDevice() {
368        return mDevice;
369    }
370
371    public String getName() {
372        return mName;
373    }
374
375    /**
376     * Populate name from BluetoothDevice.ACTION_FOUND intent
377     */
378    void setNewName(String name) {
379        if (mName == null) {
380            mName = name;
381            if (mName == null || TextUtils.isEmpty(mName)) {
382                mName = mDevice.getAddress();
383            }
384            dispatchAttributesChanged();
385        }
386    }
387
388    /**
389     * user changes the device name
390     */
391    public void setName(String name) {
392        if (!mName.equals(name)) {
393            mName = name;
394            mDevice.setAlias(name);
395            dispatchAttributesChanged();
396        }
397    }
398
399    void refreshName() {
400        fetchName();
401        dispatchAttributesChanged();
402    }
403
404    private void fetchName() {
405        mName = mDevice.getAliasName();
406
407        if (TextUtils.isEmpty(mName)) {
408            mName = mDevice.getAddress();
409            if (DEBUG) Log.d(TAG, "Device has no name (yet), use address: " + mName);
410        }
411    }
412
413    void refresh() {
414        dispatchAttributesChanged();
415    }
416
417    public boolean isVisible() {
418        return mVisible;
419    }
420
421    public void setVisible(boolean visible) {
422        if (mVisible != visible) {
423            mVisible = visible;
424            dispatchAttributesChanged();
425        }
426    }
427
428    public int getBondState() {
429        return mDevice.getBondState();
430    }
431
432    void setRssi(short rssi) {
433        if (mRssi != rssi) {
434            mRssi = rssi;
435            dispatchAttributesChanged();
436        }
437    }
438
439    /**
440     * Checks whether we are connected to this device (any profile counts).
441     *
442     * @return Whether it is connected.
443     */
444    public boolean isConnected() {
445        for (LocalBluetoothProfile profile : mProfiles) {
446            int status = getProfileConnectionState(profile);
447            if (status == BluetoothProfile.STATE_CONNECTED) {
448                return true;
449            }
450        }
451
452        return false;
453    }
454
455    public boolean isConnectedProfile(LocalBluetoothProfile profile) {
456        int status = getProfileConnectionState(profile);
457        return status == BluetoothProfile.STATE_CONNECTED;
458
459    }
460
461    public boolean isBusy() {
462        for (LocalBluetoothProfile profile : mProfiles) {
463            int status = getProfileConnectionState(profile);
464            if (status == BluetoothProfile.STATE_CONNECTING
465                    || status == BluetoothProfile.STATE_DISCONNECTING) {
466                return true;
467            }
468        }
469        return getBondState() == BluetoothDevice.BOND_BONDING;
470    }
471
472    /**
473     * Fetches a new value for the cached BT class.
474     */
475    private void fetchBtClass() {
476        mBtClass = mDevice.getBluetoothClass();
477    }
478
479    private boolean updateProfiles() {
480        ParcelUuid[] uuids = mDevice.getUuids();
481        if (uuids == null) return false;
482
483        ParcelUuid[] localUuids = mLocalAdapter.getUuids();
484        if (localUuids == null) return false;
485
486        /**
487         * Now we know if the device supports PBAP, update permissions...
488         */
489        processPhonebookAccess();
490
491        mProfileManager.updateProfiles(uuids, localUuids, mProfiles, mRemovedProfiles,
492                                       mLocalNapRoleConnected, mDevice);
493
494        if (DEBUG) {
495            Log.e(TAG, "updating profiles for " + mDevice.getAliasName());
496            BluetoothClass bluetoothClass = mDevice.getBluetoothClass();
497
498            if (bluetoothClass != null) Log.v(TAG, "Class: " + bluetoothClass.toString());
499            Log.v(TAG, "UUID:");
500            for (ParcelUuid uuid : uuids) {
501                Log.v(TAG, "  " + uuid);
502            }
503        }
504        return true;
505    }
506
507    /**
508     * Refreshes the UI for the BT class, including fetching the latest value
509     * for the class.
510     */
511    void refreshBtClass() {
512        fetchBtClass();
513        dispatchAttributesChanged();
514    }
515
516    /**
517     * Refreshes the UI when framework alerts us of a UUID change.
518     */
519    void onUuidChanged() {
520        updateProfiles();
521        ParcelUuid[] uuids = mDevice.getUuids();
522
523        long timeout = MAX_UUID_DELAY_FOR_AUTO_CONNECT;
524        if (BluetoothUuid.isUuidPresent(uuids, BluetoothUuid.Hogp)) {
525            timeout = MAX_HOGP_DELAY_FOR_AUTO_CONNECT;
526        }
527
528        if (DEBUG) {
529            Log.d(TAG, "onUuidChanged: Time since last connect"
530                    + (SystemClock.elapsedRealtime() - mConnectAttempted));
531        }
532
533        /*
534         * If a connect was attempted earlier without any UUID, we will do the connect now.
535         * Otherwise, allow the connect on UUID change.
536         */
537        if (!mProfiles.isEmpty()
538                && ((mConnectAttempted + timeout) > SystemClock.elapsedRealtime())) {
539            connectWithoutResettingTimer(false);
540        }
541
542        dispatchAttributesChanged();
543    }
544
545    void onBondingStateChanged(int bondState) {
546        if (bondState == BluetoothDevice.BOND_NONE) {
547            mProfiles.clear();
548            setPhonebookPermissionChoice(ACCESS_UNKNOWN);
549            setMessagePermissionChoice(ACCESS_UNKNOWN);
550            setSimPermissionChoice(ACCESS_UNKNOWN);
551            mMessageRejectionCount = 0;
552            saveMessageRejectionCount();
553        }
554
555        refresh();
556
557        if (bondState == BluetoothDevice.BOND_BONDED) {
558            if (mDevice.isBluetoothDock()) {
559                onBondingDockConnect();
560            } else if (mDevice.isBondingInitiatedLocally()) {
561                connect(false);
562            }
563        }
564    }
565
566    void setBtClass(BluetoothClass btClass) {
567        if (btClass != null && mBtClass != btClass) {
568            mBtClass = btClass;
569            dispatchAttributesChanged();
570        }
571    }
572
573    public BluetoothClass getBtClass() {
574        return mBtClass;
575    }
576
577    public List<LocalBluetoothProfile> getProfiles() {
578        return Collections.unmodifiableList(mProfiles);
579    }
580
581    public List<LocalBluetoothProfile> getConnectableProfiles() {
582        List<LocalBluetoothProfile> connectableProfiles =
583                new ArrayList<LocalBluetoothProfile>();
584        for (LocalBluetoothProfile profile : mProfiles) {
585            if (profile.isConnectable()) {
586                connectableProfiles.add(profile);
587            }
588        }
589        return connectableProfiles;
590    }
591
592    public List<LocalBluetoothProfile> getRemovedProfiles() {
593        return mRemovedProfiles;
594    }
595
596    public void registerCallback(Callback callback) {
597        synchronized (mCallbacks) {
598            mCallbacks.add(callback);
599        }
600    }
601
602    public void unregisterCallback(Callback callback) {
603        synchronized (mCallbacks) {
604            mCallbacks.remove(callback);
605        }
606    }
607
608    private void dispatchAttributesChanged() {
609        synchronized (mCallbacks) {
610            for (Callback callback : mCallbacks) {
611                callback.onDeviceAttributesChanged();
612            }
613        }
614    }
615
616    @Override
617    public String toString() {
618        return mDevice.toString();
619    }
620
621    @Override
622    public boolean equals(Object o) {
623        if ((o == null) || !(o instanceof CachedBluetoothDevice)) {
624            return false;
625        }
626        return mDevice.equals(((CachedBluetoothDevice) o).mDevice);
627    }
628
629    @Override
630    public int hashCode() {
631        return mDevice.getAddress().hashCode();
632    }
633
634    // This comparison uses non-final fields so the sort order may change
635    // when device attributes change (such as bonding state). Settings
636    // will completely refresh the device list when this happens.
637    public int compareTo(CachedBluetoothDevice another) {
638        // Connected above not connected
639        int comparison = (another.isConnected() ? 1 : 0) - (isConnected() ? 1 : 0);
640        if (comparison != 0) return comparison;
641
642        // Paired above not paired
643        comparison = (another.getBondState() == BluetoothDevice.BOND_BONDED ? 1 : 0) -
644            (getBondState() == BluetoothDevice.BOND_BONDED ? 1 : 0);
645        if (comparison != 0) return comparison;
646
647        // Visible above not visible
648        comparison = (another.mVisible ? 1 : 0) - (mVisible ? 1 : 0);
649        if (comparison != 0) return comparison;
650
651        // Stronger signal above weaker signal
652        comparison = another.mRssi - mRssi;
653        if (comparison != 0) return comparison;
654
655        // Fallback on name
656        return mName.compareTo(another.mName);
657    }
658
659    public interface Callback {
660        void onDeviceAttributesChanged();
661    }
662
663    public int getPhonebookPermissionChoice() {
664        int permission = mDevice.getPhonebookAccessPermission();
665        if (permission == BluetoothDevice.ACCESS_ALLOWED) {
666            return ACCESS_ALLOWED;
667        } else if (permission == BluetoothDevice.ACCESS_REJECTED) {
668            return ACCESS_REJECTED;
669        }
670        return ACCESS_UNKNOWN;
671    }
672
673    public void setPhonebookPermissionChoice(int permissionChoice) {
674        int permission = BluetoothDevice.ACCESS_UNKNOWN;
675        if (permissionChoice == ACCESS_ALLOWED) {
676            permission = BluetoothDevice.ACCESS_ALLOWED;
677        } else if (permissionChoice == ACCESS_REJECTED) {
678            permission = BluetoothDevice.ACCESS_REJECTED;
679        }
680        mDevice.setPhonebookAccessPermission(permission);
681    }
682
683    // Migrates data from old data store (in Settings app's shared preferences) to new (in Bluetooth
684    // app's shared preferences).
685    private void migratePhonebookPermissionChoice() {
686        SharedPreferences preferences = mContext.getSharedPreferences(
687                "bluetooth_phonebook_permission", Context.MODE_PRIVATE);
688        if (!preferences.contains(mDevice.getAddress())) {
689            return;
690        }
691
692        if (mDevice.getPhonebookAccessPermission() == BluetoothDevice.ACCESS_UNKNOWN) {
693            int oldPermission = preferences.getInt(mDevice.getAddress(), ACCESS_UNKNOWN);
694            if (oldPermission == ACCESS_ALLOWED) {
695                mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_ALLOWED);
696            } else if (oldPermission == ACCESS_REJECTED) {
697                mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_REJECTED);
698            }
699        }
700
701        SharedPreferences.Editor editor = preferences.edit();
702        editor.remove(mDevice.getAddress());
703        editor.commit();
704    }
705
706    public int getMessagePermissionChoice() {
707        int permission = mDevice.getMessageAccessPermission();
708        if (permission == BluetoothDevice.ACCESS_ALLOWED) {
709            return ACCESS_ALLOWED;
710        } else if (permission == BluetoothDevice.ACCESS_REJECTED) {
711            return ACCESS_REJECTED;
712        }
713        return ACCESS_UNKNOWN;
714    }
715
716    public void setMessagePermissionChoice(int permissionChoice) {
717        int permission = BluetoothDevice.ACCESS_UNKNOWN;
718        if (permissionChoice == ACCESS_ALLOWED) {
719            permission = BluetoothDevice.ACCESS_ALLOWED;
720        } else if (permissionChoice == ACCESS_REJECTED) {
721            permission = BluetoothDevice.ACCESS_REJECTED;
722        }
723        mDevice.setMessageAccessPermission(permission);
724    }
725
726    public int getSimPermissionChoice() {
727        int permission = mDevice.getSimAccessPermission();
728        if (permission == BluetoothDevice.ACCESS_ALLOWED) {
729            return ACCESS_ALLOWED;
730        } else if (permission == BluetoothDevice.ACCESS_REJECTED) {
731            return ACCESS_REJECTED;
732        }
733        return ACCESS_UNKNOWN;
734    }
735
736    void setSimPermissionChoice(int permissionChoice) {
737        int permission = BluetoothDevice.ACCESS_UNKNOWN;
738        if (permissionChoice == ACCESS_ALLOWED) {
739            permission = BluetoothDevice.ACCESS_ALLOWED;
740        } else if (permissionChoice == ACCESS_REJECTED) {
741            permission = BluetoothDevice.ACCESS_REJECTED;
742        }
743        mDevice.setSimAccessPermission(permission);
744    }
745
746    // Migrates data from old data store (in Settings app's shared preferences) to new (in Bluetooth
747    // app's shared preferences).
748    private void migrateMessagePermissionChoice() {
749        SharedPreferences preferences = mContext.getSharedPreferences(
750                "bluetooth_message_permission", Context.MODE_PRIVATE);
751        if (!preferences.contains(mDevice.getAddress())) {
752            return;
753        }
754
755        if (mDevice.getMessageAccessPermission() == BluetoothDevice.ACCESS_UNKNOWN) {
756            int oldPermission = preferences.getInt(mDevice.getAddress(), ACCESS_UNKNOWN);
757            if (oldPermission == ACCESS_ALLOWED) {
758                mDevice.setMessageAccessPermission(BluetoothDevice.ACCESS_ALLOWED);
759            } else if (oldPermission == ACCESS_REJECTED) {
760                mDevice.setMessageAccessPermission(BluetoothDevice.ACCESS_REJECTED);
761            }
762        }
763
764        SharedPreferences.Editor editor = preferences.edit();
765        editor.remove(mDevice.getAddress());
766        editor.commit();
767    }
768
769    /**
770     * @return Whether this rejection should persist.
771     */
772    public boolean checkAndIncreaseMessageRejectionCount() {
773        if (mMessageRejectionCount < MESSAGE_REJECTION_COUNT_LIMIT_TO_PERSIST) {
774            mMessageRejectionCount++;
775            saveMessageRejectionCount();
776        }
777        return mMessageRejectionCount >= MESSAGE_REJECTION_COUNT_LIMIT_TO_PERSIST;
778    }
779
780    private void fetchMessageRejectionCount() {
781        SharedPreferences preference = mContext.getSharedPreferences(
782                MESSAGE_REJECTION_COUNT_PREFS_NAME, Context.MODE_PRIVATE);
783        mMessageRejectionCount = preference.getInt(mDevice.getAddress(), 0);
784    }
785
786    private void saveMessageRejectionCount() {
787        SharedPreferences.Editor editor = mContext.getSharedPreferences(
788                MESSAGE_REJECTION_COUNT_PREFS_NAME, Context.MODE_PRIVATE).edit();
789        if (mMessageRejectionCount == 0) {
790            editor.remove(mDevice.getAddress());
791        } else {
792            editor.putInt(mDevice.getAddress(), mMessageRejectionCount);
793        }
794        editor.commit();
795    }
796
797    private void processPhonebookAccess() {
798        if (mDevice.getBondState() != BluetoothDevice.BOND_BONDED) return;
799
800        ParcelUuid[] uuids = mDevice.getUuids();
801        if (BluetoothUuid.containsAnyUuid(uuids, PbapServerProfile.PBAB_CLIENT_UUIDS)) {
802            // The pairing dialog now warns of phone-book access for paired devices.
803            // No separate prompt is displayed after pairing.
804            if (getPhonebookPermissionChoice() == CachedBluetoothDevice.ACCESS_UNKNOWN) {
805                if (mDevice.getBluetoothClass().getDeviceClass()
806                        == BluetoothClass.Device.AUDIO_VIDEO_HANDSFREE ||
807                    mDevice.getBluetoothClass().getDeviceClass()
808                        == BluetoothClass.Device.AUDIO_VIDEO_WEARABLE_HEADSET) {
809                    setPhonebookPermissionChoice(CachedBluetoothDevice.ACCESS_ALLOWED);
810                } else {
811                    setPhonebookPermissionChoice(CachedBluetoothDevice.ACCESS_REJECTED);
812                }
813            }
814        }
815    }
816
817    public int getMaxConnectionState() {
818        int maxState = BluetoothProfile.STATE_DISCONNECTED;
819        for (LocalBluetoothProfile profile : getProfiles()) {
820            int connectionStatus = getProfileConnectionState(profile);
821            if (connectionStatus > maxState) {
822                maxState = connectionStatus;
823            }
824        }
825        return maxState;
826    }
827
828    /**
829     * @return resource for string that discribes the connection state of this device.
830     */
831    public int getConnectionSummary() {
832        boolean profileConnected = false;       // at least one profile is connected
833        boolean a2dpNotConnected = false;       // A2DP is preferred but not connected
834        boolean hfpNotConnected = false;    // HFP is preferred but not connected
835
836        for (LocalBluetoothProfile profile : getProfiles()) {
837            int connectionStatus = getProfileConnectionState(profile);
838
839            switch (connectionStatus) {
840                case BluetoothProfile.STATE_CONNECTING:
841                case BluetoothProfile.STATE_DISCONNECTING:
842                    return Utils.getConnectionStateSummary(connectionStatus);
843
844                case BluetoothProfile.STATE_CONNECTED:
845                    profileConnected = true;
846                    break;
847
848                case BluetoothProfile.STATE_DISCONNECTED:
849                    if (profile.isProfileReady()) {
850                        if ((profile instanceof A2dpProfile) ||
851                            (profile instanceof A2dpSinkProfile)){
852                            a2dpNotConnected = true;
853                        } else if ((profile instanceof HeadsetProfile) ||
854                                   (profile instanceof HfpClientProfile)) {
855                            hfpNotConnected = true;
856                        }
857                    }
858                    break;
859            }
860        }
861
862        if (profileConnected) {
863            if (a2dpNotConnected && hfpNotConnected) {
864                return R.string.bluetooth_connected_no_headset_no_a2dp;
865            } else if (a2dpNotConnected) {
866                return R.string.bluetooth_connected_no_a2dp;
867            } else if (hfpNotConnected) {
868                return R.string.bluetooth_connected_no_headset;
869            } else {
870                return R.string.bluetooth_connected;
871            }
872        }
873
874        return getBondState() == BluetoothDevice.BOND_BONDING ? R.string.bluetooth_pairing : 0;
875    }
876}
877