blob: ce4aef31b395bd198a20e6b608b53f9d0ed882bd [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;
timhypengf0509322018-03-29 14:23:21 +080026import android.media.AudioManager;
Jason Monk7ce96b92015-02-02 11:27:58 -050027import android.os.ParcelUuid;
28import android.os.SystemClock;
29import android.text.TextUtils;
30import android.util.Log;
31import android.bluetooth.BluetoothAdapter;
Pavlin Radoslavovc285d552018-02-06 16:14:00 -080032import android.support.annotation.VisibleForTesting;
Jason Monk7ce96b92015-02-02 11:27:58 -050033
Jason Monkbe3c5db2015-02-04 13:00:55 -050034import com.android.settingslib.R;
35
Jason Monk7ce96b92015-02-02 11:27:58 -050036import java.util.ArrayList;
37import java.util.Collection;
38import java.util.Collections;
39import java.util.HashMap;
40import java.util.List;
41
42/**
43 * CachedBluetoothDevice represents a remote Bluetooth device. It contains
44 * attributes of the device (such as the address, name, RSSI, etc.) and
45 * functionality that can be performed on the device (connect, pair, disconnect,
46 * etc.).
47 */
Fan Zhang82dd3b02016-12-27 13:13:00 -080048public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> {
Jason Monk7ce96b92015-02-02 11:27:58 -050049 private static final String TAG = "CachedBluetoothDevice";
50 private static final boolean DEBUG = Utils.V;
51
52 private final Context mContext;
53 private final LocalBluetoothAdapter mLocalAdapter;
54 private final LocalBluetoothProfileManager mProfileManager;
timhypengf0509322018-03-29 14:23:21 +080055 private final AudioManager mAudioManager;
Jason Monk7ce96b92015-02-02 11:27:58 -050056 private final BluetoothDevice mDevice;
Jack Hec219bc92017-07-24 14:55:59 -070057 //TODO: consider remove, BluetoothDevice.getName() is already cached
Jason Monk7ce96b92015-02-02 11:27:58 -050058 private String mName;
Isha Bobrac3d94132018-02-08 16:04:36 -080059 private long mHiSyncId;
Jack Hec219bc92017-07-24 14:55:59 -070060 // Need this since there is no method for getting RSSI
Jason Monk7ce96b92015-02-02 11:27:58 -050061 private short mRssi;
Jack Hec219bc92017-07-24 14:55:59 -070062 //TODO: consider remove, BluetoothDevice.getBluetoothClass() is already cached
Jason Monk7ce96b92015-02-02 11:27:58 -050063 private BluetoothClass mBtClass;
64 private HashMap<LocalBluetoothProfile, Integer> mProfileConnectionState;
65
66 private final List<LocalBluetoothProfile> mProfiles =
67 new ArrayList<LocalBluetoothProfile>();
68
69 // List of profiles that were previously in mProfiles, but have been removed
70 private final List<LocalBluetoothProfile> mRemovedProfiles =
71 new ArrayList<LocalBluetoothProfile>();
72
73 // Device supports PANU but not NAP: remove PanProfile after device disconnects from NAP
74 private boolean mLocalNapRoleConnected;
75
Jack He51520472017-07-24 12:30:08 -070076 private boolean mJustDiscovered;
Jason Monk7ce96b92015-02-02 11:27:58 -050077
Jason Monk7ce96b92015-02-02 11:27:58 -050078 private int mMessageRejectionCount;
79
80 private final Collection<Callback> mCallbacks = new ArrayList<Callback>();
81
82 // Following constants indicate the user's choices of Phone book/message access settings
83 // User hasn't made any choice or settings app has wiped out the memory
84 public final static int ACCESS_UNKNOWN = 0;
85 // User has accepted the connection and let Settings app remember the decision
86 public final static int ACCESS_ALLOWED = 1;
87 // User has rejected the connection and let Settings app remember the decision
88 public final static int ACCESS_REJECTED = 2;
89
90 // How many times user should reject the connection to make the choice persist.
91 private final static int MESSAGE_REJECTION_COUNT_LIMIT_TO_PERSIST = 2;
92
93 private final static String MESSAGE_REJECTION_COUNT_PREFS_NAME = "bluetooth_message_reject";
94
95 /**
96 * When we connect to multiple profiles, we only want to display a single
97 * error even if they all fail. This tracks that state.
98 */
99 private boolean mIsConnectingErrorPossible;
100
Isha Bobrac3d94132018-02-08 16:04:36 -0800101 public long getHiSyncId() {
102 return mHiSyncId;
103 }
104
105 public void setHiSyncId(long id) {
106 if (Utils.D) {
107 Log.d(TAG, "setHiSyncId: mDevice " + mDevice + ", id " + id);
108 }
109 mHiSyncId = id;
110 }
111
Jason Monk7ce96b92015-02-02 11:27:58 -0500112 /**
113 * Last time a bt profile auto-connect was attempted.
114 * If an ACTION_UUID intent comes in within
115 * MAX_UUID_DELAY_FOR_AUTO_CONNECT milliseconds, we will try auto-connect
116 * again with the new UUIDs
117 */
118 private long mConnectAttempted;
119
120 // See mConnectAttempted
121 private static final long MAX_UUID_DELAY_FOR_AUTO_CONNECT = 5000;
Etan Cohen50d47612015-03-31 12:45:23 -0700122 private static final long MAX_HOGP_DELAY_FOR_AUTO_CONNECT = 30000;
Jason Monk7ce96b92015-02-02 11:27:58 -0500123
Pavlin Radoslavov1af33a12018-01-21 02:59:15 -0800124 // Active device state
125 private boolean mIsActiveDeviceA2dp = false;
126 private boolean mIsActiveDeviceHeadset = false;
Hansong Zhangd7b35912018-03-16 09:15:48 -0700127 private boolean mIsActiveDeviceHearingAid = false;
Jason Monk7ce96b92015-02-02 11:27:58 -0500128 /**
129 * Describes the current device and profile for logging.
130 *
131 * @param profile Profile to describe
132 * @return Description of the device and profile
133 */
134 private String describe(LocalBluetoothProfile profile) {
135 StringBuilder sb = new StringBuilder();
136 sb.append("Address:").append(mDevice);
137 if (profile != null) {
138 sb.append(" Profile:").append(profile);
139 }
140
141 return sb.toString();
142 }
143
144 void onProfileStateChanged(LocalBluetoothProfile profile, int newProfileState) {
145 if (Utils.D) {
146 Log.d(TAG, "onProfileStateChanged: profile " + profile +
147 " newProfileState " + newProfileState);
148 }
149 if (mLocalAdapter.getBluetoothState() == BluetoothAdapter.STATE_TURNING_OFF)
150 {
151 if (Utils.D) Log.d(TAG, " BT Turninig Off...Profile conn state change ignored...");
152 return;
153 }
154 mProfileConnectionState.put(profile, newProfileState);
155 if (newProfileState == BluetoothProfile.STATE_CONNECTED) {
156 if (profile instanceof MapProfile) {
157 profile.setPreferred(mDevice, true);
Hemant Guptadbc3d8d2017-05-12 21:14:44 +0530158 }
159 if (!mProfiles.contains(profile)) {
Jason Monk7ce96b92015-02-02 11:27:58 -0500160 mRemovedProfiles.remove(profile);
161 mProfiles.add(profile);
162 if (profile instanceof PanProfile &&
163 ((PanProfile) profile).isLocalRoleNap(mDevice)) {
164 // Device doesn't support NAP, so remove PanProfile on disconnect
165 mLocalNapRoleConnected = true;
166 }
167 }
168 } else if (profile instanceof MapProfile &&
169 newProfileState == BluetoothProfile.STATE_DISCONNECTED) {
170 profile.setPreferred(mDevice, false);
171 } else if (mLocalNapRoleConnected && profile instanceof PanProfile &&
172 ((PanProfile) profile).isLocalRoleNap(mDevice) &&
173 newProfileState == BluetoothProfile.STATE_DISCONNECTED) {
174 Log.d(TAG, "Removing PanProfile from device after NAP disconnect");
175 mProfiles.remove(profile);
176 mRemovedProfiles.add(profile);
177 mLocalNapRoleConnected = false;
178 }
Pavlin Radoslavov1af33a12018-01-21 02:59:15 -0800179 fetchActiveDevices();
Jason Monk7ce96b92015-02-02 11:27:58 -0500180 }
181
182 CachedBluetoothDevice(Context context,
183 LocalBluetoothAdapter adapter,
184 LocalBluetoothProfileManager profileManager,
185 BluetoothDevice device) {
186 mContext = context;
187 mLocalAdapter = adapter;
188 mProfileManager = profileManager;
timhypengf0509322018-03-29 14:23:21 +0800189 mAudioManager = context.getSystemService(AudioManager.class);
Jason Monk7ce96b92015-02-02 11:27:58 -0500190 mDevice = device;
191 mProfileConnectionState = new HashMap<LocalBluetoothProfile, Integer>();
192 fillData();
Isha Bobrac3d94132018-02-08 16:04:36 -0800193 mHiSyncId = BluetoothHearingAid.HI_SYNC_ID_INVALID;
Jason Monk7ce96b92015-02-02 11:27:58 -0500194 }
195
196 public void disconnect() {
197 for (LocalBluetoothProfile profile : mProfiles) {
198 disconnect(profile);
199 }
200 // Disconnect PBAP server in case its connected
201 // This is to ensure all the profiles are disconnected as some CK/Hs do not
202 // disconnect PBAP connection when HF connection is brought down
203 PbapServerProfile PbapProfile = mProfileManager.getPbapProfile();
204 if (PbapProfile.getConnectionStatus(mDevice) == BluetoothProfile.STATE_CONNECTED)
205 {
206 PbapProfile.disconnect(mDevice);
207 }
208 }
209
210 public void disconnect(LocalBluetoothProfile profile) {
211 if (profile.disconnect(mDevice)) {
212 if (Utils.D) {
213 Log.d(TAG, "Command sent successfully:DISCONNECT " + describe(profile));
214 }
215 }
216 }
217
218 public void connect(boolean connectAllProfiles) {
219 if (!ensurePaired()) {
220 return;
221 }
222
223 mConnectAttempted = SystemClock.elapsedRealtime();
224 connectWithoutResettingTimer(connectAllProfiles);
225 }
226
227 void onBondingDockConnect() {
228 // Attempt to connect if UUIDs are available. Otherwise,
229 // we will connect when the ACTION_UUID intent arrives.
230 connect(false);
231 }
232
233 private void connectWithoutResettingTimer(boolean connectAllProfiles) {
234 // Try to initialize the profiles if they were not.
235 if (mProfiles.isEmpty()) {
236 // if mProfiles is empty, then do not invoke updateProfiles. This causes a race
237 // condition with carkits during pairing, wherein RemoteDevice.UUIDs have been updated
238 // from bluetooth stack but ACTION.uuid is not sent yet.
239 // Eventually ACTION.uuid will be received which shall trigger the connection of the
240 // various profiles
241 // If UUIDs are not available yet, connect will be happen
242 // upon arrival of the ACTION_UUID intent.
243 Log.d(TAG, "No profiles. Maybe we will connect later");
244 return;
245 }
246
247 // Reset the only-show-one-error-dialog tracking variable
248 mIsConnectingErrorPossible = true;
249
250 int preferredProfiles = 0;
251 for (LocalBluetoothProfile profile : mProfiles) {
252 if (connectAllProfiles ? profile.isConnectable() : profile.isAutoConnectable()) {
253 if (profile.isPreferred(mDevice)) {
254 ++preferredProfiles;
255 connectInt(profile);
256 }
257 }
258 }
259 if (DEBUG) Log.d(TAG, "Preferred profiles = " + preferredProfiles);
260
261 if (preferredProfiles == 0) {
262 connectAutoConnectableProfiles();
263 }
264 }
265
266 private void connectAutoConnectableProfiles() {
267 if (!ensurePaired()) {
268 return;
269 }
270 // Reset the only-show-one-error-dialog tracking variable
271 mIsConnectingErrorPossible = true;
272
273 for (LocalBluetoothProfile profile : mProfiles) {
274 if (profile.isAutoConnectable()) {
275 profile.setPreferred(mDevice, true);
276 connectInt(profile);
277 }
278 }
279 }
280
281 /**
282 * Connect this device to the specified profile.
283 *
284 * @param profile the profile to use with the remote device
285 */
286 public void connectProfile(LocalBluetoothProfile profile) {
287 mConnectAttempted = SystemClock.elapsedRealtime();
288 // Reset the only-show-one-error-dialog tracking variable
289 mIsConnectingErrorPossible = true;
290 connectInt(profile);
291 // Refresh the UI based on profile.connect() call
292 refresh();
293 }
294
295 synchronized void connectInt(LocalBluetoothProfile profile) {
296 if (!ensurePaired()) {
297 return;
298 }
299 if (profile.connect(mDevice)) {
300 if (Utils.D) {
301 Log.d(TAG, "Command sent successfully:CONNECT " + describe(profile));
302 }
303 return;
304 }
305 Log.i(TAG, "Failed to connect " + profile.toString() + " to " + mName);
306 }
307
308 private boolean ensurePaired() {
309 if (getBondState() == BluetoothDevice.BOND_NONE) {
310 startPairing();
311 return false;
312 } else {
313 return true;
314 }
315 }
316
317 public boolean startPairing() {
318 // Pairing is unreliable while scanning, so cancel discovery
319 if (mLocalAdapter.isDiscovering()) {
320 mLocalAdapter.cancelDiscovery();
321 }
322
323 if (!mDevice.createBond()) {
324 return false;
325 }
326
Jason Monk7ce96b92015-02-02 11:27:58 -0500327 return true;
328 }
329
330 /**
331 * Return true if user initiated pairing on this device. The message text is
332 * slightly different for local vs. remote initiated pairing dialogs.
333 */
334 boolean isUserInitiatedPairing() {
Jakub Pawlowski0278ab92016-07-20 11:55:48 -0700335 return mDevice.isBondingInitiatedLocally();
Jason Monk7ce96b92015-02-02 11:27:58 -0500336 }
337
338 public void unpair() {
339 int state = getBondState();
340
341 if (state == BluetoothDevice.BOND_BONDING) {
342 mDevice.cancelBondProcess();
343 }
344
345 if (state != BluetoothDevice.BOND_NONE) {
346 final BluetoothDevice dev = mDevice;
347 if (dev != null) {
348 final boolean successful = dev.removeBond();
349 if (successful) {
350 if (Utils.D) {
351 Log.d(TAG, "Command sent successfully:REMOVE_BOND " + describe(null));
352 }
353 } else if (Utils.V) {
354 Log.v(TAG, "Framework rejected command immediately:REMOVE_BOND " +
Isha Bobrac3d94132018-02-08 16:04:36 -0800355 describe(null));
Jason Monk7ce96b92015-02-02 11:27:58 -0500356 }
357 }
358 }
359 }
360
361 public int getProfileConnectionState(LocalBluetoothProfile profile) {
xutianguo22bbb8192016-06-22 11:32:00 +0800362 if (mProfileConnectionState.get(profile) == null) {
Jason Monk7ce96b92015-02-02 11:27:58 -0500363 // If cache is empty make the binder call to get the state
364 int state = profile.getConnectionStatus(mDevice);
365 mProfileConnectionState.put(profile, state);
366 }
367 return mProfileConnectionState.get(profile);
368 }
369
370 public void clearProfileConnectionState ()
371 {
372 if (Utils.D) {
373 Log.d(TAG," Clearing all connection state for dev:" + mDevice.getName());
374 }
375 for (LocalBluetoothProfile profile :getProfiles()) {
376 mProfileConnectionState.put(profile, BluetoothProfile.STATE_DISCONNECTED);
377 }
378 }
379
380 // TODO: do any of these need to run async on a background thread?
381 private void fillData() {
382 fetchName();
383 fetchBtClass();
384 updateProfiles();
Pavlin Radoslavov1af33a12018-01-21 02:59:15 -0800385 fetchActiveDevices();
Jason Monk7ce96b92015-02-02 11:27:58 -0500386 migratePhonebookPermissionChoice();
387 migrateMessagePermissionChoice();
388 fetchMessageRejectionCount();
389
Jason Monk7ce96b92015-02-02 11:27:58 -0500390 dispatchAttributesChanged();
391 }
392
393 public BluetoothDevice getDevice() {
394 return mDevice;
395 }
396
Antony Sargent7ad051e2017-06-29 15:23:13 -0700397 /**
398 * Convenience method that can be mocked - it lets tests avoid having to call getDevice() which
399 * causes problems in tests since BluetoothDevice is final and cannot be mocked.
400 * @return the address of this device
401 */
402 public String getAddress() {
403 return mDevice.getAddress();
404 }
405
Jason Monk7ce96b92015-02-02 11:27:58 -0500406 public String getName() {
407 return mName;
408 }
409
410 /**
411 * Populate name from BluetoothDevice.ACTION_FOUND intent
412 */
413 void setNewName(String name) {
414 if (mName == null) {
415 mName = name;
416 if (mName == null || TextUtils.isEmpty(mName)) {
417 mName = mDevice.getAddress();
418 }
419 dispatchAttributesChanged();
420 }
421 }
422
423 /**
Jack Hec219bc92017-07-24 14:55:59 -0700424 * User changes the device name
425 * @param name new alias name to be set, should never be null
Jason Monk7ce96b92015-02-02 11:27:58 -0500426 */
427 public void setName(String name) {
Jack Hec219bc92017-07-24 14:55:59 -0700428 // Prevent mName to be set to null if setName(null) is called
429 if (name != null && !TextUtils.equals(name, mName)) {
Jason Monk7ce96b92015-02-02 11:27:58 -0500430 mName = name;
431 mDevice.setAlias(name);
432 dispatchAttributesChanged();
433 }
434 }
435
Hansong Zhang6a416322018-03-19 18:20:38 -0700436 /**
437 * Set this device as active device
438 * @return true if at least one profile on this device is set to active, false otherwise
439 */
440 public boolean setActive() {
441 boolean result = false;
442 A2dpProfile a2dpProfile = mProfileManager.getA2dpProfile();
443 if (a2dpProfile != null && isConnectedProfile(a2dpProfile)) {
444 if (a2dpProfile.setActiveDevice(getDevice())) {
445 Log.i(TAG, "OnPreferenceClickListener: A2DP active device=" + this);
446 result = true;
447 }
448 }
449 HeadsetProfile headsetProfile = mProfileManager.getHeadsetProfile();
450 if ((headsetProfile != null) && isConnectedProfile(headsetProfile)) {
451 if (headsetProfile.setActiveDevice(getDevice())) {
452 Log.i(TAG, "OnPreferenceClickListener: Headset active device=" + this);
453 result = true;
454 }
455 }
Hansong Zhangd7b35912018-03-16 09:15:48 -0700456 HearingAidProfile hearingAidProfile = mProfileManager.getHearingAidProfile();
457 if ((hearingAidProfile != null) && isConnectedProfile(hearingAidProfile)) {
458 if (hearingAidProfile.setActiveDevice(getDevice())) {
459 Log.i(TAG, "OnPreferenceClickListener: Hearing Aid active device=" + this);
460 result = true;
461 }
462 }
Hansong Zhang6a416322018-03-19 18:20:38 -0700463 return result;
464 }
465
Jason Monk7ce96b92015-02-02 11:27:58 -0500466 void refreshName() {
467 fetchName();
468 dispatchAttributesChanged();
469 }
470
471 private void fetchName() {
472 mName = mDevice.getAliasName();
473
474 if (TextUtils.isEmpty(mName)) {
475 mName = mDevice.getAddress();
476 if (DEBUG) Log.d(TAG, "Device has no name (yet), use address: " + mName);
477 }
478 }
479
Jack He6258aae2017-06-29 17:01:23 -0700480 /**
Jack Hec219bc92017-07-24 14:55:59 -0700481 * Checks if device has a human readable name besides MAC address
482 * @return true if device's alias name is not null nor empty, false otherwise
483 */
484 public boolean hasHumanReadableName() {
485 return !TextUtils.isEmpty(mDevice.getAliasName());
486 }
487
488 /**
Jack He6258aae2017-06-29 17:01:23 -0700489 * Get battery level from remote device
490 * @return battery level in percentage [0-100], or {@link BluetoothDevice#BATTERY_LEVEL_UNKNOWN}
491 */
492 public int getBatteryLevel() {
493 return mDevice.getBatteryLevel();
494 }
495
Jason Monk7ce96b92015-02-02 11:27:58 -0500496 void refresh() {
497 dispatchAttributesChanged();
498 }
499
Jack He51520472017-07-24 12:30:08 -0700500 public void setJustDiscovered(boolean justDiscovered) {
501 if (mJustDiscovered != justDiscovered) {
502 mJustDiscovered = justDiscovered;
Jason Monk7ce96b92015-02-02 11:27:58 -0500503 dispatchAttributesChanged();
504 }
505 }
506
507 public int getBondState() {
508 return mDevice.getBondState();
509 }
510
Pavlin Radoslavov1af33a12018-01-21 02:59:15 -0800511 /**
Pavlin Radoslavovc285d552018-02-06 16:14:00 -0800512 * Update the device status as active or non-active per Bluetooth profile.
Pavlin Radoslavov1af33a12018-01-21 02:59:15 -0800513 *
514 * @param isActive true if the device is active
515 * @param bluetoothProfile the Bluetooth profile
516 */
Pavlin Radoslavovc285d552018-02-06 16:14:00 -0800517 public void onActiveDeviceChanged(boolean isActive, int bluetoothProfile) {
Pavlin Radoslavov1af33a12018-01-21 02:59:15 -0800518 boolean changed = false;
519 switch (bluetoothProfile) {
520 case BluetoothProfile.A2DP:
521 changed = (mIsActiveDeviceA2dp != isActive);
522 mIsActiveDeviceA2dp = isActive;
523 break;
524 case BluetoothProfile.HEADSET:
525 changed = (mIsActiveDeviceHeadset != isActive);
526 mIsActiveDeviceHeadset = isActive;
527 break;
Hansong Zhangd7b35912018-03-16 09:15:48 -0700528 case BluetoothProfile.HEARING_AID:
529 changed = (mIsActiveDeviceHearingAid != isActive);
530 mIsActiveDeviceHearingAid = isActive;
531 break;
Pavlin Radoslavov1af33a12018-01-21 02:59:15 -0800532 default:
Pavlin Radoslavovc285d552018-02-06 16:14:00 -0800533 Log.w(TAG, "onActiveDeviceChanged: unknown profile " + bluetoothProfile +
Pavlin Radoslavov1af33a12018-01-21 02:59:15 -0800534 " isActive " + isActive);
535 break;
536 }
537 if (changed) {
538 dispatchAttributesChanged();
539 }
540 }
541
Pavlin Radoslavovc285d552018-02-06 16:14:00 -0800542 /**
timhypengf0509322018-03-29 14:23:21 +0800543 * Update the profile audio state.
544 */
545 void onAudioModeChanged() {
546 dispatchAttributesChanged();
547 }
548 /**
Pavlin Radoslavovc285d552018-02-06 16:14:00 -0800549 * Get the device status as active or non-active per Bluetooth profile.
550 *
551 * @param bluetoothProfile the Bluetooth profile
552 * @return true if the device is active
553 */
554 @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
555 public boolean isActiveDevice(int bluetoothProfile) {
556 switch (bluetoothProfile) {
557 case BluetoothProfile.A2DP:
558 return mIsActiveDeviceA2dp;
559 case BluetoothProfile.HEADSET:
560 return mIsActiveDeviceHeadset;
Hansong Zhangd7b35912018-03-16 09:15:48 -0700561 case BluetoothProfile.HEARING_AID:
562 return mIsActiveDeviceHearingAid;
Pavlin Radoslavovc285d552018-02-06 16:14:00 -0800563 default:
564 Log.w(TAG, "getActiveDevice: unknown profile " + bluetoothProfile);
565 break;
566 }
567 return false;
568 }
569
Jason Monk7ce96b92015-02-02 11:27:58 -0500570 void setRssi(short rssi) {
571 if (mRssi != rssi) {
572 mRssi = rssi;
573 dispatchAttributesChanged();
574 }
575 }
576
577 /**
578 * Checks whether we are connected to this device (any profile counts).
579 *
580 * @return Whether it is connected.
581 */
582 public boolean isConnected() {
583 for (LocalBluetoothProfile profile : mProfiles) {
584 int status = getProfileConnectionState(profile);
585 if (status == BluetoothProfile.STATE_CONNECTED) {
586 return true;
587 }
588 }
589
590 return false;
591 }
592
593 public boolean isConnectedProfile(LocalBluetoothProfile profile) {
594 int status = getProfileConnectionState(profile);
595 return status == BluetoothProfile.STATE_CONNECTED;
596
597 }
598
599 public boolean isBusy() {
600 for (LocalBluetoothProfile profile : mProfiles) {
601 int status = getProfileConnectionState(profile);
602 if (status == BluetoothProfile.STATE_CONNECTING
603 || status == BluetoothProfile.STATE_DISCONNECTING) {
604 return true;
605 }
606 }
607 return getBondState() == BluetoothDevice.BOND_BONDING;
608 }
609
610 /**
611 * Fetches a new value for the cached BT class.
612 */
613 private void fetchBtClass() {
614 mBtClass = mDevice.getBluetoothClass();
615 }
616
617 private boolean updateProfiles() {
618 ParcelUuid[] uuids = mDevice.getUuids();
619 if (uuids == null) return false;
620
621 ParcelUuid[] localUuids = mLocalAdapter.getUuids();
622 if (localUuids == null) return false;
623
Jack Hec219bc92017-07-24 14:55:59 -0700624 /*
Jason Monk7ce96b92015-02-02 11:27:58 -0500625 * Now we know if the device supports PBAP, update permissions...
626 */
627 processPhonebookAccess();
628
629 mProfileManager.updateProfiles(uuids, localUuids, mProfiles, mRemovedProfiles,
630 mLocalNapRoleConnected, mDevice);
631
632 if (DEBUG) {
633 Log.e(TAG, "updating profiles for " + mDevice.getAliasName());
634 BluetoothClass bluetoothClass = mDevice.getBluetoothClass();
635
636 if (bluetoothClass != null) Log.v(TAG, "Class: " + bluetoothClass.toString());
637 Log.v(TAG, "UUID:");
638 for (ParcelUuid uuid : uuids) {
639 Log.v(TAG, " " + uuid);
640 }
641 }
642 return true;
643 }
644
Pavlin Radoslavov1af33a12018-01-21 02:59:15 -0800645 private void fetchActiveDevices() {
646 A2dpProfile a2dpProfile = mProfileManager.getA2dpProfile();
647 if (a2dpProfile != null) {
648 mIsActiveDeviceA2dp = mDevice.equals(a2dpProfile.getActiveDevice());
649 }
650 HeadsetProfile headsetProfile = mProfileManager.getHeadsetProfile();
651 if (headsetProfile != null) {
652 mIsActiveDeviceHeadset = mDevice.equals(headsetProfile.getActiveDevice());
653 }
Hansong Zhangd7b35912018-03-16 09:15:48 -0700654 HearingAidProfile hearingAidProfile = mProfileManager.getHearingAidProfile();
655 if (hearingAidProfile != null) {
Hansong Zhang3b8f09b2018-03-28 16:53:10 -0700656 mIsActiveDeviceHearingAid = hearingAidProfile.getActiveDevices().contains(mDevice);
Hansong Zhangd7b35912018-03-16 09:15:48 -0700657 }
Pavlin Radoslavov1af33a12018-01-21 02:59:15 -0800658 }
659
Jason Monk7ce96b92015-02-02 11:27:58 -0500660 /**
661 * Refreshes the UI for the BT class, including fetching the latest value
662 * for the class.
663 */
664 void refreshBtClass() {
665 fetchBtClass();
666 dispatchAttributesChanged();
667 }
668
669 /**
670 * Refreshes the UI when framework alerts us of a UUID change.
671 */
672 void onUuidChanged() {
673 updateProfiles();
Etan Cohen50d47612015-03-31 12:45:23 -0700674 ParcelUuid[] uuids = mDevice.getUuids();
Venkat Raghavan05e08c32015-04-06 16:26:11 -0700675
Etan Cohen50d47612015-03-31 12:45:23 -0700676 long timeout = MAX_UUID_DELAY_FOR_AUTO_CONNECT;
Venkat Raghavan05e08c32015-04-06 16:26:11 -0700677 if (BluetoothUuid.isUuidPresent(uuids, BluetoothUuid.Hogp)) {
678 timeout = MAX_HOGP_DELAY_FOR_AUTO_CONNECT;
679 }
Jason Monk7ce96b92015-02-02 11:27:58 -0500680
681 if (DEBUG) {
Etan Cohen50d47612015-03-31 12:45:23 -0700682 Log.d(TAG, "onUuidChanged: Time since last connect"
Jason Monk7ce96b92015-02-02 11:27:58 -0500683 + (SystemClock.elapsedRealtime() - mConnectAttempted));
684 }
685
686 /*
Venkat Raghavan05e08c32015-04-06 16:26:11 -0700687 * If a connect was attempted earlier without any UUID, we will do the connect now.
688 * Otherwise, allow the connect on UUID change.
Jason Monk7ce96b92015-02-02 11:27:58 -0500689 */
690 if (!mProfiles.isEmpty()
Andre Eisenbach7be83c52015-09-03 15:20:09 -0700691 && ((mConnectAttempted + timeout) > SystemClock.elapsedRealtime())) {
Jason Monk7ce96b92015-02-02 11:27:58 -0500692 connectWithoutResettingTimer(false);
693 }
Andre Eisenbach7be83c52015-09-03 15:20:09 -0700694
Jason Monk7ce96b92015-02-02 11:27:58 -0500695 dispatchAttributesChanged();
696 }
697
698 void onBondingStateChanged(int bondState) {
699 if (bondState == BluetoothDevice.BOND_NONE) {
700 mProfiles.clear();
Jason Monk7ce96b92015-02-02 11:27:58 -0500701 setPhonebookPermissionChoice(ACCESS_UNKNOWN);
702 setMessagePermissionChoice(ACCESS_UNKNOWN);
Casper Bonde424681e2015-05-04 22:07:45 -0700703 setSimPermissionChoice(ACCESS_UNKNOWN);
Jason Monk7ce96b92015-02-02 11:27:58 -0500704 mMessageRejectionCount = 0;
705 saveMessageRejectionCount();
706 }
707
708 refresh();
709
710 if (bondState == BluetoothDevice.BOND_BONDED) {
711 if (mDevice.isBluetoothDock()) {
712 onBondingDockConnect();
Jakub Pawlowski0278ab92016-07-20 11:55:48 -0700713 } else if (mDevice.isBondingInitiatedLocally()) {
Jason Monk7ce96b92015-02-02 11:27:58 -0500714 connect(false);
715 }
Jason Monk7ce96b92015-02-02 11:27:58 -0500716 }
717 }
718
719 void setBtClass(BluetoothClass btClass) {
720 if (btClass != null && mBtClass != btClass) {
721 mBtClass = btClass;
722 dispatchAttributesChanged();
723 }
724 }
725
726 public BluetoothClass getBtClass() {
727 return mBtClass;
728 }
729
730 public List<LocalBluetoothProfile> getProfiles() {
731 return Collections.unmodifiableList(mProfiles);
732 }
733
734 public List<LocalBluetoothProfile> getConnectableProfiles() {
735 List<LocalBluetoothProfile> connectableProfiles =
736 new ArrayList<LocalBluetoothProfile>();
737 for (LocalBluetoothProfile profile : mProfiles) {
738 if (profile.isConnectable()) {
739 connectableProfiles.add(profile);
740 }
741 }
742 return connectableProfiles;
743 }
744
745 public List<LocalBluetoothProfile> getRemovedProfiles() {
746 return mRemovedProfiles;
747 }
748
749 public void registerCallback(Callback callback) {
750 synchronized (mCallbacks) {
751 mCallbacks.add(callback);
752 }
753 }
754
755 public void unregisterCallback(Callback callback) {
756 synchronized (mCallbacks) {
757 mCallbacks.remove(callback);
758 }
759 }
760
761 private void dispatchAttributesChanged() {
762 synchronized (mCallbacks) {
763 for (Callback callback : mCallbacks) {
764 callback.onDeviceAttributesChanged();
765 }
766 }
767 }
768
769 @Override
770 public String toString() {
771 return mDevice.toString();
772 }
773
774 @Override
775 public boolean equals(Object o) {
776 if ((o == null) || !(o instanceof CachedBluetoothDevice)) {
777 return false;
778 }
779 return mDevice.equals(((CachedBluetoothDevice) o).mDevice);
780 }
781
782 @Override
783 public int hashCode() {
784 return mDevice.getAddress().hashCode();
785 }
786
787 // This comparison uses non-final fields so the sort order may change
788 // when device attributes change (such as bonding state). Settings
789 // will completely refresh the device list when this happens.
790 public int compareTo(CachedBluetoothDevice another) {
791 // Connected above not connected
792 int comparison = (another.isConnected() ? 1 : 0) - (isConnected() ? 1 : 0);
793 if (comparison != 0) return comparison;
794
795 // Paired above not paired
796 comparison = (another.getBondState() == BluetoothDevice.BOND_BONDED ? 1 : 0) -
797 (getBondState() == BluetoothDevice.BOND_BONDED ? 1 : 0);
798 if (comparison != 0) return comparison;
799
Jack He51520472017-07-24 12:30:08 -0700800 // Just discovered above discovered in the past
801 comparison = (another.mJustDiscovered ? 1 : 0) - (mJustDiscovered ? 1 : 0);
Jason Monk7ce96b92015-02-02 11:27:58 -0500802 if (comparison != 0) return comparison;
803
804 // Stronger signal above weaker signal
805 comparison = another.mRssi - mRssi;
806 if (comparison != 0) return comparison;
807
808 // Fallback on name
809 return mName.compareTo(another.mName);
810 }
811
812 public interface Callback {
813 void onDeviceAttributesChanged();
814 }
815
816 public int getPhonebookPermissionChoice() {
817 int permission = mDevice.getPhonebookAccessPermission();
818 if (permission == BluetoothDevice.ACCESS_ALLOWED) {
819 return ACCESS_ALLOWED;
820 } else if (permission == BluetoothDevice.ACCESS_REJECTED) {
821 return ACCESS_REJECTED;
822 }
823 return ACCESS_UNKNOWN;
824 }
825
826 public void setPhonebookPermissionChoice(int permissionChoice) {
827 int permission = BluetoothDevice.ACCESS_UNKNOWN;
828 if (permissionChoice == ACCESS_ALLOWED) {
829 permission = BluetoothDevice.ACCESS_ALLOWED;
830 } else if (permissionChoice == ACCESS_REJECTED) {
831 permission = BluetoothDevice.ACCESS_REJECTED;
832 }
833 mDevice.setPhonebookAccessPermission(permission);
834 }
835
836 // Migrates data from old data store (in Settings app's shared preferences) to new (in Bluetooth
837 // app's shared preferences).
838 private void migratePhonebookPermissionChoice() {
839 SharedPreferences preferences = mContext.getSharedPreferences(
840 "bluetooth_phonebook_permission", Context.MODE_PRIVATE);
841 if (!preferences.contains(mDevice.getAddress())) {
842 return;
843 }
844
845 if (mDevice.getPhonebookAccessPermission() == BluetoothDevice.ACCESS_UNKNOWN) {
846 int oldPermission = preferences.getInt(mDevice.getAddress(), ACCESS_UNKNOWN);
847 if (oldPermission == ACCESS_ALLOWED) {
848 mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_ALLOWED);
849 } else if (oldPermission == ACCESS_REJECTED) {
850 mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_REJECTED);
851 }
852 }
853
854 SharedPreferences.Editor editor = preferences.edit();
855 editor.remove(mDevice.getAddress());
856 editor.commit();
857 }
858
859 public int getMessagePermissionChoice() {
860 int permission = mDevice.getMessageAccessPermission();
861 if (permission == BluetoothDevice.ACCESS_ALLOWED) {
862 return ACCESS_ALLOWED;
863 } else if (permission == BluetoothDevice.ACCESS_REJECTED) {
864 return ACCESS_REJECTED;
865 }
866 return ACCESS_UNKNOWN;
867 }
868
869 public void setMessagePermissionChoice(int permissionChoice) {
870 int permission = BluetoothDevice.ACCESS_UNKNOWN;
871 if (permissionChoice == ACCESS_ALLOWED) {
872 permission = BluetoothDevice.ACCESS_ALLOWED;
873 } else if (permissionChoice == ACCESS_REJECTED) {
874 permission = BluetoothDevice.ACCESS_REJECTED;
875 }
876 mDevice.setMessageAccessPermission(permission);
877 }
878
Casper Bonde424681e2015-05-04 22:07:45 -0700879 public int getSimPermissionChoice() {
880 int permission = mDevice.getSimAccessPermission();
881 if (permission == BluetoothDevice.ACCESS_ALLOWED) {
882 return ACCESS_ALLOWED;
883 } else if (permission == BluetoothDevice.ACCESS_REJECTED) {
884 return ACCESS_REJECTED;
885 }
886 return ACCESS_UNKNOWN;
887 }
888
889 void setSimPermissionChoice(int permissionChoice) {
890 int permission = BluetoothDevice.ACCESS_UNKNOWN;
891 if (permissionChoice == ACCESS_ALLOWED) {
892 permission = BluetoothDevice.ACCESS_ALLOWED;
893 } else if (permissionChoice == ACCESS_REJECTED) {
894 permission = BluetoothDevice.ACCESS_REJECTED;
895 }
896 mDevice.setSimAccessPermission(permission);
897 }
898
Jason Monk7ce96b92015-02-02 11:27:58 -0500899 // Migrates data from old data store (in Settings app's shared preferences) to new (in Bluetooth
900 // app's shared preferences).
901 private void migrateMessagePermissionChoice() {
902 SharedPreferences preferences = mContext.getSharedPreferences(
903 "bluetooth_message_permission", Context.MODE_PRIVATE);
904 if (!preferences.contains(mDevice.getAddress())) {
905 return;
906 }
907
908 if (mDevice.getMessageAccessPermission() == BluetoothDevice.ACCESS_UNKNOWN) {
909 int oldPermission = preferences.getInt(mDevice.getAddress(), ACCESS_UNKNOWN);
910 if (oldPermission == ACCESS_ALLOWED) {
911 mDevice.setMessageAccessPermission(BluetoothDevice.ACCESS_ALLOWED);
912 } else if (oldPermission == ACCESS_REJECTED) {
913 mDevice.setMessageAccessPermission(BluetoothDevice.ACCESS_REJECTED);
914 }
915 }
916
917 SharedPreferences.Editor editor = preferences.edit();
918 editor.remove(mDevice.getAddress());
919 editor.commit();
920 }
921
922 /**
923 * @return Whether this rejection should persist.
924 */
925 public boolean checkAndIncreaseMessageRejectionCount() {
926 if (mMessageRejectionCount < MESSAGE_REJECTION_COUNT_LIMIT_TO_PERSIST) {
927 mMessageRejectionCount++;
928 saveMessageRejectionCount();
929 }
930 return mMessageRejectionCount >= MESSAGE_REJECTION_COUNT_LIMIT_TO_PERSIST;
931 }
932
933 private void fetchMessageRejectionCount() {
934 SharedPreferences preference = mContext.getSharedPreferences(
935 MESSAGE_REJECTION_COUNT_PREFS_NAME, Context.MODE_PRIVATE);
936 mMessageRejectionCount = preference.getInt(mDevice.getAddress(), 0);
937 }
938
939 private void saveMessageRejectionCount() {
940 SharedPreferences.Editor editor = mContext.getSharedPreferences(
941 MESSAGE_REJECTION_COUNT_PREFS_NAME, Context.MODE_PRIVATE).edit();
942 if (mMessageRejectionCount == 0) {
943 editor.remove(mDevice.getAddress());
944 } else {
945 editor.putInt(mDevice.getAddress(), mMessageRejectionCount);
946 }
947 editor.commit();
948 }
949
950 private void processPhonebookAccess() {
951 if (mDevice.getBondState() != BluetoothDevice.BOND_BONDED) return;
952
953 ParcelUuid[] uuids = mDevice.getUuids();
954 if (BluetoothUuid.containsAnyUuid(uuids, PbapServerProfile.PBAB_CLIENT_UUIDS)) {
955 // The pairing dialog now warns of phone-book access for paired devices.
956 // No separate prompt is displayed after pairing.
Sanket Padawe78c23762015-06-01 15:55:30 -0700957 if (getPhonebookPermissionChoice() == CachedBluetoothDevice.ACCESS_UNKNOWN) {
Sanket Padawe07533db2015-11-11 15:01:35 -0800958 if (mDevice.getBluetoothClass().getDeviceClass()
Hemant Guptab8d267a2016-06-07 12:53:59 +0530959 == BluetoothClass.Device.AUDIO_VIDEO_HANDSFREE ||
960 mDevice.getBluetoothClass().getDeviceClass()
961 == BluetoothClass.Device.AUDIO_VIDEO_WEARABLE_HEADSET) {
Sanket Padawe07533db2015-11-11 15:01:35 -0800962 setPhonebookPermissionChoice(CachedBluetoothDevice.ACCESS_ALLOWED);
963 } else {
964 setPhonebookPermissionChoice(CachedBluetoothDevice.ACCESS_REJECTED);
965 }
Sanket Padawe78c23762015-06-01 15:55:30 -0700966 }
Jason Monk7ce96b92015-02-02 11:27:58 -0500967 }
968 }
Jason Monkbe3c5db2015-02-04 13:00:55 -0500969
970 public int getMaxConnectionState() {
971 int maxState = BluetoothProfile.STATE_DISCONNECTED;
972 for (LocalBluetoothProfile profile : getProfiles()) {
973 int connectionStatus = getProfileConnectionState(profile);
974 if (connectionStatus > maxState) {
975 maxState = connectionStatus;
976 }
977 }
978 return maxState;
979 }
980
981 /**
982 * @return resource for string that discribes the connection state of this device.
timhypengf0509322018-03-29 14:23:21 +0800983 * case 1: idle or playing media, show "Active" on the only one A2DP active device.
984 * case 2: in phone call, show "Active" on the only one HFP active device
Jason Monkbe3c5db2015-02-04 13:00:55 -0500985 */
Jack He6258aae2017-06-29 17:01:23 -0700986 public String getConnectionSummary() {
timhypengf0509322018-03-29 14:23:21 +0800987 boolean profileConnected = false; // Updated as long as BluetoothProfile is connected
988 boolean a2dpConnected = true; // A2DP is connected
989 boolean hfpConnected = true; // HFP is connected
990 boolean hearingAidConnected = true; // Hearing Aid is connected
Jason Monkbe3c5db2015-02-04 13:00:55 -0500991
992 for (LocalBluetoothProfile profile : getProfiles()) {
993 int connectionStatus = getProfileConnectionState(profile);
994
995 switch (connectionStatus) {
996 case BluetoothProfile.STATE_CONNECTING:
997 case BluetoothProfile.STATE_DISCONNECTING:
Jack He6258aae2017-06-29 17:01:23 -0700998 return mContext.getString(Utils.getConnectionStateSummary(connectionStatus));
Jason Monkbe3c5db2015-02-04 13:00:55 -0500999
1000 case BluetoothProfile.STATE_CONNECTED:
1001 profileConnected = true;
1002 break;
1003
1004 case BluetoothProfile.STATE_DISCONNECTED:
1005 if (profile.isProfileReady()) {
Sanket Agarwalf8a1c912016-01-26 20:12:52 -08001006 if ((profile instanceof A2dpProfile) ||
timhypengf0509322018-03-29 14:23:21 +08001007 (profile instanceof A2dpSinkProfile)) {
1008 a2dpConnected = false;
Sanket Agarwalf8a1c912016-01-26 20:12:52 -08001009 } else if ((profile instanceof HeadsetProfile) ||
timhypengf0509322018-03-29 14:23:21 +08001010 (profile instanceof HfpClientProfile)) {
1011 hfpConnected = false;
1012 } else if (profile instanceof HearingAidProfile) {
1013 hearingAidConnected = false;
Jason Monkbe3c5db2015-02-04 13:00:55 -05001014 }
1015 }
1016 break;
1017 }
1018 }
1019
Jack He6258aae2017-06-29 17:01:23 -07001020 String batteryLevelPercentageString = null;
1021 // Android framework should only set mBatteryLevel to valid range [0-100] or
1022 // BluetoothDevice.BATTERY_LEVEL_UNKNOWN, any other value should be a framework bug.
1023 // Thus assume here that if value is not BluetoothDevice.BATTERY_LEVEL_UNKNOWN, it must
1024 // be valid
1025 final int batteryLevel = getBatteryLevel();
1026 if (batteryLevel != BluetoothDevice.BATTERY_LEVEL_UNKNOWN) {
1027 // TODO: name com.android.settingslib.bluetooth.Utils something different
1028 batteryLevelPercentageString =
1029 com.android.settingslib.Utils.formatPercentage(batteryLevel);
1030 }
1031
timhypengf0509322018-03-29 14:23:21 +08001032 int stringRes = R.string.bluetooth_pairing;
1033 //when profile is connected, information would be available
Jason Monkbe3c5db2015-02-04 13:00:55 -05001034 if (profileConnected) {
timhypengf0509322018-03-29 14:23:21 +08001035 if (a2dpConnected || hfpConnected || hearingAidConnected) {
1036 //contain battery information
Jack He6258aae2017-06-29 17:01:23 -07001037 if (batteryLevelPercentageString != null) {
timhypengf0509322018-03-29 14:23:21 +08001038 //device is in phone call
1039 if (com.android.settingslib.Utils.isAudioModeOngoingCall(mContext)) {
1040 if (mIsActiveDeviceHeadset) {
1041 stringRes = R.string.bluetooth_active_battery_level;
1042 } else {
1043 stringRes = R.string.bluetooth_battery_level;
1044 }
1045 } else {//device is not in phone call(ex. idle or playing media)
1046 //need to check if A2DP and HearingAid are exclusive
1047 if (mIsActiveDeviceHearingAid || mIsActiveDeviceA2dp) {
1048 stringRes = R.string.bluetooth_active_battery_level;
1049 } else {
1050 stringRes = R.string.bluetooth_battery_level;
1051 }
1052 }
Jack He6258aae2017-06-29 17:01:23 -07001053 } else {
timhypengf0509322018-03-29 14:23:21 +08001054 //no battery information
1055 if (com.android.settingslib.Utils.isAudioModeOngoingCall(mContext)) {
1056 if (mIsActiveDeviceHeadset) {
1057 stringRes = R.string.bluetooth_active_no_battery_level;
1058 }
1059 } else {
1060 if (mIsActiveDeviceHearingAid || mIsActiveDeviceA2dp) {
1061 stringRes = R.string.bluetooth_active_no_battery_level;
1062 }
1063 }
Jack He6258aae2017-06-29 17:01:23 -07001064 }
timhypengf0509322018-03-29 14:23:21 +08001065 } else {//unknown profile with battery information
Jack He6258aae2017-06-29 17:01:23 -07001066 if (batteryLevelPercentageString != null) {
timhypengf0509322018-03-29 14:23:21 +08001067 stringRes = R.string.bluetooth_battery_level;
Jack He6258aae2017-06-29 17:01:23 -07001068 }
Jason Monkbe3c5db2015-02-04 13:00:55 -05001069 }
1070 }
1071
timhypengf0509322018-03-29 14:23:21 +08001072 return (stringRes != R.string.bluetooth_pairing
1073 || getBondState() == BluetoothDevice.BOND_BONDING)
1074 ? mContext.getString(stringRes, batteryLevelPercentageString)
1075 : null;
Jason Monkbe3c5db2015-02-04 13:00:55 -05001076 }
hughchen23b947e2018-03-31 17:32:53 +08001077
1078 /**
1079 * @return {@code true} if {@code cachedBluetoothDevice} is a2dp device
1080 */
1081 public boolean isA2dpDevice() {
1082 return mProfileManager.getA2dpProfile().getConnectionStatus(mDevice) ==
1083 BluetoothProfile.STATE_CONNECTED;
1084 }
1085
1086 /**
1087 * @return {@code true} if {@code cachedBluetoothDevice} is HFP device
1088 */
1089 public boolean isHfpDevice() {
1090 return mProfileManager.getHeadsetProfile().getConnectionStatus(mDevice) ==
1091 BluetoothProfile.STATE_CONNECTED;
1092 }
Jason Monk7ce96b92015-02-02 11:27:58 -05001093}