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