• 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.settings.bluetooth;
18 
19 import android.bluetooth.BluetoothClass;
20 import android.bluetooth.BluetoothDevice;
21 import android.bluetooth.BluetoothProfile;
22 import android.content.Context;
23 import android.content.SharedPreferences;
24 import android.os.ParcelUuid;
25 import android.os.SystemClock;
26 import android.text.TextUtils;
27 import android.util.Log;
28 
29 import java.util.ArrayList;
30 import java.util.Collection;
31 import java.util.Collections;
32 import java.util.HashMap;
33 import java.util.List;
34 
35 /**
36  * CachedBluetoothDevice represents a remote Bluetooth device. It contains
37  * attributes of the device (such as the address, name, RSSI, etc.) and
38  * functionality that can be performed on the device (connect, pair, disconnect,
39  * etc.).
40  */
41 final class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> {
42     private static final String TAG = "CachedBluetoothDevice";
43     private static final boolean DEBUG = Utils.V;
44 
45     private final Context mContext;
46     private final LocalBluetoothAdapter mLocalAdapter;
47     private final LocalBluetoothProfileManager mProfileManager;
48     private final BluetoothDevice mDevice;
49     private String mName;
50     private short mRssi;
51     private BluetoothClass mBtClass;
52     private HashMap<LocalBluetoothProfile, Integer> mProfileConnectionState;
53 
54     private final List<LocalBluetoothProfile> mProfiles =
55             new ArrayList<LocalBluetoothProfile>();
56 
57     // List of profiles that were previously in mProfiles, but have been removed
58     private final List<LocalBluetoothProfile> mRemovedProfiles =
59             new ArrayList<LocalBluetoothProfile>();
60 
61     // Device supports PANU but not NAP: remove PanProfile after device disconnects from NAP
62     private boolean mLocalNapRoleConnected;
63 
64     private boolean mVisible;
65 
66     private int mPhonebookPermissionChoice;
67 
68     private final Collection<Callback> mCallbacks = new ArrayList<Callback>();
69 
70     // Following constants indicate the user's choices of Phone book access settings
71     // User hasn't made any choice or settings app has wiped out the memory
72     final static int PHONEBOOK_ACCESS_UNKNOWN = 0;
73     // User has accepted the connection and let Settings app remember the decision
74     final static int PHONEBOOK_ACCESS_ALLOWED = 1;
75     // User has rejected the connection and let Settings app remember the decision
76     final static int PHONEBOOK_ACCESS_REJECTED = 2;
77 
78     private final static String PHONEBOOK_PREFS_NAME = "bluetooth_phonebook_permission";
79 
80     /**
81      * When we connect to multiple profiles, we only want to display a single
82      * error even if they all fail. This tracks that state.
83      */
84     private boolean mIsConnectingErrorPossible;
85 
86     /**
87      * Last time a bt profile auto-connect was attempted.
88      * If an ACTION_UUID intent comes in within
89      * MAX_UUID_DELAY_FOR_AUTO_CONNECT milliseconds, we will try auto-connect
90      * again with the new UUIDs
91      */
92     private long mConnectAttempted;
93 
94     // See mConnectAttempted
95     private static final long MAX_UUID_DELAY_FOR_AUTO_CONNECT = 5000;
96 
97     /** Auto-connect after pairing only if locally initiated. */
98     private boolean mConnectAfterPairing;
99 
100     /**
101      * Describes the current device and profile for logging.
102      *
103      * @param profile Profile to describe
104      * @return Description of the device and profile
105      */
describe(LocalBluetoothProfile profile)106     private String describe(LocalBluetoothProfile profile) {
107         StringBuilder sb = new StringBuilder();
108         sb.append("Address:").append(mDevice);
109         if (profile != null) {
110             sb.append(" Profile:").append(profile);
111         }
112 
113         return sb.toString();
114     }
115 
onProfileStateChanged(LocalBluetoothProfile profile, int newProfileState)116     void onProfileStateChanged(LocalBluetoothProfile profile, int newProfileState) {
117         if (Utils.D) {
118             Log.d(TAG, "onProfileStateChanged: profile " + profile +
119                     " newProfileState " + newProfileState);
120         }
121 
122         mProfileConnectionState.put(profile, newProfileState);
123         if (newProfileState == BluetoothProfile.STATE_CONNECTED) {
124             if (!mProfiles.contains(profile)) {
125                 mRemovedProfiles.remove(profile);
126                 mProfiles.add(profile);
127                 if (profile instanceof PanProfile &&
128                         ((PanProfile) profile).isLocalRoleNap(mDevice)) {
129                     // Device doesn't support NAP, so remove PanProfile on disconnect
130                     mLocalNapRoleConnected = true;
131                 }
132             }
133         } else if (mLocalNapRoleConnected && profile instanceof PanProfile &&
134                 ((PanProfile) profile).isLocalRoleNap(mDevice) &&
135                 newProfileState == BluetoothProfile.STATE_DISCONNECTED) {
136             Log.d(TAG, "Removing PanProfile from device after NAP disconnect");
137             mProfiles.remove(profile);
138             mRemovedProfiles.add(profile);
139             mLocalNapRoleConnected = false;
140         }
141     }
142 
CachedBluetoothDevice(Context context, LocalBluetoothAdapter adapter, LocalBluetoothProfileManager profileManager, BluetoothDevice device)143     CachedBluetoothDevice(Context context,
144                           LocalBluetoothAdapter adapter,
145                           LocalBluetoothProfileManager profileManager,
146                           BluetoothDevice device) {
147         mContext = context;
148         mLocalAdapter = adapter;
149         mProfileManager = profileManager;
150         mDevice = device;
151         mProfileConnectionState = new HashMap<LocalBluetoothProfile, Integer>();
152         fillData();
153     }
154 
disconnect()155     void disconnect() {
156         for (LocalBluetoothProfile profile : mProfiles) {
157             disconnect(profile);
158         }
159     }
160 
disconnect(LocalBluetoothProfile profile)161     void disconnect(LocalBluetoothProfile profile) {
162         if (profile.disconnect(mDevice)) {
163             if (Utils.D) {
164                 Log.d(TAG, "Command sent successfully:DISCONNECT " + describe(profile));
165             }
166         }
167     }
168 
connect(boolean connectAllProfiles)169     void connect(boolean connectAllProfiles) {
170         if (!ensurePaired()) {
171             return;
172         }
173 
174         mConnectAttempted = SystemClock.elapsedRealtime();
175         connectWithoutResettingTimer(connectAllProfiles);
176     }
177 
onBondingDockConnect()178     void onBondingDockConnect() {
179         // Attempt to connect if UUIDs are available. Otherwise,
180         // we will connect when the ACTION_UUID intent arrives.
181         connect(false);
182     }
183 
connectWithoutResettingTimer(boolean connectAllProfiles)184     private void connectWithoutResettingTimer(boolean connectAllProfiles) {
185         // Try to initialize the profiles if they were not.
186         if (mProfiles.isEmpty()) {
187             if (!updateProfiles()) {
188                 // If UUIDs are not available yet, connect will be happen
189                 // upon arrival of the ACTION_UUID intent.
190                 if (DEBUG) Log.d(TAG, "No profiles. Maybe we will connect later");
191                 return;
192             }
193         }
194 
195         // Reset the only-show-one-error-dialog tracking variable
196         mIsConnectingErrorPossible = true;
197 
198         int preferredProfiles = 0;
199         for (LocalBluetoothProfile profile : mProfiles) {
200             if (connectAllProfiles ? profile.isConnectable() : profile.isAutoConnectable()) {
201                 if (profile.isPreferred(mDevice)) {
202                     ++preferredProfiles;
203                     connectInt(profile);
204                 }
205             }
206         }
207         if (DEBUG) Log.d(TAG, "Preferred profiles = " + preferredProfiles);
208 
209         if (preferredProfiles == 0) {
210             connectAutoConnectableProfiles();
211         }
212     }
213 
connectAutoConnectableProfiles()214     private void connectAutoConnectableProfiles() {
215         if (!ensurePaired()) {
216             return;
217         }
218         // Reset the only-show-one-error-dialog tracking variable
219         mIsConnectingErrorPossible = true;
220 
221         for (LocalBluetoothProfile profile : mProfiles) {
222             if (profile.isAutoConnectable()) {
223                 profile.setPreferred(mDevice, true);
224                 connectInt(profile);
225             }
226         }
227     }
228 
229     /**
230      * Connect this device to the specified profile.
231      *
232      * @param profile the profile to use with the remote device
233      */
connectProfile(LocalBluetoothProfile profile)234     void connectProfile(LocalBluetoothProfile profile) {
235         mConnectAttempted = SystemClock.elapsedRealtime();
236         // Reset the only-show-one-error-dialog tracking variable
237         mIsConnectingErrorPossible = true;
238         connectInt(profile);
239     }
240 
connectInt(LocalBluetoothProfile profile)241     private void connectInt(LocalBluetoothProfile profile) {
242         if (!ensurePaired()) {
243             return;
244         }
245         if (profile.connect(mDevice)) {
246             if (Utils.D) {
247                 Log.d(TAG, "Command sent successfully:CONNECT " + describe(profile));
248             }
249             return;
250         }
251         Log.i(TAG, "Failed to connect " + profile.toString() + " to " + mName);
252     }
253 
ensurePaired()254     private boolean ensurePaired() {
255         if (getBondState() == BluetoothDevice.BOND_NONE) {
256             startPairing();
257             return false;
258         } else {
259             return true;
260         }
261     }
262 
startPairing()263     boolean startPairing() {
264         // Pairing is unreliable while scanning, so cancel discovery
265         if (mLocalAdapter.isDiscovering()) {
266             mLocalAdapter.cancelDiscovery();
267         }
268 
269         if (!mDevice.createBond()) {
270             return false;
271         }
272 
273         mConnectAfterPairing = true;  // auto-connect after pairing
274         return true;
275     }
276 
277     /**
278      * Return true if user initiated pairing on this device. The message text is
279      * slightly different for local vs. remote initiated pairing dialogs.
280      */
isUserInitiatedPairing()281     boolean isUserInitiatedPairing() {
282         return mConnectAfterPairing;
283     }
284 
unpair()285     void unpair() {
286         disconnect();
287 
288         int state = getBondState();
289 
290         if (state == BluetoothDevice.BOND_BONDING) {
291             mDevice.cancelBondProcess();
292         }
293 
294         if (state != BluetoothDevice.BOND_NONE) {
295             final BluetoothDevice dev = mDevice;
296             if (dev != null) {
297                 final boolean successful = dev.removeBond();
298                 if (successful) {
299                     if (Utils.D) {
300                         Log.d(TAG, "Command sent successfully:REMOVE_BOND " + describe(null));
301                     }
302                 } else if (Utils.V) {
303                     Log.v(TAG, "Framework rejected command immediately:REMOVE_BOND " +
304                             describe(null));
305                 }
306             }
307         }
308     }
309 
getProfileConnectionState(LocalBluetoothProfile profile)310     int getProfileConnectionState(LocalBluetoothProfile profile) {
311         if (mProfileConnectionState == null ||
312                 mProfileConnectionState.get(profile) == null) {
313             // If cache is empty make the binder call to get the state
314             int state = profile.getConnectionStatus(mDevice);
315             mProfileConnectionState.put(profile, state);
316         }
317         return mProfileConnectionState.get(profile);
318     }
319 
320     // TODO: do any of these need to run async on a background thread?
fillData()321     private void fillData() {
322         fetchName();
323         fetchBtClass();
324         updateProfiles();
325         fetchPhonebookPermissionChoice();
326 
327         mVisible = false;
328         dispatchAttributesChanged();
329     }
330 
getDevice()331     BluetoothDevice getDevice() {
332         return mDevice;
333     }
334 
getName()335     String getName() {
336         return mName;
337     }
338 
setName(String name)339     void setName(String name) {
340         if (!mName.equals(name)) {
341             if (TextUtils.isEmpty(name)) {
342                 // TODO: use friendly name for unknown device (bug 1181856)
343                 mName = mDevice.getAddress();
344             } else {
345                 mName = name;
346                 mDevice.setAlias(name);
347             }
348             dispatchAttributesChanged();
349         }
350     }
351 
refreshName()352     void refreshName() {
353         fetchName();
354         dispatchAttributesChanged();
355     }
356 
fetchName()357     private void fetchName() {
358         mName = mDevice.getAliasName();
359 
360         if (TextUtils.isEmpty(mName)) {
361             mName = mDevice.getAddress();
362             if (DEBUG) Log.d(TAG, "Device has no name (yet), use address: " + mName);
363         }
364     }
365 
refresh()366     void refresh() {
367         dispatchAttributesChanged();
368     }
369 
isVisible()370     boolean isVisible() {
371         return mVisible;
372     }
373 
setVisible(boolean visible)374     void setVisible(boolean visible) {
375         if (mVisible != visible) {
376             mVisible = visible;
377             dispatchAttributesChanged();
378         }
379     }
380 
getBondState()381     int getBondState() {
382         return mDevice.getBondState();
383     }
384 
setRssi(short rssi)385     void setRssi(short rssi) {
386         if (mRssi != rssi) {
387             mRssi = rssi;
388             dispatchAttributesChanged();
389         }
390     }
391 
392     /**
393      * Checks whether we are connected to this device (any profile counts).
394      *
395      * @return Whether it is connected.
396      */
isConnected()397     boolean isConnected() {
398         for (LocalBluetoothProfile profile : mProfiles) {
399             int status = getProfileConnectionState(profile);
400             if (status == BluetoothProfile.STATE_CONNECTED) {
401                 return true;
402             }
403         }
404 
405         return false;
406     }
407 
isConnectedProfile(LocalBluetoothProfile profile)408     boolean isConnectedProfile(LocalBluetoothProfile profile) {
409         int status = getProfileConnectionState(profile);
410         return status == BluetoothProfile.STATE_CONNECTED;
411 
412     }
413 
isBusy()414     boolean isBusy() {
415         for (LocalBluetoothProfile profile : mProfiles) {
416             int status = getProfileConnectionState(profile);
417             if (status == BluetoothProfile.STATE_CONNECTING
418                     || status == BluetoothProfile.STATE_DISCONNECTING) {
419                 return true;
420             }
421         }
422         return getBondState() == BluetoothDevice.BOND_BONDING;
423     }
424 
425     /**
426      * Fetches a new value for the cached BT class.
427      */
fetchBtClass()428     private void fetchBtClass() {
429         mBtClass = mDevice.getBluetoothClass();
430     }
431 
updateProfiles()432     private boolean updateProfiles() {
433         ParcelUuid[] uuids = mDevice.getUuids();
434         if (uuids == null) return false;
435 
436         ParcelUuid[] localUuids = mLocalAdapter.getUuids();
437         if (localUuids == null) return false;
438 
439         mProfileManager.updateProfiles(uuids, localUuids, mProfiles, mRemovedProfiles);
440 
441         if (DEBUG) {
442             Log.e(TAG, "updating profiles for " + mDevice.getAliasName());
443             BluetoothClass bluetoothClass = mDevice.getBluetoothClass();
444 
445             if (bluetoothClass != null) Log.v(TAG, "Class: " + bluetoothClass.toString());
446             Log.v(TAG, "UUID:");
447             for (ParcelUuid uuid : uuids) {
448                 Log.v(TAG, "  " + uuid);
449             }
450         }
451         return true;
452     }
453 
454     /**
455      * Refreshes the UI for the BT class, including fetching the latest value
456      * for the class.
457      */
refreshBtClass()458     void refreshBtClass() {
459         fetchBtClass();
460         dispatchAttributesChanged();
461     }
462 
463     /**
464      * Refreshes the UI when framework alerts us of a UUID change.
465      */
onUuidChanged()466     void onUuidChanged() {
467         updateProfiles();
468 
469         if (DEBUG) {
470             Log.e(TAG, "onUuidChanged: Time since last connect"
471                     + (SystemClock.elapsedRealtime() - mConnectAttempted));
472         }
473 
474         /*
475          * If a connect was attempted earlier without any UUID, we will do the
476          * connect now.
477          */
478         if (!mProfiles.isEmpty()
479                 && (mConnectAttempted + MAX_UUID_DELAY_FOR_AUTO_CONNECT) > SystemClock
480                         .elapsedRealtime()) {
481             connectWithoutResettingTimer(false);
482         }
483         dispatchAttributesChanged();
484     }
485 
onBondingStateChanged(int bondState)486     void onBondingStateChanged(int bondState) {
487         if (bondState == BluetoothDevice.BOND_NONE) {
488             mProfiles.clear();
489             mConnectAfterPairing = false;  // cancel auto-connect
490             setPhonebookPermissionChoice(PHONEBOOK_ACCESS_UNKNOWN);
491         }
492 
493         refresh();
494 
495         if (bondState == BluetoothDevice.BOND_BONDED) {
496             if (mDevice.isBluetoothDock()) {
497                 onBondingDockConnect();
498             } else if (mConnectAfterPairing) {
499                 connect(false);
500             }
501             mConnectAfterPairing = false;
502         }
503     }
504 
setBtClass(BluetoothClass btClass)505     void setBtClass(BluetoothClass btClass) {
506         if (btClass != null && mBtClass != btClass) {
507             mBtClass = btClass;
508             dispatchAttributesChanged();
509         }
510     }
511 
getBtClass()512     BluetoothClass getBtClass() {
513         return mBtClass;
514     }
515 
getProfiles()516     List<LocalBluetoothProfile> getProfiles() {
517         return Collections.unmodifiableList(mProfiles);
518     }
519 
getConnectableProfiles()520     List<LocalBluetoothProfile> getConnectableProfiles() {
521         List<LocalBluetoothProfile> connectableProfiles =
522                 new ArrayList<LocalBluetoothProfile>();
523         for (LocalBluetoothProfile profile : mProfiles) {
524             if (profile.isConnectable()) {
525                 connectableProfiles.add(profile);
526             }
527         }
528         return connectableProfiles;
529     }
530 
getRemovedProfiles()531     List<LocalBluetoothProfile> getRemovedProfiles() {
532         return mRemovedProfiles;
533     }
534 
registerCallback(Callback callback)535     void registerCallback(Callback callback) {
536         synchronized (mCallbacks) {
537             mCallbacks.add(callback);
538         }
539     }
540 
unregisterCallback(Callback callback)541     void unregisterCallback(Callback callback) {
542         synchronized (mCallbacks) {
543             mCallbacks.remove(callback);
544         }
545     }
546 
dispatchAttributesChanged()547     private void dispatchAttributesChanged() {
548         synchronized (mCallbacks) {
549             for (Callback callback : mCallbacks) {
550                 callback.onDeviceAttributesChanged();
551             }
552         }
553     }
554 
555     @Override
toString()556     public String toString() {
557         return mDevice.toString();
558     }
559 
560     @Override
equals(Object o)561     public boolean equals(Object o) {
562         if ((o == null) || !(o instanceof CachedBluetoothDevice)) {
563             return false;
564         }
565         return mDevice.equals(((CachedBluetoothDevice) o).mDevice);
566     }
567 
568     @Override
hashCode()569     public int hashCode() {
570         return mDevice.getAddress().hashCode();
571     }
572 
573     // This comparison uses non-final fields so the sort order may change
574     // when device attributes change (such as bonding state). Settings
575     // will completely refresh the device list when this happens.
compareTo(CachedBluetoothDevice another)576     public int compareTo(CachedBluetoothDevice another) {
577         // Connected above not connected
578         int comparison = (another.isConnected() ? 1 : 0) - (isConnected() ? 1 : 0);
579         if (comparison != 0) return comparison;
580 
581         // Paired above not paired
582         comparison = (another.getBondState() == BluetoothDevice.BOND_BONDED ? 1 : 0) -
583             (getBondState() == BluetoothDevice.BOND_BONDED ? 1 : 0);
584         if (comparison != 0) return comparison;
585 
586         // Visible above not visible
587         comparison = (another.mVisible ? 1 : 0) - (mVisible ? 1 : 0);
588         if (comparison != 0) return comparison;
589 
590         // Stronger signal above weaker signal
591         comparison = another.mRssi - mRssi;
592         if (comparison != 0) return comparison;
593 
594         // Fallback on name
595         return mName.compareTo(another.mName);
596     }
597 
598     public interface Callback {
onDeviceAttributesChanged()599         void onDeviceAttributesChanged();
600     }
601 
getPhonebookPermissionChoice()602     int getPhonebookPermissionChoice() {
603         return mPhonebookPermissionChoice;
604     }
605 
setPhonebookPermissionChoice(int permissionChoice)606     void setPhonebookPermissionChoice(int permissionChoice) {
607         SharedPreferences.Editor editor =
608             mContext.getSharedPreferences(PHONEBOOK_PREFS_NAME, Context.MODE_PRIVATE).edit();
609         if (permissionChoice == PHONEBOOK_ACCESS_UNKNOWN) {
610             editor.remove(mDevice.getAddress());
611         } else {
612             editor.putInt(mDevice.getAddress(), permissionChoice);
613         }
614         editor.commit();
615         mPhonebookPermissionChoice = permissionChoice;
616     }
617 
fetchPhonebookPermissionChoice()618     private void fetchPhonebookPermissionChoice() {
619         SharedPreferences preference = mContext.getSharedPreferences(PHONEBOOK_PREFS_NAME,
620                                                                      Context.MODE_PRIVATE);
621         mPhonebookPermissionChoice = preference.getInt(mDevice.getAddress(),
622                                                        PHONEBOOK_ACCESS_UNKNOWN);
623     }
624 
625 }
626