blob: dc2eceace066a270385e616b7a6fa7ff2523d136 [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;
Hansong Zhangd7b35912018-03-16 09:15:48 -0700112 private boolean mIsActiveDeviceHearingAid = false;
Pavlin Radoslavov1af33a12018-01-21 02:59:15 -0800113
Jason Monk7ce96b92015-02-02 11:27:58 -0500114 /**
115 * Describes the current device and profile for logging.
116 *
117 * @param profile Profile to describe
118 * @return Description of the device and profile
119 */
120 private String describe(LocalBluetoothProfile profile) {
121 StringBuilder sb = new StringBuilder();
122 sb.append("Address:").append(mDevice);
123 if (profile != null) {
124 sb.append(" Profile:").append(profile);
125 }
126
127 return sb.toString();
128 }
129
130 void onProfileStateChanged(LocalBluetoothProfile profile, int newProfileState) {
131 if (Utils.D) {
132 Log.d(TAG, "onProfileStateChanged: profile " + profile +
133 " newProfileState " + newProfileState);
134 }
135 if (mLocalAdapter.getBluetoothState() == BluetoothAdapter.STATE_TURNING_OFF)
136 {
137 if (Utils.D) Log.d(TAG, " BT Turninig Off...Profile conn state change ignored...");
138 return;
139 }
140 mProfileConnectionState.put(profile, newProfileState);
141 if (newProfileState == BluetoothProfile.STATE_CONNECTED) {
142 if (profile instanceof MapProfile) {
143 profile.setPreferred(mDevice, true);
Hemant Guptadbc3d8d2017-05-12 21:14:44 +0530144 }
145 if (!mProfiles.contains(profile)) {
Jason Monk7ce96b92015-02-02 11:27:58 -0500146 mRemovedProfiles.remove(profile);
147 mProfiles.add(profile);
148 if (profile instanceof PanProfile &&
149 ((PanProfile) profile).isLocalRoleNap(mDevice)) {
150 // Device doesn't support NAP, so remove PanProfile on disconnect
151 mLocalNapRoleConnected = true;
152 }
153 }
154 } else if (profile instanceof MapProfile &&
155 newProfileState == BluetoothProfile.STATE_DISCONNECTED) {
156 profile.setPreferred(mDevice, false);
157 } else if (mLocalNapRoleConnected && profile instanceof PanProfile &&
158 ((PanProfile) profile).isLocalRoleNap(mDevice) &&
159 newProfileState == BluetoothProfile.STATE_DISCONNECTED) {
160 Log.d(TAG, "Removing PanProfile from device after NAP disconnect");
161 mProfiles.remove(profile);
162 mRemovedProfiles.add(profile);
163 mLocalNapRoleConnected = false;
164 }
Pavlin Radoslavov1af33a12018-01-21 02:59:15 -0800165 fetchActiveDevices();
Jason Monk7ce96b92015-02-02 11:27:58 -0500166 }
167
168 CachedBluetoothDevice(Context context,
169 LocalBluetoothAdapter adapter,
170 LocalBluetoothProfileManager profileManager,
171 BluetoothDevice device) {
172 mContext = context;
173 mLocalAdapter = adapter;
174 mProfileManager = profileManager;
175 mDevice = device;
176 mProfileConnectionState = new HashMap<LocalBluetoothProfile, Integer>();
177 fillData();
178 }
179
180 public void disconnect() {
181 for (LocalBluetoothProfile profile : mProfiles) {
182 disconnect(profile);
183 }
184 // Disconnect PBAP server in case its connected
185 // This is to ensure all the profiles are disconnected as some CK/Hs do not
186 // disconnect PBAP connection when HF connection is brought down
187 PbapServerProfile PbapProfile = mProfileManager.getPbapProfile();
188 if (PbapProfile.getConnectionStatus(mDevice) == BluetoothProfile.STATE_CONNECTED)
189 {
190 PbapProfile.disconnect(mDevice);
191 }
192 }
193
194 public void disconnect(LocalBluetoothProfile profile) {
195 if (profile.disconnect(mDevice)) {
196 if (Utils.D) {
197 Log.d(TAG, "Command sent successfully:DISCONNECT " + describe(profile));
198 }
199 }
200 }
201
202 public void connect(boolean connectAllProfiles) {
203 if (!ensurePaired()) {
204 return;
205 }
206
207 mConnectAttempted = SystemClock.elapsedRealtime();
208 connectWithoutResettingTimer(connectAllProfiles);
209 }
210
211 void onBondingDockConnect() {
212 // Attempt to connect if UUIDs are available. Otherwise,
213 // we will connect when the ACTION_UUID intent arrives.
214 connect(false);
215 }
216
217 private void connectWithoutResettingTimer(boolean connectAllProfiles) {
218 // Try to initialize the profiles if they were not.
219 if (mProfiles.isEmpty()) {
220 // if mProfiles is empty, then do not invoke updateProfiles. This causes a race
221 // condition with carkits during pairing, wherein RemoteDevice.UUIDs have been updated
222 // from bluetooth stack but ACTION.uuid is not sent yet.
223 // Eventually ACTION.uuid will be received which shall trigger the connection of the
224 // various profiles
225 // If UUIDs are not available yet, connect will be happen
226 // upon arrival of the ACTION_UUID intent.
227 Log.d(TAG, "No profiles. Maybe we will connect later");
228 return;
229 }
230
231 // Reset the only-show-one-error-dialog tracking variable
232 mIsConnectingErrorPossible = true;
233
234 int preferredProfiles = 0;
235 for (LocalBluetoothProfile profile : mProfiles) {
236 if (connectAllProfiles ? profile.isConnectable() : profile.isAutoConnectable()) {
237 if (profile.isPreferred(mDevice)) {
238 ++preferredProfiles;
239 connectInt(profile);
240 }
241 }
242 }
243 if (DEBUG) Log.d(TAG, "Preferred profiles = " + preferredProfiles);
244
245 if (preferredProfiles == 0) {
246 connectAutoConnectableProfiles();
247 }
248 }
249
250 private void connectAutoConnectableProfiles() {
251 if (!ensurePaired()) {
252 return;
253 }
254 // Reset the only-show-one-error-dialog tracking variable
255 mIsConnectingErrorPossible = true;
256
257 for (LocalBluetoothProfile profile : mProfiles) {
258 if (profile.isAutoConnectable()) {
259 profile.setPreferred(mDevice, true);
260 connectInt(profile);
261 }
262 }
263 }
264
265 /**
266 * Connect this device to the specified profile.
267 *
268 * @param profile the profile to use with the remote device
269 */
270 public void connectProfile(LocalBluetoothProfile profile) {
271 mConnectAttempted = SystemClock.elapsedRealtime();
272 // Reset the only-show-one-error-dialog tracking variable
273 mIsConnectingErrorPossible = true;
274 connectInt(profile);
275 // Refresh the UI based on profile.connect() call
276 refresh();
277 }
278
279 synchronized void connectInt(LocalBluetoothProfile profile) {
280 if (!ensurePaired()) {
281 return;
282 }
283 if (profile.connect(mDevice)) {
284 if (Utils.D) {
285 Log.d(TAG, "Command sent successfully:CONNECT " + describe(profile));
286 }
287 return;
288 }
289 Log.i(TAG, "Failed to connect " + profile.toString() + " to " + mName);
290 }
291
292 private boolean ensurePaired() {
293 if (getBondState() == BluetoothDevice.BOND_NONE) {
294 startPairing();
295 return false;
296 } else {
297 return true;
298 }
299 }
300
301 public boolean startPairing() {
302 // Pairing is unreliable while scanning, so cancel discovery
303 if (mLocalAdapter.isDiscovering()) {
304 mLocalAdapter.cancelDiscovery();
305 }
306
307 if (!mDevice.createBond()) {
308 return false;
309 }
310
Jason Monk7ce96b92015-02-02 11:27:58 -0500311 return true;
312 }
313
314 /**
315 * Return true if user initiated pairing on this device. The message text is
316 * slightly different for local vs. remote initiated pairing dialogs.
317 */
318 boolean isUserInitiatedPairing() {
Jakub Pawlowski0278ab92016-07-20 11:55:48 -0700319 return mDevice.isBondingInitiatedLocally();
Jason Monk7ce96b92015-02-02 11:27:58 -0500320 }
321
322 public void unpair() {
323 int state = getBondState();
324
325 if (state == BluetoothDevice.BOND_BONDING) {
326 mDevice.cancelBondProcess();
327 }
328
329 if (state != BluetoothDevice.BOND_NONE) {
330 final BluetoothDevice dev = mDevice;
331 if (dev != null) {
332 final boolean successful = dev.removeBond();
333 if (successful) {
334 if (Utils.D) {
335 Log.d(TAG, "Command sent successfully:REMOVE_BOND " + describe(null));
336 }
337 } else if (Utils.V) {
338 Log.v(TAG, "Framework rejected command immediately:REMOVE_BOND " +
339 describe(null));
340 }
341 }
342 }
343 }
344
345 public int getProfileConnectionState(LocalBluetoothProfile profile) {
xutianguo22bbb8192016-06-22 11:32:00 +0800346 if (mProfileConnectionState.get(profile) == null) {
Jason Monk7ce96b92015-02-02 11:27:58 -0500347 // If cache is empty make the binder call to get the state
348 int state = profile.getConnectionStatus(mDevice);
349 mProfileConnectionState.put(profile, state);
350 }
351 return mProfileConnectionState.get(profile);
352 }
353
354 public void clearProfileConnectionState ()
355 {
356 if (Utils.D) {
357 Log.d(TAG," Clearing all connection state for dev:" + mDevice.getName());
358 }
359 for (LocalBluetoothProfile profile :getProfiles()) {
360 mProfileConnectionState.put(profile, BluetoothProfile.STATE_DISCONNECTED);
361 }
362 }
363
364 // TODO: do any of these need to run async on a background thread?
365 private void fillData() {
366 fetchName();
367 fetchBtClass();
368 updateProfiles();
Pavlin Radoslavov1af33a12018-01-21 02:59:15 -0800369 fetchActiveDevices();
Jason Monk7ce96b92015-02-02 11:27:58 -0500370 migratePhonebookPermissionChoice();
371 migrateMessagePermissionChoice();
372 fetchMessageRejectionCount();
373
Jason Monk7ce96b92015-02-02 11:27:58 -0500374 dispatchAttributesChanged();
375 }
376
377 public BluetoothDevice getDevice() {
378 return mDevice;
379 }
380
Antony Sargent7ad051e2017-06-29 15:23:13 -0700381 /**
382 * Convenience method that can be mocked - it lets tests avoid having to call getDevice() which
383 * causes problems in tests since BluetoothDevice is final and cannot be mocked.
384 * @return the address of this device
385 */
386 public String getAddress() {
387 return mDevice.getAddress();
388 }
389
Jason Monk7ce96b92015-02-02 11:27:58 -0500390 public String getName() {
391 return mName;
392 }
393
394 /**
395 * Populate name from BluetoothDevice.ACTION_FOUND intent
396 */
397 void setNewName(String name) {
398 if (mName == null) {
399 mName = name;
400 if (mName == null || TextUtils.isEmpty(mName)) {
401 mName = mDevice.getAddress();
402 }
403 dispatchAttributesChanged();
404 }
405 }
406
407 /**
Jack Hec219bc92017-07-24 14:55:59 -0700408 * User changes the device name
409 * @param name new alias name to be set, should never be null
Jason Monk7ce96b92015-02-02 11:27:58 -0500410 */
411 public void setName(String name) {
Jack Hec219bc92017-07-24 14:55:59 -0700412 // Prevent mName to be set to null if setName(null) is called
413 if (name != null && !TextUtils.equals(name, mName)) {
Jason Monk7ce96b92015-02-02 11:27:58 -0500414 mName = name;
415 mDevice.setAlias(name);
416 dispatchAttributesChanged();
417 }
418 }
419
Hansong Zhang6a416322018-03-19 18:20:38 -0700420 /**
421 * Set this device as active device
422 * @return true if at least one profile on this device is set to active, false otherwise
423 */
424 public boolean setActive() {
425 boolean result = false;
426 A2dpProfile a2dpProfile = mProfileManager.getA2dpProfile();
427 if (a2dpProfile != null && isConnectedProfile(a2dpProfile)) {
428 if (a2dpProfile.setActiveDevice(getDevice())) {
429 Log.i(TAG, "OnPreferenceClickListener: A2DP active device=" + this);
430 result = true;
431 }
432 }
433 HeadsetProfile headsetProfile = mProfileManager.getHeadsetProfile();
434 if ((headsetProfile != null) && isConnectedProfile(headsetProfile)) {
435 if (headsetProfile.setActiveDevice(getDevice())) {
436 Log.i(TAG, "OnPreferenceClickListener: Headset active device=" + this);
437 result = true;
438 }
439 }
Hansong Zhangd7b35912018-03-16 09:15:48 -0700440 HearingAidProfile hearingAidProfile = mProfileManager.getHearingAidProfile();
441 if ((hearingAidProfile != null) && isConnectedProfile(hearingAidProfile)) {
442 if (hearingAidProfile.setActiveDevice(getDevice())) {
443 Log.i(TAG, "OnPreferenceClickListener: Hearing Aid active device=" + this);
444 result = true;
445 }
446 }
Hansong Zhang6a416322018-03-19 18:20:38 -0700447 return result;
448 }
449
Jason Monk7ce96b92015-02-02 11:27:58 -0500450 void refreshName() {
451 fetchName();
452 dispatchAttributesChanged();
453 }
454
455 private void fetchName() {
456 mName = mDevice.getAliasName();
457
458 if (TextUtils.isEmpty(mName)) {
459 mName = mDevice.getAddress();
460 if (DEBUG) Log.d(TAG, "Device has no name (yet), use address: " + mName);
461 }
462 }
463
Jack He6258aae2017-06-29 17:01:23 -0700464 /**
Jack Hec219bc92017-07-24 14:55:59 -0700465 * Checks if device has a human readable name besides MAC address
466 * @return true if device's alias name is not null nor empty, false otherwise
467 */
468 public boolean hasHumanReadableName() {
469 return !TextUtils.isEmpty(mDevice.getAliasName());
470 }
471
472 /**
Jack He6258aae2017-06-29 17:01:23 -0700473 * Get battery level from remote device
474 * @return battery level in percentage [0-100], or {@link BluetoothDevice#BATTERY_LEVEL_UNKNOWN}
475 */
476 public int getBatteryLevel() {
477 return mDevice.getBatteryLevel();
478 }
479
Jason Monk7ce96b92015-02-02 11:27:58 -0500480 void refresh() {
481 dispatchAttributesChanged();
482 }
483
Jack He51520472017-07-24 12:30:08 -0700484 public void setJustDiscovered(boolean justDiscovered) {
485 if (mJustDiscovered != justDiscovered) {
486 mJustDiscovered = justDiscovered;
Jason Monk7ce96b92015-02-02 11:27:58 -0500487 dispatchAttributesChanged();
488 }
489 }
490
491 public int getBondState() {
492 return mDevice.getBondState();
493 }
494
Pavlin Radoslavov1af33a12018-01-21 02:59:15 -0800495 /**
Pavlin Radoslavovc285d552018-02-06 16:14:00 -0800496 * Update the device status as active or non-active per Bluetooth profile.
Pavlin Radoslavov1af33a12018-01-21 02:59:15 -0800497 *
498 * @param isActive true if the device is active
499 * @param bluetoothProfile the Bluetooth profile
500 */
Pavlin Radoslavovc285d552018-02-06 16:14:00 -0800501 public void onActiveDeviceChanged(boolean isActive, int bluetoothProfile) {
Pavlin Radoslavov1af33a12018-01-21 02:59:15 -0800502 boolean changed = false;
503 switch (bluetoothProfile) {
504 case BluetoothProfile.A2DP:
505 changed = (mIsActiveDeviceA2dp != isActive);
506 mIsActiveDeviceA2dp = isActive;
507 break;
508 case BluetoothProfile.HEADSET:
509 changed = (mIsActiveDeviceHeadset != isActive);
510 mIsActiveDeviceHeadset = isActive;
511 break;
Hansong Zhangd7b35912018-03-16 09:15:48 -0700512 case BluetoothProfile.HEARING_AID:
513 changed = (mIsActiveDeviceHearingAid != isActive);
514 mIsActiveDeviceHearingAid = isActive;
515 break;
Pavlin Radoslavov1af33a12018-01-21 02:59:15 -0800516 default:
Pavlin Radoslavovc285d552018-02-06 16:14:00 -0800517 Log.w(TAG, "onActiveDeviceChanged: unknown profile " + bluetoothProfile +
Pavlin Radoslavov1af33a12018-01-21 02:59:15 -0800518 " isActive " + isActive);
519 break;
520 }
521 if (changed) {
522 dispatchAttributesChanged();
523 }
524 }
525
Pavlin Radoslavovc285d552018-02-06 16:14:00 -0800526 /**
527 * Get the device status as active or non-active per Bluetooth profile.
528 *
529 * @param bluetoothProfile the Bluetooth profile
530 * @return true if the device is active
531 */
532 @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
533 public boolean isActiveDevice(int bluetoothProfile) {
534 switch (bluetoothProfile) {
535 case BluetoothProfile.A2DP:
536 return mIsActiveDeviceA2dp;
537 case BluetoothProfile.HEADSET:
538 return mIsActiveDeviceHeadset;
Hansong Zhangd7b35912018-03-16 09:15:48 -0700539 case BluetoothProfile.HEARING_AID:
540 return mIsActiveDeviceHearingAid;
Pavlin Radoslavovc285d552018-02-06 16:14:00 -0800541 default:
542 Log.w(TAG, "getActiveDevice: unknown profile " + bluetoothProfile);
543 break;
544 }
545 return false;
546 }
547
Jason Monk7ce96b92015-02-02 11:27:58 -0500548 void setRssi(short rssi) {
549 if (mRssi != rssi) {
550 mRssi = rssi;
551 dispatchAttributesChanged();
552 }
553 }
554
555 /**
556 * Checks whether we are connected to this device (any profile counts).
557 *
558 * @return Whether it is connected.
559 */
560 public boolean isConnected() {
561 for (LocalBluetoothProfile profile : mProfiles) {
562 int status = getProfileConnectionState(profile);
563 if (status == BluetoothProfile.STATE_CONNECTED) {
564 return true;
565 }
566 }
567
568 return false;
569 }
570
571 public boolean isConnectedProfile(LocalBluetoothProfile profile) {
572 int status = getProfileConnectionState(profile);
573 return status == BluetoothProfile.STATE_CONNECTED;
574
575 }
576
577 public boolean isBusy() {
578 for (LocalBluetoothProfile profile : mProfiles) {
579 int status = getProfileConnectionState(profile);
580 if (status == BluetoothProfile.STATE_CONNECTING
581 || status == BluetoothProfile.STATE_DISCONNECTING) {
582 return true;
583 }
584 }
585 return getBondState() == BluetoothDevice.BOND_BONDING;
586 }
587
588 /**
589 * Fetches a new value for the cached BT class.
590 */
591 private void fetchBtClass() {
592 mBtClass = mDevice.getBluetoothClass();
593 }
594
595 private boolean updateProfiles() {
596 ParcelUuid[] uuids = mDevice.getUuids();
597 if (uuids == null) return false;
598
599 ParcelUuid[] localUuids = mLocalAdapter.getUuids();
600 if (localUuids == null) return false;
601
Jack Hec219bc92017-07-24 14:55:59 -0700602 /*
Jason Monk7ce96b92015-02-02 11:27:58 -0500603 * Now we know if the device supports PBAP, update permissions...
604 */
605 processPhonebookAccess();
606
607 mProfileManager.updateProfiles(uuids, localUuids, mProfiles, mRemovedProfiles,
608 mLocalNapRoleConnected, mDevice);
609
610 if (DEBUG) {
611 Log.e(TAG, "updating profiles for " + mDevice.getAliasName());
612 BluetoothClass bluetoothClass = mDevice.getBluetoothClass();
613
614 if (bluetoothClass != null) Log.v(TAG, "Class: " + bluetoothClass.toString());
615 Log.v(TAG, "UUID:");
616 for (ParcelUuid uuid : uuids) {
617 Log.v(TAG, " " + uuid);
618 }
619 }
620 return true;
621 }
622
Pavlin Radoslavov1af33a12018-01-21 02:59:15 -0800623 private void fetchActiveDevices() {
624 A2dpProfile a2dpProfile = mProfileManager.getA2dpProfile();
625 if (a2dpProfile != null) {
626 mIsActiveDeviceA2dp = mDevice.equals(a2dpProfile.getActiveDevice());
627 }
628 HeadsetProfile headsetProfile = mProfileManager.getHeadsetProfile();
629 if (headsetProfile != null) {
630 mIsActiveDeviceHeadset = mDevice.equals(headsetProfile.getActiveDevice());
631 }
Hansong Zhangd7b35912018-03-16 09:15:48 -0700632 HearingAidProfile hearingAidProfile = mProfileManager.getHearingAidProfile();
633 if (hearingAidProfile != null) {
Hansong Zhang3b8f09b2018-03-28 16:53:10 -0700634 mIsActiveDeviceHearingAid = hearingAidProfile.getActiveDevices().contains(mDevice);
Hansong Zhangd7b35912018-03-16 09:15:48 -0700635 }
Pavlin Radoslavov1af33a12018-01-21 02:59:15 -0800636 }
637
Jason Monk7ce96b92015-02-02 11:27:58 -0500638 /**
639 * Refreshes the UI for the BT class, including fetching the latest value
640 * for the class.
641 */
642 void refreshBtClass() {
643 fetchBtClass();
644 dispatchAttributesChanged();
645 }
646
647 /**
648 * Refreshes the UI when framework alerts us of a UUID change.
649 */
650 void onUuidChanged() {
651 updateProfiles();
Etan Cohen50d47612015-03-31 12:45:23 -0700652 ParcelUuid[] uuids = mDevice.getUuids();
Venkat Raghavan05e08c32015-04-06 16:26:11 -0700653
Etan Cohen50d47612015-03-31 12:45:23 -0700654 long timeout = MAX_UUID_DELAY_FOR_AUTO_CONNECT;
Venkat Raghavan05e08c32015-04-06 16:26:11 -0700655 if (BluetoothUuid.isUuidPresent(uuids, BluetoothUuid.Hogp)) {
656 timeout = MAX_HOGP_DELAY_FOR_AUTO_CONNECT;
657 }
Jason Monk7ce96b92015-02-02 11:27:58 -0500658
659 if (DEBUG) {
Etan Cohen50d47612015-03-31 12:45:23 -0700660 Log.d(TAG, "onUuidChanged: Time since last connect"
Jason Monk7ce96b92015-02-02 11:27:58 -0500661 + (SystemClock.elapsedRealtime() - mConnectAttempted));
662 }
663
664 /*
Venkat Raghavan05e08c32015-04-06 16:26:11 -0700665 * If a connect was attempted earlier without any UUID, we will do the connect now.
666 * Otherwise, allow the connect on UUID change.
Jason Monk7ce96b92015-02-02 11:27:58 -0500667 */
668 if (!mProfiles.isEmpty()
Andre Eisenbach7be83c52015-09-03 15:20:09 -0700669 && ((mConnectAttempted + timeout) > SystemClock.elapsedRealtime())) {
Jason Monk7ce96b92015-02-02 11:27:58 -0500670 connectWithoutResettingTimer(false);
671 }
Andre Eisenbach7be83c52015-09-03 15:20:09 -0700672
Jason Monk7ce96b92015-02-02 11:27:58 -0500673 dispatchAttributesChanged();
674 }
675
676 void onBondingStateChanged(int bondState) {
677 if (bondState == BluetoothDevice.BOND_NONE) {
678 mProfiles.clear();
Jason Monk7ce96b92015-02-02 11:27:58 -0500679 setPhonebookPermissionChoice(ACCESS_UNKNOWN);
680 setMessagePermissionChoice(ACCESS_UNKNOWN);
Casper Bonde424681e2015-05-04 22:07:45 -0700681 setSimPermissionChoice(ACCESS_UNKNOWN);
Jason Monk7ce96b92015-02-02 11:27:58 -0500682 mMessageRejectionCount = 0;
683 saveMessageRejectionCount();
684 }
685
686 refresh();
687
688 if (bondState == BluetoothDevice.BOND_BONDED) {
689 if (mDevice.isBluetoothDock()) {
690 onBondingDockConnect();
Jakub Pawlowski0278ab92016-07-20 11:55:48 -0700691 } else if (mDevice.isBondingInitiatedLocally()) {
Jason Monk7ce96b92015-02-02 11:27:58 -0500692 connect(false);
693 }
Jason Monk7ce96b92015-02-02 11:27:58 -0500694 }
695 }
696
697 void setBtClass(BluetoothClass btClass) {
698 if (btClass != null && mBtClass != btClass) {
699 mBtClass = btClass;
700 dispatchAttributesChanged();
701 }
702 }
703
704 public BluetoothClass getBtClass() {
705 return mBtClass;
706 }
707
708 public List<LocalBluetoothProfile> getProfiles() {
709 return Collections.unmodifiableList(mProfiles);
710 }
711
712 public List<LocalBluetoothProfile> getConnectableProfiles() {
713 List<LocalBluetoothProfile> connectableProfiles =
714 new ArrayList<LocalBluetoothProfile>();
715 for (LocalBluetoothProfile profile : mProfiles) {
716 if (profile.isConnectable()) {
717 connectableProfiles.add(profile);
718 }
719 }
720 return connectableProfiles;
721 }
722
723 public List<LocalBluetoothProfile> getRemovedProfiles() {
724 return mRemovedProfiles;
725 }
726
727 public void registerCallback(Callback callback) {
728 synchronized (mCallbacks) {
729 mCallbacks.add(callback);
730 }
731 }
732
733 public void unregisterCallback(Callback callback) {
734 synchronized (mCallbacks) {
735 mCallbacks.remove(callback);
736 }
737 }
738
739 private void dispatchAttributesChanged() {
740 synchronized (mCallbacks) {
741 for (Callback callback : mCallbacks) {
742 callback.onDeviceAttributesChanged();
743 }
744 }
745 }
746
747 @Override
748 public String toString() {
749 return mDevice.toString();
750 }
751
752 @Override
753 public boolean equals(Object o) {
754 if ((o == null) || !(o instanceof CachedBluetoothDevice)) {
755 return false;
756 }
757 return mDevice.equals(((CachedBluetoothDevice) o).mDevice);
758 }
759
760 @Override
761 public int hashCode() {
762 return mDevice.getAddress().hashCode();
763 }
764
765 // This comparison uses non-final fields so the sort order may change
766 // when device attributes change (such as bonding state). Settings
767 // will completely refresh the device list when this happens.
768 public int compareTo(CachedBluetoothDevice another) {
769 // Connected above not connected
770 int comparison = (another.isConnected() ? 1 : 0) - (isConnected() ? 1 : 0);
771 if (comparison != 0) return comparison;
772
773 // Paired above not paired
774 comparison = (another.getBondState() == BluetoothDevice.BOND_BONDED ? 1 : 0) -
775 (getBondState() == BluetoothDevice.BOND_BONDED ? 1 : 0);
776 if (comparison != 0) return comparison;
777
Jack He51520472017-07-24 12:30:08 -0700778 // Just discovered above discovered in the past
779 comparison = (another.mJustDiscovered ? 1 : 0) - (mJustDiscovered ? 1 : 0);
Jason Monk7ce96b92015-02-02 11:27:58 -0500780 if (comparison != 0) return comparison;
781
782 // Stronger signal above weaker signal
783 comparison = another.mRssi - mRssi;
784 if (comparison != 0) return comparison;
785
786 // Fallback on name
787 return mName.compareTo(another.mName);
788 }
789
790 public interface Callback {
791 void onDeviceAttributesChanged();
792 }
793
794 public int getPhonebookPermissionChoice() {
795 int permission = mDevice.getPhonebookAccessPermission();
796 if (permission == BluetoothDevice.ACCESS_ALLOWED) {
797 return ACCESS_ALLOWED;
798 } else if (permission == BluetoothDevice.ACCESS_REJECTED) {
799 return ACCESS_REJECTED;
800 }
801 return ACCESS_UNKNOWN;
802 }
803
804 public void setPhonebookPermissionChoice(int permissionChoice) {
805 int permission = BluetoothDevice.ACCESS_UNKNOWN;
806 if (permissionChoice == ACCESS_ALLOWED) {
807 permission = BluetoothDevice.ACCESS_ALLOWED;
808 } else if (permissionChoice == ACCESS_REJECTED) {
809 permission = BluetoothDevice.ACCESS_REJECTED;
810 }
811 mDevice.setPhonebookAccessPermission(permission);
812 }
813
814 // Migrates data from old data store (in Settings app's shared preferences) to new (in Bluetooth
815 // app's shared preferences).
816 private void migratePhonebookPermissionChoice() {
817 SharedPreferences preferences = mContext.getSharedPreferences(
818 "bluetooth_phonebook_permission", Context.MODE_PRIVATE);
819 if (!preferences.contains(mDevice.getAddress())) {
820 return;
821 }
822
823 if (mDevice.getPhonebookAccessPermission() == BluetoothDevice.ACCESS_UNKNOWN) {
824 int oldPermission = preferences.getInt(mDevice.getAddress(), ACCESS_UNKNOWN);
825 if (oldPermission == ACCESS_ALLOWED) {
826 mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_ALLOWED);
827 } else if (oldPermission == ACCESS_REJECTED) {
828 mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_REJECTED);
829 }
830 }
831
832 SharedPreferences.Editor editor = preferences.edit();
833 editor.remove(mDevice.getAddress());
834 editor.commit();
835 }
836
837 public int getMessagePermissionChoice() {
838 int permission = mDevice.getMessageAccessPermission();
839 if (permission == BluetoothDevice.ACCESS_ALLOWED) {
840 return ACCESS_ALLOWED;
841 } else if (permission == BluetoothDevice.ACCESS_REJECTED) {
842 return ACCESS_REJECTED;
843 }
844 return ACCESS_UNKNOWN;
845 }
846
847 public void setMessagePermissionChoice(int permissionChoice) {
848 int permission = BluetoothDevice.ACCESS_UNKNOWN;
849 if (permissionChoice == ACCESS_ALLOWED) {
850 permission = BluetoothDevice.ACCESS_ALLOWED;
851 } else if (permissionChoice == ACCESS_REJECTED) {
852 permission = BluetoothDevice.ACCESS_REJECTED;
853 }
854 mDevice.setMessageAccessPermission(permission);
855 }
856
Casper Bonde424681e2015-05-04 22:07:45 -0700857 public int getSimPermissionChoice() {
858 int permission = mDevice.getSimAccessPermission();
859 if (permission == BluetoothDevice.ACCESS_ALLOWED) {
860 return ACCESS_ALLOWED;
861 } else if (permission == BluetoothDevice.ACCESS_REJECTED) {
862 return ACCESS_REJECTED;
863 }
864 return ACCESS_UNKNOWN;
865 }
866
867 void setSimPermissionChoice(int permissionChoice) {
868 int permission = BluetoothDevice.ACCESS_UNKNOWN;
869 if (permissionChoice == ACCESS_ALLOWED) {
870 permission = BluetoothDevice.ACCESS_ALLOWED;
871 } else if (permissionChoice == ACCESS_REJECTED) {
872 permission = BluetoothDevice.ACCESS_REJECTED;
873 }
874 mDevice.setSimAccessPermission(permission);
875 }
876
Jason Monk7ce96b92015-02-02 11:27:58 -0500877 // Migrates data from old data store (in Settings app's shared preferences) to new (in Bluetooth
878 // app's shared preferences).
879 private void migrateMessagePermissionChoice() {
880 SharedPreferences preferences = mContext.getSharedPreferences(
881 "bluetooth_message_permission", Context.MODE_PRIVATE);
882 if (!preferences.contains(mDevice.getAddress())) {
883 return;
884 }
885
886 if (mDevice.getMessageAccessPermission() == BluetoothDevice.ACCESS_UNKNOWN) {
887 int oldPermission = preferences.getInt(mDevice.getAddress(), ACCESS_UNKNOWN);
888 if (oldPermission == ACCESS_ALLOWED) {
889 mDevice.setMessageAccessPermission(BluetoothDevice.ACCESS_ALLOWED);
890 } else if (oldPermission == ACCESS_REJECTED) {
891 mDevice.setMessageAccessPermission(BluetoothDevice.ACCESS_REJECTED);
892 }
893 }
894
895 SharedPreferences.Editor editor = preferences.edit();
896 editor.remove(mDevice.getAddress());
897 editor.commit();
898 }
899
900 /**
901 * @return Whether this rejection should persist.
902 */
903 public boolean checkAndIncreaseMessageRejectionCount() {
904 if (mMessageRejectionCount < MESSAGE_REJECTION_COUNT_LIMIT_TO_PERSIST) {
905 mMessageRejectionCount++;
906 saveMessageRejectionCount();
907 }
908 return mMessageRejectionCount >= MESSAGE_REJECTION_COUNT_LIMIT_TO_PERSIST;
909 }
910
911 private void fetchMessageRejectionCount() {
912 SharedPreferences preference = mContext.getSharedPreferences(
913 MESSAGE_REJECTION_COUNT_PREFS_NAME, Context.MODE_PRIVATE);
914 mMessageRejectionCount = preference.getInt(mDevice.getAddress(), 0);
915 }
916
917 private void saveMessageRejectionCount() {
918 SharedPreferences.Editor editor = mContext.getSharedPreferences(
919 MESSAGE_REJECTION_COUNT_PREFS_NAME, Context.MODE_PRIVATE).edit();
920 if (mMessageRejectionCount == 0) {
921 editor.remove(mDevice.getAddress());
922 } else {
923 editor.putInt(mDevice.getAddress(), mMessageRejectionCount);
924 }
925 editor.commit();
926 }
927
928 private void processPhonebookAccess() {
929 if (mDevice.getBondState() != BluetoothDevice.BOND_BONDED) return;
930
931 ParcelUuid[] uuids = mDevice.getUuids();
932 if (BluetoothUuid.containsAnyUuid(uuids, PbapServerProfile.PBAB_CLIENT_UUIDS)) {
933 // The pairing dialog now warns of phone-book access for paired devices.
934 // No separate prompt is displayed after pairing.
Sanket Padawe78c23762015-06-01 15:55:30 -0700935 if (getPhonebookPermissionChoice() == CachedBluetoothDevice.ACCESS_UNKNOWN) {
Sanket Padawe07533db2015-11-11 15:01:35 -0800936 if (mDevice.getBluetoothClass().getDeviceClass()
Hemant Guptab8d267a2016-06-07 12:53:59 +0530937 == BluetoothClass.Device.AUDIO_VIDEO_HANDSFREE ||
938 mDevice.getBluetoothClass().getDeviceClass()
939 == BluetoothClass.Device.AUDIO_VIDEO_WEARABLE_HEADSET) {
Sanket Padawe07533db2015-11-11 15:01:35 -0800940 setPhonebookPermissionChoice(CachedBluetoothDevice.ACCESS_ALLOWED);
941 } else {
942 setPhonebookPermissionChoice(CachedBluetoothDevice.ACCESS_REJECTED);
943 }
Sanket Padawe78c23762015-06-01 15:55:30 -0700944 }
Jason Monk7ce96b92015-02-02 11:27:58 -0500945 }
946 }
Jason Monkbe3c5db2015-02-04 13:00:55 -0500947
948 public int getMaxConnectionState() {
949 int maxState = BluetoothProfile.STATE_DISCONNECTED;
950 for (LocalBluetoothProfile profile : getProfiles()) {
951 int connectionStatus = getProfileConnectionState(profile);
952 if (connectionStatus > maxState) {
953 maxState = connectionStatus;
954 }
955 }
956 return maxState;
957 }
958
959 /**
960 * @return resource for string that discribes the connection state of this device.
961 */
Jack He6258aae2017-06-29 17:01:23 -0700962 public String getConnectionSummary() {
Jason Monkbe3c5db2015-02-04 13:00:55 -0500963 boolean profileConnected = false; // at least one profile is connected
964 boolean a2dpNotConnected = false; // A2DP is preferred but not connected
Sanket Agarwalf8a1c912016-01-26 20:12:52 -0800965 boolean hfpNotConnected = false; // HFP is preferred but not connected
Hansong Zhangd7b35912018-03-16 09:15:48 -0700966 boolean hearingAidNotConnected = false; // Hearing Aid is preferred but not connected
Jason Monkbe3c5db2015-02-04 13:00:55 -0500967
968 for (LocalBluetoothProfile profile : getProfiles()) {
969 int connectionStatus = getProfileConnectionState(profile);
970
971 switch (connectionStatus) {
972 case BluetoothProfile.STATE_CONNECTING:
973 case BluetoothProfile.STATE_DISCONNECTING:
Jack He6258aae2017-06-29 17:01:23 -0700974 return mContext.getString(Utils.getConnectionStateSummary(connectionStatus));
Jason Monkbe3c5db2015-02-04 13:00:55 -0500975
976 case BluetoothProfile.STATE_CONNECTED:
977 profileConnected = true;
978 break;
979
980 case BluetoothProfile.STATE_DISCONNECTED:
981 if (profile.isProfileReady()) {
Sanket Agarwalf8a1c912016-01-26 20:12:52 -0800982 if ((profile instanceof A2dpProfile) ||
Sanket Agarwal1bec6a52015-10-21 18:23:27 -0700983 (profile instanceof A2dpSinkProfile)){
Jason Monkbe3c5db2015-02-04 13:00:55 -0500984 a2dpNotConnected = true;
Sanket Agarwalf8a1c912016-01-26 20:12:52 -0800985 } else if ((profile instanceof HeadsetProfile) ||
986 (profile instanceof HfpClientProfile)) {
987 hfpNotConnected = true;
Hansong Zhangd7b35912018-03-16 09:15:48 -0700988 } else if (profile instanceof HearingAidProfile) {
989 hearingAidNotConnected = true;
Jason Monkbe3c5db2015-02-04 13:00:55 -0500990 }
991 }
992 break;
993 }
994 }
995
Jack He6258aae2017-06-29 17:01:23 -0700996 String batteryLevelPercentageString = null;
997 // Android framework should only set mBatteryLevel to valid range [0-100] or
998 // BluetoothDevice.BATTERY_LEVEL_UNKNOWN, any other value should be a framework bug.
999 // Thus assume here that if value is not BluetoothDevice.BATTERY_LEVEL_UNKNOWN, it must
1000 // be valid
1001 final int batteryLevel = getBatteryLevel();
1002 if (batteryLevel != BluetoothDevice.BATTERY_LEVEL_UNKNOWN) {
1003 // TODO: name com.android.settingslib.bluetooth.Utils something different
1004 batteryLevelPercentageString =
1005 com.android.settingslib.Utils.formatPercentage(batteryLevel);
1006 }
1007
Pavlin Radoslavove6e080f2018-02-06 12:21:34 -08001008 // Prepare the string for the Active Device summary
1009 String[] activeDeviceStringsArray = mContext.getResources().getStringArray(
1010 R.array.bluetooth_audio_active_device_summaries);
1011 String activeDeviceString = activeDeviceStringsArray[0]; // Default value: not active
Pavlin Radoslavov1af33a12018-01-21 02:59:15 -08001012 if (mIsActiveDeviceA2dp && mIsActiveDeviceHeadset) {
Pavlin Radoslavove6e080f2018-02-06 12:21:34 -08001013 activeDeviceString = activeDeviceStringsArray[1]; // Active for Media and Phone
Pavlin Radoslavov1af33a12018-01-21 02:59:15 -08001014 } else {
1015 if (mIsActiveDeviceA2dp) {
Pavlin Radoslavove6e080f2018-02-06 12:21:34 -08001016 activeDeviceString = activeDeviceStringsArray[2]; // Active for Media only
Pavlin Radoslavov1af33a12018-01-21 02:59:15 -08001017 }
1018 if (mIsActiveDeviceHeadset) {
Pavlin Radoslavove6e080f2018-02-06 12:21:34 -08001019 activeDeviceString = activeDeviceStringsArray[3]; // Active for Phone only
Pavlin Radoslavov1af33a12018-01-21 02:59:15 -08001020 }
1021 }
Hansong Zhangd7b35912018-03-16 09:15:48 -07001022 if (!hearingAidNotConnected && mIsActiveDeviceHearingAid) {
1023 activeDeviceString = activeDeviceStringsArray[1];
1024 return mContext.getString(R.string.bluetooth_connected, activeDeviceString);
1025 }
Pavlin Radoslavov1af33a12018-01-21 02:59:15 -08001026
Jason Monkbe3c5db2015-02-04 13:00:55 -05001027 if (profileConnected) {
Sanket Agarwalf8a1c912016-01-26 20:12:52 -08001028 if (a2dpNotConnected && hfpNotConnected) {
Jack He6258aae2017-06-29 17:01:23 -07001029 if (batteryLevelPercentageString != null) {
1030 return mContext.getString(
1031 R.string.bluetooth_connected_no_headset_no_a2dp_battery_level,
Pavlin Radoslavove6e080f2018-02-06 12:21:34 -08001032 batteryLevelPercentageString, activeDeviceString);
Jack He6258aae2017-06-29 17:01:23 -07001033 } else {
Pavlin Radoslavove6e080f2018-02-06 12:21:34 -08001034 return mContext.getString(R.string.bluetooth_connected_no_headset_no_a2dp,
1035 activeDeviceString);
Jack He6258aae2017-06-29 17:01:23 -07001036 }
1037
Jason Monkbe3c5db2015-02-04 13:00:55 -05001038 } else if (a2dpNotConnected) {
Jack He6258aae2017-06-29 17:01:23 -07001039 if (batteryLevelPercentageString != null) {
1040 return mContext.getString(R.string.bluetooth_connected_no_a2dp_battery_level,
Pavlin Radoslavove6e080f2018-02-06 12:21:34 -08001041 batteryLevelPercentageString, activeDeviceString);
Jack He6258aae2017-06-29 17:01:23 -07001042 } else {
Pavlin Radoslavove6e080f2018-02-06 12:21:34 -08001043 return mContext.getString(R.string.bluetooth_connected_no_a2dp,
1044 activeDeviceString);
Jack He6258aae2017-06-29 17:01:23 -07001045 }
1046
Sanket Agarwalf8a1c912016-01-26 20:12:52 -08001047 } else if (hfpNotConnected) {
Jack He6258aae2017-06-29 17:01:23 -07001048 if (batteryLevelPercentageString != null) {
1049 return mContext.getString(R.string.bluetooth_connected_no_headset_battery_level,
Pavlin Radoslavove6e080f2018-02-06 12:21:34 -08001050 batteryLevelPercentageString, activeDeviceString);
Jack He6258aae2017-06-29 17:01:23 -07001051 } else {
Pavlin Radoslavove6e080f2018-02-06 12:21:34 -08001052 return mContext.getString(R.string.bluetooth_connected_no_headset,
1053 activeDeviceString);
Jack He6258aae2017-06-29 17:01:23 -07001054 }
Jason Monkbe3c5db2015-02-04 13:00:55 -05001055 } else {
Jack He6258aae2017-06-29 17:01:23 -07001056 if (batteryLevelPercentageString != null) {
1057 return mContext.getString(R.string.bluetooth_connected_battery_level,
Pavlin Radoslavove6e080f2018-02-06 12:21:34 -08001058 batteryLevelPercentageString, activeDeviceString);
Jack He6258aae2017-06-29 17:01:23 -07001059 } else {
Pavlin Radoslavove6e080f2018-02-06 12:21:34 -08001060 return mContext.getString(R.string.bluetooth_connected, activeDeviceString);
Jack He6258aae2017-06-29 17:01:23 -07001061 }
Jason Monkbe3c5db2015-02-04 13:00:55 -05001062 }
1063 }
1064
Jack He6258aae2017-06-29 17:01:23 -07001065 return getBondState() == BluetoothDevice.BOND_BONDING ?
1066 mContext.getString(R.string.bluetooth_pairing) : null;
Jason Monkbe3c5db2015-02-04 13:00:55 -05001067 }
Jason Monk7ce96b92015-02-02 11:27:58 -05001068}