OSDN Git Service

[automerger] Add MANAGED_PROVISIONING_DPC_DOWNLOADED (nyc). am: 20e5d92613 am: c1ec4d...
[android-x86/frameworks-base.git] / packages / SettingsLib / src / com / android / settingslib / bluetooth / CachedBluetoothDevice.java
1 /*
2  * Copyright (C) 2008 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16
17 package com.android.settingslib.bluetooth;
18
19 import android.bluetooth.BluetoothClass;
20 import android.bluetooth.BluetoothDevice;
21 import android.bluetooth.BluetoothProfile;
22 import android.bluetooth.BluetoothUuid;
23 import android.content.Context;
24 import android.content.SharedPreferences;
25 import android.os.ParcelUuid;
26 import android.os.SystemClock;
27 import android.text.TextUtils;
28 import android.util.Log;
29 import android.bluetooth.BluetoothAdapter;
30
31 import com.android.settingslib.R;
32
33 import java.util.ArrayList;
34 import java.util.Collection;
35 import java.util.Collections;
36 import java.util.HashMap;
37 import 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  */
45 public 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     //TODO: consider remove, BluetoothDevice.getName() is already cached
54     private String mName;
55     // Need this since there is no method for getting RSSI
56     private short mRssi;
57     //TODO: consider remove, BluetoothDevice.getBluetoothClass() is already cached
58     private BluetoothClass mBtClass;
59     private HashMap<LocalBluetoothProfile, Integer> mProfileConnectionState;
60
61     private final List<LocalBluetoothProfile> mProfiles =
62             new ArrayList<LocalBluetoothProfile>();
63
64     // List of profiles that were previously in mProfiles, but have been removed
65     private final List<LocalBluetoothProfile> mRemovedProfiles =
66             new ArrayList<LocalBluetoothProfile>();
67
68     // Device supports PANU but not NAP: remove PanProfile after device disconnects from NAP
69     private boolean mLocalNapRoleConnected;
70
71     private boolean mJustDiscovered;
72
73     private int mMessageRejectionCount;
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     public final static int ACCESS_UNKNOWN = 0;
80     // User has accepted the connection and let Settings app remember the decision
81     public final static int ACCESS_ALLOWED = 1;
82     // User has rejected the connection and let Settings app remember the decision
83     public final static int ACCESS_REJECTED = 2;
84
85     // How many times user should reject the connection to make the choice persist.
86     private final static int MESSAGE_REJECTION_COUNT_LIMIT_TO_PERSIST = 2;
87
88     private final static String MESSAGE_REJECTION_COUNT_PREFS_NAME = "bluetooth_message_reject";
89
90     /**
91      * When we connect to multiple profiles, we only want to display a single
92      * error even if they all fail. This tracks that state.
93      */
94     private boolean mIsConnectingErrorPossible;
95
96     /**
97      * Last time a bt profile auto-connect was attempted.
98      * If an ACTION_UUID intent comes in within
99      * MAX_UUID_DELAY_FOR_AUTO_CONNECT milliseconds, we will try auto-connect
100      * again with the new UUIDs
101      */
102     private long mConnectAttempted;
103
104     // See mConnectAttempted
105     private static final long MAX_UUID_DELAY_FOR_AUTO_CONNECT = 5000;
106     private static final long MAX_HOGP_DELAY_FOR_AUTO_CONNECT = 30000;
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         return true;
304     }
305
306     /**
307      * Return true if user initiated pairing on this device. The message text is
308      * slightly different for local vs. remote initiated pairing dialogs.
309      */
310     boolean isUserInitiatedPairing() {
311         return mDevice.isBondingInitiatedLocally();
312     }
313
314     public void unpair() {
315         int state = getBondState();
316
317         if (state == BluetoothDevice.BOND_BONDING) {
318             mDevice.cancelBondProcess();
319         }
320
321         if (state != BluetoothDevice.BOND_NONE) {
322             final BluetoothDevice dev = mDevice;
323             if (dev != null) {
324                 final boolean successful = dev.removeBond();
325                 if (successful) {
326                     if (Utils.D) {
327                         Log.d(TAG, "Command sent successfully:REMOVE_BOND " + describe(null));
328                     }
329                 } else if (Utils.V) {
330                     Log.v(TAG, "Framework rejected command immediately:REMOVE_BOND " +
331                             describe(null));
332                 }
333             }
334         }
335     }
336
337     public int getProfileConnectionState(LocalBluetoothProfile profile) {
338         if (mProfileConnectionState == null ||
339                 mProfileConnectionState.get(profile) == null) {
340             // If cache is empty make the binder call to get the state
341             int state = profile.getConnectionStatus(mDevice);
342             mProfileConnectionState.put(profile, state);
343         }
344         return mProfileConnectionState.get(profile);
345     }
346
347     public void clearProfileConnectionState ()
348     {
349         if (Utils.D) {
350             Log.d(TAG," Clearing all connection state for dev:" + mDevice.getName());
351         }
352         for (LocalBluetoothProfile profile :getProfiles()) {
353             mProfileConnectionState.put(profile, BluetoothProfile.STATE_DISCONNECTED);
354         }
355     }
356
357     // TODO: do any of these need to run async on a background thread?
358     private void fillData() {
359         fetchName();
360         fetchBtClass();
361         updateProfiles();
362         migratePhonebookPermissionChoice();
363         migrateMessagePermissionChoice();
364         fetchMessageRejectionCount();
365
366         dispatchAttributesChanged();
367     }
368
369     public BluetoothDevice getDevice() {
370         return mDevice;
371     }
372
373     /**
374      * Convenience method that can be mocked - it lets tests avoid having to call getDevice() which
375      * causes problems in tests since BluetoothDevice is final and cannot be mocked.
376      * @return the address of this device
377      */
378     public String getAddress() {
379         return mDevice.getAddress();
380     }
381
382     public String getName() {
383         return mName;
384     }
385
386     /**
387      * Populate name from BluetoothDevice.ACTION_FOUND intent
388      */
389     void setNewName(String name) {
390         if (mName == null) {
391             mName = name;
392             if (mName == null || TextUtils.isEmpty(mName)) {
393                 mName = mDevice.getAddress();
394             }
395             dispatchAttributesChanged();
396         }
397     }
398
399     /**
400      * User changes the device name
401      * @param name new alias name to be set, should never be null
402      */
403     public void setName(String name) {
404         // Prevent mName to be set to null if setName(null) is called
405         if (name != null && !TextUtils.equals(name, mName)) {
406             mName = name;
407             mDevice.setAlias(name);
408             dispatchAttributesChanged();
409         }
410     }
411
412     void refreshName() {
413         fetchName();
414         dispatchAttributesChanged();
415     }
416
417     private void fetchName() {
418         mName = mDevice.getAliasName();
419
420         if (TextUtils.isEmpty(mName)) {
421             mName = mDevice.getAddress();
422             if (DEBUG) Log.d(TAG, "Device has no name (yet), use address: " + mName);
423         }
424     }
425
426     /**
427      * Checks if device has a human readable name besides MAC address
428      * @return true if device's alias name is not null nor empty, false otherwise
429      */
430     public boolean hasHumanReadableName() {
431         return !TextUtils.isEmpty(mDevice.getAliasName());
432     }
433
434     /**
435      * Get battery level from remote device
436      * @return battery level in percentage [0-100], or {@link BluetoothDevice#BATTERY_LEVEL_UNKNOWN}
437      */
438     public int getBatteryLevel() {
439         return mDevice.getBatteryLevel();
440     }
441
442     void refresh() {
443         dispatchAttributesChanged();
444     }
445
446     public void setJustDiscovered(boolean justDiscovered) {
447         if (mJustDiscovered != justDiscovered) {
448             mJustDiscovered = justDiscovered;
449             dispatchAttributesChanged();
450         }
451     }
452
453     public int getBondState() {
454         return mDevice.getBondState();
455     }
456
457     void setRssi(short rssi) {
458         if (mRssi != rssi) {
459             mRssi = rssi;
460             dispatchAttributesChanged();
461         }
462     }
463
464     /**
465      * Checks whether we are connected to this device (any profile counts).
466      *
467      * @return Whether it is connected.
468      */
469     public boolean isConnected() {
470         for (LocalBluetoothProfile profile : mProfiles) {
471             int status = getProfileConnectionState(profile);
472             if (status == BluetoothProfile.STATE_CONNECTED) {
473                 return true;
474             }
475         }
476
477         return false;
478     }
479
480     public boolean isConnectedProfile(LocalBluetoothProfile profile) {
481         int status = getProfileConnectionState(profile);
482         return status == BluetoothProfile.STATE_CONNECTED;
483
484     }
485
486     public boolean isBusy() {
487         for (LocalBluetoothProfile profile : mProfiles) {
488             int status = getProfileConnectionState(profile);
489             if (status == BluetoothProfile.STATE_CONNECTING
490                     || status == BluetoothProfile.STATE_DISCONNECTING) {
491                 return true;
492             }
493         }
494         return getBondState() == BluetoothDevice.BOND_BONDING;
495     }
496
497     /**
498      * Fetches a new value for the cached BT class.
499      */
500     private void fetchBtClass() {
501         mBtClass = mDevice.getBluetoothClass();
502     }
503
504     private boolean updateProfiles() {
505         ParcelUuid[] uuids = mDevice.getUuids();
506         if (uuids == null) return false;
507
508         ParcelUuid[] localUuids = mLocalAdapter.getUuids();
509         if (localUuids == null) return false;
510
511         /*
512          * Now we know if the device supports PBAP, update permissions...
513          */
514         processPhonebookAccess();
515
516         mProfileManager.updateProfiles(uuids, localUuids, mProfiles, mRemovedProfiles,
517                                        mLocalNapRoleConnected, mDevice);
518
519         if (DEBUG) {
520             Log.e(TAG, "updating profiles for " + mDevice.getAliasName());
521             BluetoothClass bluetoothClass = mDevice.getBluetoothClass();
522
523             if (bluetoothClass != null) Log.v(TAG, "Class: " + bluetoothClass.toString());
524             Log.v(TAG, "UUID:");
525             for (ParcelUuid uuid : uuids) {
526                 Log.v(TAG, "  " + uuid);
527             }
528         }
529         return true;
530     }
531
532     /**
533      * Refreshes the UI for the BT class, including fetching the latest value
534      * for the class.
535      */
536     void refreshBtClass() {
537         fetchBtClass();
538         dispatchAttributesChanged();
539     }
540
541     /**
542      * Refreshes the UI when framework alerts us of a UUID change.
543      */
544     void onUuidChanged() {
545         updateProfiles();
546         ParcelUuid[] uuids = mDevice.getUuids();
547
548         long timeout = MAX_UUID_DELAY_FOR_AUTO_CONNECT;
549         if (BluetoothUuid.isUuidPresent(uuids, BluetoothUuid.Hogp)) {
550             timeout = MAX_HOGP_DELAY_FOR_AUTO_CONNECT;
551         }
552
553         if (DEBUG) {
554             Log.d(TAG, "onUuidChanged: Time since last connect"
555                     + (SystemClock.elapsedRealtime() - mConnectAttempted));
556         }
557
558         /*
559          * If a connect was attempted earlier without any UUID, we will do the connect now.
560          * Otherwise, allow the connect on UUID change.
561          */
562         if (!mProfiles.isEmpty()
563                 && ((mConnectAttempted + timeout) > SystemClock.elapsedRealtime())) {
564             connectWithoutResettingTimer(false);
565         }
566
567         dispatchAttributesChanged();
568     }
569
570     void onBondingStateChanged(int bondState) {
571         if (bondState == BluetoothDevice.BOND_NONE) {
572             mProfiles.clear();
573             setPhonebookPermissionChoice(ACCESS_UNKNOWN);
574             setMessagePermissionChoice(ACCESS_UNKNOWN);
575             setSimPermissionChoice(ACCESS_UNKNOWN);
576             mMessageRejectionCount = 0;
577             saveMessageRejectionCount();
578         }
579
580         refresh();
581
582         if (bondState == BluetoothDevice.BOND_BONDED) {
583             if (mDevice.isBluetoothDock()) {
584                 onBondingDockConnect();
585             } else if (mDevice.isBondingInitiatedLocally()) {
586                 connect(false);
587             }
588         }
589     }
590
591     void setBtClass(BluetoothClass btClass) {
592         if (btClass != null && mBtClass != btClass) {
593             mBtClass = btClass;
594             dispatchAttributesChanged();
595         }
596     }
597
598     public BluetoothClass getBtClass() {
599         return mBtClass;
600     }
601
602     public List<LocalBluetoothProfile> getProfiles() {
603         return Collections.unmodifiableList(mProfiles);
604     }
605
606     public List<LocalBluetoothProfile> getConnectableProfiles() {
607         List<LocalBluetoothProfile> connectableProfiles =
608                 new ArrayList<LocalBluetoothProfile>();
609         for (LocalBluetoothProfile profile : mProfiles) {
610             if (profile.isConnectable()) {
611                 connectableProfiles.add(profile);
612             }
613         }
614         return connectableProfiles;
615     }
616
617     public List<LocalBluetoothProfile> getRemovedProfiles() {
618         return mRemovedProfiles;
619     }
620
621     public void registerCallback(Callback callback) {
622         synchronized (mCallbacks) {
623             mCallbacks.add(callback);
624         }
625     }
626
627     public void unregisterCallback(Callback callback) {
628         synchronized (mCallbacks) {
629             mCallbacks.remove(callback);
630         }
631     }
632
633     private void dispatchAttributesChanged() {
634         synchronized (mCallbacks) {
635             for (Callback callback : mCallbacks) {
636                 callback.onDeviceAttributesChanged();
637             }
638         }
639     }
640
641     @Override
642     public String toString() {
643         return mDevice.toString();
644     }
645
646     @Override
647     public boolean equals(Object o) {
648         if ((o == null) || !(o instanceof CachedBluetoothDevice)) {
649             return false;
650         }
651         return mDevice.equals(((CachedBluetoothDevice) o).mDevice);
652     }
653
654     @Override
655     public int hashCode() {
656         return mDevice.getAddress().hashCode();
657     }
658
659     // This comparison uses non-final fields so the sort order may change
660     // when device attributes change (such as bonding state). Settings
661     // will completely refresh the device list when this happens.
662     public int compareTo(CachedBluetoothDevice another) {
663         // Connected above not connected
664         int comparison = (another.isConnected() ? 1 : 0) - (isConnected() ? 1 : 0);
665         if (comparison != 0) return comparison;
666
667         // Paired above not paired
668         comparison = (another.getBondState() == BluetoothDevice.BOND_BONDED ? 1 : 0) -
669             (getBondState() == BluetoothDevice.BOND_BONDED ? 1 : 0);
670         if (comparison != 0) return comparison;
671
672         // Just discovered above discovered in the past
673         comparison = (another.mJustDiscovered ? 1 : 0) - (mJustDiscovered ? 1 : 0);
674         if (comparison != 0) return comparison;
675
676         // Stronger signal above weaker signal
677         comparison = another.mRssi - mRssi;
678         if (comparison != 0) return comparison;
679
680         // Fallback on name
681         return mName.compareTo(another.mName);
682     }
683
684     public interface Callback {
685         void onDeviceAttributesChanged();
686     }
687
688     public int getPhonebookPermissionChoice() {
689         int permission = mDevice.getPhonebookAccessPermission();
690         if (permission == BluetoothDevice.ACCESS_ALLOWED) {
691             return ACCESS_ALLOWED;
692         } else if (permission == BluetoothDevice.ACCESS_REJECTED) {
693             return ACCESS_REJECTED;
694         }
695         return ACCESS_UNKNOWN;
696     }
697
698     public void setPhonebookPermissionChoice(int permissionChoice) {
699         int permission = BluetoothDevice.ACCESS_UNKNOWN;
700         if (permissionChoice == ACCESS_ALLOWED) {
701             permission = BluetoothDevice.ACCESS_ALLOWED;
702         } else if (permissionChoice == ACCESS_REJECTED) {
703             permission = BluetoothDevice.ACCESS_REJECTED;
704         }
705         mDevice.setPhonebookAccessPermission(permission);
706     }
707
708     // Migrates data from old data store (in Settings app's shared preferences) to new (in Bluetooth
709     // app's shared preferences).
710     private void migratePhonebookPermissionChoice() {
711         SharedPreferences preferences = mContext.getSharedPreferences(
712                 "bluetooth_phonebook_permission", Context.MODE_PRIVATE);
713         if (!preferences.contains(mDevice.getAddress())) {
714             return;
715         }
716
717         if (mDevice.getPhonebookAccessPermission() == BluetoothDevice.ACCESS_UNKNOWN) {
718             int oldPermission = preferences.getInt(mDevice.getAddress(), ACCESS_UNKNOWN);
719             if (oldPermission == ACCESS_ALLOWED) {
720                 mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_ALLOWED);
721             } else if (oldPermission == ACCESS_REJECTED) {
722                 mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_REJECTED);
723             }
724         }
725
726         SharedPreferences.Editor editor = preferences.edit();
727         editor.remove(mDevice.getAddress());
728         editor.commit();
729     }
730
731     public int getMessagePermissionChoice() {
732         int permission = mDevice.getMessageAccessPermission();
733         if (permission == BluetoothDevice.ACCESS_ALLOWED) {
734             return ACCESS_ALLOWED;
735         } else if (permission == BluetoothDevice.ACCESS_REJECTED) {
736             return ACCESS_REJECTED;
737         }
738         return ACCESS_UNKNOWN;
739     }
740
741     public void setMessagePermissionChoice(int permissionChoice) {
742         int permission = BluetoothDevice.ACCESS_UNKNOWN;
743         if (permissionChoice == ACCESS_ALLOWED) {
744             permission = BluetoothDevice.ACCESS_ALLOWED;
745         } else if (permissionChoice == ACCESS_REJECTED) {
746             permission = BluetoothDevice.ACCESS_REJECTED;
747         }
748         mDevice.setMessageAccessPermission(permission);
749     }
750
751     public int getSimPermissionChoice() {
752         int permission = mDevice.getSimAccessPermission();
753         if (permission == BluetoothDevice.ACCESS_ALLOWED) {
754             return ACCESS_ALLOWED;
755         } else if (permission == BluetoothDevice.ACCESS_REJECTED) {
756             return ACCESS_REJECTED;
757         }
758         return ACCESS_UNKNOWN;
759     }
760
761     void setSimPermissionChoice(int permissionChoice) {
762         int permission = BluetoothDevice.ACCESS_UNKNOWN;
763         if (permissionChoice == ACCESS_ALLOWED) {
764             permission = BluetoothDevice.ACCESS_ALLOWED;
765         } else if (permissionChoice == ACCESS_REJECTED) {
766             permission = BluetoothDevice.ACCESS_REJECTED;
767         }
768         mDevice.setSimAccessPermission(permission);
769     }
770
771     // Migrates data from old data store (in Settings app's shared preferences) to new (in Bluetooth
772     // app's shared preferences).
773     private void migrateMessagePermissionChoice() {
774         SharedPreferences preferences = mContext.getSharedPreferences(
775                 "bluetooth_message_permission", Context.MODE_PRIVATE);
776         if (!preferences.contains(mDevice.getAddress())) {
777             return;
778         }
779
780         if (mDevice.getMessageAccessPermission() == BluetoothDevice.ACCESS_UNKNOWN) {
781             int oldPermission = preferences.getInt(mDevice.getAddress(), ACCESS_UNKNOWN);
782             if (oldPermission == ACCESS_ALLOWED) {
783                 mDevice.setMessageAccessPermission(BluetoothDevice.ACCESS_ALLOWED);
784             } else if (oldPermission == ACCESS_REJECTED) {
785                 mDevice.setMessageAccessPermission(BluetoothDevice.ACCESS_REJECTED);
786             }
787         }
788
789         SharedPreferences.Editor editor = preferences.edit();
790         editor.remove(mDevice.getAddress());
791         editor.commit();
792     }
793
794     /**
795      * @return Whether this rejection should persist.
796      */
797     public boolean checkAndIncreaseMessageRejectionCount() {
798         if (mMessageRejectionCount < MESSAGE_REJECTION_COUNT_LIMIT_TO_PERSIST) {
799             mMessageRejectionCount++;
800             saveMessageRejectionCount();
801         }
802         return mMessageRejectionCount >= MESSAGE_REJECTION_COUNT_LIMIT_TO_PERSIST;
803     }
804
805     private void fetchMessageRejectionCount() {
806         SharedPreferences preference = mContext.getSharedPreferences(
807                 MESSAGE_REJECTION_COUNT_PREFS_NAME, Context.MODE_PRIVATE);
808         mMessageRejectionCount = preference.getInt(mDevice.getAddress(), 0);
809     }
810
811     private void saveMessageRejectionCount() {
812         SharedPreferences.Editor editor = mContext.getSharedPreferences(
813                 MESSAGE_REJECTION_COUNT_PREFS_NAME, Context.MODE_PRIVATE).edit();
814         if (mMessageRejectionCount == 0) {
815             editor.remove(mDevice.getAddress());
816         } else {
817             editor.putInt(mDevice.getAddress(), mMessageRejectionCount);
818         }
819         editor.commit();
820     }
821
822     private void processPhonebookAccess() {
823         if (mDevice.getBondState() != BluetoothDevice.BOND_BONDED) return;
824
825         ParcelUuid[] uuids = mDevice.getUuids();
826         if (BluetoothUuid.containsAnyUuid(uuids, PbapServerProfile.PBAB_CLIENT_UUIDS)) {
827             // The pairing dialog now warns of phone-book access for paired devices.
828             // No separate prompt is displayed after pairing.
829             if (getPhonebookPermissionChoice() == CachedBluetoothDevice.ACCESS_UNKNOWN) {
830                 if (mDevice.getBluetoothClass().getDeviceClass()
831                         == BluetoothClass.Device.AUDIO_VIDEO_HANDSFREE ||
832                     mDevice.getBluetoothClass().getDeviceClass()
833                         == BluetoothClass.Device.AUDIO_VIDEO_WEARABLE_HEADSET) {
834                     setPhonebookPermissionChoice(CachedBluetoothDevice.ACCESS_ALLOWED);
835                 } else {
836                     setPhonebookPermissionChoice(CachedBluetoothDevice.ACCESS_REJECTED);
837                 }
838             }
839         }
840     }
841
842     public int getMaxConnectionState() {
843         int maxState = BluetoothProfile.STATE_DISCONNECTED;
844         for (LocalBluetoothProfile profile : getProfiles()) {
845             int connectionStatus = getProfileConnectionState(profile);
846             if (connectionStatus > maxState) {
847                 maxState = connectionStatus;
848             }
849         }
850         return maxState;
851     }
852
853     /**
854      * @return resource for string that discribes the connection state of this device.
855      */
856     public String getConnectionSummary() {
857         boolean profileConnected = false;       // at least one profile is connected
858         boolean a2dpNotConnected = false;       // A2DP is preferred but not connected
859         boolean hfpNotConnected = false;    // HFP is preferred but not connected
860
861         for (LocalBluetoothProfile profile : getProfiles()) {
862             int connectionStatus = getProfileConnectionState(profile);
863
864             switch (connectionStatus) {
865                 case BluetoothProfile.STATE_CONNECTING:
866                 case BluetoothProfile.STATE_DISCONNECTING:
867                     return mContext.getString(Utils.getConnectionStateSummary(connectionStatus));
868
869                 case BluetoothProfile.STATE_CONNECTED:
870                     profileConnected = true;
871                     break;
872
873                 case BluetoothProfile.STATE_DISCONNECTED:
874                     if (profile.isProfileReady()) {
875                         if ((profile instanceof A2dpProfile) ||
876                             (profile instanceof A2dpSinkProfile)){
877                             a2dpNotConnected = true;
878                         } else if ((profile instanceof HeadsetProfile) ||
879                                    (profile instanceof HfpClientProfile)) {
880                             hfpNotConnected = true;
881                         }
882                     }
883                     break;
884             }
885         }
886
887         String batteryLevelPercentageString = null;
888         // Android framework should only set mBatteryLevel to valid range [0-100] or
889         // BluetoothDevice.BATTERY_LEVEL_UNKNOWN, any other value should be a framework bug.
890         // Thus assume here that if value is not BluetoothDevice.BATTERY_LEVEL_UNKNOWN, it must
891         // be valid
892         final int batteryLevel = getBatteryLevel();
893         if (batteryLevel != BluetoothDevice.BATTERY_LEVEL_UNKNOWN) {
894             // TODO: name com.android.settingslib.bluetooth.Utils something different
895             batteryLevelPercentageString =
896                     com.android.settingslib.Utils.formatPercentage(batteryLevel);
897         }
898
899         if (profileConnected) {
900             if (a2dpNotConnected && hfpNotConnected) {
901                 if (batteryLevelPercentageString != null) {
902                     return mContext.getString(
903                             R.string.bluetooth_connected_no_headset_no_a2dp_battery_level,
904                             batteryLevelPercentageString);
905                 } else {
906                     return mContext.getString(R.string.bluetooth_connected_no_headset_no_a2dp);
907                 }
908
909             } else if (a2dpNotConnected) {
910                 if (batteryLevelPercentageString != null) {
911                     return mContext.getString(R.string.bluetooth_connected_no_a2dp_battery_level,
912                             batteryLevelPercentageString);
913                 } else {
914                     return mContext.getString(R.string.bluetooth_connected_no_a2dp);
915                 }
916
917             } else if (hfpNotConnected) {
918                 if (batteryLevelPercentageString != null) {
919                     return mContext.getString(R.string.bluetooth_connected_no_headset_battery_level,
920                             batteryLevelPercentageString);
921                 } else {
922                     return mContext.getString(R.string.bluetooth_connected_no_headset);
923                 }
924             } else {
925                 if (batteryLevelPercentageString != null) {
926                     return mContext.getString(R.string.bluetooth_connected_battery_level,
927                             batteryLevelPercentageString);
928                 } else {
929                     return mContext.getString(R.string.bluetooth_connected);
930                 }
931             }
932         }
933
934         return getBondState() == BluetoothDevice.BOND_BONDING ?
935                 mContext.getString(R.string.bluetooth_pairing) : null;
936     }
937 }