• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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     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      */
describe(LocalBluetoothProfile profile)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 
onProfileStateChanged(LocalBluetoothProfile profile, int newProfileState)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 
CachedBluetoothDevice(Context context, LocalBluetoothAdapter adapter, LocalBluetoothProfileManager profileManager, BluetoothDevice device)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 
disconnect()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 
disconnect(LocalBluetoothProfile profile)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 
connect(boolean connectAllProfiles)191     public void connect(boolean connectAllProfiles) {
192         if (!ensurePaired()) {
193             return;
194         }
195 
196         mConnectAttempted = SystemClock.elapsedRealtime();
197         connectWithoutResettingTimer(connectAllProfiles);
198     }
199 
onBondingDockConnect()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 
connectWithoutResettingTimer(boolean connectAllProfiles)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 
connectAutoConnectableProfiles()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      */
connectProfile(LocalBluetoothProfile profile)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 
connectInt(LocalBluetoothProfile profile)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 
ensurePaired()281     private boolean ensurePaired() {
282         if (getBondState() == BluetoothDevice.BOND_NONE) {
283             startPairing();
284             return false;
285         } else {
286             return true;
287         }
288     }
289 
startPairing()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      */
isUserInitiatedPairing()307     boolean isUserInitiatedPairing() {
308         return mDevice.isBondingInitiatedLocally();
309     }
310 
unpair()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 
getProfileConnectionState(LocalBluetoothProfile profile)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 
clearProfileConnectionState()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?
fillData()355     private void fillData() {
356         fetchName();
357         fetchBtClass();
358         updateProfiles();
359         migratePhonebookPermissionChoice();
360         migrateMessagePermissionChoice();
361         fetchMessageRejectionCount();
362 
363         mVisible = false;
364         dispatchAttributesChanged();
365     }
366 
getDevice()367     public BluetoothDevice getDevice() {
368         return mDevice;
369     }
370 
getName()371     public String getName() {
372         return mName;
373     }
374 
375     /**
376      * Populate name from BluetoothDevice.ACTION_FOUND intent
377      */
setNewName(String name)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      */
setName(String name)391     public void setName(String name) {
392         if (!mName.equals(name)) {
393             mName = name;
394             mDevice.setAlias(name);
395             dispatchAttributesChanged();
396         }
397     }
398 
refreshName()399     void refreshName() {
400         fetchName();
401         dispatchAttributesChanged();
402     }
403 
fetchName()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 
refresh()413     void refresh() {
414         dispatchAttributesChanged();
415     }
416 
isVisible()417     public boolean isVisible() {
418         return mVisible;
419     }
420 
setVisible(boolean visible)421     public void setVisible(boolean visible) {
422         if (mVisible != visible) {
423             mVisible = visible;
424             dispatchAttributesChanged();
425         }
426     }
427 
getBondState()428     public int getBondState() {
429         return mDevice.getBondState();
430     }
431 
setRssi(short rssi)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      */
isConnected()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 
isConnectedProfile(LocalBluetoothProfile profile)455     public boolean isConnectedProfile(LocalBluetoothProfile profile) {
456         int status = getProfileConnectionState(profile);
457         return status == BluetoothProfile.STATE_CONNECTED;
458 
459     }
460 
isBusy()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      */
fetchBtClass()475     private void fetchBtClass() {
476         mBtClass = mDevice.getBluetoothClass();
477     }
478 
updateProfiles()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      */
refreshBtClass()511     void refreshBtClass() {
512         fetchBtClass();
513         dispatchAttributesChanged();
514     }
515 
516     /**
517      * Refreshes the UI when framework alerts us of a UUID change.
518      */
onUuidChanged()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 
onBondingStateChanged(int bondState)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 
setBtClass(BluetoothClass btClass)566     void setBtClass(BluetoothClass btClass) {
567         if (btClass != null && mBtClass != btClass) {
568             mBtClass = btClass;
569             dispatchAttributesChanged();
570         }
571     }
572 
getBtClass()573     public BluetoothClass getBtClass() {
574         return mBtClass;
575     }
576 
getProfiles()577     public List<LocalBluetoothProfile> getProfiles() {
578         return Collections.unmodifiableList(mProfiles);
579     }
580 
getConnectableProfiles()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 
getRemovedProfiles()592     public List<LocalBluetoothProfile> getRemovedProfiles() {
593         return mRemovedProfiles;
594     }
595 
registerCallback(Callback callback)596     public void registerCallback(Callback callback) {
597         synchronized (mCallbacks) {
598             mCallbacks.add(callback);
599         }
600     }
601 
unregisterCallback(Callback callback)602     public void unregisterCallback(Callback callback) {
603         synchronized (mCallbacks) {
604             mCallbacks.remove(callback);
605         }
606     }
607 
dispatchAttributesChanged()608     private void dispatchAttributesChanged() {
609         synchronized (mCallbacks) {
610             for (Callback callback : mCallbacks) {
611                 callback.onDeviceAttributesChanged();
612             }
613         }
614     }
615 
616     @Override
toString()617     public String toString() {
618         return mDevice.toString();
619     }
620 
621     @Override
equals(Object o)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
hashCode()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.
compareTo(CachedBluetoothDevice another)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 {
onDeviceAttributesChanged()660         void onDeviceAttributesChanged();
661     }
662 
getPhonebookPermissionChoice()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 
setPhonebookPermissionChoice(int permissionChoice)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).
migratePhonebookPermissionChoice()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 
getMessagePermissionChoice()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 
setMessagePermissionChoice(int permissionChoice)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 
getSimPermissionChoice()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 
setSimPermissionChoice(int permissionChoice)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).
migrateMessagePermissionChoice()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      */
checkAndIncreaseMessageRejectionCount()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 
fetchMessageRejectionCount()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 
saveMessageRejectionCount()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 
processPhonebookAccess()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 
getMaxConnectionState()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      */
getConnectionSummary()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