blob: ec25d2ddba62df0d04c7ba64b1a0d5c59af2d76b [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;
Pavlin Radoslavovc285d552018-02-06 16:14:00 -080030import android.support.annotation.VisibleForTesting;
Jason Monk7ce96b92015-02-02 11:27:58 -050031
Jason Monkbe3c5db2015-02-04 13:00:55 -050032import com.android.settingslib.R;
33
Jason Monk7ce96b92015-02-02 11:27:58 -050034import java.util.ArrayList;
35import java.util.Collection;
36import java.util.Collections;
37import java.util.HashMap;
38import java.util.List;
39
40/**
41 * CachedBluetoothDevice represents a remote Bluetooth device. It contains
42 * attributes of the device (such as the address, name, RSSI, etc.) and
43 * functionality that can be performed on the device (connect, pair, disconnect,
44 * etc.).
45 */
Fan Zhang82dd3b02016-12-27 13:13:00 -080046public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> {
Jason Monk7ce96b92015-02-02 11:27:58 -050047 private static final String TAG = "CachedBluetoothDevice";
48 private static final boolean DEBUG = Utils.V;
49
50 private final Context mContext;
51 private final LocalBluetoothAdapter mLocalAdapter;
52 private final LocalBluetoothProfileManager mProfileManager;
53 private final BluetoothDevice mDevice;
Jack Hec219bc92017-07-24 14:55:59 -070054 //TODO: consider remove, BluetoothDevice.getName() is already cached
Jason Monk7ce96b92015-02-02 11:27:58 -050055 private String mName;
Jack Hec219bc92017-07-24 14:55:59 -070056 // Need this since there is no method for getting RSSI
Jason Monk7ce96b92015-02-02 11:27:58 -050057 private short mRssi;
Jack Hec219bc92017-07-24 14:55:59 -070058 //TODO: consider remove, BluetoothDevice.getBluetoothClass() is already cached
Jason Monk7ce96b92015-02-02 11:27:58 -050059 private BluetoothClass mBtClass;
60 private HashMap<LocalBluetoothProfile, Integer> mProfileConnectionState;
61
62 private final List<LocalBluetoothProfile> mProfiles =
63 new ArrayList<LocalBluetoothProfile>();
64
65 // List of profiles that were previously in mProfiles, but have been removed
66 private final List<LocalBluetoothProfile> mRemovedProfiles =
67 new ArrayList<LocalBluetoothProfile>();
68
69 // Device supports PANU but not NAP: remove PanProfile after device disconnects from NAP
70 private boolean mLocalNapRoleConnected;
71
Jack He51520472017-07-24 12:30:08 -070072 private boolean mJustDiscovered;
Jason Monk7ce96b92015-02-02 11:27:58 -050073
Jason Monk7ce96b92015-02-02 11:27:58 -050074 private int mMessageRejectionCount;
75
76 private final Collection<Callback> mCallbacks = new ArrayList<Callback>();
77
78 // Following constants indicate the user's choices of Phone book/message access settings
79 // User hasn't made any choice or settings app has wiped out the memory
80 public final static int ACCESS_UNKNOWN = 0;
81 // User has accepted the connection and let Settings app remember the decision
82 public final static int ACCESS_ALLOWED = 1;
83 // User has rejected the connection and let Settings app remember the decision
84 public final static int ACCESS_REJECTED = 2;
85
86 // How many times user should reject the connection to make the choice persist.
87 private final static int MESSAGE_REJECTION_COUNT_LIMIT_TO_PERSIST = 2;
88
89 private final static String MESSAGE_REJECTION_COUNT_PREFS_NAME = "bluetooth_message_reject";
90
91 /**
92 * When we connect to multiple profiles, we only want to display a single
93 * error even if they all fail. This tracks that state.
94 */
95 private boolean mIsConnectingErrorPossible;
96
97 /**
98 * Last time a bt profile auto-connect was attempted.
99 * If an ACTION_UUID intent comes in within
100 * MAX_UUID_DELAY_FOR_AUTO_CONNECT milliseconds, we will try auto-connect
101 * again with the new UUIDs
102 */
103 private long mConnectAttempted;
104
105 // See mConnectAttempted
106 private static final long MAX_UUID_DELAY_FOR_AUTO_CONNECT = 5000;
Etan Cohen50d47612015-03-31 12:45:23 -0700107 private static final long MAX_HOGP_DELAY_FOR_AUTO_CONNECT = 30000;
Jason Monk7ce96b92015-02-02 11:27:58 -0500108
Pavlin Radoslavov1af33a12018-01-21 02:59:15 -0800109 // Active device state
110 private boolean mIsActiveDeviceA2dp = false;
111 private boolean mIsActiveDeviceHeadset = false;
112
Jason Monk7ce96b92015-02-02 11:27:58 -0500113 /**
114 * Describes the current device and profile for logging.
115 *
116 * @param profile Profile to describe
117 * @return Description of the device and profile
118 */
119 private String describe(LocalBluetoothProfile profile) {
120 StringBuilder sb = new StringBuilder();
121 sb.append("Address:").append(mDevice);
122 if (profile != null) {
123 sb.append(" Profile:").append(profile);
124 }
125
126 return sb.toString();
127 }
128
129 void onProfileStateChanged(LocalBluetoothProfile profile, int newProfileState) {
130 if (Utils.D) {
131 Log.d(TAG, "onProfileStateChanged: profile " + profile +
132 " newProfileState " + newProfileState);
133 }
134 if (mLocalAdapter.getBluetoothState() == BluetoothAdapter.STATE_TURNING_OFF)
135 {
136 if (Utils.D) Log.d(TAG, " BT Turninig Off...Profile conn state change ignored...");
137 return;
138 }
139 mProfileConnectionState.put(profile, newProfileState);
140 if (newProfileState == BluetoothProfile.STATE_CONNECTED) {
141 if (profile instanceof MapProfile) {
142 profile.setPreferred(mDevice, true);
Hemant Guptadbc3d8d2017-05-12 21:14:44 +0530143 }
144 if (!mProfiles.contains(profile)) {
Jason Monk7ce96b92015-02-02 11:27:58 -0500145 mRemovedProfiles.remove(profile);
146 mProfiles.add(profile);
147 if (profile instanceof PanProfile &&
148 ((PanProfile) profile).isLocalRoleNap(mDevice)) {
149 // Device doesn't support NAP, so remove PanProfile on disconnect
150 mLocalNapRoleConnected = true;
151 }
152 }
153 } else if (profile instanceof MapProfile &&
154 newProfileState == BluetoothProfile.STATE_DISCONNECTED) {
155 profile.setPreferred(mDevice, false);
156 } else if (mLocalNapRoleConnected && profile instanceof PanProfile &&
157 ((PanProfile) profile).isLocalRoleNap(mDevice) &&
158 newProfileState == BluetoothProfile.STATE_DISCONNECTED) {
159 Log.d(TAG, "Removing PanProfile from device after NAP disconnect");
160 mProfiles.remove(profile);
161 mRemovedProfiles.add(profile);
162 mLocalNapRoleConnected = false;
163 }
Pavlin Radoslavov1af33a12018-01-21 02:59:15 -0800164 fetchActiveDevices();
Jason Monk7ce96b92015-02-02 11:27:58 -0500165 }
166
167 CachedBluetoothDevice(Context context,
168 LocalBluetoothAdapter adapter,
169 LocalBluetoothProfileManager profileManager,
170 BluetoothDevice device) {
171 mContext = context;
172 mLocalAdapter = adapter;
173 mProfileManager = profileManager;
174 mDevice = device;
175 mProfileConnectionState = new HashMap<LocalBluetoothProfile, Integer>();
176 fillData();
177 }
178
179 public void disconnect() {
180 for (LocalBluetoothProfile profile : mProfiles) {
181 disconnect(profile);
182 }
183 // Disconnect PBAP server in case its connected
184 // This is to ensure all the profiles are disconnected as some CK/Hs do not
185 // disconnect PBAP connection when HF connection is brought down
186 PbapServerProfile PbapProfile = mProfileManager.getPbapProfile();
187 if (PbapProfile.getConnectionStatus(mDevice) == BluetoothProfile.STATE_CONNECTED)
188 {
189 PbapProfile.disconnect(mDevice);
190 }
191 }
192
193 public void disconnect(LocalBluetoothProfile profile) {
194 if (profile.disconnect(mDevice)) {
195 if (Utils.D) {
196 Log.d(TAG, "Command sent successfully:DISCONNECT " + describe(profile));
197 }
198 }
199 }
200
201 public void connect(boolean connectAllProfiles) {
202 if (!ensurePaired()) {
203 return;
204 }
205
206 mConnectAttempted = SystemClock.elapsedRealtime();
207 connectWithoutResettingTimer(connectAllProfiles);
208 }
209
210 void onBondingDockConnect() {
211 // Attempt to connect if UUIDs are available. Otherwise,
212 // we will connect when the ACTION_UUID intent arrives.
213 connect(false);
214 }
215
216 private void connectWithoutResettingTimer(boolean connectAllProfiles) {
217 // Try to initialize the profiles if they were not.
218 if (mProfiles.isEmpty()) {
219 // if mProfiles is empty, then do not invoke updateProfiles. This causes a race
220 // condition with carkits during pairing, wherein RemoteDevice.UUIDs have been updated
221 // from bluetooth stack but ACTION.uuid is not sent yet.
222 // Eventually ACTION.uuid will be received which shall trigger the connection of the
223 // various profiles
224 // If UUIDs are not available yet, connect will be happen
225 // upon arrival of the ACTION_UUID intent.
226 Log.d(TAG, "No profiles. Maybe we will connect later");
227 return;
228 }
229
230 // Reset the only-show-one-error-dialog tracking variable
231 mIsConnectingErrorPossible = true;
232
233 int preferredProfiles = 0;
234 for (LocalBluetoothProfile profile : mProfiles) {
235 if (connectAllProfiles ? profile.isConnectable() : profile.isAutoConnectable()) {
236 if (profile.isPreferred(mDevice)) {
237 ++preferredProfiles;
238 connectInt(profile);
239 }
240 }
241 }
242 if (DEBUG) Log.d(TAG, "Preferred profiles = " + preferredProfiles);
243
244 if (preferredProfiles == 0) {
245 connectAutoConnectableProfiles();
246 }
247 }
248
249 private void connectAutoConnectableProfiles() {
250 if (!ensurePaired()) {
251 return;
252 }
253 // Reset the only-show-one-error-dialog tracking variable
254 mIsConnectingErrorPossible = true;
255
256 for (LocalBluetoothProfile profile : mProfiles) {
257 if (profile.isAutoConnectable()) {
258 profile.setPreferred(mDevice, true);
259 connectInt(profile);
260 }
261 }
262 }
263
264 /**
265 * Connect this device to the specified profile.
266 *
267 * @param profile the profile to use with the remote device
268 */
269 public void connectProfile(LocalBluetoothProfile profile) {
270 mConnectAttempted = SystemClock.elapsedRealtime();
271 // Reset the only-show-one-error-dialog tracking variable
272 mIsConnectingErrorPossible = true;
273 connectInt(profile);
274 // Refresh the UI based on profile.connect() call
275 refresh();
276 }
277
278 synchronized void connectInt(LocalBluetoothProfile profile) {
279 if (!ensurePaired()) {
280 return;
281 }
282 if (profile.connect(mDevice)) {
283 if (Utils.D) {
284 Log.d(TAG, "Command sent successfully:CONNECT " + describe(profile));
285 }
286 return;
287 }
288 Log.i(TAG, "Failed to connect " + profile.toString() + " to " + mName);
289 }
290
291 private boolean ensurePaired() {
292 if (getBondState() == BluetoothDevice.BOND_NONE) {
293 startPairing();
294 return false;
295 } else {
296 return true;
297 }
298 }
299
300 public boolean startPairing() {
301 // Pairing is unreliable while scanning, so cancel discovery
302 if (mLocalAdapter.isDiscovering()) {
303 mLocalAdapter.cancelDiscovery();
304 }
305
306 if (!mDevice.createBond()) {
307 return false;
308 }
309
Jason Monk7ce96b92015-02-02 11:27:58 -0500310 return true;
311 }
312
313 /**
314 * Return true if user initiated pairing on this device. The message text is
315 * slightly different for local vs. remote initiated pairing dialogs.
316 */
317 boolean isUserInitiatedPairing() {
Jakub Pawlowski0278ab92016-07-20 11:55:48 -0700318 return mDevice.isBondingInitiatedLocally();
Jason Monk7ce96b92015-02-02 11:27:58 -0500319 }
320
321 public void unpair() {
322 int state = getBondState();
323
324 if (state == BluetoothDevice.BOND_BONDING) {
325 mDevice.cancelBondProcess();
326 }
327
328 if (state != BluetoothDevice.BOND_NONE) {
329 final BluetoothDevice dev = mDevice;
330 if (dev != null) {
331 final boolean successful = dev.removeBond();
332 if (successful) {
333 if (Utils.D) {
334 Log.d(TAG, "Command sent successfully:REMOVE_BOND " + describe(null));
335 }
336 } else if (Utils.V) {
337 Log.v(TAG, "Framework rejected command immediately:REMOVE_BOND " +
338 describe(null));
339 }
340 }
341 }
342 }
343
344 public int getProfileConnectionState(LocalBluetoothProfile profile) {
xutianguo22bbb8192016-06-22 11:32:00 +0800345 if (mProfileConnectionState.get(profile) == null) {
Jason Monk7ce96b92015-02-02 11:27:58 -0500346 // If cache is empty make the binder call to get the state
347 int state = profile.getConnectionStatus(mDevice);
348 mProfileConnectionState.put(profile, state);
349 }
350 return mProfileConnectionState.get(profile);
351 }
352
353 public void clearProfileConnectionState ()
354 {
355 if (Utils.D) {
356 Log.d(TAG," Clearing all connection state for dev:" + mDevice.getName());
357 }
358 for (LocalBluetoothProfile profile :getProfiles()) {
359 mProfileConnectionState.put(profile, BluetoothProfile.STATE_DISCONNECTED);
360 }
361 }
362
363 // TODO: do any of these need to run async on a background thread?
364 private void fillData() {
365 fetchName();
366 fetchBtClass();
367 updateProfiles();
Pavlin Radoslavov1af33a12018-01-21 02:59:15 -0800368 fetchActiveDevices();
Jason Monk7ce96b92015-02-02 11:27:58 -0500369 migratePhonebookPermissionChoice();
370 migrateMessagePermissionChoice();
371 fetchMessageRejectionCount();
372
Jason Monk7ce96b92015-02-02 11:27:58 -0500373 dispatchAttributesChanged();
374 }
375
376 public BluetoothDevice getDevice() {
377 return mDevice;
378 }
379
Antony Sargent7ad051e2017-06-29 15:23:13 -0700380 /**
381 * Convenience method that can be mocked - it lets tests avoid having to call getDevice() which
382 * causes problems in tests since BluetoothDevice is final and cannot be mocked.
383 * @return the address of this device
384 */
385 public String getAddress() {
386 return mDevice.getAddress();
387 }
388
Jason Monk7ce96b92015-02-02 11:27:58 -0500389 public String getName() {
390 return mName;
391 }
392
393 /**
394 * Populate name from BluetoothDevice.ACTION_FOUND intent
395 */
396 void setNewName(String name) {
397 if (mName == null) {
398 mName = name;
399 if (mName == null || TextUtils.isEmpty(mName)) {
400 mName = mDevice.getAddress();
401 }
402 dispatchAttributesChanged();
403 }
404 }
405
406 /**
Jack Hec219bc92017-07-24 14:55:59 -0700407 * User changes the device name
408 * @param name new alias name to be set, should never be null
Jason Monk7ce96b92015-02-02 11:27:58 -0500409 */
410 public void setName(String name) {
Jack Hec219bc92017-07-24 14:55:59 -0700411 // Prevent mName to be set to null if setName(null) is called
412 if (name != null && !TextUtils.equals(name, mName)) {
Jason Monk7ce96b92015-02-02 11:27:58 -0500413 mName = name;
414 mDevice.setAlias(name);
415 dispatchAttributesChanged();
416 }
417 }
418
Hansong Zhang6a416322018-03-19 18:20:38 -0700419 /**
420 * Set this device as active device
421 * @return true if at least one profile on this device is set to active, false otherwise
422 */
423 public boolean setActive() {
424 boolean result = false;
425 A2dpProfile a2dpProfile = mProfileManager.getA2dpProfile();
426 if (a2dpProfile != null && isConnectedProfile(a2dpProfile)) {
427 if (a2dpProfile.setActiveDevice(getDevice())) {
428 Log.i(TAG, "OnPreferenceClickListener: A2DP active device=" + this);
429 result = true;
430 }
431 }
432 HeadsetProfile headsetProfile = mProfileManager.getHeadsetProfile();
433 if ((headsetProfile != null) && isConnectedProfile(headsetProfile)) {
434 if (headsetProfile.setActiveDevice(getDevice())) {
435 Log.i(TAG, "OnPreferenceClickListener: Headset active device=" + this);
436 result = true;
437 }
438 }
439 return result;
440 }
441
Jason Monk7ce96b92015-02-02 11:27:58 -0500442 void refreshName() {
443 fetchName();
444 dispatchAttributesChanged();
445 }
446
447 private void fetchName() {
448 mName = mDevice.getAliasName();
449
450 if (TextUtils.isEmpty(mName)) {
451 mName = mDevice.getAddress();
452 if (DEBUG) Log.d(TAG, "Device has no name (yet), use address: " + mName);
453 }
454 }
455
Jack He6258aae2017-06-29 17:01:23 -0700456 /**
Jack Hec219bc92017-07-24 14:55:59 -0700457 * Checks if device has a human readable name besides MAC address
458 * @return true if device's alias name is not null nor empty, false otherwise
459 */
460 public boolean hasHumanReadableName() {
461 return !TextUtils.isEmpty(mDevice.getAliasName());
462 }
463
464 /**
Jack He6258aae2017-06-29 17:01:23 -0700465 * Get battery level from remote device
466 * @return battery level in percentage [0-100], or {@link BluetoothDevice#BATTERY_LEVEL_UNKNOWN}
467 */
468 public int getBatteryLevel() {
469 return mDevice.getBatteryLevel();
470 }
471
Jason Monk7ce96b92015-02-02 11:27:58 -0500472 void refresh() {
473 dispatchAttributesChanged();
474 }
475
Jack He51520472017-07-24 12:30:08 -0700476 public void setJustDiscovered(boolean justDiscovered) {
477 if (mJustDiscovered != justDiscovered) {
478 mJustDiscovered = justDiscovered;
Jason Monk7ce96b92015-02-02 11:27:58 -0500479 dispatchAttributesChanged();
480 }
481 }
482
483 public int getBondState() {
484 return mDevice.getBondState();
485 }
486
Pavlin Radoslavov1af33a12018-01-21 02:59:15 -0800487 /**
Pavlin Radoslavovc285d552018-02-06 16:14:00 -0800488 * Update the device status as active or non-active per Bluetooth profile.
Pavlin Radoslavov1af33a12018-01-21 02:59:15 -0800489 *
490 * @param isActive true if the device is active
491 * @param bluetoothProfile the Bluetooth profile
492 */
Pavlin Radoslavovc285d552018-02-06 16:14:00 -0800493 public void onActiveDeviceChanged(boolean isActive, int bluetoothProfile) {
Pavlin Radoslavov1af33a12018-01-21 02:59:15 -0800494 boolean changed = false;
495 switch (bluetoothProfile) {
496 case BluetoothProfile.A2DP:
497 changed = (mIsActiveDeviceA2dp != isActive);
498 mIsActiveDeviceA2dp = isActive;
499 break;
500 case BluetoothProfile.HEADSET:
501 changed = (mIsActiveDeviceHeadset != isActive);
502 mIsActiveDeviceHeadset = isActive;
503 break;
504 default:
Pavlin Radoslavovc285d552018-02-06 16:14:00 -0800505 Log.w(TAG, "onActiveDeviceChanged: unknown profile " + bluetoothProfile +
Pavlin Radoslavov1af33a12018-01-21 02:59:15 -0800506 " isActive " + isActive);
507 break;
508 }
509 if (changed) {
510 dispatchAttributesChanged();
511 }
512 }
513
Pavlin Radoslavovc285d552018-02-06 16:14:00 -0800514 /**
515 * Get the device status as active or non-active per Bluetooth profile.
516 *
517 * @param bluetoothProfile the Bluetooth profile
518 * @return true if the device is active
519 */
520 @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
521 public boolean isActiveDevice(int bluetoothProfile) {
522 switch (bluetoothProfile) {
523 case BluetoothProfile.A2DP:
524 return mIsActiveDeviceA2dp;
525 case BluetoothProfile.HEADSET:
526 return mIsActiveDeviceHeadset;
527 default:
528 Log.w(TAG, "getActiveDevice: unknown profile " + bluetoothProfile);
529 break;
530 }
531 return false;
532 }
533
Jason Monk7ce96b92015-02-02 11:27:58 -0500534 void setRssi(short rssi) {
535 if (mRssi != rssi) {
536 mRssi = rssi;
537 dispatchAttributesChanged();
538 }
539 }
540
541 /**
542 * Checks whether we are connected to this device (any profile counts).
543 *
544 * @return Whether it is connected.
545 */
546 public boolean isConnected() {
547 for (LocalBluetoothProfile profile : mProfiles) {
548 int status = getProfileConnectionState(profile);
549 if (status == BluetoothProfile.STATE_CONNECTED) {
550 return true;
551 }
552 }
553
554 return false;
555 }
556
557 public boolean isConnectedProfile(LocalBluetoothProfile profile) {
558 int status = getProfileConnectionState(profile);
559 return status == BluetoothProfile.STATE_CONNECTED;
560
561 }
562
563 public boolean isBusy() {
564 for (LocalBluetoothProfile profile : mProfiles) {
565 int status = getProfileConnectionState(profile);
566 if (status == BluetoothProfile.STATE_CONNECTING
567 || status == BluetoothProfile.STATE_DISCONNECTING) {
568 return true;
569 }
570 }
571 return getBondState() == BluetoothDevice.BOND_BONDING;
572 }
573
574 /**
575 * Fetches a new value for the cached BT class.
576 */
577 private void fetchBtClass() {
578 mBtClass = mDevice.getBluetoothClass();
579 }
580
581 private boolean updateProfiles() {
582 ParcelUuid[] uuids = mDevice.getUuids();
583 if (uuids == null) return false;
584
585 ParcelUuid[] localUuids = mLocalAdapter.getUuids();
586 if (localUuids == null) return false;
587
Jack Hec219bc92017-07-24 14:55:59 -0700588 /*
Jason Monk7ce96b92015-02-02 11:27:58 -0500589 * Now we know if the device supports PBAP, update permissions...
590 */
591 processPhonebookAccess();
592
593 mProfileManager.updateProfiles(uuids, localUuids, mProfiles, mRemovedProfiles,
594 mLocalNapRoleConnected, mDevice);
595
596 if (DEBUG) {
597 Log.e(TAG, "updating profiles for " + mDevice.getAliasName());
598 BluetoothClass bluetoothClass = mDevice.getBluetoothClass();
599
600 if (bluetoothClass != null) Log.v(TAG, "Class: " + bluetoothClass.toString());
601 Log.v(TAG, "UUID:");
602 for (ParcelUuid uuid : uuids) {
603 Log.v(TAG, " " + uuid);
604 }
605 }
606 return true;
607 }
608
Pavlin Radoslavov1af33a12018-01-21 02:59:15 -0800609 private void fetchActiveDevices() {
610 A2dpProfile a2dpProfile = mProfileManager.getA2dpProfile();
611 if (a2dpProfile != null) {
612 mIsActiveDeviceA2dp = mDevice.equals(a2dpProfile.getActiveDevice());
613 }
614 HeadsetProfile headsetProfile = mProfileManager.getHeadsetProfile();
615 if (headsetProfile != null) {
616 mIsActiveDeviceHeadset = mDevice.equals(headsetProfile.getActiveDevice());
617 }
618 }
619
Jason Monk7ce96b92015-02-02 11:27:58 -0500620 /**
621 * Refreshes the UI for the BT class, including fetching the latest value
622 * for the class.
623 */
624 void refreshBtClass() {
625 fetchBtClass();
626 dispatchAttributesChanged();
627 }
628
629 /**
630 * Refreshes the UI when framework alerts us of a UUID change.
631 */
632 void onUuidChanged() {
633 updateProfiles();
Etan Cohen50d47612015-03-31 12:45:23 -0700634 ParcelUuid[] uuids = mDevice.getUuids();
Venkat Raghavan05e08c32015-04-06 16:26:11 -0700635
Etan Cohen50d47612015-03-31 12:45:23 -0700636 long timeout = MAX_UUID_DELAY_FOR_AUTO_CONNECT;
Venkat Raghavan05e08c32015-04-06 16:26:11 -0700637 if (BluetoothUuid.isUuidPresent(uuids, BluetoothUuid.Hogp)) {
638 timeout = MAX_HOGP_DELAY_FOR_AUTO_CONNECT;
639 }
Jason Monk7ce96b92015-02-02 11:27:58 -0500640
641 if (DEBUG) {
Etan Cohen50d47612015-03-31 12:45:23 -0700642 Log.d(TAG, "onUuidChanged: Time since last connect"
Jason Monk7ce96b92015-02-02 11:27:58 -0500643 + (SystemClock.elapsedRealtime() - mConnectAttempted));
644 }
645
646 /*
Venkat Raghavan05e08c32015-04-06 16:26:11 -0700647 * If a connect was attempted earlier without any UUID, we will do the connect now.
648 * Otherwise, allow the connect on UUID change.
Jason Monk7ce96b92015-02-02 11:27:58 -0500649 */
650 if (!mProfiles.isEmpty()
Andre Eisenbach7be83c52015-09-03 15:20:09 -0700651 && ((mConnectAttempted + timeout) > SystemClock.elapsedRealtime())) {
Jason Monk7ce96b92015-02-02 11:27:58 -0500652 connectWithoutResettingTimer(false);
653 }
Andre Eisenbach7be83c52015-09-03 15:20:09 -0700654
Jason Monk7ce96b92015-02-02 11:27:58 -0500655 dispatchAttributesChanged();
656 }
657
658 void onBondingStateChanged(int bondState) {
659 if (bondState == BluetoothDevice.BOND_NONE) {
660 mProfiles.clear();
Jason Monk7ce96b92015-02-02 11:27:58 -0500661 setPhonebookPermissionChoice(ACCESS_UNKNOWN);
662 setMessagePermissionChoice(ACCESS_UNKNOWN);
Casper Bonde424681e2015-05-04 22:07:45 -0700663 setSimPermissionChoice(ACCESS_UNKNOWN);
Jason Monk7ce96b92015-02-02 11:27:58 -0500664 mMessageRejectionCount = 0;
665 saveMessageRejectionCount();
666 }
667
668 refresh();
669
670 if (bondState == BluetoothDevice.BOND_BONDED) {
671 if (mDevice.isBluetoothDock()) {
672 onBondingDockConnect();
Jakub Pawlowski0278ab92016-07-20 11:55:48 -0700673 } else if (mDevice.isBondingInitiatedLocally()) {
Jason Monk7ce96b92015-02-02 11:27:58 -0500674 connect(false);
675 }
Jason Monk7ce96b92015-02-02 11:27:58 -0500676 }
677 }
678
679 void setBtClass(BluetoothClass btClass) {
680 if (btClass != null && mBtClass != btClass) {
681 mBtClass = btClass;
682 dispatchAttributesChanged();
683 }
684 }
685
686 public BluetoothClass getBtClass() {
687 return mBtClass;
688 }
689
690 public List<LocalBluetoothProfile> getProfiles() {
691 return Collections.unmodifiableList(mProfiles);
692 }
693
694 public List<LocalBluetoothProfile> getConnectableProfiles() {
695 List<LocalBluetoothProfile> connectableProfiles =
696 new ArrayList<LocalBluetoothProfile>();
697 for (LocalBluetoothProfile profile : mProfiles) {
698 if (profile.isConnectable()) {
699 connectableProfiles.add(profile);
700 }
701 }
702 return connectableProfiles;
703 }
704
705 public List<LocalBluetoothProfile> getRemovedProfiles() {
706 return mRemovedProfiles;
707 }
708
709 public void registerCallback(Callback callback) {
710 synchronized (mCallbacks) {
711 mCallbacks.add(callback);
712 }
713 }
714
715 public void unregisterCallback(Callback callback) {
716 synchronized (mCallbacks) {
717 mCallbacks.remove(callback);
718 }
719 }
720
721 private void dispatchAttributesChanged() {
722 synchronized (mCallbacks) {
723 for (Callback callback : mCallbacks) {
724 callback.onDeviceAttributesChanged();
725 }
726 }
727 }
728
729 @Override
730 public String toString() {
731 return mDevice.toString();
732 }
733
734 @Override
735 public boolean equals(Object o) {
736 if ((o == null) || !(o instanceof CachedBluetoothDevice)) {
737 return false;
738 }
739 return mDevice.equals(((CachedBluetoothDevice) o).mDevice);
740 }
741
742 @Override
743 public int hashCode() {
744 return mDevice.getAddress().hashCode();
745 }
746
747 // This comparison uses non-final fields so the sort order may change
748 // when device attributes change (such as bonding state). Settings
749 // will completely refresh the device list when this happens.
750 public int compareTo(CachedBluetoothDevice another) {
751 // Connected above not connected
752 int comparison = (another.isConnected() ? 1 : 0) - (isConnected() ? 1 : 0);
753 if (comparison != 0) return comparison;
754
755 // Paired above not paired
756 comparison = (another.getBondState() == BluetoothDevice.BOND_BONDED ? 1 : 0) -
757 (getBondState() == BluetoothDevice.BOND_BONDED ? 1 : 0);
758 if (comparison != 0) return comparison;
759
Jack He51520472017-07-24 12:30:08 -0700760 // Just discovered above discovered in the past
761 comparison = (another.mJustDiscovered ? 1 : 0) - (mJustDiscovered ? 1 : 0);
Jason Monk7ce96b92015-02-02 11:27:58 -0500762 if (comparison != 0) return comparison;
763
764 // Stronger signal above weaker signal
765 comparison = another.mRssi - mRssi;
766 if (comparison != 0) return comparison;
767
768 // Fallback on name
769 return mName.compareTo(another.mName);
770 }
771
772 public interface Callback {
773 void onDeviceAttributesChanged();
774 }
775
776 public int getPhonebookPermissionChoice() {
777 int permission = mDevice.getPhonebookAccessPermission();
778 if (permission == BluetoothDevice.ACCESS_ALLOWED) {
779 return ACCESS_ALLOWED;
780 } else if (permission == BluetoothDevice.ACCESS_REJECTED) {
781 return ACCESS_REJECTED;
782 }
783 return ACCESS_UNKNOWN;
784 }
785
786 public void setPhonebookPermissionChoice(int permissionChoice) {
787 int permission = BluetoothDevice.ACCESS_UNKNOWN;
788 if (permissionChoice == ACCESS_ALLOWED) {
789 permission = BluetoothDevice.ACCESS_ALLOWED;
790 } else if (permissionChoice == ACCESS_REJECTED) {
791 permission = BluetoothDevice.ACCESS_REJECTED;
792 }
793 mDevice.setPhonebookAccessPermission(permission);
794 }
795
796 // Migrates data from old data store (in Settings app's shared preferences) to new (in Bluetooth
797 // app's shared preferences).
798 private void migratePhonebookPermissionChoice() {
799 SharedPreferences preferences = mContext.getSharedPreferences(
800 "bluetooth_phonebook_permission", Context.MODE_PRIVATE);
801 if (!preferences.contains(mDevice.getAddress())) {
802 return;
803 }
804
805 if (mDevice.getPhonebookAccessPermission() == BluetoothDevice.ACCESS_UNKNOWN) {
806 int oldPermission = preferences.getInt(mDevice.getAddress(), ACCESS_UNKNOWN);
807 if (oldPermission == ACCESS_ALLOWED) {
808 mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_ALLOWED);
809 } else if (oldPermission == ACCESS_REJECTED) {
810 mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_REJECTED);
811 }
812 }
813
814 SharedPreferences.Editor editor = preferences.edit();
815 editor.remove(mDevice.getAddress());
816 editor.commit();
817 }
818
819 public int getMessagePermissionChoice() {
820 int permission = mDevice.getMessageAccessPermission();
821 if (permission == BluetoothDevice.ACCESS_ALLOWED) {
822 return ACCESS_ALLOWED;
823 } else if (permission == BluetoothDevice.ACCESS_REJECTED) {
824 return ACCESS_REJECTED;
825 }
826 return ACCESS_UNKNOWN;
827 }
828
829 public void setMessagePermissionChoice(int permissionChoice) {
830 int permission = BluetoothDevice.ACCESS_UNKNOWN;
831 if (permissionChoice == ACCESS_ALLOWED) {
832 permission = BluetoothDevice.ACCESS_ALLOWED;
833 } else if (permissionChoice == ACCESS_REJECTED) {
834 permission = BluetoothDevice.ACCESS_REJECTED;
835 }
836 mDevice.setMessageAccessPermission(permission);
837 }
838
Casper Bonde424681e2015-05-04 22:07:45 -0700839 public int getSimPermissionChoice() {
840 int permission = mDevice.getSimAccessPermission();
841 if (permission == BluetoothDevice.ACCESS_ALLOWED) {
842 return ACCESS_ALLOWED;
843 } else if (permission == BluetoothDevice.ACCESS_REJECTED) {
844 return ACCESS_REJECTED;
845 }
846 return ACCESS_UNKNOWN;
847 }
848
849 void setSimPermissionChoice(int permissionChoice) {
850 int permission = BluetoothDevice.ACCESS_UNKNOWN;
851 if (permissionChoice == ACCESS_ALLOWED) {
852 permission = BluetoothDevice.ACCESS_ALLOWED;
853 } else if (permissionChoice == ACCESS_REJECTED) {
854 permission = BluetoothDevice.ACCESS_REJECTED;
855 }
856 mDevice.setSimAccessPermission(permission);
857 }
858
Jason Monk7ce96b92015-02-02 11:27:58 -0500859 // Migrates data from old data store (in Settings app's shared preferences) to new (in Bluetooth
860 // app's shared preferences).
861 private void migrateMessagePermissionChoice() {
862 SharedPreferences preferences = mContext.getSharedPreferences(
863 "bluetooth_message_permission", Context.MODE_PRIVATE);
864 if (!preferences.contains(mDevice.getAddress())) {
865 return;
866 }
867
868 if (mDevice.getMessageAccessPermission() == BluetoothDevice.ACCESS_UNKNOWN) {
869 int oldPermission = preferences.getInt(mDevice.getAddress(), ACCESS_UNKNOWN);
870 if (oldPermission == ACCESS_ALLOWED) {
871 mDevice.setMessageAccessPermission(BluetoothDevice.ACCESS_ALLOWED);
872 } else if (oldPermission == ACCESS_REJECTED) {
873 mDevice.setMessageAccessPermission(BluetoothDevice.ACCESS_REJECTED);
874 }
875 }
876
877 SharedPreferences.Editor editor = preferences.edit();
878 editor.remove(mDevice.getAddress());
879 editor.commit();
880 }
881
882 /**
883 * @return Whether this rejection should persist.
884 */
885 public boolean checkAndIncreaseMessageRejectionCount() {
886 if (mMessageRejectionCount < MESSAGE_REJECTION_COUNT_LIMIT_TO_PERSIST) {
887 mMessageRejectionCount++;
888 saveMessageRejectionCount();
889 }
890 return mMessageRejectionCount >= MESSAGE_REJECTION_COUNT_LIMIT_TO_PERSIST;
891 }
892
893 private void fetchMessageRejectionCount() {
894 SharedPreferences preference = mContext.getSharedPreferences(
895 MESSAGE_REJECTION_COUNT_PREFS_NAME, Context.MODE_PRIVATE);
896 mMessageRejectionCount = preference.getInt(mDevice.getAddress(), 0);
897 }
898
899 private void saveMessageRejectionCount() {
900 SharedPreferences.Editor editor = mContext.getSharedPreferences(
901 MESSAGE_REJECTION_COUNT_PREFS_NAME, Context.MODE_PRIVATE).edit();
902 if (mMessageRejectionCount == 0) {
903 editor.remove(mDevice.getAddress());
904 } else {
905 editor.putInt(mDevice.getAddress(), mMessageRejectionCount);
906 }
907 editor.commit();
908 }
909
910 private void processPhonebookAccess() {
911 if (mDevice.getBondState() != BluetoothDevice.BOND_BONDED) return;
912
913 ParcelUuid[] uuids = mDevice.getUuids();
914 if (BluetoothUuid.containsAnyUuid(uuids, PbapServerProfile.PBAB_CLIENT_UUIDS)) {
915 // The pairing dialog now warns of phone-book access for paired devices.
916 // No separate prompt is displayed after pairing.
Sanket Padawe78c23762015-06-01 15:55:30 -0700917 if (getPhonebookPermissionChoice() == CachedBluetoothDevice.ACCESS_UNKNOWN) {
Sanket Padawe07533db2015-11-11 15:01:35 -0800918 if (mDevice.getBluetoothClass().getDeviceClass()
Hemant Guptab8d267a2016-06-07 12:53:59 +0530919 == BluetoothClass.Device.AUDIO_VIDEO_HANDSFREE ||
920 mDevice.getBluetoothClass().getDeviceClass()
921 == BluetoothClass.Device.AUDIO_VIDEO_WEARABLE_HEADSET) {
Sanket Padawe07533db2015-11-11 15:01:35 -0800922 setPhonebookPermissionChoice(CachedBluetoothDevice.ACCESS_ALLOWED);
923 } else {
924 setPhonebookPermissionChoice(CachedBluetoothDevice.ACCESS_REJECTED);
925 }
Sanket Padawe78c23762015-06-01 15:55:30 -0700926 }
Jason Monk7ce96b92015-02-02 11:27:58 -0500927 }
928 }
Jason Monkbe3c5db2015-02-04 13:00:55 -0500929
930 public int getMaxConnectionState() {
931 int maxState = BluetoothProfile.STATE_DISCONNECTED;
932 for (LocalBluetoothProfile profile : getProfiles()) {
933 int connectionStatus = getProfileConnectionState(profile);
934 if (connectionStatus > maxState) {
935 maxState = connectionStatus;
936 }
937 }
938 return maxState;
939 }
940
941 /**
942 * @return resource for string that discribes the connection state of this device.
943 */
Jack He6258aae2017-06-29 17:01:23 -0700944 public String getConnectionSummary() {
Jason Monkbe3c5db2015-02-04 13:00:55 -0500945 boolean profileConnected = false; // at least one profile is connected
946 boolean a2dpNotConnected = false; // A2DP is preferred but not connected
Sanket Agarwalf8a1c912016-01-26 20:12:52 -0800947 boolean hfpNotConnected = false; // HFP is preferred but not connected
Jason Monkbe3c5db2015-02-04 13:00:55 -0500948
949 for (LocalBluetoothProfile profile : getProfiles()) {
950 int connectionStatus = getProfileConnectionState(profile);
951
952 switch (connectionStatus) {
953 case BluetoothProfile.STATE_CONNECTING:
954 case BluetoothProfile.STATE_DISCONNECTING:
Jack He6258aae2017-06-29 17:01:23 -0700955 return mContext.getString(Utils.getConnectionStateSummary(connectionStatus));
Jason Monkbe3c5db2015-02-04 13:00:55 -0500956
957 case BluetoothProfile.STATE_CONNECTED:
958 profileConnected = true;
959 break;
960
961 case BluetoothProfile.STATE_DISCONNECTED:
962 if (profile.isProfileReady()) {
Sanket Agarwalf8a1c912016-01-26 20:12:52 -0800963 if ((profile instanceof A2dpProfile) ||
Sanket Agarwal1bec6a52015-10-21 18:23:27 -0700964 (profile instanceof A2dpSinkProfile)){
Jason Monkbe3c5db2015-02-04 13:00:55 -0500965 a2dpNotConnected = true;
Sanket Agarwalf8a1c912016-01-26 20:12:52 -0800966 } else if ((profile instanceof HeadsetProfile) ||
967 (profile instanceof HfpClientProfile)) {
968 hfpNotConnected = true;
Jason Monkbe3c5db2015-02-04 13:00:55 -0500969 }
970 }
971 break;
972 }
973 }
974
Jack He6258aae2017-06-29 17:01:23 -0700975 String batteryLevelPercentageString = null;
976 // Android framework should only set mBatteryLevel to valid range [0-100] or
977 // BluetoothDevice.BATTERY_LEVEL_UNKNOWN, any other value should be a framework bug.
978 // Thus assume here that if value is not BluetoothDevice.BATTERY_LEVEL_UNKNOWN, it must
979 // be valid
980 final int batteryLevel = getBatteryLevel();
981 if (batteryLevel != BluetoothDevice.BATTERY_LEVEL_UNKNOWN) {
982 // TODO: name com.android.settingslib.bluetooth.Utils something different
983 batteryLevelPercentageString =
984 com.android.settingslib.Utils.formatPercentage(batteryLevel);
985 }
986
Pavlin Radoslavove6e080f2018-02-06 12:21:34 -0800987 // Prepare the string for the Active Device summary
988 String[] activeDeviceStringsArray = mContext.getResources().getStringArray(
989 R.array.bluetooth_audio_active_device_summaries);
990 String activeDeviceString = activeDeviceStringsArray[0]; // Default value: not active
Pavlin Radoslavov1af33a12018-01-21 02:59:15 -0800991 if (mIsActiveDeviceA2dp && mIsActiveDeviceHeadset) {
Pavlin Radoslavove6e080f2018-02-06 12:21:34 -0800992 activeDeviceString = activeDeviceStringsArray[1]; // Active for Media and Phone
Pavlin Radoslavov1af33a12018-01-21 02:59:15 -0800993 } else {
994 if (mIsActiveDeviceA2dp) {
Pavlin Radoslavove6e080f2018-02-06 12:21:34 -0800995 activeDeviceString = activeDeviceStringsArray[2]; // Active for Media only
Pavlin Radoslavov1af33a12018-01-21 02:59:15 -0800996 }
997 if (mIsActiveDeviceHeadset) {
Pavlin Radoslavove6e080f2018-02-06 12:21:34 -0800998 activeDeviceString = activeDeviceStringsArray[3]; // Active for Phone only
Pavlin Radoslavov1af33a12018-01-21 02:59:15 -0800999 }
1000 }
Pavlin Radoslavov1af33a12018-01-21 02:59:15 -08001001
Jason Monkbe3c5db2015-02-04 13:00:55 -05001002 if (profileConnected) {
Sanket Agarwalf8a1c912016-01-26 20:12:52 -08001003 if (a2dpNotConnected && hfpNotConnected) {
Jack He6258aae2017-06-29 17:01:23 -07001004 if (batteryLevelPercentageString != null) {
1005 return mContext.getString(
1006 R.string.bluetooth_connected_no_headset_no_a2dp_battery_level,
Pavlin Radoslavove6e080f2018-02-06 12:21:34 -08001007 batteryLevelPercentageString, activeDeviceString);
Jack He6258aae2017-06-29 17:01:23 -07001008 } else {
Pavlin Radoslavove6e080f2018-02-06 12:21:34 -08001009 return mContext.getString(R.string.bluetooth_connected_no_headset_no_a2dp,
1010 activeDeviceString);
Jack He6258aae2017-06-29 17:01:23 -07001011 }
1012
Jason Monkbe3c5db2015-02-04 13:00:55 -05001013 } else if (a2dpNotConnected) {
Jack He6258aae2017-06-29 17:01:23 -07001014 if (batteryLevelPercentageString != null) {
1015 return mContext.getString(R.string.bluetooth_connected_no_a2dp_battery_level,
Pavlin Radoslavove6e080f2018-02-06 12:21:34 -08001016 batteryLevelPercentageString, activeDeviceString);
Jack He6258aae2017-06-29 17:01:23 -07001017 } else {
Pavlin Radoslavove6e080f2018-02-06 12:21:34 -08001018 return mContext.getString(R.string.bluetooth_connected_no_a2dp,
1019 activeDeviceString);
Jack He6258aae2017-06-29 17:01:23 -07001020 }
1021
Sanket Agarwalf8a1c912016-01-26 20:12:52 -08001022 } else if (hfpNotConnected) {
Jack He6258aae2017-06-29 17:01:23 -07001023 if (batteryLevelPercentageString != null) {
1024 return mContext.getString(R.string.bluetooth_connected_no_headset_battery_level,
Pavlin Radoslavove6e080f2018-02-06 12:21:34 -08001025 batteryLevelPercentageString, activeDeviceString);
Jack He6258aae2017-06-29 17:01:23 -07001026 } else {
Pavlin Radoslavove6e080f2018-02-06 12:21:34 -08001027 return mContext.getString(R.string.bluetooth_connected_no_headset,
1028 activeDeviceString);
Jack He6258aae2017-06-29 17:01:23 -07001029 }
Jason Monkbe3c5db2015-02-04 13:00:55 -05001030 } else {
Jack He6258aae2017-06-29 17:01:23 -07001031 if (batteryLevelPercentageString != null) {
1032 return mContext.getString(R.string.bluetooth_connected_battery_level,
Pavlin Radoslavove6e080f2018-02-06 12:21:34 -08001033 batteryLevelPercentageString, activeDeviceString);
Jack He6258aae2017-06-29 17:01:23 -07001034 } else {
Pavlin Radoslavove6e080f2018-02-06 12:21:34 -08001035 return mContext.getString(R.string.bluetooth_connected, activeDeviceString);
Jack He6258aae2017-06-29 17:01:23 -07001036 }
Jason Monkbe3c5db2015-02-04 13:00:55 -05001037 }
1038 }
1039
Jack He6258aae2017-06-29 17:01:23 -07001040 return getBondState() == BluetoothDevice.BOND_BONDING ?
1041 mContext.getString(R.string.bluetooth_pairing) : null;
Jason Monkbe3c5db2015-02-04 13:00:55 -05001042 }
Jason Monk7ce96b92015-02-02 11:27:58 -05001043}