blob: 734f70d84f840873a6e5f4539aa188809d64a40d [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
Jack He51520472017-07-24 12:30:08 -070068 private boolean mJustDiscovered;
Jason Monk7ce96b92015-02-02 11:27:58 -050069
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);
135 } else if (!mProfiles.contains(profile)) {
136 mRemovedProfiles.remove(profile);
137 mProfiles.add(profile);
138 if (profile instanceof PanProfile &&
139 ((PanProfile) profile).isLocalRoleNap(mDevice)) {
140 // Device doesn't support NAP, so remove PanProfile on disconnect
141 mLocalNapRoleConnected = true;
142 }
143 }
144 } else if (profile instanceof MapProfile &&
145 newProfileState == BluetoothProfile.STATE_DISCONNECTED) {
146 profile.setPreferred(mDevice, false);
147 } else if (mLocalNapRoleConnected && profile instanceof PanProfile &&
148 ((PanProfile) profile).isLocalRoleNap(mDevice) &&
149 newProfileState == BluetoothProfile.STATE_DISCONNECTED) {
150 Log.d(TAG, "Removing PanProfile from device after NAP disconnect");
151 mProfiles.remove(profile);
152 mRemovedProfiles.add(profile);
153 mLocalNapRoleConnected = false;
154 }
155 }
156
157 CachedBluetoothDevice(Context context,
158 LocalBluetoothAdapter adapter,
159 LocalBluetoothProfileManager profileManager,
160 BluetoothDevice device) {
161 mContext = context;
162 mLocalAdapter = adapter;
163 mProfileManager = profileManager;
164 mDevice = device;
165 mProfileConnectionState = new HashMap<LocalBluetoothProfile, Integer>();
166 fillData();
167 }
168
169 public void disconnect() {
170 for (LocalBluetoothProfile profile : mProfiles) {
171 disconnect(profile);
172 }
173 // Disconnect PBAP server in case its connected
174 // This is to ensure all the profiles are disconnected as some CK/Hs do not
175 // disconnect PBAP connection when HF connection is brought down
176 PbapServerProfile PbapProfile = mProfileManager.getPbapProfile();
177 if (PbapProfile.getConnectionStatus(mDevice) == BluetoothProfile.STATE_CONNECTED)
178 {
179 PbapProfile.disconnect(mDevice);
180 }
181 }
182
183 public void disconnect(LocalBluetoothProfile profile) {
184 if (profile.disconnect(mDevice)) {
185 if (Utils.D) {
186 Log.d(TAG, "Command sent successfully:DISCONNECT " + describe(profile));
187 }
188 }
189 }
190
191 public void connect(boolean connectAllProfiles) {
192 if (!ensurePaired()) {
193 return;
194 }
195
196 mConnectAttempted = SystemClock.elapsedRealtime();
197 connectWithoutResettingTimer(connectAllProfiles);
198 }
199
200 void onBondingDockConnect() {
201 // Attempt to connect if UUIDs are available. Otherwise,
202 // we will connect when the ACTION_UUID intent arrives.
203 connect(false);
204 }
205
206 private void connectWithoutResettingTimer(boolean connectAllProfiles) {
207 // Try to initialize the profiles if they were not.
208 if (mProfiles.isEmpty()) {
209 // if mProfiles is empty, then do not invoke updateProfiles. This causes a race
210 // condition with carkits during pairing, wherein RemoteDevice.UUIDs have been updated
211 // from bluetooth stack but ACTION.uuid is not sent yet.
212 // Eventually ACTION.uuid will be received which shall trigger the connection of the
213 // various profiles
214 // If UUIDs are not available yet, connect will be happen
215 // upon arrival of the ACTION_UUID intent.
216 Log.d(TAG, "No profiles. Maybe we will connect later");
217 return;
218 }
219
220 // Reset the only-show-one-error-dialog tracking variable
221 mIsConnectingErrorPossible = true;
222
223 int preferredProfiles = 0;
224 for (LocalBluetoothProfile profile : mProfiles) {
225 if (connectAllProfiles ? profile.isConnectable() : profile.isAutoConnectable()) {
226 if (profile.isPreferred(mDevice)) {
227 ++preferredProfiles;
228 connectInt(profile);
229 }
230 }
231 }
232 if (DEBUG) Log.d(TAG, "Preferred profiles = " + preferredProfiles);
233
234 if (preferredProfiles == 0) {
235 connectAutoConnectableProfiles();
236 }
237 }
238
239 private void connectAutoConnectableProfiles() {
240 if (!ensurePaired()) {
241 return;
242 }
243 // Reset the only-show-one-error-dialog tracking variable
244 mIsConnectingErrorPossible = true;
245
246 for (LocalBluetoothProfile profile : mProfiles) {
247 if (profile.isAutoConnectable()) {
248 profile.setPreferred(mDevice, true);
249 connectInt(profile);
250 }
251 }
252 }
253
254 /**
255 * Connect this device to the specified profile.
256 *
257 * @param profile the profile to use with the remote device
258 */
259 public void connectProfile(LocalBluetoothProfile profile) {
260 mConnectAttempted = SystemClock.elapsedRealtime();
261 // Reset the only-show-one-error-dialog tracking variable
262 mIsConnectingErrorPossible = true;
263 connectInt(profile);
264 // Refresh the UI based on profile.connect() call
265 refresh();
266 }
267
268 synchronized void connectInt(LocalBluetoothProfile profile) {
269 if (!ensurePaired()) {
270 return;
271 }
272 if (profile.connect(mDevice)) {
273 if (Utils.D) {
274 Log.d(TAG, "Command sent successfully:CONNECT " + describe(profile));
275 }
276 return;
277 }
278 Log.i(TAG, "Failed to connect " + profile.toString() + " to " + mName);
279 }
280
281 private boolean ensurePaired() {
282 if (getBondState() == BluetoothDevice.BOND_NONE) {
283 startPairing();
284 return false;
285 } else {
286 return true;
287 }
288 }
289
290 public boolean startPairing() {
291 // Pairing is unreliable while scanning, so cancel discovery
292 if (mLocalAdapter.isDiscovering()) {
293 mLocalAdapter.cancelDiscovery();
294 }
295
296 if (!mDevice.createBond()) {
297 return false;
298 }
299
Jason Monk7ce96b92015-02-02 11:27:58 -0500300 return true;
301 }
302
303 /**
304 * Return true if user initiated pairing on this device. The message text is
305 * slightly different for local vs. remote initiated pairing dialogs.
306 */
307 boolean isUserInitiatedPairing() {
Jakub Pawlowski0278ab92016-07-20 11:55:48 -0700308 return mDevice.isBondingInitiatedLocally();
Jason Monk7ce96b92015-02-02 11:27:58 -0500309 }
310
311 public void unpair() {
312 int state = getBondState();
313
314 if (state == BluetoothDevice.BOND_BONDING) {
315 mDevice.cancelBondProcess();
316 }
317
318 if (state != BluetoothDevice.BOND_NONE) {
319 final BluetoothDevice dev = mDevice;
320 if (dev != null) {
321 final boolean successful = dev.removeBond();
322 if (successful) {
323 if (Utils.D) {
324 Log.d(TAG, "Command sent successfully:REMOVE_BOND " + describe(null));
325 }
326 } else if (Utils.V) {
327 Log.v(TAG, "Framework rejected command immediately:REMOVE_BOND " +
328 describe(null));
329 }
330 }
331 }
332 }
333
334 public int getProfileConnectionState(LocalBluetoothProfile profile) {
335 if (mProfileConnectionState == null ||
336 mProfileConnectionState.get(profile) == null) {
337 // 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
Jason Monk7ce96b92015-02-02 11:27:58 -0500363 dispatchAttributesChanged();
364 }
365
366 public BluetoothDevice getDevice() {
367 return mDevice;
368 }
369
Antony Sargent7ad051e2017-06-29 15:23:13 -0700370 /**
371 * Convenience method that can be mocked - it lets tests avoid having to call getDevice() which
372 * causes problems in tests since BluetoothDevice is final and cannot be mocked.
373 * @return the address of this device
374 */
375 public String getAddress() {
376 return mDevice.getAddress();
377 }
378
Jason Monk7ce96b92015-02-02 11:27:58 -0500379 public String getName() {
380 return mName;
381 }
382
383 /**
384 * Populate name from BluetoothDevice.ACTION_FOUND intent
385 */
386 void setNewName(String name) {
387 if (mName == null) {
388 mName = name;
389 if (mName == null || TextUtils.isEmpty(mName)) {
390 mName = mDevice.getAddress();
391 }
392 dispatchAttributesChanged();
393 }
394 }
395
396 /**
397 * user changes the device name
398 */
399 public void setName(String name) {
400 if (!mName.equals(name)) {
401 mName = name;
402 mDevice.setAlias(name);
403 dispatchAttributesChanged();
404 }
405 }
406
407 void refreshName() {
408 fetchName();
409 dispatchAttributesChanged();
410 }
411
412 private void fetchName() {
413 mName = mDevice.getAliasName();
414
415 if (TextUtils.isEmpty(mName)) {
416 mName = mDevice.getAddress();
417 if (DEBUG) Log.d(TAG, "Device has no name (yet), use address: " + mName);
418 }
419 }
420
Jack He6258aae2017-06-29 17:01:23 -0700421 /**
422 * Get battery level from remote device
423 * @return battery level in percentage [0-100], or {@link BluetoothDevice#BATTERY_LEVEL_UNKNOWN}
424 */
425 public int getBatteryLevel() {
426 return mDevice.getBatteryLevel();
427 }
428
Jason Monk7ce96b92015-02-02 11:27:58 -0500429 void refresh() {
430 dispatchAttributesChanged();
431 }
432
Jack He51520472017-07-24 12:30:08 -0700433 public void setJustDiscovered(boolean justDiscovered) {
434 if (mJustDiscovered != justDiscovered) {
435 mJustDiscovered = justDiscovered;
Jason Monk7ce96b92015-02-02 11:27:58 -0500436 dispatchAttributesChanged();
437 }
438 }
439
440 public int getBondState() {
441 return mDevice.getBondState();
442 }
443
444 void setRssi(short rssi) {
445 if (mRssi != rssi) {
446 mRssi = rssi;
447 dispatchAttributesChanged();
448 }
449 }
450
451 /**
452 * Checks whether we are connected to this device (any profile counts).
453 *
454 * @return Whether it is connected.
455 */
456 public boolean isConnected() {
457 for (LocalBluetoothProfile profile : mProfiles) {
458 int status = getProfileConnectionState(profile);
459 if (status == BluetoothProfile.STATE_CONNECTED) {
460 return true;
461 }
462 }
463
464 return false;
465 }
466
467 public boolean isConnectedProfile(LocalBluetoothProfile profile) {
468 int status = getProfileConnectionState(profile);
469 return status == BluetoothProfile.STATE_CONNECTED;
470
471 }
472
473 public boolean isBusy() {
474 for (LocalBluetoothProfile profile : mProfiles) {
475 int status = getProfileConnectionState(profile);
476 if (status == BluetoothProfile.STATE_CONNECTING
477 || status == BluetoothProfile.STATE_DISCONNECTING) {
478 return true;
479 }
480 }
481 return getBondState() == BluetoothDevice.BOND_BONDING;
482 }
483
484 /**
485 * Fetches a new value for the cached BT class.
486 */
487 private void fetchBtClass() {
488 mBtClass = mDevice.getBluetoothClass();
489 }
490
491 private boolean updateProfiles() {
492 ParcelUuid[] uuids = mDevice.getUuids();
493 if (uuids == null) return false;
494
495 ParcelUuid[] localUuids = mLocalAdapter.getUuids();
496 if (localUuids == null) return false;
497
498 /**
499 * Now we know if the device supports PBAP, update permissions...
500 */
501 processPhonebookAccess();
502
503 mProfileManager.updateProfiles(uuids, localUuids, mProfiles, mRemovedProfiles,
504 mLocalNapRoleConnected, mDevice);
505
506 if (DEBUG) {
507 Log.e(TAG, "updating profiles for " + mDevice.getAliasName());
508 BluetoothClass bluetoothClass = mDevice.getBluetoothClass();
509
510 if (bluetoothClass != null) Log.v(TAG, "Class: " + bluetoothClass.toString());
511 Log.v(TAG, "UUID:");
512 for (ParcelUuid uuid : uuids) {
513 Log.v(TAG, " " + uuid);
514 }
515 }
516 return true;
517 }
518
519 /**
520 * Refreshes the UI for the BT class, including fetching the latest value
521 * for the class.
522 */
523 void refreshBtClass() {
524 fetchBtClass();
525 dispatchAttributesChanged();
526 }
527
528 /**
529 * Refreshes the UI when framework alerts us of a UUID change.
530 */
531 void onUuidChanged() {
532 updateProfiles();
Etan Cohen50d47612015-03-31 12:45:23 -0700533 ParcelUuid[] uuids = mDevice.getUuids();
Venkat Raghavan05e08c32015-04-06 16:26:11 -0700534
Etan Cohen50d47612015-03-31 12:45:23 -0700535 long timeout = MAX_UUID_DELAY_FOR_AUTO_CONNECT;
Venkat Raghavan05e08c32015-04-06 16:26:11 -0700536 if (BluetoothUuid.isUuidPresent(uuids, BluetoothUuid.Hogp)) {
537 timeout = MAX_HOGP_DELAY_FOR_AUTO_CONNECT;
538 }
Jason Monk7ce96b92015-02-02 11:27:58 -0500539
540 if (DEBUG) {
Etan Cohen50d47612015-03-31 12:45:23 -0700541 Log.d(TAG, "onUuidChanged: Time since last connect"
Jason Monk7ce96b92015-02-02 11:27:58 -0500542 + (SystemClock.elapsedRealtime() - mConnectAttempted));
543 }
544
545 /*
Venkat Raghavan05e08c32015-04-06 16:26:11 -0700546 * If a connect was attempted earlier without any UUID, we will do the connect now.
547 * Otherwise, allow the connect on UUID change.
Jason Monk7ce96b92015-02-02 11:27:58 -0500548 */
549 if (!mProfiles.isEmpty()
Andre Eisenbach7be83c52015-09-03 15:20:09 -0700550 && ((mConnectAttempted + timeout) > SystemClock.elapsedRealtime())) {
Jason Monk7ce96b92015-02-02 11:27:58 -0500551 connectWithoutResettingTimer(false);
552 }
Andre Eisenbach7be83c52015-09-03 15:20:09 -0700553
Jason Monk7ce96b92015-02-02 11:27:58 -0500554 dispatchAttributesChanged();
555 }
556
557 void onBondingStateChanged(int bondState) {
558 if (bondState == BluetoothDevice.BOND_NONE) {
559 mProfiles.clear();
Jason Monk7ce96b92015-02-02 11:27:58 -0500560 setPhonebookPermissionChoice(ACCESS_UNKNOWN);
561 setMessagePermissionChoice(ACCESS_UNKNOWN);
Casper Bonde424681e2015-05-04 22:07:45 -0700562 setSimPermissionChoice(ACCESS_UNKNOWN);
Jason Monk7ce96b92015-02-02 11:27:58 -0500563 mMessageRejectionCount = 0;
564 saveMessageRejectionCount();
565 }
566
567 refresh();
568
569 if (bondState == BluetoothDevice.BOND_BONDED) {
570 if (mDevice.isBluetoothDock()) {
571 onBondingDockConnect();
Jakub Pawlowski0278ab92016-07-20 11:55:48 -0700572 } else if (mDevice.isBondingInitiatedLocally()) {
Jason Monk7ce96b92015-02-02 11:27:58 -0500573 connect(false);
574 }
Jason Monk7ce96b92015-02-02 11:27:58 -0500575 }
576 }
577
578 void setBtClass(BluetoothClass btClass) {
579 if (btClass != null && mBtClass != btClass) {
580 mBtClass = btClass;
581 dispatchAttributesChanged();
582 }
583 }
584
585 public BluetoothClass getBtClass() {
586 return mBtClass;
587 }
588
589 public List<LocalBluetoothProfile> getProfiles() {
590 return Collections.unmodifiableList(mProfiles);
591 }
592
593 public List<LocalBluetoothProfile> getConnectableProfiles() {
594 List<LocalBluetoothProfile> connectableProfiles =
595 new ArrayList<LocalBluetoothProfile>();
596 for (LocalBluetoothProfile profile : mProfiles) {
597 if (profile.isConnectable()) {
598 connectableProfiles.add(profile);
599 }
600 }
601 return connectableProfiles;
602 }
603
604 public List<LocalBluetoothProfile> getRemovedProfiles() {
605 return mRemovedProfiles;
606 }
607
608 public void registerCallback(Callback callback) {
609 synchronized (mCallbacks) {
610 mCallbacks.add(callback);
611 }
612 }
613
614 public void unregisterCallback(Callback callback) {
615 synchronized (mCallbacks) {
616 mCallbacks.remove(callback);
617 }
618 }
619
620 private void dispatchAttributesChanged() {
621 synchronized (mCallbacks) {
622 for (Callback callback : mCallbacks) {
623 callback.onDeviceAttributesChanged();
624 }
625 }
626 }
627
628 @Override
629 public String toString() {
630 return mDevice.toString();
631 }
632
633 @Override
634 public boolean equals(Object o) {
635 if ((o == null) || !(o instanceof CachedBluetoothDevice)) {
636 return false;
637 }
638 return mDevice.equals(((CachedBluetoothDevice) o).mDevice);
639 }
640
641 @Override
642 public int hashCode() {
643 return mDevice.getAddress().hashCode();
644 }
645
646 // This comparison uses non-final fields so the sort order may change
647 // when device attributes change (such as bonding state). Settings
648 // will completely refresh the device list when this happens.
649 public int compareTo(CachedBluetoothDevice another) {
650 // Connected above not connected
651 int comparison = (another.isConnected() ? 1 : 0) - (isConnected() ? 1 : 0);
652 if (comparison != 0) return comparison;
653
654 // Paired above not paired
655 comparison = (another.getBondState() == BluetoothDevice.BOND_BONDED ? 1 : 0) -
656 (getBondState() == BluetoothDevice.BOND_BONDED ? 1 : 0);
657 if (comparison != 0) return comparison;
658
Jack He51520472017-07-24 12:30:08 -0700659 // Just discovered above discovered in the past
660 comparison = (another.mJustDiscovered ? 1 : 0) - (mJustDiscovered ? 1 : 0);
Jason Monk7ce96b92015-02-02 11:27:58 -0500661 if (comparison != 0) return comparison;
662
663 // Stronger signal above weaker signal
664 comparison = another.mRssi - mRssi;
665 if (comparison != 0) return comparison;
666
667 // Fallback on name
668 return mName.compareTo(another.mName);
669 }
670
671 public interface Callback {
672 void onDeviceAttributesChanged();
673 }
674
675 public int getPhonebookPermissionChoice() {
676 int permission = mDevice.getPhonebookAccessPermission();
677 if (permission == BluetoothDevice.ACCESS_ALLOWED) {
678 return ACCESS_ALLOWED;
679 } else if (permission == BluetoothDevice.ACCESS_REJECTED) {
680 return ACCESS_REJECTED;
681 }
682 return ACCESS_UNKNOWN;
683 }
684
685 public void setPhonebookPermissionChoice(int permissionChoice) {
686 int permission = BluetoothDevice.ACCESS_UNKNOWN;
687 if (permissionChoice == ACCESS_ALLOWED) {
688 permission = BluetoothDevice.ACCESS_ALLOWED;
689 } else if (permissionChoice == ACCESS_REJECTED) {
690 permission = BluetoothDevice.ACCESS_REJECTED;
691 }
692 mDevice.setPhonebookAccessPermission(permission);
693 }
694
695 // Migrates data from old data store (in Settings app's shared preferences) to new (in Bluetooth
696 // app's shared preferences).
697 private void migratePhonebookPermissionChoice() {
698 SharedPreferences preferences = mContext.getSharedPreferences(
699 "bluetooth_phonebook_permission", Context.MODE_PRIVATE);
700 if (!preferences.contains(mDevice.getAddress())) {
701 return;
702 }
703
704 if (mDevice.getPhonebookAccessPermission() == BluetoothDevice.ACCESS_UNKNOWN) {
705 int oldPermission = preferences.getInt(mDevice.getAddress(), ACCESS_UNKNOWN);
706 if (oldPermission == ACCESS_ALLOWED) {
707 mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_ALLOWED);
708 } else if (oldPermission == ACCESS_REJECTED) {
709 mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_REJECTED);
710 }
711 }
712
713 SharedPreferences.Editor editor = preferences.edit();
714 editor.remove(mDevice.getAddress());
715 editor.commit();
716 }
717
718 public int getMessagePermissionChoice() {
719 int permission = mDevice.getMessageAccessPermission();
720 if (permission == BluetoothDevice.ACCESS_ALLOWED) {
721 return ACCESS_ALLOWED;
722 } else if (permission == BluetoothDevice.ACCESS_REJECTED) {
723 return ACCESS_REJECTED;
724 }
725 return ACCESS_UNKNOWN;
726 }
727
728 public void setMessagePermissionChoice(int permissionChoice) {
729 int permission = BluetoothDevice.ACCESS_UNKNOWN;
730 if (permissionChoice == ACCESS_ALLOWED) {
731 permission = BluetoothDevice.ACCESS_ALLOWED;
732 } else if (permissionChoice == ACCESS_REJECTED) {
733 permission = BluetoothDevice.ACCESS_REJECTED;
734 }
735 mDevice.setMessageAccessPermission(permission);
736 }
737
Casper Bonde424681e2015-05-04 22:07:45 -0700738 public int getSimPermissionChoice() {
739 int permission = mDevice.getSimAccessPermission();
740 if (permission == BluetoothDevice.ACCESS_ALLOWED) {
741 return ACCESS_ALLOWED;
742 } else if (permission == BluetoothDevice.ACCESS_REJECTED) {
743 return ACCESS_REJECTED;
744 }
745 return ACCESS_UNKNOWN;
746 }
747
748 void setSimPermissionChoice(int permissionChoice) {
749 int permission = BluetoothDevice.ACCESS_UNKNOWN;
750 if (permissionChoice == ACCESS_ALLOWED) {
751 permission = BluetoothDevice.ACCESS_ALLOWED;
752 } else if (permissionChoice == ACCESS_REJECTED) {
753 permission = BluetoothDevice.ACCESS_REJECTED;
754 }
755 mDevice.setSimAccessPermission(permission);
756 }
757
Jason Monk7ce96b92015-02-02 11:27:58 -0500758 // Migrates data from old data store (in Settings app's shared preferences) to new (in Bluetooth
759 // app's shared preferences).
760 private void migrateMessagePermissionChoice() {
761 SharedPreferences preferences = mContext.getSharedPreferences(
762 "bluetooth_message_permission", Context.MODE_PRIVATE);
763 if (!preferences.contains(mDevice.getAddress())) {
764 return;
765 }
766
767 if (mDevice.getMessageAccessPermission() == BluetoothDevice.ACCESS_UNKNOWN) {
768 int oldPermission = preferences.getInt(mDevice.getAddress(), ACCESS_UNKNOWN);
769 if (oldPermission == ACCESS_ALLOWED) {
770 mDevice.setMessageAccessPermission(BluetoothDevice.ACCESS_ALLOWED);
771 } else if (oldPermission == ACCESS_REJECTED) {
772 mDevice.setMessageAccessPermission(BluetoothDevice.ACCESS_REJECTED);
773 }
774 }
775
776 SharedPreferences.Editor editor = preferences.edit();
777 editor.remove(mDevice.getAddress());
778 editor.commit();
779 }
780
781 /**
782 * @return Whether this rejection should persist.
783 */
784 public boolean checkAndIncreaseMessageRejectionCount() {
785 if (mMessageRejectionCount < MESSAGE_REJECTION_COUNT_LIMIT_TO_PERSIST) {
786 mMessageRejectionCount++;
787 saveMessageRejectionCount();
788 }
789 return mMessageRejectionCount >= MESSAGE_REJECTION_COUNT_LIMIT_TO_PERSIST;
790 }
791
792 private void fetchMessageRejectionCount() {
793 SharedPreferences preference = mContext.getSharedPreferences(
794 MESSAGE_REJECTION_COUNT_PREFS_NAME, Context.MODE_PRIVATE);
795 mMessageRejectionCount = preference.getInt(mDevice.getAddress(), 0);
796 }
797
798 private void saveMessageRejectionCount() {
799 SharedPreferences.Editor editor = mContext.getSharedPreferences(
800 MESSAGE_REJECTION_COUNT_PREFS_NAME, Context.MODE_PRIVATE).edit();
801 if (mMessageRejectionCount == 0) {
802 editor.remove(mDevice.getAddress());
803 } else {
804 editor.putInt(mDevice.getAddress(), mMessageRejectionCount);
805 }
806 editor.commit();
807 }
808
809 private void processPhonebookAccess() {
810 if (mDevice.getBondState() != BluetoothDevice.BOND_BONDED) return;
811
812 ParcelUuid[] uuids = mDevice.getUuids();
813 if (BluetoothUuid.containsAnyUuid(uuids, PbapServerProfile.PBAB_CLIENT_UUIDS)) {
814 // The pairing dialog now warns of phone-book access for paired devices.
815 // No separate prompt is displayed after pairing.
Sanket Padawe78c23762015-06-01 15:55:30 -0700816 if (getPhonebookPermissionChoice() == CachedBluetoothDevice.ACCESS_UNKNOWN) {
Sanket Padawe07533db2015-11-11 15:01:35 -0800817 if (mDevice.getBluetoothClass().getDeviceClass()
Hemant Guptab8d267a2016-06-07 12:53:59 +0530818 == BluetoothClass.Device.AUDIO_VIDEO_HANDSFREE ||
819 mDevice.getBluetoothClass().getDeviceClass()
820 == BluetoothClass.Device.AUDIO_VIDEO_WEARABLE_HEADSET) {
Sanket Padawe07533db2015-11-11 15:01:35 -0800821 setPhonebookPermissionChoice(CachedBluetoothDevice.ACCESS_ALLOWED);
822 } else {
823 setPhonebookPermissionChoice(CachedBluetoothDevice.ACCESS_REJECTED);
824 }
Sanket Padawe78c23762015-06-01 15:55:30 -0700825 }
Jason Monk7ce96b92015-02-02 11:27:58 -0500826 }
827 }
Jason Monkbe3c5db2015-02-04 13:00:55 -0500828
829 public int getMaxConnectionState() {
830 int maxState = BluetoothProfile.STATE_DISCONNECTED;
831 for (LocalBluetoothProfile profile : getProfiles()) {
832 int connectionStatus = getProfileConnectionState(profile);
833 if (connectionStatus > maxState) {
834 maxState = connectionStatus;
835 }
836 }
837 return maxState;
838 }
839
840 /**
841 * @return resource for string that discribes the connection state of this device.
842 */
Jack He6258aae2017-06-29 17:01:23 -0700843 public String getConnectionSummary() {
Jason Monkbe3c5db2015-02-04 13:00:55 -0500844 boolean profileConnected = false; // at least one profile is connected
845 boolean a2dpNotConnected = false; // A2DP is preferred but not connected
Sanket Agarwalf8a1c912016-01-26 20:12:52 -0800846 boolean hfpNotConnected = false; // HFP is preferred but not connected
Jason Monkbe3c5db2015-02-04 13:00:55 -0500847
848 for (LocalBluetoothProfile profile : getProfiles()) {
849 int connectionStatus = getProfileConnectionState(profile);
850
851 switch (connectionStatus) {
852 case BluetoothProfile.STATE_CONNECTING:
853 case BluetoothProfile.STATE_DISCONNECTING:
Jack He6258aae2017-06-29 17:01:23 -0700854 return mContext.getString(Utils.getConnectionStateSummary(connectionStatus));
Jason Monkbe3c5db2015-02-04 13:00:55 -0500855
856 case BluetoothProfile.STATE_CONNECTED:
857 profileConnected = true;
858 break;
859
860 case BluetoothProfile.STATE_DISCONNECTED:
861 if (profile.isProfileReady()) {
Sanket Agarwalf8a1c912016-01-26 20:12:52 -0800862 if ((profile instanceof A2dpProfile) ||
Sanket Agarwal1bec6a52015-10-21 18:23:27 -0700863 (profile instanceof A2dpSinkProfile)){
Jason Monkbe3c5db2015-02-04 13:00:55 -0500864 a2dpNotConnected = true;
Sanket Agarwalf8a1c912016-01-26 20:12:52 -0800865 } else if ((profile instanceof HeadsetProfile) ||
866 (profile instanceof HfpClientProfile)) {
867 hfpNotConnected = true;
Jason Monkbe3c5db2015-02-04 13:00:55 -0500868 }
869 }
870 break;
871 }
872 }
873
Jack He6258aae2017-06-29 17:01:23 -0700874 String batteryLevelPercentageString = null;
875 // Android framework should only set mBatteryLevel to valid range [0-100] or
876 // BluetoothDevice.BATTERY_LEVEL_UNKNOWN, any other value should be a framework bug.
877 // Thus assume here that if value is not BluetoothDevice.BATTERY_LEVEL_UNKNOWN, it must
878 // be valid
879 final int batteryLevel = getBatteryLevel();
880 if (batteryLevel != BluetoothDevice.BATTERY_LEVEL_UNKNOWN) {
881 // TODO: name com.android.settingslib.bluetooth.Utils something different
882 batteryLevelPercentageString =
883 com.android.settingslib.Utils.formatPercentage(batteryLevel);
884 }
885
Jason Monkbe3c5db2015-02-04 13:00:55 -0500886 if (profileConnected) {
Sanket Agarwalf8a1c912016-01-26 20:12:52 -0800887 if (a2dpNotConnected && hfpNotConnected) {
Jack He6258aae2017-06-29 17:01:23 -0700888 if (batteryLevelPercentageString != null) {
889 return mContext.getString(
890 R.string.bluetooth_connected_no_headset_no_a2dp_battery_level,
891 batteryLevelPercentageString);
892 } else {
893 return mContext.getString(R.string.bluetooth_connected_no_headset_no_a2dp);
894 }
895
Jason Monkbe3c5db2015-02-04 13:00:55 -0500896 } else if (a2dpNotConnected) {
Jack He6258aae2017-06-29 17:01:23 -0700897 if (batteryLevelPercentageString != null) {
898 return mContext.getString(R.string.bluetooth_connected_no_a2dp_battery_level,
899 batteryLevelPercentageString);
900 } else {
901 return mContext.getString(R.string.bluetooth_connected_no_a2dp);
902 }
903
Sanket Agarwalf8a1c912016-01-26 20:12:52 -0800904 } else if (hfpNotConnected) {
Jack He6258aae2017-06-29 17:01:23 -0700905 if (batteryLevelPercentageString != null) {
906 return mContext.getString(R.string.bluetooth_connected_no_headset_battery_level,
907 batteryLevelPercentageString);
908 } else {
909 return mContext.getString(R.string.bluetooth_connected_no_headset);
910 }
Jason Monkbe3c5db2015-02-04 13:00:55 -0500911 } else {
Jack He6258aae2017-06-29 17:01:23 -0700912 if (batteryLevelPercentageString != null) {
913 return mContext.getString(R.string.bluetooth_connected_battery_level,
914 batteryLevelPercentageString);
915 } else {
916 return mContext.getString(R.string.bluetooth_connected);
917 }
Jason Monkbe3c5db2015-02-04 13:00:55 -0500918 }
919 }
920
Jack He6258aae2017-06-29 17:01:23 -0700921 return getBondState() == BluetoothDevice.BOND_BONDING ?
922 mContext.getString(R.string.bluetooth_pairing) : null;
Jason Monkbe3c5db2015-02-04 13:00:55 -0500923 }
Jason Monk7ce96b92015-02-02 11:27:58 -0500924}