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