blob: e1ebbc4c166c33cf5b7cff3db61186094f8ddc7d [file] [log] [blame]
Jason Monk7ce96b92015-02-02 11:27:58 -05001/*
2 * Copyright (C) 2008 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.settingslib.bluetooth;
18
19import android.bluetooth.BluetoothClass;
20import android.bluetooth.BluetoothDevice;
21import android.bluetooth.BluetoothProfile;
22import android.bluetooth.BluetoothUuid;
23import android.content.Context;
24import android.content.SharedPreferences;
25import android.os.ParcelUuid;
26import android.os.SystemClock;
27import android.text.TextUtils;
28import android.util.Log;
29import android.bluetooth.BluetoothAdapter;
30
Jason Monkbe3c5db2015-02-04 13:00:55 -050031import com.android.settingslib.R;
32
Jason Monk7ce96b92015-02-02 11:27:58 -050033import java.util.ArrayList;
34import java.util.Collection;
35import java.util.Collections;
36import java.util.HashMap;
37import java.util.List;
38
39/**
40 * CachedBluetoothDevice represents a remote Bluetooth device. It contains
41 * attributes of the device (such as the address, name, RSSI, etc.) and
42 * functionality that can be performed on the device (connect, pair, disconnect,
43 * etc.).
44 */
Fan Zhang82dd3b02016-12-27 13:13:00 -080045public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> {
Jason Monk7ce96b92015-02-02 11:27:58 -050046 private static final String TAG = "CachedBluetoothDevice";
47 private static final boolean DEBUG = Utils.V;
48
49 private final Context mContext;
50 private final LocalBluetoothAdapter mLocalAdapter;
51 private final LocalBluetoothProfileManager mProfileManager;
52 private final BluetoothDevice mDevice;
Jack Hec219bc92017-07-24 14:55:59 -070053 //TODO: consider remove, BluetoothDevice.getName() is already cached
Jason Monk7ce96b92015-02-02 11:27:58 -050054 private String mName;
Jack Hec219bc92017-07-24 14:55:59 -070055 // Need this since there is no method for getting RSSI
Jason Monk7ce96b92015-02-02 11:27:58 -050056 private short mRssi;
Jack Hec219bc92017-07-24 14:55:59 -070057 //TODO: consider remove, BluetoothDevice.getBluetoothClass() is already cached
Jason Monk7ce96b92015-02-02 11:27:58 -050058 private BluetoothClass mBtClass;
59 private HashMap<LocalBluetoothProfile, Integer> mProfileConnectionState;
60
61 private final List<LocalBluetoothProfile> mProfiles =
62 new ArrayList<LocalBluetoothProfile>();
63
64 // List of profiles that were previously in mProfiles, but have been removed
65 private final List<LocalBluetoothProfile> mRemovedProfiles =
66 new ArrayList<LocalBluetoothProfile>();
67
68 // Device supports PANU but not NAP: remove PanProfile after device disconnects from NAP
69 private boolean mLocalNapRoleConnected;
70
Jack He51520472017-07-24 12:30:08 -070071 private boolean mJustDiscovered;
Jason Monk7ce96b92015-02-02 11:27:58 -050072
Jason Monk7ce96b92015-02-02 11:27:58 -050073 private int mMessageRejectionCount;
74
75 private final Collection<Callback> mCallbacks = new ArrayList<Callback>();
76
77 // Following constants indicate the user's choices of Phone book/message access settings
78 // User hasn't made any choice or settings app has wiped out the memory
79 public final static int ACCESS_UNKNOWN = 0;
80 // User has accepted the connection and let Settings app remember the decision
81 public final static int ACCESS_ALLOWED = 1;
82 // User has rejected the connection and let Settings app remember the decision
83 public final static int ACCESS_REJECTED = 2;
84
85 // How many times user should reject the connection to make the choice persist.
86 private final static int MESSAGE_REJECTION_COUNT_LIMIT_TO_PERSIST = 2;
87
88 private final static String MESSAGE_REJECTION_COUNT_PREFS_NAME = "bluetooth_message_reject";
89
90 /**
91 * When we connect to multiple profiles, we only want to display a single
92 * error even if they all fail. This tracks that state.
93 */
94 private boolean mIsConnectingErrorPossible;
95
96 /**
97 * Last time a bt profile auto-connect was attempted.
98 * If an ACTION_UUID intent comes in within
99 * MAX_UUID_DELAY_FOR_AUTO_CONNECT milliseconds, we will try auto-connect
100 * again with the new UUIDs
101 */
102 private long mConnectAttempted;
103
104 // See mConnectAttempted
105 private static final long MAX_UUID_DELAY_FOR_AUTO_CONNECT = 5000;
Etan Cohen50d47612015-03-31 12:45:23 -0700106 private static final long MAX_HOGP_DELAY_FOR_AUTO_CONNECT = 30000;
Jason Monk7ce96b92015-02-02 11:27:58 -0500107
Pavlin Radoslavov1af33a12018-01-21 02:59:15 -0800108 // Active device state
109 private boolean mIsActiveDeviceA2dp = false;
110 private boolean mIsActiveDeviceHeadset = false;
111
Jason Monk7ce96b92015-02-02 11:27:58 -0500112 /**
113 * Describes the current device and profile for logging.
114 *
115 * @param profile Profile to describe
116 * @return Description of the device and profile
117 */
118 private String describe(LocalBluetoothProfile profile) {
119 StringBuilder sb = new StringBuilder();
120 sb.append("Address:").append(mDevice);
121 if (profile != null) {
122 sb.append(" Profile:").append(profile);
123 }
124
125 return sb.toString();
126 }
127
128 void onProfileStateChanged(LocalBluetoothProfile profile, int newProfileState) {
129 if (Utils.D) {
130 Log.d(TAG, "onProfileStateChanged: profile " + profile +
131 " newProfileState " + newProfileState);
132 }
133 if (mLocalAdapter.getBluetoothState() == BluetoothAdapter.STATE_TURNING_OFF)
134 {
135 if (Utils.D) Log.d(TAG, " BT Turninig Off...Profile conn state change ignored...");
136 return;
137 }
138 mProfileConnectionState.put(profile, newProfileState);
139 if (newProfileState == BluetoothProfile.STATE_CONNECTED) {
140 if (profile instanceof MapProfile) {
141 profile.setPreferred(mDevice, true);
Hemant Guptadbc3d8d2017-05-12 21:14:44 +0530142 }
143 if (!mProfiles.contains(profile)) {
Jason Monk7ce96b92015-02-02 11:27:58 -0500144 mRemovedProfiles.remove(profile);
145 mProfiles.add(profile);
146 if (profile instanceof PanProfile &&
147 ((PanProfile) profile).isLocalRoleNap(mDevice)) {
148 // Device doesn't support NAP, so remove PanProfile on disconnect
149 mLocalNapRoleConnected = true;
150 }
151 }
152 } else if (profile instanceof MapProfile &&
153 newProfileState == BluetoothProfile.STATE_DISCONNECTED) {
154 profile.setPreferred(mDevice, false);
155 } else if (mLocalNapRoleConnected && profile instanceof PanProfile &&
156 ((PanProfile) profile).isLocalRoleNap(mDevice) &&
157 newProfileState == BluetoothProfile.STATE_DISCONNECTED) {
158 Log.d(TAG, "Removing PanProfile from device after NAP disconnect");
159 mProfiles.remove(profile);
160 mRemovedProfiles.add(profile);
161 mLocalNapRoleConnected = false;
162 }
Pavlin Radoslavov1af33a12018-01-21 02:59:15 -0800163 fetchActiveDevices();
Jason Monk7ce96b92015-02-02 11:27:58 -0500164 }
165
166 CachedBluetoothDevice(Context context,
167 LocalBluetoothAdapter adapter,
168 LocalBluetoothProfileManager profileManager,
169 BluetoothDevice device) {
170 mContext = context;
171 mLocalAdapter = adapter;
172 mProfileManager = profileManager;
173 mDevice = device;
174 mProfileConnectionState = new HashMap<LocalBluetoothProfile, Integer>();
175 fillData();
176 }
177
178 public void disconnect() {
179 for (LocalBluetoothProfile profile : mProfiles) {
180 disconnect(profile);
181 }
182 // Disconnect PBAP server in case its connected
183 // This is to ensure all the profiles are disconnected as some CK/Hs do not
184 // disconnect PBAP connection when HF connection is brought down
185 PbapServerProfile PbapProfile = mProfileManager.getPbapProfile();
186 if (PbapProfile.getConnectionStatus(mDevice) == BluetoothProfile.STATE_CONNECTED)
187 {
188 PbapProfile.disconnect(mDevice);
189 }
190 }
191
192 public void disconnect(LocalBluetoothProfile profile) {
193 if (profile.disconnect(mDevice)) {
194 if (Utils.D) {
195 Log.d(TAG, "Command sent successfully:DISCONNECT " + describe(profile));
196 }
197 }
198 }
199
200 public void connect(boolean connectAllProfiles) {
201 if (!ensurePaired()) {
202 return;
203 }
204
205 mConnectAttempted = SystemClock.elapsedRealtime();
206 connectWithoutResettingTimer(connectAllProfiles);
207 }
208
209 void onBondingDockConnect() {
210 // Attempt to connect if UUIDs are available. Otherwise,
211 // we will connect when the ACTION_UUID intent arrives.
212 connect(false);
213 }
214
215 private void connectWithoutResettingTimer(boolean connectAllProfiles) {
216 // Try to initialize the profiles if they were not.
217 if (mProfiles.isEmpty()) {
218 // if mProfiles is empty, then do not invoke updateProfiles. This causes a race
219 // condition with carkits during pairing, wherein RemoteDevice.UUIDs have been updated
220 // from bluetooth stack but ACTION.uuid is not sent yet.
221 // Eventually ACTION.uuid will be received which shall trigger the connection of the
222 // various profiles
223 // If UUIDs are not available yet, connect will be happen
224 // upon arrival of the ACTION_UUID intent.
225 Log.d(TAG, "No profiles. Maybe we will connect later");
226 return;
227 }
228
229 // Reset the only-show-one-error-dialog tracking variable
230 mIsConnectingErrorPossible = true;
231
232 int preferredProfiles = 0;
233 for (LocalBluetoothProfile profile : mProfiles) {
234 if (connectAllProfiles ? profile.isConnectable() : profile.isAutoConnectable()) {
235 if (profile.isPreferred(mDevice)) {
236 ++preferredProfiles;
237 connectInt(profile);
238 }
239 }
240 }
241 if (DEBUG) Log.d(TAG, "Preferred profiles = " + preferredProfiles);
242
243 if (preferredProfiles == 0) {
244 connectAutoConnectableProfiles();
245 }
246 }
247
248 private void connectAutoConnectableProfiles() {
249 if (!ensurePaired()) {
250 return;
251 }
252 // Reset the only-show-one-error-dialog tracking variable
253 mIsConnectingErrorPossible = true;
254
255 for (LocalBluetoothProfile profile : mProfiles) {
256 if (profile.isAutoConnectable()) {
257 profile.setPreferred(mDevice, true);
258 connectInt(profile);
259 }
260 }
261 }
262
263 /**
264 * Connect this device to the specified profile.
265 *
266 * @param profile the profile to use with the remote device
267 */
268 public void connectProfile(LocalBluetoothProfile profile) {
269 mConnectAttempted = SystemClock.elapsedRealtime();
270 // Reset the only-show-one-error-dialog tracking variable
271 mIsConnectingErrorPossible = true;
272 connectInt(profile);
273 // Refresh the UI based on profile.connect() call
274 refresh();
275 }
276
277 synchronized void connectInt(LocalBluetoothProfile profile) {
278 if (!ensurePaired()) {
279 return;
280 }
281 if (profile.connect(mDevice)) {
282 if (Utils.D) {
283 Log.d(TAG, "Command sent successfully:CONNECT " + describe(profile));
284 }
285 return;
286 }
287 Log.i(TAG, "Failed to connect " + profile.toString() + " to " + mName);
288 }
289
290 private boolean ensurePaired() {
291 if (getBondState() == BluetoothDevice.BOND_NONE) {
292 startPairing();
293 return false;
294 } else {
295 return true;
296 }
297 }
298
299 public boolean startPairing() {
300 // Pairing is unreliable while scanning, so cancel discovery
301 if (mLocalAdapter.isDiscovering()) {
302 mLocalAdapter.cancelDiscovery();
303 }
304
305 if (!mDevice.createBond()) {
306 return false;
307 }
308
Jason Monk7ce96b92015-02-02 11:27:58 -0500309 return true;
310 }
311
312 /**
313 * Return true if user initiated pairing on this device. The message text is
314 * slightly different for local vs. remote initiated pairing dialogs.
315 */
316 boolean isUserInitiatedPairing() {
Jakub Pawlowski0278ab92016-07-20 11:55:48 -0700317 return mDevice.isBondingInitiatedLocally();
Jason Monk7ce96b92015-02-02 11:27:58 -0500318 }
319
320 public void unpair() {
321 int state = getBondState();
322
323 if (state == BluetoothDevice.BOND_BONDING) {
324 mDevice.cancelBondProcess();
325 }
326
327 if (state != BluetoothDevice.BOND_NONE) {
328 final BluetoothDevice dev = mDevice;
329 if (dev != null) {
330 final boolean successful = dev.removeBond();
331 if (successful) {
332 if (Utils.D) {
333 Log.d(TAG, "Command sent successfully:REMOVE_BOND " + describe(null));
334 }
335 } else if (Utils.V) {
336 Log.v(TAG, "Framework rejected command immediately:REMOVE_BOND " +
337 describe(null));
338 }
339 }
340 }
341 }
342
343 public int getProfileConnectionState(LocalBluetoothProfile profile) {
xutianguo22bbb8192016-06-22 11:32:00 +0800344 if (mProfileConnectionState.get(profile) == null) {
Jason Monk7ce96b92015-02-02 11:27:58 -0500345 // If cache is empty make the binder call to get the state
346 int state = profile.getConnectionStatus(mDevice);
347 mProfileConnectionState.put(profile, state);
348 }
349 return mProfileConnectionState.get(profile);
350 }
351
352 public void clearProfileConnectionState ()
353 {
354 if (Utils.D) {
355 Log.d(TAG," Clearing all connection state for dev:" + mDevice.getName());
356 }
357 for (LocalBluetoothProfile profile :getProfiles()) {
358 mProfileConnectionState.put(profile, BluetoothProfile.STATE_DISCONNECTED);
359 }
360 }
361
362 // TODO: do any of these need to run async on a background thread?
363 private void fillData() {
364 fetchName();
365 fetchBtClass();
366 updateProfiles();
Pavlin Radoslavov1af33a12018-01-21 02:59:15 -0800367 fetchActiveDevices();
Jason Monk7ce96b92015-02-02 11:27:58 -0500368 migratePhonebookPermissionChoice();
369 migrateMessagePermissionChoice();
370 fetchMessageRejectionCount();
371
Jason Monk7ce96b92015-02-02 11:27:58 -0500372 dispatchAttributesChanged();
373 }
374
375 public BluetoothDevice getDevice() {
376 return mDevice;
377 }
378
Antony Sargent7ad051e2017-06-29 15:23:13 -0700379 /**
380 * Convenience method that can be mocked - it lets tests avoid having to call getDevice() which
381 * causes problems in tests since BluetoothDevice is final and cannot be mocked.
382 * @return the address of this device
383 */
384 public String getAddress() {
385 return mDevice.getAddress();
386 }
387
Jason Monk7ce96b92015-02-02 11:27:58 -0500388 public String getName() {
389 return mName;
390 }
391
392 /**
393 * Populate name from BluetoothDevice.ACTION_FOUND intent
394 */
395 void setNewName(String name) {
396 if (mName == null) {
397 mName = name;
398 if (mName == null || TextUtils.isEmpty(mName)) {
399 mName = mDevice.getAddress();
400 }
401 dispatchAttributesChanged();
402 }
403 }
404
405 /**
Jack Hec219bc92017-07-24 14:55:59 -0700406 * User changes the device name
407 * @param name new alias name to be set, should never be null
Jason Monk7ce96b92015-02-02 11:27:58 -0500408 */
409 public void setName(String name) {
Jack Hec219bc92017-07-24 14:55:59 -0700410 // Prevent mName to be set to null if setName(null) is called
411 if (name != null && !TextUtils.equals(name, mName)) {
Jason Monk7ce96b92015-02-02 11:27:58 -0500412 mName = name;
413 mDevice.setAlias(name);
414 dispatchAttributesChanged();
415 }
416 }
417
418 void refreshName() {
419 fetchName();
420 dispatchAttributesChanged();
421 }
422
423 private void fetchName() {
424 mName = mDevice.getAliasName();
425
426 if (TextUtils.isEmpty(mName)) {
427 mName = mDevice.getAddress();
428 if (DEBUG) Log.d(TAG, "Device has no name (yet), use address: " + mName);
429 }
430 }
431
Jack He6258aae2017-06-29 17:01:23 -0700432 /**
Jack Hec219bc92017-07-24 14:55:59 -0700433 * Checks if device has a human readable name besides MAC address
434 * @return true if device's alias name is not null nor empty, false otherwise
435 */
436 public boolean hasHumanReadableName() {
437 return !TextUtils.isEmpty(mDevice.getAliasName());
438 }
439
440 /**
Jack He6258aae2017-06-29 17:01:23 -0700441 * Get battery level from remote device
442 * @return battery level in percentage [0-100], or {@link BluetoothDevice#BATTERY_LEVEL_UNKNOWN}
443 */
444 public int getBatteryLevel() {
445 return mDevice.getBatteryLevel();
446 }
447
Jason Monk7ce96b92015-02-02 11:27:58 -0500448 void refresh() {
449 dispatchAttributesChanged();
450 }
451
Jack He51520472017-07-24 12:30:08 -0700452 public void setJustDiscovered(boolean justDiscovered) {
453 if (mJustDiscovered != justDiscovered) {
454 mJustDiscovered = justDiscovered;
Jason Monk7ce96b92015-02-02 11:27:58 -0500455 dispatchAttributesChanged();
456 }
457 }
458
459 public int getBondState() {
460 return mDevice.getBondState();
461 }
462
Pavlin Radoslavov1af33a12018-01-21 02:59:15 -0800463 /**
464 * Set the device status as active or non-active per Bluetooth profile.
465 *
466 * @param isActive true if the device is active
467 * @param bluetoothProfile the Bluetooth profile
468 */
469 public void setActiveDevice(boolean isActive, int bluetoothProfile) {
470 boolean changed = false;
471 switch (bluetoothProfile) {
472 case BluetoothProfile.A2DP:
473 changed = (mIsActiveDeviceA2dp != isActive);
474 mIsActiveDeviceA2dp = isActive;
475 break;
476 case BluetoothProfile.HEADSET:
477 changed = (mIsActiveDeviceHeadset != isActive);
478 mIsActiveDeviceHeadset = isActive;
479 break;
480 default:
481 Log.w(TAG, "setActiveDevice: unknown profile " + bluetoothProfile +
482 " isActive " + isActive);
483 break;
484 }
485 if (changed) {
486 dispatchAttributesChanged();
487 }
488 }
489
Jason Monk7ce96b92015-02-02 11:27:58 -0500490 void setRssi(short rssi) {
491 if (mRssi != rssi) {
492 mRssi = rssi;
493 dispatchAttributesChanged();
494 }
495 }
496
497 /**
498 * Checks whether we are connected to this device (any profile counts).
499 *
500 * @return Whether it is connected.
501 */
502 public boolean isConnected() {
503 for (LocalBluetoothProfile profile : mProfiles) {
504 int status = getProfileConnectionState(profile);
505 if (status == BluetoothProfile.STATE_CONNECTED) {
506 return true;
507 }
508 }
509
510 return false;
511 }
512
513 public boolean isConnectedProfile(LocalBluetoothProfile profile) {
514 int status = getProfileConnectionState(profile);
515 return status == BluetoothProfile.STATE_CONNECTED;
516
517 }
518
519 public boolean isBusy() {
520 for (LocalBluetoothProfile profile : mProfiles) {
521 int status = getProfileConnectionState(profile);
522 if (status == BluetoothProfile.STATE_CONNECTING
523 || status == BluetoothProfile.STATE_DISCONNECTING) {
524 return true;
525 }
526 }
527 return getBondState() == BluetoothDevice.BOND_BONDING;
528 }
529
530 /**
531 * Fetches a new value for the cached BT class.
532 */
533 private void fetchBtClass() {
534 mBtClass = mDevice.getBluetoothClass();
535 }
536
537 private boolean updateProfiles() {
538 ParcelUuid[] uuids = mDevice.getUuids();
539 if (uuids == null) return false;
540
541 ParcelUuid[] localUuids = mLocalAdapter.getUuids();
542 if (localUuids == null) return false;
543
Jack Hec219bc92017-07-24 14:55:59 -0700544 /*
Jason Monk7ce96b92015-02-02 11:27:58 -0500545 * Now we know if the device supports PBAP, update permissions...
546 */
547 processPhonebookAccess();
548
549 mProfileManager.updateProfiles(uuids, localUuids, mProfiles, mRemovedProfiles,
550 mLocalNapRoleConnected, mDevice);
551
552 if (DEBUG) {
553 Log.e(TAG, "updating profiles for " + mDevice.getAliasName());
554 BluetoothClass bluetoothClass = mDevice.getBluetoothClass();
555
556 if (bluetoothClass != null) Log.v(TAG, "Class: " + bluetoothClass.toString());
557 Log.v(TAG, "UUID:");
558 for (ParcelUuid uuid : uuids) {
559 Log.v(TAG, " " + uuid);
560 }
561 }
562 return true;
563 }
564
Pavlin Radoslavov1af33a12018-01-21 02:59:15 -0800565 private void fetchActiveDevices() {
566 A2dpProfile a2dpProfile = mProfileManager.getA2dpProfile();
567 if (a2dpProfile != null) {
568 mIsActiveDeviceA2dp = mDevice.equals(a2dpProfile.getActiveDevice());
569 }
570 HeadsetProfile headsetProfile = mProfileManager.getHeadsetProfile();
571 if (headsetProfile != null) {
572 mIsActiveDeviceHeadset = mDevice.equals(headsetProfile.getActiveDevice());
573 }
574 }
575
Jason Monk7ce96b92015-02-02 11:27:58 -0500576 /**
577 * Refreshes the UI for the BT class, including fetching the latest value
578 * for the class.
579 */
580 void refreshBtClass() {
581 fetchBtClass();
582 dispatchAttributesChanged();
583 }
584
585 /**
586 * Refreshes the UI when framework alerts us of a UUID change.
587 */
588 void onUuidChanged() {
589 updateProfiles();
Etan Cohen50d47612015-03-31 12:45:23 -0700590 ParcelUuid[] uuids = mDevice.getUuids();
Venkat Raghavan05e08c32015-04-06 16:26:11 -0700591
Etan Cohen50d47612015-03-31 12:45:23 -0700592 long timeout = MAX_UUID_DELAY_FOR_AUTO_CONNECT;
Venkat Raghavan05e08c32015-04-06 16:26:11 -0700593 if (BluetoothUuid.isUuidPresent(uuids, BluetoothUuid.Hogp)) {
594 timeout = MAX_HOGP_DELAY_FOR_AUTO_CONNECT;
595 }
Jason Monk7ce96b92015-02-02 11:27:58 -0500596
597 if (DEBUG) {
Etan Cohen50d47612015-03-31 12:45:23 -0700598 Log.d(TAG, "onUuidChanged: Time since last connect"
Jason Monk7ce96b92015-02-02 11:27:58 -0500599 + (SystemClock.elapsedRealtime() - mConnectAttempted));
600 }
601
602 /*
Venkat Raghavan05e08c32015-04-06 16:26:11 -0700603 * If a connect was attempted earlier without any UUID, we will do the connect now.
604 * Otherwise, allow the connect on UUID change.
Jason Monk7ce96b92015-02-02 11:27:58 -0500605 */
606 if (!mProfiles.isEmpty()
Andre Eisenbach7be83c52015-09-03 15:20:09 -0700607 && ((mConnectAttempted + timeout) > SystemClock.elapsedRealtime())) {
Jason Monk7ce96b92015-02-02 11:27:58 -0500608 connectWithoutResettingTimer(false);
609 }
Andre Eisenbach7be83c52015-09-03 15:20:09 -0700610
Jason Monk7ce96b92015-02-02 11:27:58 -0500611 dispatchAttributesChanged();
612 }
613
614 void onBondingStateChanged(int bondState) {
615 if (bondState == BluetoothDevice.BOND_NONE) {
616 mProfiles.clear();
Jason Monk7ce96b92015-02-02 11:27:58 -0500617 setPhonebookPermissionChoice(ACCESS_UNKNOWN);
618 setMessagePermissionChoice(ACCESS_UNKNOWN);
Casper Bonde424681e2015-05-04 22:07:45 -0700619 setSimPermissionChoice(ACCESS_UNKNOWN);
Jason Monk7ce96b92015-02-02 11:27:58 -0500620 mMessageRejectionCount = 0;
621 saveMessageRejectionCount();
622 }
623
624 refresh();
625
626 if (bondState == BluetoothDevice.BOND_BONDED) {
627 if (mDevice.isBluetoothDock()) {
628 onBondingDockConnect();
Jakub Pawlowski0278ab92016-07-20 11:55:48 -0700629 } else if (mDevice.isBondingInitiatedLocally()) {
Jason Monk7ce96b92015-02-02 11:27:58 -0500630 connect(false);
631 }
Jason Monk7ce96b92015-02-02 11:27:58 -0500632 }
633 }
634
635 void setBtClass(BluetoothClass btClass) {
636 if (btClass != null && mBtClass != btClass) {
637 mBtClass = btClass;
638 dispatchAttributesChanged();
639 }
640 }
641
642 public BluetoothClass getBtClass() {
643 return mBtClass;
644 }
645
646 public List<LocalBluetoothProfile> getProfiles() {
647 return Collections.unmodifiableList(mProfiles);
648 }
649
650 public List<LocalBluetoothProfile> getConnectableProfiles() {
651 List<LocalBluetoothProfile> connectableProfiles =
652 new ArrayList<LocalBluetoothProfile>();
653 for (LocalBluetoothProfile profile : mProfiles) {
654 if (profile.isConnectable()) {
655 connectableProfiles.add(profile);
656 }
657 }
658 return connectableProfiles;
659 }
660
661 public List<LocalBluetoothProfile> getRemovedProfiles() {
662 return mRemovedProfiles;
663 }
664
665 public void registerCallback(Callback callback) {
666 synchronized (mCallbacks) {
667 mCallbacks.add(callback);
668 }
669 }
670
671 public void unregisterCallback(Callback callback) {
672 synchronized (mCallbacks) {
673 mCallbacks.remove(callback);
674 }
675 }
676
677 private void dispatchAttributesChanged() {
678 synchronized (mCallbacks) {
679 for (Callback callback : mCallbacks) {
680 callback.onDeviceAttributesChanged();
681 }
682 }
683 }
684
685 @Override
686 public String toString() {
687 return mDevice.toString();
688 }
689
690 @Override
691 public boolean equals(Object o) {
692 if ((o == null) || !(o instanceof CachedBluetoothDevice)) {
693 return false;
694 }
695 return mDevice.equals(((CachedBluetoothDevice) o).mDevice);
696 }
697
698 @Override
699 public int hashCode() {
700 return mDevice.getAddress().hashCode();
701 }
702
703 // This comparison uses non-final fields so the sort order may change
704 // when device attributes change (such as bonding state). Settings
705 // will completely refresh the device list when this happens.
706 public int compareTo(CachedBluetoothDevice another) {
707 // Connected above not connected
708 int comparison = (another.isConnected() ? 1 : 0) - (isConnected() ? 1 : 0);
709 if (comparison != 0) return comparison;
710
711 // Paired above not paired
712 comparison = (another.getBondState() == BluetoothDevice.BOND_BONDED ? 1 : 0) -
713 (getBondState() == BluetoothDevice.BOND_BONDED ? 1 : 0);
714 if (comparison != 0) return comparison;
715
Jack He51520472017-07-24 12:30:08 -0700716 // Just discovered above discovered in the past
717 comparison = (another.mJustDiscovered ? 1 : 0) - (mJustDiscovered ? 1 : 0);
Jason Monk7ce96b92015-02-02 11:27:58 -0500718 if (comparison != 0) return comparison;
719
720 // Stronger signal above weaker signal
721 comparison = another.mRssi - mRssi;
722 if (comparison != 0) return comparison;
723
724 // Fallback on name
725 return mName.compareTo(another.mName);
726 }
727
728 public interface Callback {
729 void onDeviceAttributesChanged();
730 }
731
732 public int getPhonebookPermissionChoice() {
733 int permission = mDevice.getPhonebookAccessPermission();
734 if (permission == BluetoothDevice.ACCESS_ALLOWED) {
735 return ACCESS_ALLOWED;
736 } else if (permission == BluetoothDevice.ACCESS_REJECTED) {
737 return ACCESS_REJECTED;
738 }
739 return ACCESS_UNKNOWN;
740 }
741
742 public void setPhonebookPermissionChoice(int permissionChoice) {
743 int permission = BluetoothDevice.ACCESS_UNKNOWN;
744 if (permissionChoice == ACCESS_ALLOWED) {
745 permission = BluetoothDevice.ACCESS_ALLOWED;
746 } else if (permissionChoice == ACCESS_REJECTED) {
747 permission = BluetoothDevice.ACCESS_REJECTED;
748 }
749 mDevice.setPhonebookAccessPermission(permission);
750 }
751
752 // Migrates data from old data store (in Settings app's shared preferences) to new (in Bluetooth
753 // app's shared preferences).
754 private void migratePhonebookPermissionChoice() {
755 SharedPreferences preferences = mContext.getSharedPreferences(
756 "bluetooth_phonebook_permission", Context.MODE_PRIVATE);
757 if (!preferences.contains(mDevice.getAddress())) {
758 return;
759 }
760
761 if (mDevice.getPhonebookAccessPermission() == BluetoothDevice.ACCESS_UNKNOWN) {
762 int oldPermission = preferences.getInt(mDevice.getAddress(), ACCESS_UNKNOWN);
763 if (oldPermission == ACCESS_ALLOWED) {
764 mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_ALLOWED);
765 } else if (oldPermission == ACCESS_REJECTED) {
766 mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_REJECTED);
767 }
768 }
769
770 SharedPreferences.Editor editor = preferences.edit();
771 editor.remove(mDevice.getAddress());
772 editor.commit();
773 }
774
775 public int getMessagePermissionChoice() {
776 int permission = mDevice.getMessageAccessPermission();
777 if (permission == BluetoothDevice.ACCESS_ALLOWED) {
778 return ACCESS_ALLOWED;
779 } else if (permission == BluetoothDevice.ACCESS_REJECTED) {
780 return ACCESS_REJECTED;
781 }
782 return ACCESS_UNKNOWN;
783 }
784
785 public void setMessagePermissionChoice(int permissionChoice) {
786 int permission = BluetoothDevice.ACCESS_UNKNOWN;
787 if (permissionChoice == ACCESS_ALLOWED) {
788 permission = BluetoothDevice.ACCESS_ALLOWED;
789 } else if (permissionChoice == ACCESS_REJECTED) {
790 permission = BluetoothDevice.ACCESS_REJECTED;
791 }
792 mDevice.setMessageAccessPermission(permission);
793 }
794
Casper Bonde424681e2015-05-04 22:07:45 -0700795 public int getSimPermissionChoice() {
796 int permission = mDevice.getSimAccessPermission();
797 if (permission == BluetoothDevice.ACCESS_ALLOWED) {
798 return ACCESS_ALLOWED;
799 } else if (permission == BluetoothDevice.ACCESS_REJECTED) {
800 return ACCESS_REJECTED;
801 }
802 return ACCESS_UNKNOWN;
803 }
804
805 void setSimPermissionChoice(int permissionChoice) {
806 int permission = BluetoothDevice.ACCESS_UNKNOWN;
807 if (permissionChoice == ACCESS_ALLOWED) {
808 permission = BluetoothDevice.ACCESS_ALLOWED;
809 } else if (permissionChoice == ACCESS_REJECTED) {
810 permission = BluetoothDevice.ACCESS_REJECTED;
811 }
812 mDevice.setSimAccessPermission(permission);
813 }
814
Jason Monk7ce96b92015-02-02 11:27:58 -0500815 // Migrates data from old data store (in Settings app's shared preferences) to new (in Bluetooth
816 // app's shared preferences).
817 private void migrateMessagePermissionChoice() {
818 SharedPreferences preferences = mContext.getSharedPreferences(
819 "bluetooth_message_permission", Context.MODE_PRIVATE);
820 if (!preferences.contains(mDevice.getAddress())) {
821 return;
822 }
823
824 if (mDevice.getMessageAccessPermission() == BluetoothDevice.ACCESS_UNKNOWN) {
825 int oldPermission = preferences.getInt(mDevice.getAddress(), ACCESS_UNKNOWN);
826 if (oldPermission == ACCESS_ALLOWED) {
827 mDevice.setMessageAccessPermission(BluetoothDevice.ACCESS_ALLOWED);
828 } else if (oldPermission == ACCESS_REJECTED) {
829 mDevice.setMessageAccessPermission(BluetoothDevice.ACCESS_REJECTED);
830 }
831 }
832
833 SharedPreferences.Editor editor = preferences.edit();
834 editor.remove(mDevice.getAddress());
835 editor.commit();
836 }
837
838 /**
839 * @return Whether this rejection should persist.
840 */
841 public boolean checkAndIncreaseMessageRejectionCount() {
842 if (mMessageRejectionCount < MESSAGE_REJECTION_COUNT_LIMIT_TO_PERSIST) {
843 mMessageRejectionCount++;
844 saveMessageRejectionCount();
845 }
846 return mMessageRejectionCount >= MESSAGE_REJECTION_COUNT_LIMIT_TO_PERSIST;
847 }
848
849 private void fetchMessageRejectionCount() {
850 SharedPreferences preference = mContext.getSharedPreferences(
851 MESSAGE_REJECTION_COUNT_PREFS_NAME, Context.MODE_PRIVATE);
852 mMessageRejectionCount = preference.getInt(mDevice.getAddress(), 0);
853 }
854
855 private void saveMessageRejectionCount() {
856 SharedPreferences.Editor editor = mContext.getSharedPreferences(
857 MESSAGE_REJECTION_COUNT_PREFS_NAME, Context.MODE_PRIVATE).edit();
858 if (mMessageRejectionCount == 0) {
859 editor.remove(mDevice.getAddress());
860 } else {
861 editor.putInt(mDevice.getAddress(), mMessageRejectionCount);
862 }
863 editor.commit();
864 }
865
866 private void processPhonebookAccess() {
867 if (mDevice.getBondState() != BluetoothDevice.BOND_BONDED) return;
868
869 ParcelUuid[] uuids = mDevice.getUuids();
870 if (BluetoothUuid.containsAnyUuid(uuids, PbapServerProfile.PBAB_CLIENT_UUIDS)) {
871 // The pairing dialog now warns of phone-book access for paired devices.
872 // No separate prompt is displayed after pairing.
Sanket Padawe78c23762015-06-01 15:55:30 -0700873 if (getPhonebookPermissionChoice() == CachedBluetoothDevice.ACCESS_UNKNOWN) {
Sanket Padawe07533db2015-11-11 15:01:35 -0800874 if (mDevice.getBluetoothClass().getDeviceClass()
Hemant Guptab8d267a2016-06-07 12:53:59 +0530875 == BluetoothClass.Device.AUDIO_VIDEO_HANDSFREE ||
876 mDevice.getBluetoothClass().getDeviceClass()
877 == BluetoothClass.Device.AUDIO_VIDEO_WEARABLE_HEADSET) {
Sanket Padawe07533db2015-11-11 15:01:35 -0800878 setPhonebookPermissionChoice(CachedBluetoothDevice.ACCESS_ALLOWED);
879 } else {
880 setPhonebookPermissionChoice(CachedBluetoothDevice.ACCESS_REJECTED);
881 }
Sanket Padawe78c23762015-06-01 15:55:30 -0700882 }
Jason Monk7ce96b92015-02-02 11:27:58 -0500883 }
884 }
Jason Monkbe3c5db2015-02-04 13:00:55 -0500885
886 public int getMaxConnectionState() {
887 int maxState = BluetoothProfile.STATE_DISCONNECTED;
888 for (LocalBluetoothProfile profile : getProfiles()) {
889 int connectionStatus = getProfileConnectionState(profile);
890 if (connectionStatus > maxState) {
891 maxState = connectionStatus;
892 }
893 }
894 return maxState;
895 }
896
897 /**
898 * @return resource for string that discribes the connection state of this device.
899 */
Jack He6258aae2017-06-29 17:01:23 -0700900 public String getConnectionSummary() {
Jason Monkbe3c5db2015-02-04 13:00:55 -0500901 boolean profileConnected = false; // at least one profile is connected
902 boolean a2dpNotConnected = false; // A2DP is preferred but not connected
Sanket Agarwalf8a1c912016-01-26 20:12:52 -0800903 boolean hfpNotConnected = false; // HFP is preferred but not connected
Jason Monkbe3c5db2015-02-04 13:00:55 -0500904
905 for (LocalBluetoothProfile profile : getProfiles()) {
906 int connectionStatus = getProfileConnectionState(profile);
907
908 switch (connectionStatus) {
909 case BluetoothProfile.STATE_CONNECTING:
910 case BluetoothProfile.STATE_DISCONNECTING:
Jack He6258aae2017-06-29 17:01:23 -0700911 return mContext.getString(Utils.getConnectionStateSummary(connectionStatus));
Jason Monkbe3c5db2015-02-04 13:00:55 -0500912
913 case BluetoothProfile.STATE_CONNECTED:
914 profileConnected = true;
915 break;
916
917 case BluetoothProfile.STATE_DISCONNECTED:
918 if (profile.isProfileReady()) {
Sanket Agarwalf8a1c912016-01-26 20:12:52 -0800919 if ((profile instanceof A2dpProfile) ||
Sanket Agarwal1bec6a52015-10-21 18:23:27 -0700920 (profile instanceof A2dpSinkProfile)){
Jason Monkbe3c5db2015-02-04 13:00:55 -0500921 a2dpNotConnected = true;
Sanket Agarwalf8a1c912016-01-26 20:12:52 -0800922 } else if ((profile instanceof HeadsetProfile) ||
923 (profile instanceof HfpClientProfile)) {
924 hfpNotConnected = true;
Jason Monkbe3c5db2015-02-04 13:00:55 -0500925 }
926 }
927 break;
928 }
929 }
930
Jack He6258aae2017-06-29 17:01:23 -0700931 String batteryLevelPercentageString = null;
932 // Android framework should only set mBatteryLevel to valid range [0-100] or
933 // BluetoothDevice.BATTERY_LEVEL_UNKNOWN, any other value should be a framework bug.
934 // Thus assume here that if value is not BluetoothDevice.BATTERY_LEVEL_UNKNOWN, it must
935 // be valid
936 final int batteryLevel = getBatteryLevel();
937 if (batteryLevel != BluetoothDevice.BATTERY_LEVEL_UNKNOWN) {
938 // TODO: name com.android.settingslib.bluetooth.Utils something different
939 batteryLevelPercentageString =
940 com.android.settingslib.Utils.formatPercentage(batteryLevel);
941 }
942
Pavlin Radoslavove6e080f2018-02-06 12:21:34 -0800943 // Prepare the string for the Active Device summary
944 String[] activeDeviceStringsArray = mContext.getResources().getStringArray(
945 R.array.bluetooth_audio_active_device_summaries);
946 String activeDeviceString = activeDeviceStringsArray[0]; // Default value: not active
Pavlin Radoslavov1af33a12018-01-21 02:59:15 -0800947 if (mIsActiveDeviceA2dp && mIsActiveDeviceHeadset) {
Pavlin Radoslavove6e080f2018-02-06 12:21:34 -0800948 activeDeviceString = activeDeviceStringsArray[1]; // Active for Media and Phone
Pavlin Radoslavov1af33a12018-01-21 02:59:15 -0800949 } else {
950 if (mIsActiveDeviceA2dp) {
Pavlin Radoslavove6e080f2018-02-06 12:21:34 -0800951 activeDeviceString = activeDeviceStringsArray[2]; // Active for Media only
Pavlin Radoslavov1af33a12018-01-21 02:59:15 -0800952 }
953 if (mIsActiveDeviceHeadset) {
Pavlin Radoslavove6e080f2018-02-06 12:21:34 -0800954 activeDeviceString = activeDeviceStringsArray[3]; // Active for Phone only
Pavlin Radoslavov1af33a12018-01-21 02:59:15 -0800955 }
956 }
Pavlin Radoslavov1af33a12018-01-21 02:59:15 -0800957
Jason Monkbe3c5db2015-02-04 13:00:55 -0500958 if (profileConnected) {
Sanket Agarwalf8a1c912016-01-26 20:12:52 -0800959 if (a2dpNotConnected && hfpNotConnected) {
Jack He6258aae2017-06-29 17:01:23 -0700960 if (batteryLevelPercentageString != null) {
961 return mContext.getString(
962 R.string.bluetooth_connected_no_headset_no_a2dp_battery_level,
Pavlin Radoslavove6e080f2018-02-06 12:21:34 -0800963 batteryLevelPercentageString, activeDeviceString);
Jack He6258aae2017-06-29 17:01:23 -0700964 } else {
Pavlin Radoslavove6e080f2018-02-06 12:21:34 -0800965 return mContext.getString(R.string.bluetooth_connected_no_headset_no_a2dp,
966 activeDeviceString);
Jack He6258aae2017-06-29 17:01:23 -0700967 }
968
Jason Monkbe3c5db2015-02-04 13:00:55 -0500969 } else if (a2dpNotConnected) {
Jack He6258aae2017-06-29 17:01:23 -0700970 if (batteryLevelPercentageString != null) {
971 return mContext.getString(R.string.bluetooth_connected_no_a2dp_battery_level,
Pavlin Radoslavove6e080f2018-02-06 12:21:34 -0800972 batteryLevelPercentageString, activeDeviceString);
Jack He6258aae2017-06-29 17:01:23 -0700973 } else {
Pavlin Radoslavove6e080f2018-02-06 12:21:34 -0800974 return mContext.getString(R.string.bluetooth_connected_no_a2dp,
975 activeDeviceString);
Jack He6258aae2017-06-29 17:01:23 -0700976 }
977
Sanket Agarwalf8a1c912016-01-26 20:12:52 -0800978 } else if (hfpNotConnected) {
Jack He6258aae2017-06-29 17:01:23 -0700979 if (batteryLevelPercentageString != null) {
980 return mContext.getString(R.string.bluetooth_connected_no_headset_battery_level,
Pavlin Radoslavove6e080f2018-02-06 12:21:34 -0800981 batteryLevelPercentageString, activeDeviceString);
Jack He6258aae2017-06-29 17:01:23 -0700982 } else {
Pavlin Radoslavove6e080f2018-02-06 12:21:34 -0800983 return mContext.getString(R.string.bluetooth_connected_no_headset,
984 activeDeviceString);
Jack He6258aae2017-06-29 17:01:23 -0700985 }
Jason Monkbe3c5db2015-02-04 13:00:55 -0500986 } else {
Jack He6258aae2017-06-29 17:01:23 -0700987 if (batteryLevelPercentageString != null) {
988 return mContext.getString(R.string.bluetooth_connected_battery_level,
Pavlin Radoslavove6e080f2018-02-06 12:21:34 -0800989 batteryLevelPercentageString, activeDeviceString);
Jack He6258aae2017-06-29 17:01:23 -0700990 } else {
Pavlin Radoslavove6e080f2018-02-06 12:21:34 -0800991 return mContext.getString(R.string.bluetooth_connected, activeDeviceString);
Jack He6258aae2017-06-29 17:01:23 -0700992 }
Jason Monkbe3c5db2015-02-04 13:00:55 -0500993 }
994 }
995
Jack He6258aae2017-06-29 17:01:23 -0700996 return getBondState() == BluetoothDevice.BOND_BONDING ?
997 mContext.getString(R.string.bluetooth_pairing) : null;
Jason Monkbe3c5db2015-02-04 13:00:55 -0500998 }
Jason Monk7ce96b92015-02-02 11:27:58 -0500999}