blob: 62856e4e9082669bf9275a23c39d22544f35ed91 [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;
Isha Bobrac3d94132018-02-08 16:04:36 -080021import android.bluetooth.BluetoothHearingAid;
Jason Monk7ce96b92015-02-02 11:27:58 -050022import android.bluetooth.BluetoothProfile;
23import android.bluetooth.BluetoothUuid;
24import android.content.Context;
25import android.content.SharedPreferences;
26import android.os.ParcelUuid;
27import android.os.SystemClock;
28import android.text.TextUtils;
29import android.util.Log;
30import android.bluetooth.BluetoothAdapter;
Pavlin Radoslavovc285d552018-02-06 16:14:00 -080031import android.support.annotation.VisibleForTesting;
Jason Monk7ce96b92015-02-02 11:27:58 -050032
Jason Monkbe3c5db2015-02-04 13:00:55 -050033import com.android.settingslib.R;
34
Jason Monk7ce96b92015-02-02 11:27:58 -050035import java.util.ArrayList;
36import java.util.Collection;
37import java.util.Collections;
38import java.util.HashMap;
39import java.util.List;
40
41/**
42 * CachedBluetoothDevice represents a remote Bluetooth device. It contains
43 * attributes of the device (such as the address, name, RSSI, etc.) and
44 * functionality that can be performed on the device (connect, pair, disconnect,
45 * etc.).
46 */
Fan Zhang82dd3b02016-12-27 13:13:00 -080047public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> {
Jason Monk7ce96b92015-02-02 11:27:58 -050048 private static final String TAG = "CachedBluetoothDevice";
49 private static final boolean DEBUG = Utils.V;
50
51 private final Context mContext;
52 private final LocalBluetoothAdapter mLocalAdapter;
53 private final LocalBluetoothProfileManager mProfileManager;
54 private final BluetoothDevice mDevice;
Jack Hec219bc92017-07-24 14:55:59 -070055 //TODO: consider remove, BluetoothDevice.getName() is already cached
Jason Monk7ce96b92015-02-02 11:27:58 -050056 private String mName;
Isha Bobrac3d94132018-02-08 16:04:36 -080057 private long mHiSyncId;
Jack Hec219bc92017-07-24 14:55:59 -070058 // Need this since there is no method for getting RSSI
Jason Monk7ce96b92015-02-02 11:27:58 -050059 private short mRssi;
Jack Hec219bc92017-07-24 14:55:59 -070060 //TODO: consider remove, BluetoothDevice.getBluetoothClass() is already cached
Jason Monk7ce96b92015-02-02 11:27:58 -050061 private BluetoothClass mBtClass;
62 private HashMap<LocalBluetoothProfile, Integer> mProfileConnectionState;
63
64 private final List<LocalBluetoothProfile> mProfiles =
65 new ArrayList<LocalBluetoothProfile>();
66
67 // List of profiles that were previously in mProfiles, but have been removed
68 private final List<LocalBluetoothProfile> mRemovedProfiles =
69 new ArrayList<LocalBluetoothProfile>();
70
71 // Device supports PANU but not NAP: remove PanProfile after device disconnects from NAP
72 private boolean mLocalNapRoleConnected;
73
Jack He51520472017-07-24 12:30:08 -070074 private boolean mJustDiscovered;
Jason Monk7ce96b92015-02-02 11:27:58 -050075
Jason Monk7ce96b92015-02-02 11:27:58 -050076 private int mMessageRejectionCount;
77
78 private final Collection<Callback> mCallbacks = new ArrayList<Callback>();
79
80 // Following constants indicate the user's choices of Phone book/message access settings
81 // User hasn't made any choice or settings app has wiped out the memory
82 public final static int ACCESS_UNKNOWN = 0;
83 // User has accepted the connection and let Settings app remember the decision
84 public final static int ACCESS_ALLOWED = 1;
85 // User has rejected the connection and let Settings app remember the decision
86 public final static int ACCESS_REJECTED = 2;
87
88 // How many times user should reject the connection to make the choice persist.
89 private final static int MESSAGE_REJECTION_COUNT_LIMIT_TO_PERSIST = 2;
90
91 private final static String MESSAGE_REJECTION_COUNT_PREFS_NAME = "bluetooth_message_reject";
92
93 /**
94 * When we connect to multiple profiles, we only want to display a single
95 * error even if they all fail. This tracks that state.
96 */
97 private boolean mIsConnectingErrorPossible;
98
Isha Bobrac3d94132018-02-08 16:04:36 -080099 public long getHiSyncId() {
100 return mHiSyncId;
101 }
102
103 public void setHiSyncId(long id) {
104 if (Utils.D) {
105 Log.d(TAG, "setHiSyncId: mDevice " + mDevice + ", id " + id);
106 }
107 mHiSyncId = id;
108 }
109
Jason Monk7ce96b92015-02-02 11:27:58 -0500110 /**
111 * Last time a bt profile auto-connect was attempted.
112 * If an ACTION_UUID intent comes in within
113 * MAX_UUID_DELAY_FOR_AUTO_CONNECT milliseconds, we will try auto-connect
114 * again with the new UUIDs
115 */
116 private long mConnectAttempted;
117
118 // See mConnectAttempted
119 private static final long MAX_UUID_DELAY_FOR_AUTO_CONNECT = 5000;
Etan Cohen50d47612015-03-31 12:45:23 -0700120 private static final long MAX_HOGP_DELAY_FOR_AUTO_CONNECT = 30000;
Jason Monk7ce96b92015-02-02 11:27:58 -0500121
Pavlin Radoslavov1af33a12018-01-21 02:59:15 -0800122 // Active device state
123 private boolean mIsActiveDeviceA2dp = false;
124 private boolean mIsActiveDeviceHeadset = false;
Hansong Zhangd7b35912018-03-16 09:15:48 -0700125 private boolean mIsActiveDeviceHearingAid = false;
Pavlin Radoslavov1af33a12018-01-21 02:59:15 -0800126
Jason Monk7ce96b92015-02-02 11:27:58 -0500127 /**
128 * Describes the current device and profile for logging.
129 *
130 * @param profile Profile to describe
131 * @return Description of the device and profile
132 */
133 private String describe(LocalBluetoothProfile profile) {
134 StringBuilder sb = new StringBuilder();
135 sb.append("Address:").append(mDevice);
136 if (profile != null) {
137 sb.append(" Profile:").append(profile);
138 }
139
140 return sb.toString();
141 }
142
143 void onProfileStateChanged(LocalBluetoothProfile profile, int newProfileState) {
144 if (Utils.D) {
145 Log.d(TAG, "onProfileStateChanged: profile " + profile +
146 " newProfileState " + newProfileState);
147 }
148 if (mLocalAdapter.getBluetoothState() == BluetoothAdapter.STATE_TURNING_OFF)
149 {
150 if (Utils.D) Log.d(TAG, " BT Turninig Off...Profile conn state change ignored...");
151 return;
152 }
153 mProfileConnectionState.put(profile, newProfileState);
154 if (newProfileState == BluetoothProfile.STATE_CONNECTED) {
155 if (profile instanceof MapProfile) {
156 profile.setPreferred(mDevice, true);
Hemant Guptadbc3d8d2017-05-12 21:14:44 +0530157 }
158 if (!mProfiles.contains(profile)) {
Jason Monk7ce96b92015-02-02 11:27:58 -0500159 mRemovedProfiles.remove(profile);
160 mProfiles.add(profile);
161 if (profile instanceof PanProfile &&
162 ((PanProfile) profile).isLocalRoleNap(mDevice)) {
163 // Device doesn't support NAP, so remove PanProfile on disconnect
164 mLocalNapRoleConnected = true;
165 }
166 }
167 } else if (profile instanceof MapProfile &&
168 newProfileState == BluetoothProfile.STATE_DISCONNECTED) {
169 profile.setPreferred(mDevice, false);
170 } else if (mLocalNapRoleConnected && profile instanceof PanProfile &&
171 ((PanProfile) profile).isLocalRoleNap(mDevice) &&
172 newProfileState == BluetoothProfile.STATE_DISCONNECTED) {
173 Log.d(TAG, "Removing PanProfile from device after NAP disconnect");
174 mProfiles.remove(profile);
175 mRemovedProfiles.add(profile);
176 mLocalNapRoleConnected = false;
177 }
Pavlin Radoslavov1af33a12018-01-21 02:59:15 -0800178 fetchActiveDevices();
Jason Monk7ce96b92015-02-02 11:27:58 -0500179 }
180
181 CachedBluetoothDevice(Context context,
182 LocalBluetoothAdapter adapter,
183 LocalBluetoothProfileManager profileManager,
184 BluetoothDevice device) {
185 mContext = context;
186 mLocalAdapter = adapter;
187 mProfileManager = profileManager;
188 mDevice = device;
189 mProfileConnectionState = new HashMap<LocalBluetoothProfile, Integer>();
190 fillData();
Isha Bobrac3d94132018-02-08 16:04:36 -0800191 mHiSyncId = BluetoothHearingAid.HI_SYNC_ID_INVALID;
Jason Monk7ce96b92015-02-02 11:27:58 -0500192 }
193
194 public void disconnect() {
195 for (LocalBluetoothProfile profile : mProfiles) {
196 disconnect(profile);
197 }
198 // Disconnect PBAP server in case its connected
199 // This is to ensure all the profiles are disconnected as some CK/Hs do not
200 // disconnect PBAP connection when HF connection is brought down
201 PbapServerProfile PbapProfile = mProfileManager.getPbapProfile();
202 if (PbapProfile.getConnectionStatus(mDevice) == BluetoothProfile.STATE_CONNECTED)
203 {
204 PbapProfile.disconnect(mDevice);
205 }
206 }
207
208 public void disconnect(LocalBluetoothProfile profile) {
209 if (profile.disconnect(mDevice)) {
210 if (Utils.D) {
211 Log.d(TAG, "Command sent successfully:DISCONNECT " + describe(profile));
212 }
213 }
214 }
215
216 public void connect(boolean connectAllProfiles) {
217 if (!ensurePaired()) {
218 return;
219 }
220
221 mConnectAttempted = SystemClock.elapsedRealtime();
222 connectWithoutResettingTimer(connectAllProfiles);
223 }
224
225 void onBondingDockConnect() {
226 // Attempt to connect if UUIDs are available. Otherwise,
227 // we will connect when the ACTION_UUID intent arrives.
228 connect(false);
229 }
230
231 private void connectWithoutResettingTimer(boolean connectAllProfiles) {
232 // Try to initialize the profiles if they were not.
233 if (mProfiles.isEmpty()) {
234 // if mProfiles is empty, then do not invoke updateProfiles. This causes a race
235 // condition with carkits during pairing, wherein RemoteDevice.UUIDs have been updated
236 // from bluetooth stack but ACTION.uuid is not sent yet.
237 // Eventually ACTION.uuid will be received which shall trigger the connection of the
238 // various profiles
239 // If UUIDs are not available yet, connect will be happen
240 // upon arrival of the ACTION_UUID intent.
241 Log.d(TAG, "No profiles. Maybe we will connect later");
242 return;
243 }
244
245 // Reset the only-show-one-error-dialog tracking variable
246 mIsConnectingErrorPossible = true;
247
248 int preferredProfiles = 0;
249 for (LocalBluetoothProfile profile : mProfiles) {
250 if (connectAllProfiles ? profile.isConnectable() : profile.isAutoConnectable()) {
251 if (profile.isPreferred(mDevice)) {
252 ++preferredProfiles;
253 connectInt(profile);
254 }
255 }
256 }
257 if (DEBUG) Log.d(TAG, "Preferred profiles = " + preferredProfiles);
258
259 if (preferredProfiles == 0) {
260 connectAutoConnectableProfiles();
261 }
262 }
263
264 private void connectAutoConnectableProfiles() {
265 if (!ensurePaired()) {
266 return;
267 }
268 // Reset the only-show-one-error-dialog tracking variable
269 mIsConnectingErrorPossible = true;
270
271 for (LocalBluetoothProfile profile : mProfiles) {
272 if (profile.isAutoConnectable()) {
273 profile.setPreferred(mDevice, true);
274 connectInt(profile);
275 }
276 }
277 }
278
279 /**
280 * Connect this device to the specified profile.
281 *
282 * @param profile the profile to use with the remote device
283 */
284 public void connectProfile(LocalBluetoothProfile profile) {
285 mConnectAttempted = SystemClock.elapsedRealtime();
286 // Reset the only-show-one-error-dialog tracking variable
287 mIsConnectingErrorPossible = true;
288 connectInt(profile);
289 // Refresh the UI based on profile.connect() call
290 refresh();
291 }
292
293 synchronized void connectInt(LocalBluetoothProfile profile) {
294 if (!ensurePaired()) {
295 return;
296 }
297 if (profile.connect(mDevice)) {
298 if (Utils.D) {
299 Log.d(TAG, "Command sent successfully:CONNECT " + describe(profile));
300 }
301 return;
302 }
303 Log.i(TAG, "Failed to connect " + profile.toString() + " to " + mName);
304 }
305
306 private boolean ensurePaired() {
307 if (getBondState() == BluetoothDevice.BOND_NONE) {
308 startPairing();
309 return false;
310 } else {
311 return true;
312 }
313 }
314
315 public boolean startPairing() {
316 // Pairing is unreliable while scanning, so cancel discovery
317 if (mLocalAdapter.isDiscovering()) {
318 mLocalAdapter.cancelDiscovery();
319 }
320
321 if (!mDevice.createBond()) {
322 return false;
323 }
324
Jason Monk7ce96b92015-02-02 11:27:58 -0500325 return true;
326 }
327
328 /**
329 * Return true if user initiated pairing on this device. The message text is
330 * slightly different for local vs. remote initiated pairing dialogs.
331 */
332 boolean isUserInitiatedPairing() {
Jakub Pawlowski0278ab92016-07-20 11:55:48 -0700333 return mDevice.isBondingInitiatedLocally();
Jason Monk7ce96b92015-02-02 11:27:58 -0500334 }
335
336 public void unpair() {
337 int state = getBondState();
338
339 if (state == BluetoothDevice.BOND_BONDING) {
340 mDevice.cancelBondProcess();
341 }
342
343 if (state != BluetoothDevice.BOND_NONE) {
344 final BluetoothDevice dev = mDevice;
345 if (dev != null) {
346 final boolean successful = dev.removeBond();
347 if (successful) {
348 if (Utils.D) {
349 Log.d(TAG, "Command sent successfully:REMOVE_BOND " + describe(null));
350 }
351 } else if (Utils.V) {
352 Log.v(TAG, "Framework rejected command immediately:REMOVE_BOND " +
Isha Bobrac3d94132018-02-08 16:04:36 -0800353 describe(null));
Jason Monk7ce96b92015-02-02 11:27:58 -0500354 }
355 }
356 }
357 }
358
359 public int getProfileConnectionState(LocalBluetoothProfile profile) {
xutianguo22bbb8192016-06-22 11:32:00 +0800360 if (mProfileConnectionState.get(profile) == null) {
Jason Monk7ce96b92015-02-02 11:27:58 -0500361 // If cache is empty make the binder call to get the state
362 int state = profile.getConnectionStatus(mDevice);
363 mProfileConnectionState.put(profile, state);
364 }
365 return mProfileConnectionState.get(profile);
366 }
367
368 public void clearProfileConnectionState ()
369 {
370 if (Utils.D) {
371 Log.d(TAG," Clearing all connection state for dev:" + mDevice.getName());
372 }
373 for (LocalBluetoothProfile profile :getProfiles()) {
374 mProfileConnectionState.put(profile, BluetoothProfile.STATE_DISCONNECTED);
375 }
376 }
377
378 // TODO: do any of these need to run async on a background thread?
379 private void fillData() {
380 fetchName();
381 fetchBtClass();
382 updateProfiles();
Pavlin Radoslavov1af33a12018-01-21 02:59:15 -0800383 fetchActiveDevices();
Jason Monk7ce96b92015-02-02 11:27:58 -0500384 migratePhonebookPermissionChoice();
385 migrateMessagePermissionChoice();
386 fetchMessageRejectionCount();
387
Jason Monk7ce96b92015-02-02 11:27:58 -0500388 dispatchAttributesChanged();
389 }
390
391 public BluetoothDevice getDevice() {
392 return mDevice;
393 }
394
Antony Sargent7ad051e2017-06-29 15:23:13 -0700395 /**
396 * Convenience method that can be mocked - it lets tests avoid having to call getDevice() which
397 * causes problems in tests since BluetoothDevice is final and cannot be mocked.
398 * @return the address of this device
399 */
400 public String getAddress() {
401 return mDevice.getAddress();
402 }
403
Jason Monk7ce96b92015-02-02 11:27:58 -0500404 public String getName() {
405 return mName;
406 }
407
408 /**
409 * Populate name from BluetoothDevice.ACTION_FOUND intent
410 */
411 void setNewName(String name) {
412 if (mName == null) {
413 mName = name;
414 if (mName == null || TextUtils.isEmpty(mName)) {
415 mName = mDevice.getAddress();
416 }
417 dispatchAttributesChanged();
418 }
419 }
420
421 /**
Jack Hec219bc92017-07-24 14:55:59 -0700422 * User changes the device name
423 * @param name new alias name to be set, should never be null
Jason Monk7ce96b92015-02-02 11:27:58 -0500424 */
425 public void setName(String name) {
Jack Hec219bc92017-07-24 14:55:59 -0700426 // Prevent mName to be set to null if setName(null) is called
427 if (name != null && !TextUtils.equals(name, mName)) {
Jason Monk7ce96b92015-02-02 11:27:58 -0500428 mName = name;
429 mDevice.setAlias(name);
430 dispatchAttributesChanged();
431 }
432 }
433
Hansong Zhang6a416322018-03-19 18:20:38 -0700434 /**
435 * Set this device as active device
436 * @return true if at least one profile on this device is set to active, false otherwise
437 */
438 public boolean setActive() {
439 boolean result = false;
440 A2dpProfile a2dpProfile = mProfileManager.getA2dpProfile();
441 if (a2dpProfile != null && isConnectedProfile(a2dpProfile)) {
442 if (a2dpProfile.setActiveDevice(getDevice())) {
443 Log.i(TAG, "OnPreferenceClickListener: A2DP active device=" + this);
444 result = true;
445 }
446 }
447 HeadsetProfile headsetProfile = mProfileManager.getHeadsetProfile();
448 if ((headsetProfile != null) && isConnectedProfile(headsetProfile)) {
449 if (headsetProfile.setActiveDevice(getDevice())) {
450 Log.i(TAG, "OnPreferenceClickListener: Headset active device=" + this);
451 result = true;
452 }
453 }
Hansong Zhangd7b35912018-03-16 09:15:48 -0700454 HearingAidProfile hearingAidProfile = mProfileManager.getHearingAidProfile();
455 if ((hearingAidProfile != null) && isConnectedProfile(hearingAidProfile)) {
456 if (hearingAidProfile.setActiveDevice(getDevice())) {
457 Log.i(TAG, "OnPreferenceClickListener: Hearing Aid active device=" + this);
458 result = true;
459 }
460 }
Hansong Zhang6a416322018-03-19 18:20:38 -0700461 return result;
462 }
463
Jason Monk7ce96b92015-02-02 11:27:58 -0500464 void refreshName() {
465 fetchName();
466 dispatchAttributesChanged();
467 }
468
469 private void fetchName() {
470 mName = mDevice.getAliasName();
471
472 if (TextUtils.isEmpty(mName)) {
473 mName = mDevice.getAddress();
474 if (DEBUG) Log.d(TAG, "Device has no name (yet), use address: " + mName);
475 }
476 }
477
Jack He6258aae2017-06-29 17:01:23 -0700478 /**
Jack Hec219bc92017-07-24 14:55:59 -0700479 * Checks if device has a human readable name besides MAC address
480 * @return true if device's alias name is not null nor empty, false otherwise
481 */
482 public boolean hasHumanReadableName() {
483 return !TextUtils.isEmpty(mDevice.getAliasName());
484 }
485
486 /**
Jack He6258aae2017-06-29 17:01:23 -0700487 * Get battery level from remote device
488 * @return battery level in percentage [0-100], or {@link BluetoothDevice#BATTERY_LEVEL_UNKNOWN}
489 */
490 public int getBatteryLevel() {
491 return mDevice.getBatteryLevel();
492 }
493
Jason Monk7ce96b92015-02-02 11:27:58 -0500494 void refresh() {
495 dispatchAttributesChanged();
496 }
497
Jack He51520472017-07-24 12:30:08 -0700498 public void setJustDiscovered(boolean justDiscovered) {
499 if (mJustDiscovered != justDiscovered) {
500 mJustDiscovered = justDiscovered;
Jason Monk7ce96b92015-02-02 11:27:58 -0500501 dispatchAttributesChanged();
502 }
503 }
504
505 public int getBondState() {
506 return mDevice.getBondState();
507 }
508
Pavlin Radoslavov1af33a12018-01-21 02:59:15 -0800509 /**
Pavlin Radoslavovc285d552018-02-06 16:14:00 -0800510 * Update the device status as active or non-active per Bluetooth profile.
Pavlin Radoslavov1af33a12018-01-21 02:59:15 -0800511 *
512 * @param isActive true if the device is active
513 * @param bluetoothProfile the Bluetooth profile
514 */
Pavlin Radoslavovc285d552018-02-06 16:14:00 -0800515 public void onActiveDeviceChanged(boolean isActive, int bluetoothProfile) {
Pavlin Radoslavov1af33a12018-01-21 02:59:15 -0800516 boolean changed = false;
517 switch (bluetoothProfile) {
518 case BluetoothProfile.A2DP:
519 changed = (mIsActiveDeviceA2dp != isActive);
520 mIsActiveDeviceA2dp = isActive;
521 break;
522 case BluetoothProfile.HEADSET:
523 changed = (mIsActiveDeviceHeadset != isActive);
524 mIsActiveDeviceHeadset = isActive;
525 break;
Hansong Zhangd7b35912018-03-16 09:15:48 -0700526 case BluetoothProfile.HEARING_AID:
527 changed = (mIsActiveDeviceHearingAid != isActive);
528 mIsActiveDeviceHearingAid = isActive;
529 break;
Pavlin Radoslavov1af33a12018-01-21 02:59:15 -0800530 default:
Pavlin Radoslavovc285d552018-02-06 16:14:00 -0800531 Log.w(TAG, "onActiveDeviceChanged: unknown profile " + bluetoothProfile +
Pavlin Radoslavov1af33a12018-01-21 02:59:15 -0800532 " isActive " + isActive);
533 break;
534 }
535 if (changed) {
536 dispatchAttributesChanged();
537 }
538 }
539
Pavlin Radoslavovc285d552018-02-06 16:14:00 -0800540 /**
541 * Get the device status as active or non-active per Bluetooth profile.
542 *
543 * @param bluetoothProfile the Bluetooth profile
544 * @return true if the device is active
545 */
546 @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
547 public boolean isActiveDevice(int bluetoothProfile) {
548 switch (bluetoothProfile) {
549 case BluetoothProfile.A2DP:
550 return mIsActiveDeviceA2dp;
551 case BluetoothProfile.HEADSET:
552 return mIsActiveDeviceHeadset;
Hansong Zhangd7b35912018-03-16 09:15:48 -0700553 case BluetoothProfile.HEARING_AID:
554 return mIsActiveDeviceHearingAid;
Pavlin Radoslavovc285d552018-02-06 16:14:00 -0800555 default:
556 Log.w(TAG, "getActiveDevice: unknown profile " + bluetoothProfile);
557 break;
558 }
559 return false;
560 }
561
Jason Monk7ce96b92015-02-02 11:27:58 -0500562 void setRssi(short rssi) {
563 if (mRssi != rssi) {
564 mRssi = rssi;
565 dispatchAttributesChanged();
566 }
567 }
568
569 /**
570 * Checks whether we are connected to this device (any profile counts).
571 *
572 * @return Whether it is connected.
573 */
574 public boolean isConnected() {
575 for (LocalBluetoothProfile profile : mProfiles) {
576 int status = getProfileConnectionState(profile);
577 if (status == BluetoothProfile.STATE_CONNECTED) {
578 return true;
579 }
580 }
581
582 return false;
583 }
584
585 public boolean isConnectedProfile(LocalBluetoothProfile profile) {
586 int status = getProfileConnectionState(profile);
587 return status == BluetoothProfile.STATE_CONNECTED;
588
589 }
590
591 public boolean isBusy() {
592 for (LocalBluetoothProfile profile : mProfiles) {
593 int status = getProfileConnectionState(profile);
594 if (status == BluetoothProfile.STATE_CONNECTING
595 || status == BluetoothProfile.STATE_DISCONNECTING) {
596 return true;
597 }
598 }
599 return getBondState() == BluetoothDevice.BOND_BONDING;
600 }
601
602 /**
603 * Fetches a new value for the cached BT class.
604 */
605 private void fetchBtClass() {
606 mBtClass = mDevice.getBluetoothClass();
607 }
608
609 private boolean updateProfiles() {
610 ParcelUuid[] uuids = mDevice.getUuids();
611 if (uuids == null) return false;
612
613 ParcelUuid[] localUuids = mLocalAdapter.getUuids();
614 if (localUuids == null) return false;
615
Jack Hec219bc92017-07-24 14:55:59 -0700616 /*
Jason Monk7ce96b92015-02-02 11:27:58 -0500617 * Now we know if the device supports PBAP, update permissions...
618 */
619 processPhonebookAccess();
620
621 mProfileManager.updateProfiles(uuids, localUuids, mProfiles, mRemovedProfiles,
622 mLocalNapRoleConnected, mDevice);
623
624 if (DEBUG) {
625 Log.e(TAG, "updating profiles for " + mDevice.getAliasName());
626 BluetoothClass bluetoothClass = mDevice.getBluetoothClass();
627
628 if (bluetoothClass != null) Log.v(TAG, "Class: " + bluetoothClass.toString());
629 Log.v(TAG, "UUID:");
630 for (ParcelUuid uuid : uuids) {
631 Log.v(TAG, " " + uuid);
632 }
633 }
634 return true;
635 }
636
Pavlin Radoslavov1af33a12018-01-21 02:59:15 -0800637 private void fetchActiveDevices() {
638 A2dpProfile a2dpProfile = mProfileManager.getA2dpProfile();
639 if (a2dpProfile != null) {
640 mIsActiveDeviceA2dp = mDevice.equals(a2dpProfile.getActiveDevice());
641 }
642 HeadsetProfile headsetProfile = mProfileManager.getHeadsetProfile();
643 if (headsetProfile != null) {
644 mIsActiveDeviceHeadset = mDevice.equals(headsetProfile.getActiveDevice());
645 }
Hansong Zhangd7b35912018-03-16 09:15:48 -0700646 HearingAidProfile hearingAidProfile = mProfileManager.getHearingAidProfile();
647 if (hearingAidProfile != null) {
Hansong Zhang3b8f09b2018-03-28 16:53:10 -0700648 mIsActiveDeviceHearingAid = hearingAidProfile.getActiveDevices().contains(mDevice);
Hansong Zhangd7b35912018-03-16 09:15:48 -0700649 }
Pavlin Radoslavov1af33a12018-01-21 02:59:15 -0800650 }
651
Jason Monk7ce96b92015-02-02 11:27:58 -0500652 /**
653 * Refreshes the UI for the BT class, including fetching the latest value
654 * for the class.
655 */
656 void refreshBtClass() {
657 fetchBtClass();
658 dispatchAttributesChanged();
659 }
660
661 /**
662 * Refreshes the UI when framework alerts us of a UUID change.
663 */
664 void onUuidChanged() {
665 updateProfiles();
Etan Cohen50d47612015-03-31 12:45:23 -0700666 ParcelUuid[] uuids = mDevice.getUuids();
Venkat Raghavan05e08c32015-04-06 16:26:11 -0700667
Etan Cohen50d47612015-03-31 12:45:23 -0700668 long timeout = MAX_UUID_DELAY_FOR_AUTO_CONNECT;
Venkat Raghavan05e08c32015-04-06 16:26:11 -0700669 if (BluetoothUuid.isUuidPresent(uuids, BluetoothUuid.Hogp)) {
670 timeout = MAX_HOGP_DELAY_FOR_AUTO_CONNECT;
671 }
Jason Monk7ce96b92015-02-02 11:27:58 -0500672
673 if (DEBUG) {
Etan Cohen50d47612015-03-31 12:45:23 -0700674 Log.d(TAG, "onUuidChanged: Time since last connect"
Jason Monk7ce96b92015-02-02 11:27:58 -0500675 + (SystemClock.elapsedRealtime() - mConnectAttempted));
676 }
677
678 /*
Venkat Raghavan05e08c32015-04-06 16:26:11 -0700679 * If a connect was attempted earlier without any UUID, we will do the connect now.
680 * Otherwise, allow the connect on UUID change.
Jason Monk7ce96b92015-02-02 11:27:58 -0500681 */
682 if (!mProfiles.isEmpty()
Andre Eisenbach7be83c52015-09-03 15:20:09 -0700683 && ((mConnectAttempted + timeout) > SystemClock.elapsedRealtime())) {
Jason Monk7ce96b92015-02-02 11:27:58 -0500684 connectWithoutResettingTimer(false);
685 }
Andre Eisenbach7be83c52015-09-03 15:20:09 -0700686
Jason Monk7ce96b92015-02-02 11:27:58 -0500687 dispatchAttributesChanged();
688 }
689
690 void onBondingStateChanged(int bondState) {
691 if (bondState == BluetoothDevice.BOND_NONE) {
692 mProfiles.clear();
Jason Monk7ce96b92015-02-02 11:27:58 -0500693 setPhonebookPermissionChoice(ACCESS_UNKNOWN);
694 setMessagePermissionChoice(ACCESS_UNKNOWN);
Casper Bonde424681e2015-05-04 22:07:45 -0700695 setSimPermissionChoice(ACCESS_UNKNOWN);
Jason Monk7ce96b92015-02-02 11:27:58 -0500696 mMessageRejectionCount = 0;
697 saveMessageRejectionCount();
698 }
699
700 refresh();
701
702 if (bondState == BluetoothDevice.BOND_BONDED) {
703 if (mDevice.isBluetoothDock()) {
704 onBondingDockConnect();
Jakub Pawlowski0278ab92016-07-20 11:55:48 -0700705 } else if (mDevice.isBondingInitiatedLocally()) {
Jason Monk7ce96b92015-02-02 11:27:58 -0500706 connect(false);
707 }
Jason Monk7ce96b92015-02-02 11:27:58 -0500708 }
709 }
710
711 void setBtClass(BluetoothClass btClass) {
712 if (btClass != null && mBtClass != btClass) {
713 mBtClass = btClass;
714 dispatchAttributesChanged();
715 }
716 }
717
718 public BluetoothClass getBtClass() {
719 return mBtClass;
720 }
721
722 public List<LocalBluetoothProfile> getProfiles() {
723 return Collections.unmodifiableList(mProfiles);
724 }
725
726 public List<LocalBluetoothProfile> getConnectableProfiles() {
727 List<LocalBluetoothProfile> connectableProfiles =
728 new ArrayList<LocalBluetoothProfile>();
729 for (LocalBluetoothProfile profile : mProfiles) {
730 if (profile.isConnectable()) {
731 connectableProfiles.add(profile);
732 }
733 }
734 return connectableProfiles;
735 }
736
737 public List<LocalBluetoothProfile> getRemovedProfiles() {
738 return mRemovedProfiles;
739 }
740
741 public void registerCallback(Callback callback) {
742 synchronized (mCallbacks) {
743 mCallbacks.add(callback);
744 }
745 }
746
747 public void unregisterCallback(Callback callback) {
748 synchronized (mCallbacks) {
749 mCallbacks.remove(callback);
750 }
751 }
752
753 private void dispatchAttributesChanged() {
754 synchronized (mCallbacks) {
755 for (Callback callback : mCallbacks) {
756 callback.onDeviceAttributesChanged();
757 }
758 }
759 }
760
761 @Override
762 public String toString() {
763 return mDevice.toString();
764 }
765
766 @Override
767 public boolean equals(Object o) {
768 if ((o == null) || !(o instanceof CachedBluetoothDevice)) {
769 return false;
770 }
771 return mDevice.equals(((CachedBluetoothDevice) o).mDevice);
772 }
773
774 @Override
775 public int hashCode() {
776 return mDevice.getAddress().hashCode();
777 }
778
779 // This comparison uses non-final fields so the sort order may change
780 // when device attributes change (such as bonding state). Settings
781 // will completely refresh the device list when this happens.
782 public int compareTo(CachedBluetoothDevice another) {
783 // Connected above not connected
784 int comparison = (another.isConnected() ? 1 : 0) - (isConnected() ? 1 : 0);
785 if (comparison != 0) return comparison;
786
787 // Paired above not paired
788 comparison = (another.getBondState() == BluetoothDevice.BOND_BONDED ? 1 : 0) -
789 (getBondState() == BluetoothDevice.BOND_BONDED ? 1 : 0);
790 if (comparison != 0) return comparison;
791
Jack He51520472017-07-24 12:30:08 -0700792 // Just discovered above discovered in the past
793 comparison = (another.mJustDiscovered ? 1 : 0) - (mJustDiscovered ? 1 : 0);
Jason Monk7ce96b92015-02-02 11:27:58 -0500794 if (comparison != 0) return comparison;
795
796 // Stronger signal above weaker signal
797 comparison = another.mRssi - mRssi;
798 if (comparison != 0) return comparison;
799
800 // Fallback on name
801 return mName.compareTo(another.mName);
802 }
803
804 public interface Callback {
805 void onDeviceAttributesChanged();
806 }
807
808 public int getPhonebookPermissionChoice() {
809 int permission = mDevice.getPhonebookAccessPermission();
810 if (permission == BluetoothDevice.ACCESS_ALLOWED) {
811 return ACCESS_ALLOWED;
812 } else if (permission == BluetoothDevice.ACCESS_REJECTED) {
813 return ACCESS_REJECTED;
814 }
815 return ACCESS_UNKNOWN;
816 }
817
818 public void setPhonebookPermissionChoice(int permissionChoice) {
819 int permission = BluetoothDevice.ACCESS_UNKNOWN;
820 if (permissionChoice == ACCESS_ALLOWED) {
821 permission = BluetoothDevice.ACCESS_ALLOWED;
822 } else if (permissionChoice == ACCESS_REJECTED) {
823 permission = BluetoothDevice.ACCESS_REJECTED;
824 }
825 mDevice.setPhonebookAccessPermission(permission);
826 }
827
828 // Migrates data from old data store (in Settings app's shared preferences) to new (in Bluetooth
829 // app's shared preferences).
830 private void migratePhonebookPermissionChoice() {
831 SharedPreferences preferences = mContext.getSharedPreferences(
832 "bluetooth_phonebook_permission", Context.MODE_PRIVATE);
833 if (!preferences.contains(mDevice.getAddress())) {
834 return;
835 }
836
837 if (mDevice.getPhonebookAccessPermission() == BluetoothDevice.ACCESS_UNKNOWN) {
838 int oldPermission = preferences.getInt(mDevice.getAddress(), ACCESS_UNKNOWN);
839 if (oldPermission == ACCESS_ALLOWED) {
840 mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_ALLOWED);
841 } else if (oldPermission == ACCESS_REJECTED) {
842 mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_REJECTED);
843 }
844 }
845
846 SharedPreferences.Editor editor = preferences.edit();
847 editor.remove(mDevice.getAddress());
848 editor.commit();
849 }
850
851 public int getMessagePermissionChoice() {
852 int permission = mDevice.getMessageAccessPermission();
853 if (permission == BluetoothDevice.ACCESS_ALLOWED) {
854 return ACCESS_ALLOWED;
855 } else if (permission == BluetoothDevice.ACCESS_REJECTED) {
856 return ACCESS_REJECTED;
857 }
858 return ACCESS_UNKNOWN;
859 }
860
861 public void setMessagePermissionChoice(int permissionChoice) {
862 int permission = BluetoothDevice.ACCESS_UNKNOWN;
863 if (permissionChoice == ACCESS_ALLOWED) {
864 permission = BluetoothDevice.ACCESS_ALLOWED;
865 } else if (permissionChoice == ACCESS_REJECTED) {
866 permission = BluetoothDevice.ACCESS_REJECTED;
867 }
868 mDevice.setMessageAccessPermission(permission);
869 }
870
Casper Bonde424681e2015-05-04 22:07:45 -0700871 public int getSimPermissionChoice() {
872 int permission = mDevice.getSimAccessPermission();
873 if (permission == BluetoothDevice.ACCESS_ALLOWED) {
874 return ACCESS_ALLOWED;
875 } else if (permission == BluetoothDevice.ACCESS_REJECTED) {
876 return ACCESS_REJECTED;
877 }
878 return ACCESS_UNKNOWN;
879 }
880
881 void setSimPermissionChoice(int permissionChoice) {
882 int permission = BluetoothDevice.ACCESS_UNKNOWN;
883 if (permissionChoice == ACCESS_ALLOWED) {
884 permission = BluetoothDevice.ACCESS_ALLOWED;
885 } else if (permissionChoice == ACCESS_REJECTED) {
886 permission = BluetoothDevice.ACCESS_REJECTED;
887 }
888 mDevice.setSimAccessPermission(permission);
889 }
890
Jason Monk7ce96b92015-02-02 11:27:58 -0500891 // Migrates data from old data store (in Settings app's shared preferences) to new (in Bluetooth
892 // app's shared preferences).
893 private void migrateMessagePermissionChoice() {
894 SharedPreferences preferences = mContext.getSharedPreferences(
895 "bluetooth_message_permission", Context.MODE_PRIVATE);
896 if (!preferences.contains(mDevice.getAddress())) {
897 return;
898 }
899
900 if (mDevice.getMessageAccessPermission() == BluetoothDevice.ACCESS_UNKNOWN) {
901 int oldPermission = preferences.getInt(mDevice.getAddress(), ACCESS_UNKNOWN);
902 if (oldPermission == ACCESS_ALLOWED) {
903 mDevice.setMessageAccessPermission(BluetoothDevice.ACCESS_ALLOWED);
904 } else if (oldPermission == ACCESS_REJECTED) {
905 mDevice.setMessageAccessPermission(BluetoothDevice.ACCESS_REJECTED);
906 }
907 }
908
909 SharedPreferences.Editor editor = preferences.edit();
910 editor.remove(mDevice.getAddress());
911 editor.commit();
912 }
913
914 /**
915 * @return Whether this rejection should persist.
916 */
917 public boolean checkAndIncreaseMessageRejectionCount() {
918 if (mMessageRejectionCount < MESSAGE_REJECTION_COUNT_LIMIT_TO_PERSIST) {
919 mMessageRejectionCount++;
920 saveMessageRejectionCount();
921 }
922 return mMessageRejectionCount >= MESSAGE_REJECTION_COUNT_LIMIT_TO_PERSIST;
923 }
924
925 private void fetchMessageRejectionCount() {
926 SharedPreferences preference = mContext.getSharedPreferences(
927 MESSAGE_REJECTION_COUNT_PREFS_NAME, Context.MODE_PRIVATE);
928 mMessageRejectionCount = preference.getInt(mDevice.getAddress(), 0);
929 }
930
931 private void saveMessageRejectionCount() {
932 SharedPreferences.Editor editor = mContext.getSharedPreferences(
933 MESSAGE_REJECTION_COUNT_PREFS_NAME, Context.MODE_PRIVATE).edit();
934 if (mMessageRejectionCount == 0) {
935 editor.remove(mDevice.getAddress());
936 } else {
937 editor.putInt(mDevice.getAddress(), mMessageRejectionCount);
938 }
939 editor.commit();
940 }
941
942 private void processPhonebookAccess() {
943 if (mDevice.getBondState() != BluetoothDevice.BOND_BONDED) return;
944
945 ParcelUuid[] uuids = mDevice.getUuids();
946 if (BluetoothUuid.containsAnyUuid(uuids, PbapServerProfile.PBAB_CLIENT_UUIDS)) {
947 // The pairing dialog now warns of phone-book access for paired devices.
948 // No separate prompt is displayed after pairing.
Sanket Padawe78c23762015-06-01 15:55:30 -0700949 if (getPhonebookPermissionChoice() == CachedBluetoothDevice.ACCESS_UNKNOWN) {
Sanket Padawe07533db2015-11-11 15:01:35 -0800950 if (mDevice.getBluetoothClass().getDeviceClass()
Hemant Guptab8d267a2016-06-07 12:53:59 +0530951 == BluetoothClass.Device.AUDIO_VIDEO_HANDSFREE ||
952 mDevice.getBluetoothClass().getDeviceClass()
953 == BluetoothClass.Device.AUDIO_VIDEO_WEARABLE_HEADSET) {
Sanket Padawe07533db2015-11-11 15:01:35 -0800954 setPhonebookPermissionChoice(CachedBluetoothDevice.ACCESS_ALLOWED);
955 } else {
956 setPhonebookPermissionChoice(CachedBluetoothDevice.ACCESS_REJECTED);
957 }
Sanket Padawe78c23762015-06-01 15:55:30 -0700958 }
Jason Monk7ce96b92015-02-02 11:27:58 -0500959 }
960 }
Jason Monkbe3c5db2015-02-04 13:00:55 -0500961
962 public int getMaxConnectionState() {
963 int maxState = BluetoothProfile.STATE_DISCONNECTED;
964 for (LocalBluetoothProfile profile : getProfiles()) {
965 int connectionStatus = getProfileConnectionState(profile);
966 if (connectionStatus > maxState) {
967 maxState = connectionStatus;
968 }
969 }
970 return maxState;
971 }
972
973 /**
974 * @return resource for string that discribes the connection state of this device.
975 */
Jack He6258aae2017-06-29 17:01:23 -0700976 public String getConnectionSummary() {
Jason Monkbe3c5db2015-02-04 13:00:55 -0500977 boolean profileConnected = false; // at least one profile is connected
978 boolean a2dpNotConnected = false; // A2DP is preferred but not connected
Sanket Agarwalf8a1c912016-01-26 20:12:52 -0800979 boolean hfpNotConnected = false; // HFP is preferred but not connected
Hansong Zhangd7b35912018-03-16 09:15:48 -0700980 boolean hearingAidNotConnected = false; // Hearing Aid is preferred but not connected
Jason Monkbe3c5db2015-02-04 13:00:55 -0500981
982 for (LocalBluetoothProfile profile : getProfiles()) {
983 int connectionStatus = getProfileConnectionState(profile);
984
985 switch (connectionStatus) {
986 case BluetoothProfile.STATE_CONNECTING:
987 case BluetoothProfile.STATE_DISCONNECTING:
Jack He6258aae2017-06-29 17:01:23 -0700988 return mContext.getString(Utils.getConnectionStateSummary(connectionStatus));
Jason Monkbe3c5db2015-02-04 13:00:55 -0500989
990 case BluetoothProfile.STATE_CONNECTED:
991 profileConnected = true;
992 break;
993
994 case BluetoothProfile.STATE_DISCONNECTED:
995 if (profile.isProfileReady()) {
Sanket Agarwalf8a1c912016-01-26 20:12:52 -0800996 if ((profile instanceof A2dpProfile) ||
Sanket Agarwal1bec6a52015-10-21 18:23:27 -0700997 (profile instanceof A2dpSinkProfile)){
Jason Monkbe3c5db2015-02-04 13:00:55 -0500998 a2dpNotConnected = true;
Sanket Agarwalf8a1c912016-01-26 20:12:52 -0800999 } else if ((profile instanceof HeadsetProfile) ||
1000 (profile instanceof HfpClientProfile)) {
1001 hfpNotConnected = true;
Hansong Zhangd7b35912018-03-16 09:15:48 -07001002 } else if (profile instanceof HearingAidProfile) {
1003 hearingAidNotConnected = true;
Jason Monkbe3c5db2015-02-04 13:00:55 -05001004 }
1005 }
1006 break;
1007 }
1008 }
1009
Jack He6258aae2017-06-29 17:01:23 -07001010 String batteryLevelPercentageString = null;
1011 // Android framework should only set mBatteryLevel to valid range [0-100] or
1012 // BluetoothDevice.BATTERY_LEVEL_UNKNOWN, any other value should be a framework bug.
1013 // Thus assume here that if value is not BluetoothDevice.BATTERY_LEVEL_UNKNOWN, it must
1014 // be valid
1015 final int batteryLevel = getBatteryLevel();
1016 if (batteryLevel != BluetoothDevice.BATTERY_LEVEL_UNKNOWN) {
1017 // TODO: name com.android.settingslib.bluetooth.Utils something different
1018 batteryLevelPercentageString =
1019 com.android.settingslib.Utils.formatPercentage(batteryLevel);
1020 }
1021
Pavlin Radoslavove6e080f2018-02-06 12:21:34 -08001022 // Prepare the string for the Active Device summary
1023 String[] activeDeviceStringsArray = mContext.getResources().getStringArray(
1024 R.array.bluetooth_audio_active_device_summaries);
1025 String activeDeviceString = activeDeviceStringsArray[0]; // Default value: not active
Pavlin Radoslavov1af33a12018-01-21 02:59:15 -08001026 if (mIsActiveDeviceA2dp && mIsActiveDeviceHeadset) {
Pavlin Radoslavove6e080f2018-02-06 12:21:34 -08001027 activeDeviceString = activeDeviceStringsArray[1]; // Active for Media and Phone
Pavlin Radoslavov1af33a12018-01-21 02:59:15 -08001028 } else {
1029 if (mIsActiveDeviceA2dp) {
Pavlin Radoslavove6e080f2018-02-06 12:21:34 -08001030 activeDeviceString = activeDeviceStringsArray[2]; // Active for Media only
Pavlin Radoslavov1af33a12018-01-21 02:59:15 -08001031 }
1032 if (mIsActiveDeviceHeadset) {
Pavlin Radoslavove6e080f2018-02-06 12:21:34 -08001033 activeDeviceString = activeDeviceStringsArray[3]; // Active for Phone only
Pavlin Radoslavov1af33a12018-01-21 02:59:15 -08001034 }
1035 }
Hansong Zhangd7b35912018-03-16 09:15:48 -07001036 if (!hearingAidNotConnected && mIsActiveDeviceHearingAid) {
1037 activeDeviceString = activeDeviceStringsArray[1];
1038 return mContext.getString(R.string.bluetooth_connected, activeDeviceString);
1039 }
Pavlin Radoslavov1af33a12018-01-21 02:59:15 -08001040
Jason Monkbe3c5db2015-02-04 13:00:55 -05001041 if (profileConnected) {
Sanket Agarwalf8a1c912016-01-26 20:12:52 -08001042 if (a2dpNotConnected && hfpNotConnected) {
Jack He6258aae2017-06-29 17:01:23 -07001043 if (batteryLevelPercentageString != null) {
1044 return mContext.getString(
1045 R.string.bluetooth_connected_no_headset_no_a2dp_battery_level,
Pavlin Radoslavove6e080f2018-02-06 12:21:34 -08001046 batteryLevelPercentageString, activeDeviceString);
Jack He6258aae2017-06-29 17:01:23 -07001047 } else {
Pavlin Radoslavove6e080f2018-02-06 12:21:34 -08001048 return mContext.getString(R.string.bluetooth_connected_no_headset_no_a2dp,
1049 activeDeviceString);
Jack He6258aae2017-06-29 17:01:23 -07001050 }
1051
Jason Monkbe3c5db2015-02-04 13:00:55 -05001052 } else if (a2dpNotConnected) {
Jack He6258aae2017-06-29 17:01:23 -07001053 if (batteryLevelPercentageString != null) {
1054 return mContext.getString(R.string.bluetooth_connected_no_a2dp_battery_level,
Pavlin Radoslavove6e080f2018-02-06 12:21:34 -08001055 batteryLevelPercentageString, activeDeviceString);
Jack He6258aae2017-06-29 17:01:23 -07001056 } else {
Pavlin Radoslavove6e080f2018-02-06 12:21:34 -08001057 return mContext.getString(R.string.bluetooth_connected_no_a2dp,
1058 activeDeviceString);
Jack He6258aae2017-06-29 17:01:23 -07001059 }
1060
Sanket Agarwalf8a1c912016-01-26 20:12:52 -08001061 } else if (hfpNotConnected) {
Jack He6258aae2017-06-29 17:01:23 -07001062 if (batteryLevelPercentageString != null) {
1063 return mContext.getString(R.string.bluetooth_connected_no_headset_battery_level,
Pavlin Radoslavove6e080f2018-02-06 12:21:34 -08001064 batteryLevelPercentageString, activeDeviceString);
Jack He6258aae2017-06-29 17:01:23 -07001065 } else {
Pavlin Radoslavove6e080f2018-02-06 12:21:34 -08001066 return mContext.getString(R.string.bluetooth_connected_no_headset,
1067 activeDeviceString);
Jack He6258aae2017-06-29 17:01:23 -07001068 }
Jason Monkbe3c5db2015-02-04 13:00:55 -05001069 } else {
Jack He6258aae2017-06-29 17:01:23 -07001070 if (batteryLevelPercentageString != null) {
1071 return mContext.getString(R.string.bluetooth_connected_battery_level,
Pavlin Radoslavove6e080f2018-02-06 12:21:34 -08001072 batteryLevelPercentageString, activeDeviceString);
Jack He6258aae2017-06-29 17:01:23 -07001073 } else {
Pavlin Radoslavove6e080f2018-02-06 12:21:34 -08001074 return mContext.getString(R.string.bluetooth_connected, activeDeviceString);
Jack He6258aae2017-06-29 17:01:23 -07001075 }
Jason Monkbe3c5db2015-02-04 13:00:55 -05001076 }
1077 }
1078
Jack He6258aae2017-06-29 17:01:23 -07001079 return getBondState() == BluetoothDevice.BOND_BONDING ?
1080 mContext.getString(R.string.bluetooth_pairing) : null;
Jason Monkbe3c5db2015-02-04 13:00:55 -05001081 }
hughchen23b947e2018-03-31 17:32:53 +08001082
1083 /**
1084 * @return {@code true} if {@code cachedBluetoothDevice} is a2dp device
1085 */
1086 public boolean isA2dpDevice() {
1087 return mProfileManager.getA2dpProfile().getConnectionStatus(mDevice) ==
1088 BluetoothProfile.STATE_CONNECTED;
1089 }
1090
1091 /**
1092 * @return {@code true} if {@code cachedBluetoothDevice} is HFP device
1093 */
1094 public boolean isHfpDevice() {
1095 return mProfileManager.getHeadsetProfile().getConnectionStatus(mDevice) ==
1096 BluetoothProfile.STATE_CONNECTED;
1097 }
Jason Monk7ce96b92015-02-02 11:27:58 -05001098}