blob: 109446d8ed4ab72049548ef4895c13749b2c1a7a [file] [log] [blame]
Jason Monk7ce96b92015-02-02 11:27:58 -05001/*
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
17package com.android.settingslib.bluetooth;
18
19import android.bluetooth.BluetoothClass;
20import android.bluetooth.BluetoothDevice;
21import android.bluetooth.BluetoothProfile;
22import android.bluetooth.BluetoothUuid;
23import android.content.Context;
24import android.content.SharedPreferences;
25import android.os.ParcelUuid;
26import android.os.SystemClock;
27import android.text.TextUtils;
28import android.util.Log;
29import android.bluetooth.BluetoothAdapter;
30
Jason Monkbe3c5db2015-02-04 13:00:55 -050031import com.android.settingslib.R;
32
Jason Monk7ce96b92015-02-02 11:27:58 -050033import java.util.ArrayList;
34import java.util.Collection;
35import java.util.Collections;
36import java.util.HashMap;
37import 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 */
Fan Zhang82dd3b02016-12-27 13:13:00 -080045public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> {
Jason Monk7ce96b92015-02-02 11:27:58 -050046 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;
Jack Hec219bc92017-07-24 14:55:59 -070053 //TODO: consider remove, BluetoothDevice.getName() is already cached
Jason Monk7ce96b92015-02-02 11:27:58 -050054 private String mName;
Jack Hec219bc92017-07-24 14:55:59 -070055 // Need this since there is no method for getting RSSI
Jason Monk7ce96b92015-02-02 11:27:58 -050056 private short mRssi;
Jack Hec219bc92017-07-24 14:55:59 -070057 //TODO: consider remove, BluetoothDevice.getBluetoothClass() is already cached
Jason Monk7ce96b92015-02-02 11:27:58 -050058 private BluetoothClass mBtClass;
59 private HashMap<LocalBluetoothProfile, Integer> mProfileConnectionState;
60
61 private final List<LocalBluetoothProfile> mProfiles =
62 new ArrayList<LocalBluetoothProfile>();
63
64 // List of profiles that were previously in mProfiles, but have been removed
65 private final List<LocalBluetoothProfile> mRemovedProfiles =
66 new ArrayList<LocalBluetoothProfile>();
67
68 // Device supports PANU but not NAP: remove PanProfile after device disconnects from NAP
69 private boolean mLocalNapRoleConnected;
70
Jack He51520472017-07-24 12:30:08 -070071 private boolean mJustDiscovered;
Jason Monk7ce96b92015-02-02 11:27:58 -050072
Jason Monk7ce96b92015-02-02 11:27:58 -050073 private int mMessageRejectionCount;
74
75 private final Collection<Callback> mCallbacks = new ArrayList<Callback>();
76
77 // Following constants indicate the user's choices of Phone book/message access settings
78 // User hasn't made any choice or settings app has wiped out the memory
79 public final static int ACCESS_UNKNOWN = 0;
80 // User has accepted the connection and let Settings app remember the decision
81 public final static int ACCESS_ALLOWED = 1;
82 // User has rejected the connection and let Settings app remember the decision
83 public final static int ACCESS_REJECTED = 2;
84
85 // How many times user should reject the connection to make the choice persist.
86 private final static int MESSAGE_REJECTION_COUNT_LIMIT_TO_PERSIST = 2;
87
88 private final static String MESSAGE_REJECTION_COUNT_PREFS_NAME = "bluetooth_message_reject";
89
90 /**
91 * When we connect to multiple profiles, we only want to display a single
92 * error even if they all fail. This tracks that state.
93 */
94 private boolean mIsConnectingErrorPossible;
95
96 /**
97 * Last time a bt profile auto-connect was attempted.
98 * If an ACTION_UUID intent comes in within
99 * MAX_UUID_DELAY_FOR_AUTO_CONNECT milliseconds, we will try auto-connect
100 * again with the new UUIDs
101 */
102 private long mConnectAttempted;
103
104 // See mConnectAttempted
105 private static final long MAX_UUID_DELAY_FOR_AUTO_CONNECT = 5000;
Etan Cohen50d47612015-03-31 12:45:23 -0700106 private static final long MAX_HOGP_DELAY_FOR_AUTO_CONNECT = 30000;
Jason Monk7ce96b92015-02-02 11:27:58 -0500107
Jason Monk7ce96b92015-02-02 11:27:58 -0500108 /**
109 * Describes the current device and profile for logging.
110 *
111 * @param profile Profile to describe
112 * @return Description of the device and profile
113 */
114 private String describe(LocalBluetoothProfile profile) {
115 StringBuilder sb = new StringBuilder();
116 sb.append("Address:").append(mDevice);
117 if (profile != null) {
118 sb.append(" Profile:").append(profile);
119 }
120
121 return sb.toString();
122 }
123
124 void onProfileStateChanged(LocalBluetoothProfile profile, int newProfileState) {
125 if (Utils.D) {
126 Log.d(TAG, "onProfileStateChanged: profile " + profile +
127 " newProfileState " + newProfileState);
128 }
129 if (mLocalAdapter.getBluetoothState() == BluetoothAdapter.STATE_TURNING_OFF)
130 {
131 if (Utils.D) Log.d(TAG, " BT Turninig Off...Profile conn state change ignored...");
132 return;
133 }
134 mProfileConnectionState.put(profile, newProfileState);
135 if (newProfileState == BluetoothProfile.STATE_CONNECTED) {
136 if (profile instanceof MapProfile) {
137 profile.setPreferred(mDevice, true);
138 } else if (!mProfiles.contains(profile)) {
139 mRemovedProfiles.remove(profile);
140 mProfiles.add(profile);
141 if (profile instanceof PanProfile &&
142 ((PanProfile) profile).isLocalRoleNap(mDevice)) {
143 // Device doesn't support NAP, so remove PanProfile on disconnect
144 mLocalNapRoleConnected = true;
145 }
146 }
147 } else if (profile instanceof MapProfile &&
148 newProfileState == BluetoothProfile.STATE_DISCONNECTED) {
149 profile.setPreferred(mDevice, false);
150 } else if (mLocalNapRoleConnected && profile instanceof PanProfile &&
151 ((PanProfile) profile).isLocalRoleNap(mDevice) &&
152 newProfileState == BluetoothProfile.STATE_DISCONNECTED) {
153 Log.d(TAG, "Removing PanProfile from device after NAP disconnect");
154 mProfiles.remove(profile);
155 mRemovedProfiles.add(profile);
156 mLocalNapRoleConnected = false;
157 }
158 }
159
160 CachedBluetoothDevice(Context context,
161 LocalBluetoothAdapter adapter,
162 LocalBluetoothProfileManager profileManager,
163 BluetoothDevice device) {
164 mContext = context;
165 mLocalAdapter = adapter;
166 mProfileManager = profileManager;
167 mDevice = device;
168 mProfileConnectionState = new HashMap<LocalBluetoothProfile, Integer>();
169 fillData();
170 }
171
172 public void disconnect() {
173 for (LocalBluetoothProfile profile : mProfiles) {
174 disconnect(profile);
175 }
176 // Disconnect PBAP server in case its connected
177 // This is to ensure all the profiles are disconnected as some CK/Hs do not
178 // disconnect PBAP connection when HF connection is brought down
179 PbapServerProfile PbapProfile = mProfileManager.getPbapProfile();
180 if (PbapProfile.getConnectionStatus(mDevice) == BluetoothProfile.STATE_CONNECTED)
181 {
182 PbapProfile.disconnect(mDevice);
183 }
184 }
185
186 public void disconnect(LocalBluetoothProfile profile) {
187 if (profile.disconnect(mDevice)) {
188 if (Utils.D) {
189 Log.d(TAG, "Command sent successfully:DISCONNECT " + describe(profile));
190 }
191 }
192 }
193
194 public void connect(boolean connectAllProfiles) {
195 if (!ensurePaired()) {
196 return;
197 }
198
199 mConnectAttempted = SystemClock.elapsedRealtime();
200 connectWithoutResettingTimer(connectAllProfiles);
201 }
202
203 void onBondingDockConnect() {
204 // Attempt to connect if UUIDs are available. Otherwise,
205 // we will connect when the ACTION_UUID intent arrives.
206 connect(false);
207 }
208
209 private void connectWithoutResettingTimer(boolean connectAllProfiles) {
210 // Try to initialize the profiles if they were not.
211 if (mProfiles.isEmpty()) {
212 // if mProfiles is empty, then do not invoke updateProfiles. This causes a race
213 // condition with carkits during pairing, wherein RemoteDevice.UUIDs have been updated
214 // from bluetooth stack but ACTION.uuid is not sent yet.
215 // Eventually ACTION.uuid will be received which shall trigger the connection of the
216 // various profiles
217 // If UUIDs are not available yet, connect will be happen
218 // upon arrival of the ACTION_UUID intent.
219 Log.d(TAG, "No profiles. Maybe we will connect later");
220 return;
221 }
222
223 // Reset the only-show-one-error-dialog tracking variable
224 mIsConnectingErrorPossible = true;
225
226 int preferredProfiles = 0;
227 for (LocalBluetoothProfile profile : mProfiles) {
228 if (connectAllProfiles ? profile.isConnectable() : profile.isAutoConnectable()) {
229 if (profile.isPreferred(mDevice)) {
230 ++preferredProfiles;
231 connectInt(profile);
232 }
233 }
234 }
235 if (DEBUG) Log.d(TAG, "Preferred profiles = " + preferredProfiles);
236
237 if (preferredProfiles == 0) {
238 connectAutoConnectableProfiles();
239 }
240 }
241
242 private void connectAutoConnectableProfiles() {
243 if (!ensurePaired()) {
244 return;
245 }
246 // Reset the only-show-one-error-dialog tracking variable
247 mIsConnectingErrorPossible = true;
248
249 for (LocalBluetoothProfile profile : mProfiles) {
250 if (profile.isAutoConnectable()) {
251 profile.setPreferred(mDevice, true);
252 connectInt(profile);
253 }
254 }
255 }
256
257 /**
258 * Connect this device to the specified profile.
259 *
260 * @param profile the profile to use with the remote device
261 */
262 public void connectProfile(LocalBluetoothProfile profile) {
263 mConnectAttempted = SystemClock.elapsedRealtime();
264 // Reset the only-show-one-error-dialog tracking variable
265 mIsConnectingErrorPossible = true;
266 connectInt(profile);
267 // Refresh the UI based on profile.connect() call
268 refresh();
269 }
270
271 synchronized void connectInt(LocalBluetoothProfile profile) {
272 if (!ensurePaired()) {
273 return;
274 }
275 if (profile.connect(mDevice)) {
276 if (Utils.D) {
277 Log.d(TAG, "Command sent successfully:CONNECT " + describe(profile));
278 }
279 return;
280 }
281 Log.i(TAG, "Failed to connect " + profile.toString() + " to " + mName);
282 }
283
284 private boolean ensurePaired() {
285 if (getBondState() == BluetoothDevice.BOND_NONE) {
286 startPairing();
287 return false;
288 } else {
289 return true;
290 }
291 }
292
293 public boolean startPairing() {
294 // Pairing is unreliable while scanning, so cancel discovery
295 if (mLocalAdapter.isDiscovering()) {
296 mLocalAdapter.cancelDiscovery();
297 }
298
299 if (!mDevice.createBond()) {
300 return false;
301 }
302
Jason Monk7ce96b92015-02-02 11:27:58 -0500303 return true;
304 }
305
306 /**
307 * Return true if user initiated pairing on this device. The message text is
308 * slightly different for local vs. remote initiated pairing dialogs.
309 */
310 boolean isUserInitiatedPairing() {
Jakub Pawlowski0278ab92016-07-20 11:55:48 -0700311 return mDevice.isBondingInitiatedLocally();
Jason Monk7ce96b92015-02-02 11:27:58 -0500312 }
313
314 public void unpair() {
315 int state = getBondState();
316
317 if (state == BluetoothDevice.BOND_BONDING) {
318 mDevice.cancelBondProcess();
319 }
320
321 if (state != BluetoothDevice.BOND_NONE) {
322 final BluetoothDevice dev = mDevice;
323 if (dev != null) {
324 final boolean successful = dev.removeBond();
325 if (successful) {
326 if (Utils.D) {
327 Log.d(TAG, "Command sent successfully:REMOVE_BOND " + describe(null));
328 }
329 } else if (Utils.V) {
330 Log.v(TAG, "Framework rejected command immediately:REMOVE_BOND " +
331 describe(null));
332 }
333 }
334 }
335 }
336
337 public int getProfileConnectionState(LocalBluetoothProfile profile) {
338 if (mProfileConnectionState == null ||
339 mProfileConnectionState.get(profile) == null) {
340 // If cache is empty make the binder call to get the state
341 int state = profile.getConnectionStatus(mDevice);
342 mProfileConnectionState.put(profile, state);
343 }
344 return mProfileConnectionState.get(profile);
345 }
346
347 public void clearProfileConnectionState ()
348 {
349 if (Utils.D) {
350 Log.d(TAG," Clearing all connection state for dev:" + mDevice.getName());
351 }
352 for (LocalBluetoothProfile profile :getProfiles()) {
353 mProfileConnectionState.put(profile, BluetoothProfile.STATE_DISCONNECTED);
354 }
355 }
356
357 // TODO: do any of these need to run async on a background thread?
358 private void fillData() {
359 fetchName();
360 fetchBtClass();
361 updateProfiles();
362 migratePhonebookPermissionChoice();
363 migrateMessagePermissionChoice();
364 fetchMessageRejectionCount();
365
Jason Monk7ce96b92015-02-02 11:27:58 -0500366 dispatchAttributesChanged();
367 }
368
369 public BluetoothDevice getDevice() {
370 return mDevice;
371 }
372
Antony Sargent7ad051e2017-06-29 15:23:13 -0700373 /**
374 * Convenience method that can be mocked - it lets tests avoid having to call getDevice() which
375 * causes problems in tests since BluetoothDevice is final and cannot be mocked.
376 * @return the address of this device
377 */
378 public String getAddress() {
379 return mDevice.getAddress();
380 }
381
Jason Monk7ce96b92015-02-02 11:27:58 -0500382 public String getName() {
383 return mName;
384 }
385
386 /**
387 * Populate name from BluetoothDevice.ACTION_FOUND intent
388 */
389 void setNewName(String name) {
390 if (mName == null) {
391 mName = name;
392 if (mName == null || TextUtils.isEmpty(mName)) {
393 mName = mDevice.getAddress();
394 }
395 dispatchAttributesChanged();
396 }
397 }
398
399 /**
Jack Hec219bc92017-07-24 14:55:59 -0700400 * User changes the device name
401 * @param name new alias name to be set, should never be null
Jason Monk7ce96b92015-02-02 11:27:58 -0500402 */
403 public void setName(String name) {
Jack Hec219bc92017-07-24 14:55:59 -0700404 // Prevent mName to be set to null if setName(null) is called
405 if (name != null && !TextUtils.equals(name, mName)) {
Jason Monk7ce96b92015-02-02 11:27:58 -0500406 mName = name;
407 mDevice.setAlias(name);
408 dispatchAttributesChanged();
409 }
410 }
411
412 void refreshName() {
413 fetchName();
414 dispatchAttributesChanged();
415 }
416
417 private void fetchName() {
418 mName = mDevice.getAliasName();
419
420 if (TextUtils.isEmpty(mName)) {
421 mName = mDevice.getAddress();
422 if (DEBUG) Log.d(TAG, "Device has no name (yet), use address: " + mName);
423 }
424 }
425
Jack He6258aae2017-06-29 17:01:23 -0700426 /**
Jack Hec219bc92017-07-24 14:55:59 -0700427 * Checks if device has a human readable name besides MAC address
428 * @return true if device's alias name is not null nor empty, false otherwise
429 */
430 public boolean hasHumanReadableName() {
431 return !TextUtils.isEmpty(mDevice.getAliasName());
432 }
433
434 /**
Jack He6258aae2017-06-29 17:01:23 -0700435 * Get battery level from remote device
436 * @return battery level in percentage [0-100], or {@link BluetoothDevice#BATTERY_LEVEL_UNKNOWN}
437 */
438 public int getBatteryLevel() {
439 return mDevice.getBatteryLevel();
440 }
441
Jason Monk7ce96b92015-02-02 11:27:58 -0500442 void refresh() {
443 dispatchAttributesChanged();
444 }
445
Jack He51520472017-07-24 12:30:08 -0700446 public void setJustDiscovered(boolean justDiscovered) {
447 if (mJustDiscovered != justDiscovered) {
448 mJustDiscovered = justDiscovered;
Jason Monk7ce96b92015-02-02 11:27:58 -0500449 dispatchAttributesChanged();
450 }
451 }
452
453 public int getBondState() {
454 return mDevice.getBondState();
455 }
456
457 void setRssi(short rssi) {
458 if (mRssi != rssi) {
459 mRssi = rssi;
460 dispatchAttributesChanged();
461 }
462 }
463
464 /**
465 * Checks whether we are connected to this device (any profile counts).
466 *
467 * @return Whether it is connected.
468 */
469 public boolean isConnected() {
470 for (LocalBluetoothProfile profile : mProfiles) {
471 int status = getProfileConnectionState(profile);
472 if (status == BluetoothProfile.STATE_CONNECTED) {
473 return true;
474 }
475 }
476
477 return false;
478 }
479
480 public boolean isConnectedProfile(LocalBluetoothProfile profile) {
481 int status = getProfileConnectionState(profile);
482 return status == BluetoothProfile.STATE_CONNECTED;
483
484 }
485
486 public boolean isBusy() {
487 for (LocalBluetoothProfile profile : mProfiles) {
488 int status = getProfileConnectionState(profile);
489 if (status == BluetoothProfile.STATE_CONNECTING
490 || status == BluetoothProfile.STATE_DISCONNECTING) {
491 return true;
492 }
493 }
494 return getBondState() == BluetoothDevice.BOND_BONDING;
495 }
496
497 /**
498 * Fetches a new value for the cached BT class.
499 */
500 private void fetchBtClass() {
501 mBtClass = mDevice.getBluetoothClass();
502 }
503
504 private boolean updateProfiles() {
505 ParcelUuid[] uuids = mDevice.getUuids();
506 if (uuids == null) return false;
507
508 ParcelUuid[] localUuids = mLocalAdapter.getUuids();
509 if (localUuids == null) return false;
510
Jack Hec219bc92017-07-24 14:55:59 -0700511 /*
Jason Monk7ce96b92015-02-02 11:27:58 -0500512 * Now we know if the device supports PBAP, update permissions...
513 */
514 processPhonebookAccess();
515
516 mProfileManager.updateProfiles(uuids, localUuids, mProfiles, mRemovedProfiles,
517 mLocalNapRoleConnected, mDevice);
518
519 if (DEBUG) {
520 Log.e(TAG, "updating profiles for " + mDevice.getAliasName());
521 BluetoothClass bluetoothClass = mDevice.getBluetoothClass();
522
523 if (bluetoothClass != null) Log.v(TAG, "Class: " + bluetoothClass.toString());
524 Log.v(TAG, "UUID:");
525 for (ParcelUuid uuid : uuids) {
526 Log.v(TAG, " " + uuid);
527 }
528 }
529 return true;
530 }
531
532 /**
533 * Refreshes the UI for the BT class, including fetching the latest value
534 * for the class.
535 */
536 void refreshBtClass() {
537 fetchBtClass();
538 dispatchAttributesChanged();
539 }
540
541 /**
542 * Refreshes the UI when framework alerts us of a UUID change.
543 */
544 void onUuidChanged() {
545 updateProfiles();
Etan Cohen50d47612015-03-31 12:45:23 -0700546 ParcelUuid[] uuids = mDevice.getUuids();
Venkat Raghavan05e08c32015-04-06 16:26:11 -0700547
Etan Cohen50d47612015-03-31 12:45:23 -0700548 long timeout = MAX_UUID_DELAY_FOR_AUTO_CONNECT;
Venkat Raghavan05e08c32015-04-06 16:26:11 -0700549 if (BluetoothUuid.isUuidPresent(uuids, BluetoothUuid.Hogp)) {
550 timeout = MAX_HOGP_DELAY_FOR_AUTO_CONNECT;
551 }
Jason Monk7ce96b92015-02-02 11:27:58 -0500552
553 if (DEBUG) {
Etan Cohen50d47612015-03-31 12:45:23 -0700554 Log.d(TAG, "onUuidChanged: Time since last connect"
Jason Monk7ce96b92015-02-02 11:27:58 -0500555 + (SystemClock.elapsedRealtime() - mConnectAttempted));
556 }
557
558 /*
Venkat Raghavan05e08c32015-04-06 16:26:11 -0700559 * If a connect was attempted earlier without any UUID, we will do the connect now.
560 * Otherwise, allow the connect on UUID change.
Jason Monk7ce96b92015-02-02 11:27:58 -0500561 */
562 if (!mProfiles.isEmpty()
Andre Eisenbach7be83c52015-09-03 15:20:09 -0700563 && ((mConnectAttempted + timeout) > SystemClock.elapsedRealtime())) {
Jason Monk7ce96b92015-02-02 11:27:58 -0500564 connectWithoutResettingTimer(false);
565 }
Andre Eisenbach7be83c52015-09-03 15:20:09 -0700566
Jason Monk7ce96b92015-02-02 11:27:58 -0500567 dispatchAttributesChanged();
568 }
569
570 void onBondingStateChanged(int bondState) {
571 if (bondState == BluetoothDevice.BOND_NONE) {
572 mProfiles.clear();
Jason Monk7ce96b92015-02-02 11:27:58 -0500573 setPhonebookPermissionChoice(ACCESS_UNKNOWN);
574 setMessagePermissionChoice(ACCESS_UNKNOWN);
Casper Bonde424681e2015-05-04 22:07:45 -0700575 setSimPermissionChoice(ACCESS_UNKNOWN);
Jason Monk7ce96b92015-02-02 11:27:58 -0500576 mMessageRejectionCount = 0;
577 saveMessageRejectionCount();
578 }
579
580 refresh();
581
582 if (bondState == BluetoothDevice.BOND_BONDED) {
583 if (mDevice.isBluetoothDock()) {
584 onBondingDockConnect();
Jakub Pawlowski0278ab92016-07-20 11:55:48 -0700585 } else if (mDevice.isBondingInitiatedLocally()) {
Jason Monk7ce96b92015-02-02 11:27:58 -0500586 connect(false);
587 }
Jason Monk7ce96b92015-02-02 11:27:58 -0500588 }
589 }
590
591 void setBtClass(BluetoothClass btClass) {
592 if (btClass != null && mBtClass != btClass) {
593 mBtClass = btClass;
594 dispatchAttributesChanged();
595 }
596 }
597
598 public BluetoothClass getBtClass() {
599 return mBtClass;
600 }
601
602 public List<LocalBluetoothProfile> getProfiles() {
603 return Collections.unmodifiableList(mProfiles);
604 }
605
606 public List<LocalBluetoothProfile> getConnectableProfiles() {
607 List<LocalBluetoothProfile> connectableProfiles =
608 new ArrayList<LocalBluetoothProfile>();
609 for (LocalBluetoothProfile profile : mProfiles) {
610 if (profile.isConnectable()) {
611 connectableProfiles.add(profile);
612 }
613 }
614 return connectableProfiles;
615 }
616
617 public List<LocalBluetoothProfile> getRemovedProfiles() {
618 return mRemovedProfiles;
619 }
620
621 public void registerCallback(Callback callback) {
622 synchronized (mCallbacks) {
623 mCallbacks.add(callback);
624 }
625 }
626
627 public void unregisterCallback(Callback callback) {
628 synchronized (mCallbacks) {
629 mCallbacks.remove(callback);
630 }
631 }
632
633 private void dispatchAttributesChanged() {
634 synchronized (mCallbacks) {
635 for (Callback callback : mCallbacks) {
636 callback.onDeviceAttributesChanged();
637 }
638 }
639 }
640
641 @Override
642 public String toString() {
643 return mDevice.toString();
644 }
645
646 @Override
647 public boolean equals(Object o) {
648 if ((o == null) || !(o instanceof CachedBluetoothDevice)) {
649 return false;
650 }
651 return mDevice.equals(((CachedBluetoothDevice) o).mDevice);
652 }
653
654 @Override
655 public int hashCode() {
656 return mDevice.getAddress().hashCode();
657 }
658
659 // This comparison uses non-final fields so the sort order may change
660 // when device attributes change (such as bonding state). Settings
661 // will completely refresh the device list when this happens.
662 public int compareTo(CachedBluetoothDevice another) {
663 // Connected above not connected
664 int comparison = (another.isConnected() ? 1 : 0) - (isConnected() ? 1 : 0);
665 if (comparison != 0) return comparison;
666
667 // Paired above not paired
668 comparison = (another.getBondState() == BluetoothDevice.BOND_BONDED ? 1 : 0) -
669 (getBondState() == BluetoothDevice.BOND_BONDED ? 1 : 0);
670 if (comparison != 0) return comparison;
671
Jack He51520472017-07-24 12:30:08 -0700672 // Just discovered above discovered in the past
673 comparison = (another.mJustDiscovered ? 1 : 0) - (mJustDiscovered ? 1 : 0);
Jason Monk7ce96b92015-02-02 11:27:58 -0500674 if (comparison != 0) return comparison;
675
676 // Stronger signal above weaker signal
677 comparison = another.mRssi - mRssi;
678 if (comparison != 0) return comparison;
679
680 // Fallback on name
681 return mName.compareTo(another.mName);
682 }
683
684 public interface Callback {
685 void onDeviceAttributesChanged();
686 }
687
688 public int getPhonebookPermissionChoice() {
689 int permission = mDevice.getPhonebookAccessPermission();
690 if (permission == BluetoothDevice.ACCESS_ALLOWED) {
691 return ACCESS_ALLOWED;
692 } else if (permission == BluetoothDevice.ACCESS_REJECTED) {
693 return ACCESS_REJECTED;
694 }
695 return ACCESS_UNKNOWN;
696 }
697
698 public void setPhonebookPermissionChoice(int permissionChoice) {
699 int permission = BluetoothDevice.ACCESS_UNKNOWN;
700 if (permissionChoice == ACCESS_ALLOWED) {
701 permission = BluetoothDevice.ACCESS_ALLOWED;
702 } else if (permissionChoice == ACCESS_REJECTED) {
703 permission = BluetoothDevice.ACCESS_REJECTED;
704 }
705 mDevice.setPhonebookAccessPermission(permission);
706 }
707
708 // Migrates data from old data store (in Settings app's shared preferences) to new (in Bluetooth
709 // app's shared preferences).
710 private void migratePhonebookPermissionChoice() {
711 SharedPreferences preferences = mContext.getSharedPreferences(
712 "bluetooth_phonebook_permission", Context.MODE_PRIVATE);
713 if (!preferences.contains(mDevice.getAddress())) {
714 return;
715 }
716
717 if (mDevice.getPhonebookAccessPermission() == BluetoothDevice.ACCESS_UNKNOWN) {
718 int oldPermission = preferences.getInt(mDevice.getAddress(), ACCESS_UNKNOWN);
719 if (oldPermission == ACCESS_ALLOWED) {
720 mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_ALLOWED);
721 } else if (oldPermission == ACCESS_REJECTED) {
722 mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_REJECTED);
723 }
724 }
725
726 SharedPreferences.Editor editor = preferences.edit();
727 editor.remove(mDevice.getAddress());
728 editor.commit();
729 }
730
731 public int getMessagePermissionChoice() {
732 int permission = mDevice.getMessageAccessPermission();
733 if (permission == BluetoothDevice.ACCESS_ALLOWED) {
734 return ACCESS_ALLOWED;
735 } else if (permission == BluetoothDevice.ACCESS_REJECTED) {
736 return ACCESS_REJECTED;
737 }
738 return ACCESS_UNKNOWN;
739 }
740
741 public void setMessagePermissionChoice(int permissionChoice) {
742 int permission = BluetoothDevice.ACCESS_UNKNOWN;
743 if (permissionChoice == ACCESS_ALLOWED) {
744 permission = BluetoothDevice.ACCESS_ALLOWED;
745 } else if (permissionChoice == ACCESS_REJECTED) {
746 permission = BluetoothDevice.ACCESS_REJECTED;
747 }
748 mDevice.setMessageAccessPermission(permission);
749 }
750
Casper Bonde424681e2015-05-04 22:07:45 -0700751 public int getSimPermissionChoice() {
752 int permission = mDevice.getSimAccessPermission();
753 if (permission == BluetoothDevice.ACCESS_ALLOWED) {
754 return ACCESS_ALLOWED;
755 } else if (permission == BluetoothDevice.ACCESS_REJECTED) {
756 return ACCESS_REJECTED;
757 }
758 return ACCESS_UNKNOWN;
759 }
760
761 void setSimPermissionChoice(int permissionChoice) {
762 int permission = BluetoothDevice.ACCESS_UNKNOWN;
763 if (permissionChoice == ACCESS_ALLOWED) {
764 permission = BluetoothDevice.ACCESS_ALLOWED;
765 } else if (permissionChoice == ACCESS_REJECTED) {
766 permission = BluetoothDevice.ACCESS_REJECTED;
767 }
768 mDevice.setSimAccessPermission(permission);
769 }
770
Jason Monk7ce96b92015-02-02 11:27:58 -0500771 // Migrates data from old data store (in Settings app's shared preferences) to new (in Bluetooth
772 // app's shared preferences).
773 private void migrateMessagePermissionChoice() {
774 SharedPreferences preferences = mContext.getSharedPreferences(
775 "bluetooth_message_permission", Context.MODE_PRIVATE);
776 if (!preferences.contains(mDevice.getAddress())) {
777 return;
778 }
779
780 if (mDevice.getMessageAccessPermission() == BluetoothDevice.ACCESS_UNKNOWN) {
781 int oldPermission = preferences.getInt(mDevice.getAddress(), ACCESS_UNKNOWN);
782 if (oldPermission == ACCESS_ALLOWED) {
783 mDevice.setMessageAccessPermission(BluetoothDevice.ACCESS_ALLOWED);
784 } else if (oldPermission == ACCESS_REJECTED) {
785 mDevice.setMessageAccessPermission(BluetoothDevice.ACCESS_REJECTED);
786 }
787 }
788
789 SharedPreferences.Editor editor = preferences.edit();
790 editor.remove(mDevice.getAddress());
791 editor.commit();
792 }
793
794 /**
795 * @return Whether this rejection should persist.
796 */
797 public boolean checkAndIncreaseMessageRejectionCount() {
798 if (mMessageRejectionCount < MESSAGE_REJECTION_COUNT_LIMIT_TO_PERSIST) {
799 mMessageRejectionCount++;
800 saveMessageRejectionCount();
801 }
802 return mMessageRejectionCount >= MESSAGE_REJECTION_COUNT_LIMIT_TO_PERSIST;
803 }
804
805 private void fetchMessageRejectionCount() {
806 SharedPreferences preference = mContext.getSharedPreferences(
807 MESSAGE_REJECTION_COUNT_PREFS_NAME, Context.MODE_PRIVATE);
808 mMessageRejectionCount = preference.getInt(mDevice.getAddress(), 0);
809 }
810
811 private void saveMessageRejectionCount() {
812 SharedPreferences.Editor editor = mContext.getSharedPreferences(
813 MESSAGE_REJECTION_COUNT_PREFS_NAME, Context.MODE_PRIVATE).edit();
814 if (mMessageRejectionCount == 0) {
815 editor.remove(mDevice.getAddress());
816 } else {
817 editor.putInt(mDevice.getAddress(), mMessageRejectionCount);
818 }
819 editor.commit();
820 }
821
822 private void processPhonebookAccess() {
823 if (mDevice.getBondState() != BluetoothDevice.BOND_BONDED) return;
824
825 ParcelUuid[] uuids = mDevice.getUuids();
826 if (BluetoothUuid.containsAnyUuid(uuids, PbapServerProfile.PBAB_CLIENT_UUIDS)) {
827 // The pairing dialog now warns of phone-book access for paired devices.
828 // No separate prompt is displayed after pairing.
Sanket Padawe78c23762015-06-01 15:55:30 -0700829 if (getPhonebookPermissionChoice() == CachedBluetoothDevice.ACCESS_UNKNOWN) {
Sanket Padawe07533db2015-11-11 15:01:35 -0800830 if (mDevice.getBluetoothClass().getDeviceClass()
Hemant Guptab8d267a2016-06-07 12:53:59 +0530831 == BluetoothClass.Device.AUDIO_VIDEO_HANDSFREE ||
832 mDevice.getBluetoothClass().getDeviceClass()
833 == BluetoothClass.Device.AUDIO_VIDEO_WEARABLE_HEADSET) {
Sanket Padawe07533db2015-11-11 15:01:35 -0800834 setPhonebookPermissionChoice(CachedBluetoothDevice.ACCESS_ALLOWED);
835 } else {
836 setPhonebookPermissionChoice(CachedBluetoothDevice.ACCESS_REJECTED);
837 }
Sanket Padawe78c23762015-06-01 15:55:30 -0700838 }
Jason Monk7ce96b92015-02-02 11:27:58 -0500839 }
840 }
Jason Monkbe3c5db2015-02-04 13:00:55 -0500841
842 public int getMaxConnectionState() {
843 int maxState = BluetoothProfile.STATE_DISCONNECTED;
844 for (LocalBluetoothProfile profile : getProfiles()) {
845 int connectionStatus = getProfileConnectionState(profile);
846 if (connectionStatus > maxState) {
847 maxState = connectionStatus;
848 }
849 }
850 return maxState;
851 }
852
853 /**
854 * @return resource for string that discribes the connection state of this device.
855 */
Jack He6258aae2017-06-29 17:01:23 -0700856 public String getConnectionSummary() {
Jason Monkbe3c5db2015-02-04 13:00:55 -0500857 boolean profileConnected = false; // at least one profile is connected
858 boolean a2dpNotConnected = false; // A2DP is preferred but not connected
Sanket Agarwalf8a1c912016-01-26 20:12:52 -0800859 boolean hfpNotConnected = false; // HFP is preferred but not connected
Jason Monkbe3c5db2015-02-04 13:00:55 -0500860
861 for (LocalBluetoothProfile profile : getProfiles()) {
862 int connectionStatus = getProfileConnectionState(profile);
863
864 switch (connectionStatus) {
865 case BluetoothProfile.STATE_CONNECTING:
866 case BluetoothProfile.STATE_DISCONNECTING:
Jack He6258aae2017-06-29 17:01:23 -0700867 return mContext.getString(Utils.getConnectionStateSummary(connectionStatus));
Jason Monkbe3c5db2015-02-04 13:00:55 -0500868
869 case BluetoothProfile.STATE_CONNECTED:
870 profileConnected = true;
871 break;
872
873 case BluetoothProfile.STATE_DISCONNECTED:
874 if (profile.isProfileReady()) {
Sanket Agarwalf8a1c912016-01-26 20:12:52 -0800875 if ((profile instanceof A2dpProfile) ||
Sanket Agarwal1bec6a52015-10-21 18:23:27 -0700876 (profile instanceof A2dpSinkProfile)){
Jason Monkbe3c5db2015-02-04 13:00:55 -0500877 a2dpNotConnected = true;
Sanket Agarwalf8a1c912016-01-26 20:12:52 -0800878 } else if ((profile instanceof HeadsetProfile) ||
879 (profile instanceof HfpClientProfile)) {
880 hfpNotConnected = true;
Jason Monkbe3c5db2015-02-04 13:00:55 -0500881 }
882 }
883 break;
884 }
885 }
886
Jack He6258aae2017-06-29 17:01:23 -0700887 String batteryLevelPercentageString = null;
888 // Android framework should only set mBatteryLevel to valid range [0-100] or
889 // BluetoothDevice.BATTERY_LEVEL_UNKNOWN, any other value should be a framework bug.
890 // Thus assume here that if value is not BluetoothDevice.BATTERY_LEVEL_UNKNOWN, it must
891 // be valid
892 final int batteryLevel = getBatteryLevel();
893 if (batteryLevel != BluetoothDevice.BATTERY_LEVEL_UNKNOWN) {
894 // TODO: name com.android.settingslib.bluetooth.Utils something different
895 batteryLevelPercentageString =
896 com.android.settingslib.Utils.formatPercentage(batteryLevel);
897 }
898
Jason Monkbe3c5db2015-02-04 13:00:55 -0500899 if (profileConnected) {
Sanket Agarwalf8a1c912016-01-26 20:12:52 -0800900 if (a2dpNotConnected && hfpNotConnected) {
Jack He6258aae2017-06-29 17:01:23 -0700901 if (batteryLevelPercentageString != null) {
902 return mContext.getString(
903 R.string.bluetooth_connected_no_headset_no_a2dp_battery_level,
904 batteryLevelPercentageString);
905 } else {
906 return mContext.getString(R.string.bluetooth_connected_no_headset_no_a2dp);
907 }
908
Jason Monkbe3c5db2015-02-04 13:00:55 -0500909 } else if (a2dpNotConnected) {
Jack He6258aae2017-06-29 17:01:23 -0700910 if (batteryLevelPercentageString != null) {
911 return mContext.getString(R.string.bluetooth_connected_no_a2dp_battery_level,
912 batteryLevelPercentageString);
913 } else {
914 return mContext.getString(R.string.bluetooth_connected_no_a2dp);
915 }
916
Sanket Agarwalf8a1c912016-01-26 20:12:52 -0800917 } else if (hfpNotConnected) {
Jack He6258aae2017-06-29 17:01:23 -0700918 if (batteryLevelPercentageString != null) {
919 return mContext.getString(R.string.bluetooth_connected_no_headset_battery_level,
920 batteryLevelPercentageString);
921 } else {
922 return mContext.getString(R.string.bluetooth_connected_no_headset);
923 }
Jason Monkbe3c5db2015-02-04 13:00:55 -0500924 } else {
Jack He6258aae2017-06-29 17:01:23 -0700925 if (batteryLevelPercentageString != null) {
926 return mContext.getString(R.string.bluetooth_connected_battery_level,
927 batteryLevelPercentageString);
928 } else {
929 return mContext.getString(R.string.bluetooth_connected);
930 }
Jason Monkbe3c5db2015-02-04 13:00:55 -0500931 }
932 }
933
Jack He6258aae2017-06-29 17:01:23 -0700934 return getBondState() == BluetoothDevice.BOND_BONDING ?
935 mContext.getString(R.string.bluetooth_pairing) : null;
Jason Monkbe3c5db2015-02-04 13:00:55 -0500936 }
Jason Monk7ce96b92015-02-02 11:27:58 -0500937}