2 * Copyright (C) 2008 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package com.android.settings.bluetooth;
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;
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;
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,
42 final class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> {
43 private static final String TAG = "CachedBluetoothDevice";
44 private static final boolean DEBUG = Utils.V;
46 private final Context mContext;
47 private final LocalBluetoothAdapter mLocalAdapter;
48 private final LocalBluetoothProfileManager mProfileManager;
49 private final BluetoothDevice mDevice;
52 private BluetoothClass mBtClass;
53 private HashMap<LocalBluetoothProfile, Integer> mProfileConnectionState;
55 private final List<LocalBluetoothProfile> mProfiles =
56 new ArrayList<LocalBluetoothProfile>();
58 // List of profiles that were previously in mProfiles, but have been removed
59 private final List<LocalBluetoothProfile> mRemovedProfiles =
60 new ArrayList<LocalBluetoothProfile>();
62 // Device supports PANU but not NAP: remove PanProfile after device disconnects from NAP
63 private boolean mLocalNapRoleConnected;
65 private boolean mVisible;
67 private int mPhonebookPermissionChoice;
69 private int mMessagePermissionChoice;
71 private int mMessageRejectedTimes;
73 private final Collection<Callback> mCallbacks = new ArrayList<Callback>();
75 // Following constants indicate the user's choices of Phone book/message access settings
76 // User hasn't made any choice or settings app has wiped out the memory
77 public final static int ACCESS_UNKNOWN = 0;
78 // User has accepted the connection and let Settings app remember the decision
79 public final static int ACCESS_ALLOWED = 1;
80 // User has rejected the connection and let Settings app remember the decision
81 public final static int ACCESS_REJECTED = 2;
83 // how many times did User reject the connection to make the rejected persist.
84 final static int PERSIST_REJECTED_TIMES_LIMIT = 2;
86 private final static String PHONEBOOK_PREFS_NAME = "bluetooth_phonebook_permission";
87 private final static String MESSAGE_PREFS_NAME = "bluetooth_message_permission";
88 private final static String MESSAGE_REJECT_TIMES = "bluetooth_message_reject";
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.
94 private boolean mIsConnectingErrorPossible;
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
102 private long mConnectAttempted;
104 // See mConnectAttempted
105 private static final long MAX_UUID_DELAY_FOR_AUTO_CONNECT = 5000;
107 /** Auto-connect after pairing only if locally initiated. */
108 private boolean mConnectAfterPairing;
111 * Describes the current device and profile for logging.
113 * @param profile Profile to describe
114 * @return Description of the device and profile
116 private String describe(LocalBluetoothProfile profile) {
117 StringBuilder sb = new StringBuilder();
118 sb.append("Address:").append(mDevice);
119 if (profile != null) {
120 sb.append(" Profile:").append(profile);
123 return sb.toString();
126 void onProfileStateChanged(LocalBluetoothProfile profile, int newProfileState) {
128 Log.d(TAG, "onProfileStateChanged: profile " + profile +
129 " newProfileState " + newProfileState);
131 if (mLocalAdapter.getBluetoothState() == BluetoothAdapter.STATE_TURNING_OFF)
133 if (Utils.D) Log.d(TAG, " BT Turninig Off...Profile conn state change ignored...");
136 mProfileConnectionState.put(profile, newProfileState);
137 if (newProfileState == BluetoothProfile.STATE_CONNECTED) {
138 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;
147 if (profile instanceof MapProfile) {
148 profile.setPreferred(mDevice, true);
150 } else if (profile instanceof MapProfile &&
151 newProfileState == BluetoothProfile.STATE_DISCONNECTED) {
152 if (mProfiles.contains(profile)) {
153 mRemovedProfiles.add(profile);
154 mProfiles.remove(profile);
156 profile.setPreferred(mDevice, false);
157 } else if (mLocalNapRoleConnected && profile instanceof PanProfile &&
158 ((PanProfile) profile).isLocalRoleNap(mDevice) &&
159 newProfileState == BluetoothProfile.STATE_DISCONNECTED) {
160 Log.d(TAG, "Removing PanProfile from device after NAP disconnect");
161 mProfiles.remove(profile);
162 mRemovedProfiles.add(profile);
163 mLocalNapRoleConnected = false;
167 CachedBluetoothDevice(Context context,
168 LocalBluetoothAdapter adapter,
169 LocalBluetoothProfileManager profileManager,
170 BluetoothDevice device) {
172 mLocalAdapter = adapter;
173 mProfileManager = profileManager;
175 mProfileConnectionState = new HashMap<LocalBluetoothProfile, Integer>();
180 for (LocalBluetoothProfile profile : mProfiles) {
183 // Disconnect PBAP server in case its connected
184 // This is to ensure all the profiles are disconnected as some CK/Hs do not
185 // disconnect PBAP connection when HF connection is brought down
186 PbapServerProfile PbapProfile = mProfileManager.getPbapProfile();
187 if (PbapProfile.getConnectionStatus(mDevice) == BluetoothProfile.STATE_CONNECTED)
189 PbapProfile.disconnect(mDevice);
193 void disconnect(LocalBluetoothProfile profile) {
194 if (profile.disconnect(mDevice)) {
196 Log.d(TAG, "Command sent successfully:DISCONNECT " + describe(profile));
201 void connect(boolean connectAllProfiles) {
202 if (!ensurePaired()) {
206 mConnectAttempted = SystemClock.elapsedRealtime();
207 connectWithoutResettingTimer(connectAllProfiles);
210 void onBondingDockConnect() {
211 // Attempt to connect if UUIDs are available. Otherwise,
212 // we will connect when the ACTION_UUID intent arrives.
216 private void connectWithoutResettingTimer(boolean connectAllProfiles) {
217 // Try to initialize the profiles if they were not.
218 if (mProfiles.isEmpty()) {
219 // if mProfiles is empty, then do not invoke updateProfiles. This causes a race
220 // condition with carkits during pairing, wherein RemoteDevice.UUIDs have been updated
221 // from bluetooth stack but ACTION.uuid is not sent yet.
222 // Eventually ACTION.uuid will be received which shall trigger the connection of the
224 // If UUIDs are not available yet, connect will be happen
225 // upon arrival of the ACTION_UUID intent.
226 Log.d(TAG, "No profiles. Maybe we will connect later");
230 // Reset the only-show-one-error-dialog tracking variable
231 mIsConnectingErrorPossible = true;
233 int preferredProfiles = 0;
234 for (LocalBluetoothProfile profile : mProfiles) {
235 if (connectAllProfiles ? profile.isConnectable() : profile.isAutoConnectable()) {
236 if (profile.isPreferred(mDevice)) {
242 if (DEBUG) Log.d(TAG, "Preferred profiles = " + preferredProfiles);
244 if (preferredProfiles == 0) {
245 connectAutoConnectableProfiles();
249 private void connectAutoConnectableProfiles() {
250 if (!ensurePaired()) {
253 // Reset the only-show-one-error-dialog tracking variable
254 mIsConnectingErrorPossible = true;
256 for (LocalBluetoothProfile profile : mProfiles) {
257 if (profile.isAutoConnectable()) {
258 profile.setPreferred(mDevice, true);
265 * Connect this device to the specified profile.
267 * @param profile the profile to use with the remote device
269 void connectProfile(LocalBluetoothProfile profile) {
270 mConnectAttempted = SystemClock.elapsedRealtime();
271 // Reset the only-show-one-error-dialog tracking variable
272 mIsConnectingErrorPossible = true;
274 // Refresh the UI based on profile.connect() call
278 synchronized void connectInt(LocalBluetoothProfile profile) {
279 if (!ensurePaired()) {
282 if (profile.connect(mDevice)) {
284 Log.d(TAG, "Command sent successfully:CONNECT " + describe(profile));
288 Log.i(TAG, "Failed to connect " + profile.toString() + " to " + mName);
291 private boolean ensurePaired() {
292 if (getBondState() == BluetoothDevice.BOND_NONE) {
300 boolean startPairing() {
301 // Pairing is unreliable while scanning, so cancel discovery
302 if (mLocalAdapter.isDiscovering()) {
303 mLocalAdapter.cancelDiscovery();
306 if (!mDevice.createBond()) {
310 mConnectAfterPairing = true; // auto-connect after pairing
315 * Return true if user initiated pairing on this device. The message text is
316 * slightly different for local vs. remote initiated pairing dialogs.
318 boolean isUserInitiatedPairing() {
319 return mConnectAfterPairing;
323 int state = getBondState();
325 if (state == BluetoothDevice.BOND_BONDING) {
326 mDevice.cancelBondProcess();
329 if (state != BluetoothDevice.BOND_NONE) {
330 final BluetoothDevice dev = mDevice;
332 final boolean successful = dev.removeBond();
335 Log.d(TAG, "Command sent successfully:REMOVE_BOND " + describe(null));
337 } else if (Utils.V) {
338 Log.v(TAG, "Framework rejected command immediately:REMOVE_BOND " +
345 int getProfileConnectionState(LocalBluetoothProfile profile) {
346 if (mProfileConnectionState == null ||
347 mProfileConnectionState.get(profile) == null) {
348 // If cache is empty make the binder call to get the state
349 int state = profile.getConnectionStatus(mDevice);
350 mProfileConnectionState.put(profile, state);
352 return mProfileConnectionState.get(profile);
355 public void clearProfileConnectionState ()
358 Log.d(TAG," Clearing all connection state for dev:" + mDevice.getName());
360 for (LocalBluetoothProfile profile :getProfiles()) {
361 mProfileConnectionState.put(profile, BluetoothProfile.STATE_DISCONNECTED);
365 // TODO: do any of these need to run async on a background thread?
366 private void fillData() {
370 fetchPhonebookPermissionChoice();
371 fetchMessagePermissionChoice();
372 fetchMessageRejectTimes();
375 dispatchAttributesChanged();
378 BluetoothDevice getDevice() {
386 void setName(String name) {
387 if (!mName.equals(name)) {
388 if (TextUtils.isEmpty(name)) {
389 // TODO: use friendly name for unknown device (bug 1181856)
390 mName = mDevice.getAddress();
393 mDevice.setAlias(name);
395 dispatchAttributesChanged();
401 dispatchAttributesChanged();
404 private void fetchName() {
405 mName = mDevice.getAliasName();
407 if (TextUtils.isEmpty(mName)) {
408 mName = mDevice.getAddress();
409 if (DEBUG) Log.d(TAG, "Device has no name (yet), use address: " + mName);
414 dispatchAttributesChanged();
417 boolean isVisible() {
421 void setVisible(boolean visible) {
422 if (mVisible != visible) {
424 dispatchAttributesChanged();
429 return mDevice.getBondState();
432 void setRssi(short rssi) {
435 dispatchAttributesChanged();
440 * Checks whether we are connected to this device (any profile counts).
442 * @return Whether it is connected.
444 boolean isConnected() {
445 for (LocalBluetoothProfile profile : mProfiles) {
446 int status = getProfileConnectionState(profile);
447 if (status == BluetoothProfile.STATE_CONNECTED) {
455 boolean isConnectedProfile(LocalBluetoothProfile profile) {
456 int status = getProfileConnectionState(profile);
457 return status == BluetoothProfile.STATE_CONNECTED;
462 for (LocalBluetoothProfile profile : mProfiles) {
463 int status = getProfileConnectionState(profile);
464 if (status == BluetoothProfile.STATE_CONNECTING
465 || status == BluetoothProfile.STATE_DISCONNECTING) {
469 return getBondState() == BluetoothDevice.BOND_BONDING;
473 * Fetches a new value for the cached BT class.
475 private void fetchBtClass() {
476 mBtClass = mDevice.getBluetoothClass();
479 private boolean updateProfiles() {
480 ParcelUuid[] uuids = mDevice.getUuids();
481 if (uuids == null) return false;
483 ParcelUuid[] localUuids = mLocalAdapter.getUuids();
484 if (localUuids == null) return false;
486 mProfileManager.updateProfiles(uuids, localUuids, mProfiles, mRemovedProfiles,
487 mLocalNapRoleConnected, mDevice);
490 Log.e(TAG, "updating profiles for " + mDevice.getAliasName());
491 BluetoothClass bluetoothClass = mDevice.getBluetoothClass();
493 if (bluetoothClass != null) Log.v(TAG, "Class: " + bluetoothClass.toString());
495 for (ParcelUuid uuid : uuids) {
496 Log.v(TAG, " " + uuid);
503 * Refreshes the UI for the BT class, including fetching the latest value
506 void refreshBtClass() {
508 dispatchAttributesChanged();
512 * Refreshes the UI when framework alerts us of a UUID change.
514 void onUuidChanged() {
518 Log.e(TAG, "onUuidChanged: Time since last connect"
519 + (SystemClock.elapsedRealtime() - mConnectAttempted));
523 * If a connect was attempted earlier without any UUID, we will do the
526 if (!mProfiles.isEmpty()
527 && (mConnectAttempted + MAX_UUID_DELAY_FOR_AUTO_CONNECT) > SystemClock
528 .elapsedRealtime()) {
529 connectWithoutResettingTimer(false);
531 dispatchAttributesChanged();
534 void onBondingStateChanged(int bondState) {
535 if (bondState == BluetoothDevice.BOND_NONE) {
537 mConnectAfterPairing = false; // cancel auto-connect
538 setPhonebookPermissionChoice(ACCESS_UNKNOWN);
539 setMessagePermissionChoice(ACCESS_UNKNOWN);
540 mMessageRejectedTimes = 0;
541 saveMessageRejectTimes();
546 if (bondState == BluetoothDevice.BOND_BONDED) {
547 if (mDevice.isBluetoothDock()) {
548 onBondingDockConnect();
549 } else if (mConnectAfterPairing) {
552 mConnectAfterPairing = false;
556 void setBtClass(BluetoothClass btClass) {
557 if (btClass != null && mBtClass != btClass) {
559 dispatchAttributesChanged();
563 BluetoothClass getBtClass() {
567 List<LocalBluetoothProfile> getProfiles() {
568 return Collections.unmodifiableList(mProfiles);
571 List<LocalBluetoothProfile> getConnectableProfiles() {
572 List<LocalBluetoothProfile> connectableProfiles =
573 new ArrayList<LocalBluetoothProfile>();
574 for (LocalBluetoothProfile profile : mProfiles) {
575 if (profile.isConnectable()) {
576 connectableProfiles.add(profile);
579 return connectableProfiles;
582 List<LocalBluetoothProfile> getRemovedProfiles() {
583 return mRemovedProfiles;
586 void registerCallback(Callback callback) {
587 synchronized (mCallbacks) {
588 mCallbacks.add(callback);
592 void unregisterCallback(Callback callback) {
593 synchronized (mCallbacks) {
594 mCallbacks.remove(callback);
598 private void dispatchAttributesChanged() {
599 synchronized (mCallbacks) {
600 for (Callback callback : mCallbacks) {
601 callback.onDeviceAttributesChanged();
607 public String toString() {
608 return mDevice.toString();
612 public boolean equals(Object o) {
613 if ((o == null) || !(o instanceof CachedBluetoothDevice)) {
616 return mDevice.equals(((CachedBluetoothDevice) o).mDevice);
620 public int hashCode() {
621 return mDevice.getAddress().hashCode();
624 // This comparison uses non-final fields so the sort order may change
625 // when device attributes change (such as bonding state). Settings
626 // will completely refresh the device list when this happens.
627 public int compareTo(CachedBluetoothDevice another) {
628 // Connected above not connected
629 int comparison = (another.isConnected() ? 1 : 0) - (isConnected() ? 1 : 0);
630 if (comparison != 0) return comparison;
632 // Paired above not paired
633 comparison = (another.getBondState() == BluetoothDevice.BOND_BONDED ? 1 : 0) -
634 (getBondState() == BluetoothDevice.BOND_BONDED ? 1 : 0);
635 if (comparison != 0) return comparison;
637 // Visible above not visible
638 comparison = (another.mVisible ? 1 : 0) - (mVisible ? 1 : 0);
639 if (comparison != 0) return comparison;
641 // Stronger signal above weaker signal
642 comparison = another.mRssi - mRssi;
643 if (comparison != 0) return comparison;
646 return mName.compareTo(another.mName);
649 public interface Callback {
650 void onDeviceAttributesChanged();
653 int getPhonebookPermissionChoice() {
654 return mPhonebookPermissionChoice;
657 void setPhonebookPermissionChoice(int permissionChoice) {
658 mPhonebookPermissionChoice = permissionChoice;
660 SharedPreferences.Editor editor =
661 mContext.getSharedPreferences(PHONEBOOK_PREFS_NAME, Context.MODE_PRIVATE).edit();
662 if (permissionChoice == ACCESS_UNKNOWN) {
663 editor.remove(mDevice.getAddress());
665 editor.putInt(mDevice.getAddress(), permissionChoice);
670 private void fetchPhonebookPermissionChoice() {
671 SharedPreferences preference = mContext.getSharedPreferences(PHONEBOOK_PREFS_NAME,
672 Context.MODE_PRIVATE);
673 mPhonebookPermissionChoice = preference.getInt(mDevice.getAddress(),
677 int getMessagePermissionChoice() {
678 return mMessagePermissionChoice;
681 void setMessagePermissionChoice(int permissionChoice) {
682 // if user reject it, only save it when reject exceed limit.
683 if (permissionChoice == ACCESS_REJECTED) {
684 mMessageRejectedTimes++;
685 saveMessageRejectTimes();
686 if (mMessageRejectedTimes < PERSIST_REJECTED_TIMES_LIMIT) {
691 mMessagePermissionChoice = permissionChoice;
693 SharedPreferences.Editor editor =
694 mContext.getSharedPreferences(MESSAGE_PREFS_NAME, Context.MODE_PRIVATE).edit();
695 if (permissionChoice == ACCESS_UNKNOWN) {
696 editor.remove(mDevice.getAddress());
698 editor.putInt(mDevice.getAddress(), permissionChoice);
703 private void fetchMessagePermissionChoice() {
704 SharedPreferences preference = mContext.getSharedPreferences(MESSAGE_PREFS_NAME,
705 Context.MODE_PRIVATE);
706 mMessagePermissionChoice = preference.getInt(mDevice.getAddress(),
710 private void fetchMessageRejectTimes() {
711 SharedPreferences preference = mContext.getSharedPreferences(MESSAGE_REJECT_TIMES,
712 Context.MODE_PRIVATE);
713 mMessageRejectedTimes = preference.getInt(mDevice.getAddress(), 0);
716 private void saveMessageRejectTimes() {
717 SharedPreferences.Editor editor =
718 mContext.getSharedPreferences(MESSAGE_REJECT_TIMES, Context.MODE_PRIVATE).edit();
719 if (mMessageRejectedTimes == 0) {
720 editor.remove(mDevice.getAddress());
722 editor.putInt(mDevice.getAddress(), mMessageRejectedTimes);