blob: 8fa95977c5cec5a0bb8e99a439d61577d0574139 [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
Fan Zhang09c5a1d2018-09-04 13:56:51 -070019import android.bluetooth.BluetoothAdapter;
Jason Monk7ce96b92015-02-02 11:27:58 -050020import android.bluetooth.BluetoothClass;
21import android.bluetooth.BluetoothDevice;
Isha Bobrac3d94132018-02-08 16:04:36 -080022import android.bluetooth.BluetoothHearingAid;
Jason Monk7ce96b92015-02-02 11:27:58 -050023import android.bluetooth.BluetoothProfile;
24import android.bluetooth.BluetoothUuid;
25import android.content.Context;
26import android.content.SharedPreferences;
timhypengf0509322018-03-29 14:23:21 +080027import android.media.AudioManager;
Jason Monk7ce96b92015-02-02 11:27:58 -050028import android.os.ParcelUuid;
29import android.os.SystemClock;
30import android.text.TextUtils;
31import android.util.Log;
Fan Zhang09c5a1d2018-09-04 13:56:51 -070032
Aurimas Liutikasb8616dc2018-04-17 09:50:46 -070033import androidx.annotation.VisibleForTesting;
Jason Monk7ce96b92015-02-02 11:27:58 -050034
Jason Monkbe3c5db2015-02-04 13:00:55 -050035import com.android.settingslib.R;
36
Jason Monk7ce96b92015-02-02 11:27:58 -050037import java.util.ArrayList;
38import java.util.Collection;
39import java.util.Collections;
40import java.util.HashMap;
41import java.util.List;
42
43/**
44 * CachedBluetoothDevice represents a remote Bluetooth device. It contains
45 * attributes of the device (such as the address, name, RSSI, etc.) and
46 * functionality that can be performed on the device (connect, pair, disconnect,
47 * etc.).
48 */
Fan Zhang82dd3b02016-12-27 13:13:00 -080049public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> {
Jason Monk7ce96b92015-02-02 11:27:58 -050050 private static final String TAG = "CachedBluetoothDevice";
51 private static final boolean DEBUG = Utils.V;
52
53 private final Context mContext;
54 private final LocalBluetoothAdapter mLocalAdapter;
55 private final LocalBluetoothProfileManager mProfileManager;
timhypengf0509322018-03-29 14:23:21 +080056 private final AudioManager mAudioManager;
Jason Monk7ce96b92015-02-02 11:27:58 -050057 private final BluetoothDevice mDevice;
Jack Hec219bc92017-07-24 14:55:59 -070058 //TODO: consider remove, BluetoothDevice.getName() is already cached
Jason Monk7ce96b92015-02-02 11:27:58 -050059 private String mName;
Isha Bobrac3d94132018-02-08 16:04:36 -080060 private long mHiSyncId;
Jack Hec219bc92017-07-24 14:55:59 -070061 // Need this since there is no method for getting RSSI
Jason Monk7ce96b92015-02-02 11:27:58 -050062 private short mRssi;
Jack Hec219bc92017-07-24 14:55:59 -070063 //TODO: consider remove, BluetoothDevice.getBluetoothClass() is already cached
Jason Monk7ce96b92015-02-02 11:27:58 -050064 private BluetoothClass mBtClass;
65 private HashMap<LocalBluetoothProfile, Integer> mProfileConnectionState;
66
67 private final List<LocalBluetoothProfile> mProfiles =
68 new ArrayList<LocalBluetoothProfile>();
69
70 // List of profiles that were previously in mProfiles, but have been removed
71 private final List<LocalBluetoothProfile> mRemovedProfiles =
72 new ArrayList<LocalBluetoothProfile>();
73
74 // Device supports PANU but not NAP: remove PanProfile after device disconnects from NAP
75 private boolean mLocalNapRoleConnected;
76
Jack He51520472017-07-24 12:30:08 -070077 private boolean mJustDiscovered;
Jason Monk7ce96b92015-02-02 11:27:58 -050078
Jason Monk7ce96b92015-02-02 11:27:58 -050079 private int mMessageRejectionCount;
80
81 private final Collection<Callback> mCallbacks = new ArrayList<Callback>();
82
83 // Following constants indicate the user's choices of Phone book/message access settings
84 // User hasn't made any choice or settings app has wiped out the memory
85 public final static int ACCESS_UNKNOWN = 0;
86 // User has accepted the connection and let Settings app remember the decision
87 public final static int ACCESS_ALLOWED = 1;
88 // User has rejected the connection and let Settings app remember the decision
89 public final static int ACCESS_REJECTED = 2;
90
91 // How many times user should reject the connection to make the choice persist.
92 private final static int MESSAGE_REJECTION_COUNT_LIMIT_TO_PERSIST = 2;
93
94 private final static String MESSAGE_REJECTION_COUNT_PREFS_NAME = "bluetooth_message_reject";
95
96 /**
97 * When we connect to multiple profiles, we only want to display a single
98 * error even if they all fail. This tracks that state.
99 */
100 private boolean mIsConnectingErrorPossible;
101
Isha Bobrac3d94132018-02-08 16:04:36 -0800102 public long getHiSyncId() {
103 return mHiSyncId;
104 }
105
106 public void setHiSyncId(long id) {
107 if (Utils.D) {
108 Log.d(TAG, "setHiSyncId: mDevice " + mDevice + ", id " + id);
109 }
110 mHiSyncId = id;
111 }
112
Jason Monk7ce96b92015-02-02 11:27:58 -0500113 /**
114 * Last time a bt profile auto-connect was attempted.
115 * If an ACTION_UUID intent comes in within
116 * MAX_UUID_DELAY_FOR_AUTO_CONNECT milliseconds, we will try auto-connect
117 * again with the new UUIDs
118 */
119 private long mConnectAttempted;
120
121 // See mConnectAttempted
122 private static final long MAX_UUID_DELAY_FOR_AUTO_CONNECT = 5000;
Etan Cohen50d47612015-03-31 12:45:23 -0700123 private static final long MAX_HOGP_DELAY_FOR_AUTO_CONNECT = 30000;
Jason Monk7ce96b92015-02-02 11:27:58 -0500124
Pavlin Radoslavov1af33a12018-01-21 02:59:15 -0800125 // Active device state
126 private boolean mIsActiveDeviceA2dp = false;
127 private boolean mIsActiveDeviceHeadset = false;
Hansong Zhangd7b35912018-03-16 09:15:48 -0700128 private boolean mIsActiveDeviceHearingAid = false;
Jason Monk7ce96b92015-02-02 11:27:58 -0500129 /**
130 * Describes the current device and profile for logging.
131 *
132 * @param profile Profile to describe
133 * @return Description of the device and profile
134 */
135 private String describe(LocalBluetoothProfile profile) {
136 StringBuilder sb = new StringBuilder();
137 sb.append("Address:").append(mDevice);
138 if (profile != null) {
139 sb.append(" Profile:").append(profile);
140 }
141
142 return sb.toString();
143 }
144
145 void onProfileStateChanged(LocalBluetoothProfile profile, int newProfileState) {
146 if (Utils.D) {
147 Log.d(TAG, "onProfileStateChanged: profile " + profile +
148 " newProfileState " + newProfileState);
149 }
150 if (mLocalAdapter.getBluetoothState() == BluetoothAdapter.STATE_TURNING_OFF)
151 {
152 if (Utils.D) Log.d(TAG, " BT Turninig Off...Profile conn state change ignored...");
153 return;
154 }
155 mProfileConnectionState.put(profile, newProfileState);
156 if (newProfileState == BluetoothProfile.STATE_CONNECTED) {
157 if (profile instanceof MapProfile) {
158 profile.setPreferred(mDevice, true);
Hemant Guptadbc3d8d2017-05-12 21:14:44 +0530159 }
160 if (!mProfiles.contains(profile)) {
Jason Monk7ce96b92015-02-02 11:27:58 -0500161 mRemovedProfiles.remove(profile);
162 mProfiles.add(profile);
163 if (profile instanceof PanProfile &&
164 ((PanProfile) profile).isLocalRoleNap(mDevice)) {
165 // Device doesn't support NAP, so remove PanProfile on disconnect
166 mLocalNapRoleConnected = true;
167 }
168 }
169 } else if (profile instanceof MapProfile &&
170 newProfileState == BluetoothProfile.STATE_DISCONNECTED) {
171 profile.setPreferred(mDevice, false);
172 } else if (mLocalNapRoleConnected && profile instanceof PanProfile &&
173 ((PanProfile) profile).isLocalRoleNap(mDevice) &&
174 newProfileState == BluetoothProfile.STATE_DISCONNECTED) {
175 Log.d(TAG, "Removing PanProfile from device after NAP disconnect");
176 mProfiles.remove(profile);
177 mRemovedProfiles.add(profile);
178 mLocalNapRoleConnected = false;
179 }
Pavlin Radoslavov1af33a12018-01-21 02:59:15 -0800180 fetchActiveDevices();
Jason Monk7ce96b92015-02-02 11:27:58 -0500181 }
182
183 CachedBluetoothDevice(Context context,
184 LocalBluetoothAdapter adapter,
185 LocalBluetoothProfileManager profileManager,
186 BluetoothDevice device) {
187 mContext = context;
188 mLocalAdapter = adapter;
189 mProfileManager = profileManager;
timhypengf0509322018-03-29 14:23:21 +0800190 mAudioManager = context.getSystemService(AudioManager.class);
Jason Monk7ce96b92015-02-02 11:27:58 -0500191 mDevice = device;
192 mProfileConnectionState = new HashMap<LocalBluetoothProfile, Integer>();
193 fillData();
Isha Bobrac3d94132018-02-08 16:04:36 -0800194 mHiSyncId = BluetoothHearingAid.HI_SYNC_ID_INVALID;
Jason Monk7ce96b92015-02-02 11:27:58 -0500195 }
196
197 public void disconnect() {
198 for (LocalBluetoothProfile profile : mProfiles) {
199 disconnect(profile);
200 }
201 // Disconnect PBAP server in case its connected
202 // This is to ensure all the profiles are disconnected as some CK/Hs do not
203 // disconnect PBAP connection when HF connection is brought down
204 PbapServerProfile PbapProfile = mProfileManager.getPbapProfile();
205 if (PbapProfile.getConnectionStatus(mDevice) == BluetoothProfile.STATE_CONNECTED)
206 {
207 PbapProfile.disconnect(mDevice);
208 }
209 }
210
211 public void disconnect(LocalBluetoothProfile profile) {
212 if (profile.disconnect(mDevice)) {
213 if (Utils.D) {
214 Log.d(TAG, "Command sent successfully:DISCONNECT " + describe(profile));
215 }
216 }
217 }
218
219 public void connect(boolean connectAllProfiles) {
220 if (!ensurePaired()) {
221 return;
222 }
223
224 mConnectAttempted = SystemClock.elapsedRealtime();
225 connectWithoutResettingTimer(connectAllProfiles);
226 }
227
228 void onBondingDockConnect() {
229 // Attempt to connect if UUIDs are available. Otherwise,
230 // we will connect when the ACTION_UUID intent arrives.
231 connect(false);
232 }
233
234 private void connectWithoutResettingTimer(boolean connectAllProfiles) {
235 // Try to initialize the profiles if they were not.
236 if (mProfiles.isEmpty()) {
237 // if mProfiles is empty, then do not invoke updateProfiles. This causes a race
238 // condition with carkits during pairing, wherein RemoteDevice.UUIDs have been updated
239 // from bluetooth stack but ACTION.uuid is not sent yet.
240 // Eventually ACTION.uuid will be received which shall trigger the connection of the
241 // various profiles
242 // If UUIDs are not available yet, connect will be happen
243 // upon arrival of the ACTION_UUID intent.
244 Log.d(TAG, "No profiles. Maybe we will connect later");
245 return;
246 }
247
248 // Reset the only-show-one-error-dialog tracking variable
249 mIsConnectingErrorPossible = true;
250
251 int preferredProfiles = 0;
252 for (LocalBluetoothProfile profile : mProfiles) {
Leon Liao3b5e6bd2018-09-18 12:45:45 +0800253 if (connectAllProfiles ? profile.accessProfileEnabled() : profile.isAutoConnectable()) {
Jason Monk7ce96b92015-02-02 11:27:58 -0500254 if (profile.isPreferred(mDevice)) {
255 ++preferredProfiles;
256 connectInt(profile);
257 }
258 }
259 }
260 if (DEBUG) Log.d(TAG, "Preferred profiles = " + preferredProfiles);
261
262 if (preferredProfiles == 0) {
263 connectAutoConnectableProfiles();
264 }
265 }
266
267 private void connectAutoConnectableProfiles() {
268 if (!ensurePaired()) {
269 return;
270 }
271 // Reset the only-show-one-error-dialog tracking variable
272 mIsConnectingErrorPossible = true;
273
274 for (LocalBluetoothProfile profile : mProfiles) {
275 if (profile.isAutoConnectable()) {
276 profile.setPreferred(mDevice, true);
277 connectInt(profile);
278 }
279 }
280 }
281
282 /**
283 * Connect this device to the specified profile.
284 *
285 * @param profile the profile to use with the remote device
286 */
287 public void connectProfile(LocalBluetoothProfile profile) {
288 mConnectAttempted = SystemClock.elapsedRealtime();
289 // Reset the only-show-one-error-dialog tracking variable
290 mIsConnectingErrorPossible = true;
291 connectInt(profile);
292 // Refresh the UI based on profile.connect() call
293 refresh();
294 }
295
296 synchronized void connectInt(LocalBluetoothProfile profile) {
297 if (!ensurePaired()) {
298 return;
299 }
300 if (profile.connect(mDevice)) {
301 if (Utils.D) {
302 Log.d(TAG, "Command sent successfully:CONNECT " + describe(profile));
303 }
304 return;
305 }
306 Log.i(TAG, "Failed to connect " + profile.toString() + " to " + mName);
307 }
308
309 private boolean ensurePaired() {
310 if (getBondState() == BluetoothDevice.BOND_NONE) {
311 startPairing();
312 return false;
313 } else {
314 return true;
315 }
316 }
317
318 public boolean startPairing() {
319 // Pairing is unreliable while scanning, so cancel discovery
320 if (mLocalAdapter.isDiscovering()) {
321 mLocalAdapter.cancelDiscovery();
322 }
323
324 if (!mDevice.createBond()) {
325 return false;
326 }
327
Jason Monk7ce96b92015-02-02 11:27:58 -0500328 return true;
329 }
330
331 /**
332 * Return true if user initiated pairing on this device. The message text is
333 * slightly different for local vs. remote initiated pairing dialogs.
334 */
335 boolean isUserInitiatedPairing() {
Jakub Pawlowski0278ab92016-07-20 11:55:48 -0700336 return mDevice.isBondingInitiatedLocally();
Jason Monk7ce96b92015-02-02 11:27:58 -0500337 }
338
339 public void unpair() {
340 int state = getBondState();
341
342 if (state == BluetoothDevice.BOND_BONDING) {
343 mDevice.cancelBondProcess();
344 }
345
346 if (state != BluetoothDevice.BOND_NONE) {
347 final BluetoothDevice dev = mDevice;
348 if (dev != null) {
349 final boolean successful = dev.removeBond();
350 if (successful) {
351 if (Utils.D) {
352 Log.d(TAG, "Command sent successfully:REMOVE_BOND " + describe(null));
353 }
354 } else if (Utils.V) {
355 Log.v(TAG, "Framework rejected command immediately:REMOVE_BOND " +
Isha Bobrac3d94132018-02-08 16:04:36 -0800356 describe(null));
Jason Monk7ce96b92015-02-02 11:27:58 -0500357 }
358 }
359 }
360 }
361
362 public int getProfileConnectionState(LocalBluetoothProfile profile) {
xutianguo22bbb8192016-06-22 11:32:00 +0800363 if (mProfileConnectionState.get(profile) == null) {
Jason Monk7ce96b92015-02-02 11:27:58 -0500364 // If cache is empty make the binder call to get the state
365 int state = profile.getConnectionStatus(mDevice);
366 mProfileConnectionState.put(profile, state);
367 }
368 return mProfileConnectionState.get(profile);
369 }
370
371 public void clearProfileConnectionState ()
372 {
373 if (Utils.D) {
374 Log.d(TAG," Clearing all connection state for dev:" + mDevice.getName());
375 }
376 for (LocalBluetoothProfile profile :getProfiles()) {
377 mProfileConnectionState.put(profile, BluetoothProfile.STATE_DISCONNECTED);
378 }
379 }
380
381 // TODO: do any of these need to run async on a background thread?
382 private void fillData() {
383 fetchName();
384 fetchBtClass();
385 updateProfiles();
Pavlin Radoslavov1af33a12018-01-21 02:59:15 -0800386 fetchActiveDevices();
Jason Monk7ce96b92015-02-02 11:27:58 -0500387 migratePhonebookPermissionChoice();
388 migrateMessagePermissionChoice();
389 fetchMessageRejectionCount();
390
Jason Monk7ce96b92015-02-02 11:27:58 -0500391 dispatchAttributesChanged();
392 }
393
394 public BluetoothDevice getDevice() {
395 return mDevice;
396 }
397
Antony Sargent7ad051e2017-06-29 15:23:13 -0700398 /**
399 * Convenience method that can be mocked - it lets tests avoid having to call getDevice() which
400 * causes problems in tests since BluetoothDevice is final and cannot be mocked.
401 * @return the address of this device
402 */
403 public String getAddress() {
404 return mDevice.getAddress();
405 }
406
Jason Monk7ce96b92015-02-02 11:27:58 -0500407 public String getName() {
408 return mName;
409 }
410
411 /**
412 * Populate name from BluetoothDevice.ACTION_FOUND intent
413 */
414 void setNewName(String name) {
415 if (mName == null) {
416 mName = name;
417 if (mName == null || TextUtils.isEmpty(mName)) {
418 mName = mDevice.getAddress();
419 }
420 dispatchAttributesChanged();
421 }
422 }
423
424 /**
Jack Hec219bc92017-07-24 14:55:59 -0700425 * User changes the device name
426 * @param name new alias name to be set, should never be null
Jason Monk7ce96b92015-02-02 11:27:58 -0500427 */
428 public void setName(String name) {
Jack Hec219bc92017-07-24 14:55:59 -0700429 // Prevent mName to be set to null if setName(null) is called
430 if (name != null && !TextUtils.equals(name, mName)) {
Jason Monk7ce96b92015-02-02 11:27:58 -0500431 mName = name;
432 mDevice.setAlias(name);
433 dispatchAttributesChanged();
434 }
435 }
436
Hansong Zhang6a416322018-03-19 18:20:38 -0700437 /**
438 * Set this device as active device
439 * @return true if at least one profile on this device is set to active, false otherwise
440 */
441 public boolean setActive() {
442 boolean result = false;
443 A2dpProfile a2dpProfile = mProfileManager.getA2dpProfile();
444 if (a2dpProfile != null && isConnectedProfile(a2dpProfile)) {
445 if (a2dpProfile.setActiveDevice(getDevice())) {
446 Log.i(TAG, "OnPreferenceClickListener: A2DP active device=" + this);
447 result = true;
448 }
449 }
450 HeadsetProfile headsetProfile = mProfileManager.getHeadsetProfile();
451 if ((headsetProfile != null) && isConnectedProfile(headsetProfile)) {
452 if (headsetProfile.setActiveDevice(getDevice())) {
453 Log.i(TAG, "OnPreferenceClickListener: Headset active device=" + this);
454 result = true;
455 }
456 }
Hansong Zhangd7b35912018-03-16 09:15:48 -0700457 HearingAidProfile hearingAidProfile = mProfileManager.getHearingAidProfile();
458 if ((hearingAidProfile != null) && isConnectedProfile(hearingAidProfile)) {
459 if (hearingAidProfile.setActiveDevice(getDevice())) {
460 Log.i(TAG, "OnPreferenceClickListener: Hearing Aid active device=" + this);
461 result = true;
462 }
463 }
Hansong Zhang6a416322018-03-19 18:20:38 -0700464 return result;
465 }
466
Jason Monk7ce96b92015-02-02 11:27:58 -0500467 void refreshName() {
468 fetchName();
469 dispatchAttributesChanged();
470 }
471
472 private void fetchName() {
473 mName = mDevice.getAliasName();
474
475 if (TextUtils.isEmpty(mName)) {
476 mName = mDevice.getAddress();
477 if (DEBUG) Log.d(TAG, "Device has no name (yet), use address: " + mName);
478 }
479 }
480
Jack He6258aae2017-06-29 17:01:23 -0700481 /**
Jack Hec219bc92017-07-24 14:55:59 -0700482 * Checks if device has a human readable name besides MAC address
483 * @return true if device's alias name is not null nor empty, false otherwise
484 */
485 public boolean hasHumanReadableName() {
486 return !TextUtils.isEmpty(mDevice.getAliasName());
487 }
488
489 /**
Jack He6258aae2017-06-29 17:01:23 -0700490 * Get battery level from remote device
491 * @return battery level in percentage [0-100], or {@link BluetoothDevice#BATTERY_LEVEL_UNKNOWN}
492 */
493 public int getBatteryLevel() {
494 return mDevice.getBatteryLevel();
495 }
496
Jason Monk7ce96b92015-02-02 11:27:58 -0500497 void refresh() {
498 dispatchAttributesChanged();
499 }
500
Jack He51520472017-07-24 12:30:08 -0700501 public void setJustDiscovered(boolean justDiscovered) {
502 if (mJustDiscovered != justDiscovered) {
503 mJustDiscovered = justDiscovered;
Jason Monk7ce96b92015-02-02 11:27:58 -0500504 dispatchAttributesChanged();
505 }
506 }
507
508 public int getBondState() {
509 return mDevice.getBondState();
510 }
511
Pavlin Radoslavov1af33a12018-01-21 02:59:15 -0800512 /**
Pavlin Radoslavovc285d552018-02-06 16:14:00 -0800513 * Update the device status as active or non-active per Bluetooth profile.
Pavlin Radoslavov1af33a12018-01-21 02:59:15 -0800514 *
515 * @param isActive true if the device is active
516 * @param bluetoothProfile the Bluetooth profile
517 */
Pavlin Radoslavovc285d552018-02-06 16:14:00 -0800518 public void onActiveDeviceChanged(boolean isActive, int bluetoothProfile) {
Pavlin Radoslavov1af33a12018-01-21 02:59:15 -0800519 boolean changed = false;
520 switch (bluetoothProfile) {
521 case BluetoothProfile.A2DP:
522 changed = (mIsActiveDeviceA2dp != isActive);
523 mIsActiveDeviceA2dp = isActive;
524 break;
525 case BluetoothProfile.HEADSET:
526 changed = (mIsActiveDeviceHeadset != isActive);
527 mIsActiveDeviceHeadset = isActive;
528 break;
Hansong Zhangd7b35912018-03-16 09:15:48 -0700529 case BluetoothProfile.HEARING_AID:
530 changed = (mIsActiveDeviceHearingAid != isActive);
531 mIsActiveDeviceHearingAid = isActive;
532 break;
Pavlin Radoslavov1af33a12018-01-21 02:59:15 -0800533 default:
Pavlin Radoslavovc285d552018-02-06 16:14:00 -0800534 Log.w(TAG, "onActiveDeviceChanged: unknown profile " + bluetoothProfile +
Pavlin Radoslavov1af33a12018-01-21 02:59:15 -0800535 " isActive " + isActive);
536 break;
537 }
538 if (changed) {
539 dispatchAttributesChanged();
540 }
541 }
542
Pavlin Radoslavovc285d552018-02-06 16:14:00 -0800543 /**
timhypengf0509322018-03-29 14:23:21 +0800544 * Update the profile audio state.
545 */
546 void onAudioModeChanged() {
547 dispatchAttributesChanged();
548 }
549 /**
Pavlin Radoslavovc285d552018-02-06 16:14:00 -0800550 * Get the device status as active or non-active per Bluetooth profile.
551 *
552 * @param bluetoothProfile the Bluetooth profile
553 * @return true if the device is active
554 */
555 @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
556 public boolean isActiveDevice(int bluetoothProfile) {
557 switch (bluetoothProfile) {
558 case BluetoothProfile.A2DP:
559 return mIsActiveDeviceA2dp;
560 case BluetoothProfile.HEADSET:
561 return mIsActiveDeviceHeadset;
Hansong Zhangd7b35912018-03-16 09:15:48 -0700562 case BluetoothProfile.HEARING_AID:
563 return mIsActiveDeviceHearingAid;
Pavlin Radoslavovc285d552018-02-06 16:14:00 -0800564 default:
565 Log.w(TAG, "getActiveDevice: unknown profile " + bluetoothProfile);
566 break;
567 }
568 return false;
569 }
570
Jason Monk7ce96b92015-02-02 11:27:58 -0500571 void setRssi(short rssi) {
572 if (mRssi != rssi) {
573 mRssi = rssi;
574 dispatchAttributesChanged();
575 }
576 }
577
578 /**
579 * Checks whether we are connected to this device (any profile counts).
580 *
581 * @return Whether it is connected.
582 */
583 public boolean isConnected() {
584 for (LocalBluetoothProfile profile : mProfiles) {
585 int status = getProfileConnectionState(profile);
586 if (status == BluetoothProfile.STATE_CONNECTED) {
587 return true;
588 }
589 }
590
591 return false;
592 }
593
594 public boolean isConnectedProfile(LocalBluetoothProfile profile) {
595 int status = getProfileConnectionState(profile);
596 return status == BluetoothProfile.STATE_CONNECTED;
597
598 }
599
600 public boolean isBusy() {
601 for (LocalBluetoothProfile profile : mProfiles) {
602 int status = getProfileConnectionState(profile);
603 if (status == BluetoothProfile.STATE_CONNECTING
604 || status == BluetoothProfile.STATE_DISCONNECTING) {
605 return true;
606 }
607 }
608 return getBondState() == BluetoothDevice.BOND_BONDING;
609 }
610
611 /**
612 * Fetches a new value for the cached BT class.
613 */
614 private void fetchBtClass() {
615 mBtClass = mDevice.getBluetoothClass();
616 }
617
618 private boolean updateProfiles() {
619 ParcelUuid[] uuids = mDevice.getUuids();
620 if (uuids == null) return false;
621
622 ParcelUuid[] localUuids = mLocalAdapter.getUuids();
623 if (localUuids == null) return false;
624
Jack Hec219bc92017-07-24 14:55:59 -0700625 /*
Jason Monk7ce96b92015-02-02 11:27:58 -0500626 * Now we know if the device supports PBAP, update permissions...
627 */
628 processPhonebookAccess();
629
630 mProfileManager.updateProfiles(uuids, localUuids, mProfiles, mRemovedProfiles,
631 mLocalNapRoleConnected, mDevice);
632
633 if (DEBUG) {
634 Log.e(TAG, "updating profiles for " + mDevice.getAliasName());
635 BluetoothClass bluetoothClass = mDevice.getBluetoothClass();
636
637 if (bluetoothClass != null) Log.v(TAG, "Class: " + bluetoothClass.toString());
638 Log.v(TAG, "UUID:");
639 for (ParcelUuid uuid : uuids) {
640 Log.v(TAG, " " + uuid);
641 }
642 }
643 return true;
644 }
645
Pavlin Radoslavov1af33a12018-01-21 02:59:15 -0800646 private void fetchActiveDevices() {
647 A2dpProfile a2dpProfile = mProfileManager.getA2dpProfile();
648 if (a2dpProfile != null) {
649 mIsActiveDeviceA2dp = mDevice.equals(a2dpProfile.getActiveDevice());
650 }
651 HeadsetProfile headsetProfile = mProfileManager.getHeadsetProfile();
652 if (headsetProfile != null) {
653 mIsActiveDeviceHeadset = mDevice.equals(headsetProfile.getActiveDevice());
654 }
Hansong Zhangd7b35912018-03-16 09:15:48 -0700655 HearingAidProfile hearingAidProfile = mProfileManager.getHearingAidProfile();
656 if (hearingAidProfile != null) {
Hansong Zhang3b8f09b2018-03-28 16:53:10 -0700657 mIsActiveDeviceHearingAid = hearingAidProfile.getActiveDevices().contains(mDevice);
Hansong Zhangd7b35912018-03-16 09:15:48 -0700658 }
Pavlin Radoslavov1af33a12018-01-21 02:59:15 -0800659 }
660
Jason Monk7ce96b92015-02-02 11:27:58 -0500661 /**
662 * Refreshes the UI for the BT class, including fetching the latest value
663 * for the class.
664 */
665 void refreshBtClass() {
666 fetchBtClass();
667 dispatchAttributesChanged();
668 }
669
670 /**
671 * Refreshes the UI when framework alerts us of a UUID change.
672 */
673 void onUuidChanged() {
674 updateProfiles();
Etan Cohen50d47612015-03-31 12:45:23 -0700675 ParcelUuid[] uuids = mDevice.getUuids();
Venkat Raghavan05e08c32015-04-06 16:26:11 -0700676
Etan Cohen50d47612015-03-31 12:45:23 -0700677 long timeout = MAX_UUID_DELAY_FOR_AUTO_CONNECT;
Venkat Raghavan05e08c32015-04-06 16:26:11 -0700678 if (BluetoothUuid.isUuidPresent(uuids, BluetoothUuid.Hogp)) {
679 timeout = MAX_HOGP_DELAY_FOR_AUTO_CONNECT;
680 }
Jason Monk7ce96b92015-02-02 11:27:58 -0500681
682 if (DEBUG) {
Etan Cohen50d47612015-03-31 12:45:23 -0700683 Log.d(TAG, "onUuidChanged: Time since last connect"
Jason Monk7ce96b92015-02-02 11:27:58 -0500684 + (SystemClock.elapsedRealtime() - mConnectAttempted));
685 }
686
687 /*
Venkat Raghavan05e08c32015-04-06 16:26:11 -0700688 * If a connect was attempted earlier without any UUID, we will do the connect now.
689 * Otherwise, allow the connect on UUID change.
Jason Monk7ce96b92015-02-02 11:27:58 -0500690 */
691 if (!mProfiles.isEmpty()
Andre Eisenbach7be83c52015-09-03 15:20:09 -0700692 && ((mConnectAttempted + timeout) > SystemClock.elapsedRealtime())) {
Jason Monk7ce96b92015-02-02 11:27:58 -0500693 connectWithoutResettingTimer(false);
694 }
Andre Eisenbach7be83c52015-09-03 15:20:09 -0700695
Jason Monk7ce96b92015-02-02 11:27:58 -0500696 dispatchAttributesChanged();
697 }
698
699 void onBondingStateChanged(int bondState) {
700 if (bondState == BluetoothDevice.BOND_NONE) {
701 mProfiles.clear();
Jason Monk7ce96b92015-02-02 11:27:58 -0500702 setPhonebookPermissionChoice(ACCESS_UNKNOWN);
703 setMessagePermissionChoice(ACCESS_UNKNOWN);
Casper Bonde424681e2015-05-04 22:07:45 -0700704 setSimPermissionChoice(ACCESS_UNKNOWN);
Jason Monk7ce96b92015-02-02 11:27:58 -0500705 mMessageRejectionCount = 0;
706 saveMessageRejectionCount();
707 }
708
709 refresh();
710
711 if (bondState == BluetoothDevice.BOND_BONDED) {
712 if (mDevice.isBluetoothDock()) {
713 onBondingDockConnect();
Jakub Pawlowski0278ab92016-07-20 11:55:48 -0700714 } else if (mDevice.isBondingInitiatedLocally()) {
Jason Monk7ce96b92015-02-02 11:27:58 -0500715 connect(false);
716 }
Jason Monk7ce96b92015-02-02 11:27:58 -0500717 }
718 }
719
720 void setBtClass(BluetoothClass btClass) {
721 if (btClass != null && mBtClass != btClass) {
722 mBtClass = btClass;
723 dispatchAttributesChanged();
724 }
725 }
726
727 public BluetoothClass getBtClass() {
728 return mBtClass;
729 }
730
731 public List<LocalBluetoothProfile> getProfiles() {
732 return Collections.unmodifiableList(mProfiles);
733 }
734
735 public List<LocalBluetoothProfile> getConnectableProfiles() {
736 List<LocalBluetoothProfile> connectableProfiles =
737 new ArrayList<LocalBluetoothProfile>();
738 for (LocalBluetoothProfile profile : mProfiles) {
Leon Liao3b5e6bd2018-09-18 12:45:45 +0800739 if (profile.accessProfileEnabled()) {
Jason Monk7ce96b92015-02-02 11:27:58 -0500740 connectableProfiles.add(profile);
741 }
742 }
743 return connectableProfiles;
744 }
745
746 public List<LocalBluetoothProfile> getRemovedProfiles() {
747 return mRemovedProfiles;
748 }
749
750 public void registerCallback(Callback callback) {
751 synchronized (mCallbacks) {
752 mCallbacks.add(callback);
753 }
754 }
755
756 public void unregisterCallback(Callback callback) {
757 synchronized (mCallbacks) {
758 mCallbacks.remove(callback);
759 }
760 }
761
762 private void dispatchAttributesChanged() {
763 synchronized (mCallbacks) {
764 for (Callback callback : mCallbacks) {
765 callback.onDeviceAttributesChanged();
766 }
767 }
768 }
769
770 @Override
771 public String toString() {
772 return mDevice.toString();
773 }
774
775 @Override
776 public boolean equals(Object o) {
777 if ((o == null) || !(o instanceof CachedBluetoothDevice)) {
778 return false;
779 }
780 return mDevice.equals(((CachedBluetoothDevice) o).mDevice);
781 }
782
783 @Override
784 public int hashCode() {
785 return mDevice.getAddress().hashCode();
786 }
787
788 // This comparison uses non-final fields so the sort order may change
789 // when device attributes change (such as bonding state). Settings
790 // will completely refresh the device list when this happens.
791 public int compareTo(CachedBluetoothDevice another) {
792 // Connected above not connected
793 int comparison = (another.isConnected() ? 1 : 0) - (isConnected() ? 1 : 0);
794 if (comparison != 0) return comparison;
795
796 // Paired above not paired
797 comparison = (another.getBondState() == BluetoothDevice.BOND_BONDED ? 1 : 0) -
798 (getBondState() == BluetoothDevice.BOND_BONDED ? 1 : 0);
799 if (comparison != 0) return comparison;
800
Jack He51520472017-07-24 12:30:08 -0700801 // Just discovered above discovered in the past
802 comparison = (another.mJustDiscovered ? 1 : 0) - (mJustDiscovered ? 1 : 0);
Jason Monk7ce96b92015-02-02 11:27:58 -0500803 if (comparison != 0) return comparison;
804
805 // Stronger signal above weaker signal
806 comparison = another.mRssi - mRssi;
807 if (comparison != 0) return comparison;
808
809 // Fallback on name
810 return mName.compareTo(another.mName);
811 }
812
813 public interface Callback {
814 void onDeviceAttributesChanged();
815 }
816
817 public int getPhonebookPermissionChoice() {
818 int permission = mDevice.getPhonebookAccessPermission();
819 if (permission == BluetoothDevice.ACCESS_ALLOWED) {
820 return ACCESS_ALLOWED;
821 } else if (permission == BluetoothDevice.ACCESS_REJECTED) {
822 return ACCESS_REJECTED;
823 }
824 return ACCESS_UNKNOWN;
825 }
826
827 public void setPhonebookPermissionChoice(int permissionChoice) {
828 int permission = BluetoothDevice.ACCESS_UNKNOWN;
829 if (permissionChoice == ACCESS_ALLOWED) {
830 permission = BluetoothDevice.ACCESS_ALLOWED;
831 } else if (permissionChoice == ACCESS_REJECTED) {
832 permission = BluetoothDevice.ACCESS_REJECTED;
833 }
834 mDevice.setPhonebookAccessPermission(permission);
835 }
836
837 // Migrates data from old data store (in Settings app's shared preferences) to new (in Bluetooth
838 // app's shared preferences).
839 private void migratePhonebookPermissionChoice() {
840 SharedPreferences preferences = mContext.getSharedPreferences(
841 "bluetooth_phonebook_permission", Context.MODE_PRIVATE);
842 if (!preferences.contains(mDevice.getAddress())) {
843 return;
844 }
845
846 if (mDevice.getPhonebookAccessPermission() == BluetoothDevice.ACCESS_UNKNOWN) {
847 int oldPermission = preferences.getInt(mDevice.getAddress(), ACCESS_UNKNOWN);
848 if (oldPermission == ACCESS_ALLOWED) {
849 mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_ALLOWED);
850 } else if (oldPermission == ACCESS_REJECTED) {
851 mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_REJECTED);
852 }
853 }
854
855 SharedPreferences.Editor editor = preferences.edit();
856 editor.remove(mDevice.getAddress());
857 editor.commit();
858 }
859
860 public int getMessagePermissionChoice() {
861 int permission = mDevice.getMessageAccessPermission();
862 if (permission == BluetoothDevice.ACCESS_ALLOWED) {
863 return ACCESS_ALLOWED;
864 } else if (permission == BluetoothDevice.ACCESS_REJECTED) {
865 return ACCESS_REJECTED;
866 }
867 return ACCESS_UNKNOWN;
868 }
869
870 public void setMessagePermissionChoice(int permissionChoice) {
871 int permission = BluetoothDevice.ACCESS_UNKNOWN;
872 if (permissionChoice == ACCESS_ALLOWED) {
873 permission = BluetoothDevice.ACCESS_ALLOWED;
874 } else if (permissionChoice == ACCESS_REJECTED) {
875 permission = BluetoothDevice.ACCESS_REJECTED;
876 }
877 mDevice.setMessageAccessPermission(permission);
878 }
879
Casper Bonde424681e2015-05-04 22:07:45 -0700880 public int getSimPermissionChoice() {
881 int permission = mDevice.getSimAccessPermission();
882 if (permission == BluetoothDevice.ACCESS_ALLOWED) {
883 return ACCESS_ALLOWED;
884 } else if (permission == BluetoothDevice.ACCESS_REJECTED) {
885 return ACCESS_REJECTED;
886 }
887 return ACCESS_UNKNOWN;
888 }
889
890 void setSimPermissionChoice(int permissionChoice) {
891 int permission = BluetoothDevice.ACCESS_UNKNOWN;
892 if (permissionChoice == ACCESS_ALLOWED) {
893 permission = BluetoothDevice.ACCESS_ALLOWED;
894 } else if (permissionChoice == ACCESS_REJECTED) {
895 permission = BluetoothDevice.ACCESS_REJECTED;
896 }
897 mDevice.setSimAccessPermission(permission);
898 }
899
Jason Monk7ce96b92015-02-02 11:27:58 -0500900 // Migrates data from old data store (in Settings app's shared preferences) to new (in Bluetooth
901 // app's shared preferences).
902 private void migrateMessagePermissionChoice() {
903 SharedPreferences preferences = mContext.getSharedPreferences(
904 "bluetooth_message_permission", Context.MODE_PRIVATE);
905 if (!preferences.contains(mDevice.getAddress())) {
906 return;
907 }
908
909 if (mDevice.getMessageAccessPermission() == BluetoothDevice.ACCESS_UNKNOWN) {
910 int oldPermission = preferences.getInt(mDevice.getAddress(), ACCESS_UNKNOWN);
911 if (oldPermission == ACCESS_ALLOWED) {
912 mDevice.setMessageAccessPermission(BluetoothDevice.ACCESS_ALLOWED);
913 } else if (oldPermission == ACCESS_REJECTED) {
914 mDevice.setMessageAccessPermission(BluetoothDevice.ACCESS_REJECTED);
915 }
916 }
917
918 SharedPreferences.Editor editor = preferences.edit();
919 editor.remove(mDevice.getAddress());
920 editor.commit();
921 }
922
923 /**
924 * @return Whether this rejection should persist.
925 */
926 public boolean checkAndIncreaseMessageRejectionCount() {
927 if (mMessageRejectionCount < MESSAGE_REJECTION_COUNT_LIMIT_TO_PERSIST) {
928 mMessageRejectionCount++;
929 saveMessageRejectionCount();
930 }
931 return mMessageRejectionCount >= MESSAGE_REJECTION_COUNT_LIMIT_TO_PERSIST;
932 }
933
934 private void fetchMessageRejectionCount() {
935 SharedPreferences preference = mContext.getSharedPreferences(
936 MESSAGE_REJECTION_COUNT_PREFS_NAME, Context.MODE_PRIVATE);
937 mMessageRejectionCount = preference.getInt(mDevice.getAddress(), 0);
938 }
939
940 private void saveMessageRejectionCount() {
941 SharedPreferences.Editor editor = mContext.getSharedPreferences(
942 MESSAGE_REJECTION_COUNT_PREFS_NAME, Context.MODE_PRIVATE).edit();
943 if (mMessageRejectionCount == 0) {
944 editor.remove(mDevice.getAddress());
945 } else {
946 editor.putInt(mDevice.getAddress(), mMessageRejectionCount);
947 }
948 editor.commit();
949 }
950
951 private void processPhonebookAccess() {
952 if (mDevice.getBondState() != BluetoothDevice.BOND_BONDED) return;
953
954 ParcelUuid[] uuids = mDevice.getUuids();
955 if (BluetoothUuid.containsAnyUuid(uuids, PbapServerProfile.PBAB_CLIENT_UUIDS)) {
956 // The pairing dialog now warns of phone-book access for paired devices.
957 // No separate prompt is displayed after pairing.
Sanket Padawe78c23762015-06-01 15:55:30 -0700958 if (getPhonebookPermissionChoice() == CachedBluetoothDevice.ACCESS_UNKNOWN) {
Sanket Padawe07533db2015-11-11 15:01:35 -0800959 if (mDevice.getBluetoothClass().getDeviceClass()
Hemant Guptab8d267a2016-06-07 12:53:59 +0530960 == BluetoothClass.Device.AUDIO_VIDEO_HANDSFREE ||
961 mDevice.getBluetoothClass().getDeviceClass()
962 == BluetoothClass.Device.AUDIO_VIDEO_WEARABLE_HEADSET) {
Sanket Padawe07533db2015-11-11 15:01:35 -0800963 setPhonebookPermissionChoice(CachedBluetoothDevice.ACCESS_ALLOWED);
964 } else {
965 setPhonebookPermissionChoice(CachedBluetoothDevice.ACCESS_REJECTED);
966 }
Sanket Padawe78c23762015-06-01 15:55:30 -0700967 }
Jason Monk7ce96b92015-02-02 11:27:58 -0500968 }
969 }
Jason Monkbe3c5db2015-02-04 13:00:55 -0500970
971 public int getMaxConnectionState() {
972 int maxState = BluetoothProfile.STATE_DISCONNECTED;
973 for (LocalBluetoothProfile profile : getProfiles()) {
974 int connectionStatus = getProfileConnectionState(profile);
975 if (connectionStatus > maxState) {
976 maxState = connectionStatus;
977 }
978 }
979 return maxState;
980 }
981
982 /**
983 * @return resource for string that discribes the connection state of this device.
timhypengf0509322018-03-29 14:23:21 +0800984 * case 1: idle or playing media, show "Active" on the only one A2DP active device.
985 * case 2: in phone call, show "Active" on the only one HFP active device
Jason Monkbe3c5db2015-02-04 13:00:55 -0500986 */
Jack He6258aae2017-06-29 17:01:23 -0700987 public String getConnectionSummary() {
timhypengf0509322018-03-29 14:23:21 +0800988 boolean profileConnected = false; // Updated as long as BluetoothProfile is connected
989 boolean a2dpConnected = true; // A2DP is connected
990 boolean hfpConnected = true; // HFP is connected
991 boolean hearingAidConnected = true; // Hearing Aid is connected
Jason Monkbe3c5db2015-02-04 13:00:55 -0500992
993 for (LocalBluetoothProfile profile : getProfiles()) {
994 int connectionStatus = getProfileConnectionState(profile);
995
996 switch (connectionStatus) {
997 case BluetoothProfile.STATE_CONNECTING:
998 case BluetoothProfile.STATE_DISCONNECTING:
Jack He6258aae2017-06-29 17:01:23 -0700999 return mContext.getString(Utils.getConnectionStateSummary(connectionStatus));
Jason Monkbe3c5db2015-02-04 13:00:55 -05001000
1001 case BluetoothProfile.STATE_CONNECTED:
1002 profileConnected = true;
1003 break;
1004
1005 case BluetoothProfile.STATE_DISCONNECTED:
1006 if (profile.isProfileReady()) {
Sanket Agarwalf8a1c912016-01-26 20:12:52 -08001007 if ((profile instanceof A2dpProfile) ||
timhypengf0509322018-03-29 14:23:21 +08001008 (profile instanceof A2dpSinkProfile)) {
1009 a2dpConnected = false;
Sanket Agarwalf8a1c912016-01-26 20:12:52 -08001010 } else if ((profile instanceof HeadsetProfile) ||
timhypengf0509322018-03-29 14:23:21 +08001011 (profile instanceof HfpClientProfile)) {
1012 hfpConnected = false;
1013 } else if (profile instanceof HearingAidProfile) {
1014 hearingAidConnected = false;
Jason Monkbe3c5db2015-02-04 13:00:55 -05001015 }
1016 }
1017 break;
1018 }
1019 }
1020
Jack He6258aae2017-06-29 17:01:23 -07001021 String batteryLevelPercentageString = null;
1022 // Android framework should only set mBatteryLevel to valid range [0-100] or
1023 // BluetoothDevice.BATTERY_LEVEL_UNKNOWN, any other value should be a framework bug.
1024 // Thus assume here that if value is not BluetoothDevice.BATTERY_LEVEL_UNKNOWN, it must
1025 // be valid
1026 final int batteryLevel = getBatteryLevel();
1027 if (batteryLevel != BluetoothDevice.BATTERY_LEVEL_UNKNOWN) {
1028 // TODO: name com.android.settingslib.bluetooth.Utils something different
1029 batteryLevelPercentageString =
1030 com.android.settingslib.Utils.formatPercentage(batteryLevel);
1031 }
1032
timhypengf0509322018-03-29 14:23:21 +08001033 int stringRes = R.string.bluetooth_pairing;
1034 //when profile is connected, information would be available
Jason Monkbe3c5db2015-02-04 13:00:55 -05001035 if (profileConnected) {
timhypengf0509322018-03-29 14:23:21 +08001036 if (a2dpConnected || hfpConnected || hearingAidConnected) {
1037 //contain battery information
Jack He6258aae2017-06-29 17:01:23 -07001038 if (batteryLevelPercentageString != null) {
timhypengf0509322018-03-29 14:23:21 +08001039 //device is in phone call
1040 if (com.android.settingslib.Utils.isAudioModeOngoingCall(mContext)) {
1041 if (mIsActiveDeviceHeadset) {
1042 stringRes = R.string.bluetooth_active_battery_level;
1043 } else {
1044 stringRes = R.string.bluetooth_battery_level;
1045 }
1046 } else {//device is not in phone call(ex. idle or playing media)
1047 //need to check if A2DP and HearingAid are exclusive
1048 if (mIsActiveDeviceHearingAid || mIsActiveDeviceA2dp) {
1049 stringRes = R.string.bluetooth_active_battery_level;
1050 } else {
1051 stringRes = R.string.bluetooth_battery_level;
1052 }
1053 }
Jack He6258aae2017-06-29 17:01:23 -07001054 } else {
timhypengf0509322018-03-29 14:23:21 +08001055 //no battery information
1056 if (com.android.settingslib.Utils.isAudioModeOngoingCall(mContext)) {
1057 if (mIsActiveDeviceHeadset) {
1058 stringRes = R.string.bluetooth_active_no_battery_level;
1059 }
1060 } else {
1061 if (mIsActiveDeviceHearingAid || mIsActiveDeviceA2dp) {
1062 stringRes = R.string.bluetooth_active_no_battery_level;
1063 }
1064 }
Jack He6258aae2017-06-29 17:01:23 -07001065 }
timhypengf0509322018-03-29 14:23:21 +08001066 } else {//unknown profile with battery information
Jack He6258aae2017-06-29 17:01:23 -07001067 if (batteryLevelPercentageString != null) {
timhypengf0509322018-03-29 14:23:21 +08001068 stringRes = R.string.bluetooth_battery_level;
Jack He6258aae2017-06-29 17:01:23 -07001069 }
Jason Monkbe3c5db2015-02-04 13:00:55 -05001070 }
1071 }
1072
timhypengf0509322018-03-29 14:23:21 +08001073 return (stringRes != R.string.bluetooth_pairing
1074 || getBondState() == BluetoothDevice.BOND_BONDING)
1075 ? mContext.getString(stringRes, batteryLevelPercentageString)
1076 : null;
Jason Monkbe3c5db2015-02-04 13:00:55 -05001077 }
hughchen23b947e2018-03-31 17:32:53 +08001078
1079 /**
ryanywline26aecd2018-05-15 14:20:50 +08001080 * @return resource for android auto string that describes the connection state of this device.
1081 */
1082 public String getCarConnectionSummary() {
1083 boolean profileConnected = false; // at least one profile is connected
1084 boolean a2dpNotConnected = false; // A2DP is preferred but not connected
1085 boolean hfpNotConnected = false; // HFP is preferred but not connected
1086 boolean hearingAidNotConnected = false; // Hearing Aid is preferred but not connected
1087
1088 for (LocalBluetoothProfile profile : getProfiles()) {
1089 int connectionStatus = getProfileConnectionState(profile);
1090
1091 switch (connectionStatus) {
1092 case BluetoothProfile.STATE_CONNECTING:
1093 case BluetoothProfile.STATE_DISCONNECTING:
1094 return mContext.getString(Utils.getConnectionStateSummary(connectionStatus));
1095
1096 case BluetoothProfile.STATE_CONNECTED:
1097 profileConnected = true;
1098 break;
1099
1100 case BluetoothProfile.STATE_DISCONNECTED:
1101 if (profile.isProfileReady()) {
1102 if ((profile instanceof A2dpProfile) ||
1103 (profile instanceof A2dpSinkProfile)){
1104 a2dpNotConnected = true;
1105 } else if ((profile instanceof HeadsetProfile) ||
1106 (profile instanceof HfpClientProfile)) {
1107 hfpNotConnected = true;
1108 } else if (profile instanceof HearingAidProfile) {
1109 hearingAidNotConnected = true;
1110 }
1111 }
1112 break;
1113 }
1114 }
1115
1116 String batteryLevelPercentageString = null;
1117 // Android framework should only set mBatteryLevel to valid range [0-100] or
1118 // BluetoothDevice.BATTERY_LEVEL_UNKNOWN, any other value should be a framework bug.
1119 // Thus assume here that if value is not BluetoothDevice.BATTERY_LEVEL_UNKNOWN, it must
1120 // be valid
1121 final int batteryLevel = getBatteryLevel();
1122 if (batteryLevel != BluetoothDevice.BATTERY_LEVEL_UNKNOWN) {
1123 // TODO: name com.android.settingslib.bluetooth.Utils something different
1124 batteryLevelPercentageString =
1125 com.android.settingslib.Utils.formatPercentage(batteryLevel);
1126 }
1127
1128 // Prepare the string for the Active Device summary
1129 String[] activeDeviceStringsArray = mContext.getResources().getStringArray(
1130 R.array.bluetooth_audio_active_device_summaries);
1131 String activeDeviceString = activeDeviceStringsArray[0]; // Default value: not active
1132 if (mIsActiveDeviceA2dp && mIsActiveDeviceHeadset) {
1133 activeDeviceString = activeDeviceStringsArray[1]; // Active for Media and Phone
1134 } else {
1135 if (mIsActiveDeviceA2dp) {
1136 activeDeviceString = activeDeviceStringsArray[2]; // Active for Media only
1137 }
1138 if (mIsActiveDeviceHeadset) {
1139 activeDeviceString = activeDeviceStringsArray[3]; // Active for Phone only
1140 }
1141 }
1142 if (!hearingAidNotConnected && mIsActiveDeviceHearingAid) {
1143 activeDeviceString = activeDeviceStringsArray[1];
1144 return mContext.getString(R.string.bluetooth_connected, activeDeviceString);
1145 }
1146
1147 if (profileConnected) {
1148 if (a2dpNotConnected && hfpNotConnected) {
1149 if (batteryLevelPercentageString != null) {
1150 return mContext.getString(
1151 R.string.bluetooth_connected_no_headset_no_a2dp_battery_level,
1152 batteryLevelPercentageString, activeDeviceString);
1153 } else {
1154 return mContext.getString(R.string.bluetooth_connected_no_headset_no_a2dp,
1155 activeDeviceString);
1156 }
1157
1158 } else if (a2dpNotConnected) {
1159 if (batteryLevelPercentageString != null) {
1160 return mContext.getString(R.string.bluetooth_connected_no_a2dp_battery_level,
1161 batteryLevelPercentageString, activeDeviceString);
1162 } else {
1163 return mContext.getString(R.string.bluetooth_connected_no_a2dp,
1164 activeDeviceString);
1165 }
1166
1167 } else if (hfpNotConnected) {
1168 if (batteryLevelPercentageString != null) {
1169 return mContext.getString(R.string.bluetooth_connected_no_headset_battery_level,
1170 batteryLevelPercentageString, activeDeviceString);
1171 } else {
1172 return mContext.getString(R.string.bluetooth_connected_no_headset,
1173 activeDeviceString);
1174 }
1175 } else {
1176 if (batteryLevelPercentageString != null) {
1177 return mContext.getString(R.string.bluetooth_connected_battery_level,
1178 batteryLevelPercentageString, activeDeviceString);
1179 } else {
1180 return mContext.getString(R.string.bluetooth_connected, activeDeviceString);
1181 }
1182 }
1183 }
1184
1185 return getBondState() == BluetoothDevice.BOND_BONDING ?
1186 mContext.getString(R.string.bluetooth_pairing) : null;
1187 }
1188
1189 /**
hughchen23b947e2018-03-31 17:32:53 +08001190 * @return {@code true} if {@code cachedBluetoothDevice} is a2dp device
1191 */
1192 public boolean isA2dpDevice() {
1193 return mProfileManager.getA2dpProfile().getConnectionStatus(mDevice) ==
1194 BluetoothProfile.STATE_CONNECTED;
1195 }
1196
1197 /**
1198 * @return {@code true} if {@code cachedBluetoothDevice} is HFP device
1199 */
1200 public boolean isHfpDevice() {
1201 return mProfileManager.getHeadsetProfile().getConnectionStatus(mDevice) ==
1202 BluetoothProfile.STATE_CONNECTED;
1203 }
Jason Monk7ce96b92015-02-02 11:27:58 -05001204}