blob: cba823c15e6220c1bb0491c5ea49a0ed432e4026 [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
Joseph Pirozzocdbab122019-01-31 10:58:22 -080019import 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;
Joseph Pirozzocdbab122019-01-31 10:58:22 -080032
Aurimas Liutikasa14377a2018-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) {
Stanley Tng2bbcc6e2018-09-30 22:43:06 -0700147 Log.d(TAG, "onProfileStateChanged: profile " + profile + ", device=" + mDevice
148 + ", newProfileState " + newProfileState);
Jason Monk7ce96b92015-02-02 11:27:58 -0500149 }
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();
Joseph Pirozzocdbab122019-01-31 10:58:22 -0800205 if (PbapProfile != null && isConnectedProfile(PbapProfile))
Jason Monk7ce96b92015-02-02 11:27:58 -0500206 {
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
Stanley Tng2bbcc6e2018-09-30 22:43:06 -0700228 public boolean isHearingAidDevice() {
229 return mHiSyncId != BluetoothHearingAid.HI_SYNC_ID_INVALID;
230 }
231
Jason Monk7ce96b92015-02-02 11:27:58 -0500232 void onBondingDockConnect() {
233 // Attempt to connect if UUIDs are available. Otherwise,
234 // we will connect when the ACTION_UUID intent arrives.
235 connect(false);
236 }
237
238 private void connectWithoutResettingTimer(boolean connectAllProfiles) {
239 // Try to initialize the profiles if they were not.
240 if (mProfiles.isEmpty()) {
241 // if mProfiles is empty, then do not invoke updateProfiles. This causes a race
242 // condition with carkits during pairing, wherein RemoteDevice.UUIDs have been updated
243 // from bluetooth stack but ACTION.uuid is not sent yet.
244 // Eventually ACTION.uuid will be received which shall trigger the connection of the
245 // various profiles
246 // If UUIDs are not available yet, connect will be happen
247 // upon arrival of the ACTION_UUID intent.
248 Log.d(TAG, "No profiles. Maybe we will connect later");
249 return;
250 }
251
252 // Reset the only-show-one-error-dialog tracking variable
253 mIsConnectingErrorPossible = true;
254
255 int preferredProfiles = 0;
256 for (LocalBluetoothProfile profile : mProfiles) {
Leon Liao3b5e6bd2018-09-18 12:45:45 +0800257 if (connectAllProfiles ? profile.accessProfileEnabled() : profile.isAutoConnectable()) {
Jason Monk7ce96b92015-02-02 11:27:58 -0500258 if (profile.isPreferred(mDevice)) {
259 ++preferredProfiles;
260 connectInt(profile);
261 }
262 }
263 }
264 if (DEBUG) Log.d(TAG, "Preferred profiles = " + preferredProfiles);
265
266 if (preferredProfiles == 0) {
267 connectAutoConnectableProfiles();
268 }
269 }
270
271 private void connectAutoConnectableProfiles() {
272 if (!ensurePaired()) {
273 return;
274 }
275 // Reset the only-show-one-error-dialog tracking variable
276 mIsConnectingErrorPossible = true;
277
278 for (LocalBluetoothProfile profile : mProfiles) {
279 if (profile.isAutoConnectable()) {
280 profile.setPreferred(mDevice, true);
281 connectInt(profile);
282 }
283 }
284 }
285
286 /**
287 * Connect this device to the specified profile.
288 *
289 * @param profile the profile to use with the remote device
290 */
291 public void connectProfile(LocalBluetoothProfile profile) {
292 mConnectAttempted = SystemClock.elapsedRealtime();
293 // Reset the only-show-one-error-dialog tracking variable
294 mIsConnectingErrorPossible = true;
295 connectInt(profile);
296 // Refresh the UI based on profile.connect() call
297 refresh();
298 }
299
300 synchronized void connectInt(LocalBluetoothProfile profile) {
301 if (!ensurePaired()) {
302 return;
303 }
304 if (profile.connect(mDevice)) {
305 if (Utils.D) {
306 Log.d(TAG, "Command sent successfully:CONNECT " + describe(profile));
307 }
308 return;
309 }
310 Log.i(TAG, "Failed to connect " + profile.toString() + " to " + mName);
311 }
312
313 private boolean ensurePaired() {
314 if (getBondState() == BluetoothDevice.BOND_NONE) {
315 startPairing();
316 return false;
317 } else {
318 return true;
319 }
320 }
321
322 public boolean startPairing() {
323 // Pairing is unreliable while scanning, so cancel discovery
324 if (mLocalAdapter.isDiscovering()) {
325 mLocalAdapter.cancelDiscovery();
326 }
327
328 if (!mDevice.createBond()) {
329 return false;
330 }
331
Jason Monk7ce96b92015-02-02 11:27:58 -0500332 return true;
333 }
334
335 /**
336 * Return true if user initiated pairing on this device. The message text is
337 * slightly different for local vs. remote initiated pairing dialogs.
338 */
339 boolean isUserInitiatedPairing() {
Jakub Pawlowski0278ab92016-07-20 11:55:48 -0700340 return mDevice.isBondingInitiatedLocally();
Jason Monk7ce96b92015-02-02 11:27:58 -0500341 }
342
343 public void unpair() {
344 int state = getBondState();
345
346 if (state == BluetoothDevice.BOND_BONDING) {
347 mDevice.cancelBondProcess();
348 }
349
350 if (state != BluetoothDevice.BOND_NONE) {
351 final BluetoothDevice dev = mDevice;
352 if (dev != null) {
353 final boolean successful = dev.removeBond();
354 if (successful) {
355 if (Utils.D) {
356 Log.d(TAG, "Command sent successfully:REMOVE_BOND " + describe(null));
357 }
358 } else if (Utils.V) {
359 Log.v(TAG, "Framework rejected command immediately:REMOVE_BOND " +
Isha Bobrac3d94132018-02-08 16:04:36 -0800360 describe(null));
Jason Monk7ce96b92015-02-02 11:27:58 -0500361 }
362 }
363 }
364 }
365
366 public int getProfileConnectionState(LocalBluetoothProfile profile) {
xutianguo22bbb8192016-06-22 11:32:00 +0800367 if (mProfileConnectionState.get(profile) == null) {
Jason Monk7ce96b92015-02-02 11:27:58 -0500368 // If cache is empty make the binder call to get the state
369 int state = profile.getConnectionStatus(mDevice);
370 mProfileConnectionState.put(profile, state);
371 }
372 return mProfileConnectionState.get(profile);
373 }
374
375 public void clearProfileConnectionState ()
376 {
377 if (Utils.D) {
378 Log.d(TAG," Clearing all connection state for dev:" + mDevice.getName());
379 }
380 for (LocalBluetoothProfile profile :getProfiles()) {
381 mProfileConnectionState.put(profile, BluetoothProfile.STATE_DISCONNECTED);
382 }
383 }
384
385 // TODO: do any of these need to run async on a background thread?
386 private void fillData() {
387 fetchName();
388 fetchBtClass();
389 updateProfiles();
Pavlin Radoslavov1af33a12018-01-21 02:59:15 -0800390 fetchActiveDevices();
Jason Monk7ce96b92015-02-02 11:27:58 -0500391 migratePhonebookPermissionChoice();
392 migrateMessagePermissionChoice();
393 fetchMessageRejectionCount();
394
Jason Monk7ce96b92015-02-02 11:27:58 -0500395 dispatchAttributesChanged();
396 }
397
398 public BluetoothDevice getDevice() {
399 return mDevice;
400 }
401
Antony Sargent7ad051e2017-06-29 15:23:13 -0700402 /**
403 * Convenience method that can be mocked - it lets tests avoid having to call getDevice() which
404 * causes problems in tests since BluetoothDevice is final and cannot be mocked.
405 * @return the address of this device
406 */
407 public String getAddress() {
408 return mDevice.getAddress();
409 }
410
Jason Monk7ce96b92015-02-02 11:27:58 -0500411 public String getName() {
412 return mName;
413 }
414
415 /**
416 * Populate name from BluetoothDevice.ACTION_FOUND intent
417 */
418 void setNewName(String name) {
419 if (mName == null) {
420 mName = name;
421 if (mName == null || TextUtils.isEmpty(mName)) {
422 mName = mDevice.getAddress();
423 }
424 dispatchAttributesChanged();
425 }
426 }
427
428 /**
Jack Hec219bc92017-07-24 14:55:59 -0700429 * User changes the device name
430 * @param name new alias name to be set, should never be null
Jason Monk7ce96b92015-02-02 11:27:58 -0500431 */
432 public void setName(String name) {
Jack Hec219bc92017-07-24 14:55:59 -0700433 // Prevent mName to be set to null if setName(null) is called
434 if (name != null && !TextUtils.equals(name, mName)) {
Jason Monk7ce96b92015-02-02 11:27:58 -0500435 mName = name;
436 mDevice.setAlias(name);
437 dispatchAttributesChanged();
438 }
439 }
440
Hansong Zhang6a416322018-03-19 18:20:38 -0700441 /**
442 * Set this device as active device
443 * @return true if at least one profile on this device is set to active, false otherwise
444 */
445 public boolean setActive() {
446 boolean result = false;
447 A2dpProfile a2dpProfile = mProfileManager.getA2dpProfile();
448 if (a2dpProfile != null && isConnectedProfile(a2dpProfile)) {
449 if (a2dpProfile.setActiveDevice(getDevice())) {
450 Log.i(TAG, "OnPreferenceClickListener: A2DP active device=" + this);
451 result = true;
452 }
453 }
454 HeadsetProfile headsetProfile = mProfileManager.getHeadsetProfile();
455 if ((headsetProfile != null) && isConnectedProfile(headsetProfile)) {
456 if (headsetProfile.setActiveDevice(getDevice())) {
457 Log.i(TAG, "OnPreferenceClickListener: Headset active device=" + this);
458 result = true;
459 }
460 }
Hansong Zhangd7b35912018-03-16 09:15:48 -0700461 HearingAidProfile hearingAidProfile = mProfileManager.getHearingAidProfile();
462 if ((hearingAidProfile != null) && isConnectedProfile(hearingAidProfile)) {
463 if (hearingAidProfile.setActiveDevice(getDevice())) {
464 Log.i(TAG, "OnPreferenceClickListener: Hearing Aid active device=" + this);
465 result = true;
466 }
467 }
Hansong Zhang6a416322018-03-19 18:20:38 -0700468 return result;
469 }
470
Jason Monk7ce96b92015-02-02 11:27:58 -0500471 void refreshName() {
472 fetchName();
473 dispatchAttributesChanged();
474 }
475
476 private void fetchName() {
477 mName = mDevice.getAliasName();
478
479 if (TextUtils.isEmpty(mName)) {
480 mName = mDevice.getAddress();
481 if (DEBUG) Log.d(TAG, "Device has no name (yet), use address: " + mName);
482 }
483 }
484
Jack He6258aae2017-06-29 17:01:23 -0700485 /**
Jack Hec219bc92017-07-24 14:55:59 -0700486 * Checks if device has a human readable name besides MAC address
487 * @return true if device's alias name is not null nor empty, false otherwise
488 */
489 public boolean hasHumanReadableName() {
490 return !TextUtils.isEmpty(mDevice.getAliasName());
491 }
492
493 /**
Jack He6258aae2017-06-29 17:01:23 -0700494 * Get battery level from remote device
495 * @return battery level in percentage [0-100], or {@link BluetoothDevice#BATTERY_LEVEL_UNKNOWN}
496 */
497 public int getBatteryLevel() {
498 return mDevice.getBatteryLevel();
499 }
500
Jason Monk7ce96b92015-02-02 11:27:58 -0500501 void refresh() {
502 dispatchAttributesChanged();
503 }
504
Jack He51520472017-07-24 12:30:08 -0700505 public void setJustDiscovered(boolean justDiscovered) {
506 if (mJustDiscovered != justDiscovered) {
507 mJustDiscovered = justDiscovered;
Jason Monk7ce96b92015-02-02 11:27:58 -0500508 dispatchAttributesChanged();
509 }
510 }
511
512 public int getBondState() {
513 return mDevice.getBondState();
514 }
515
Pavlin Radoslavov1af33a12018-01-21 02:59:15 -0800516 /**
Pavlin Radoslavovc285d552018-02-06 16:14:00 -0800517 * Update the device status as active or non-active per Bluetooth profile.
Pavlin Radoslavov1af33a12018-01-21 02:59:15 -0800518 *
519 * @param isActive true if the device is active
520 * @param bluetoothProfile the Bluetooth profile
521 */
Pavlin Radoslavovc285d552018-02-06 16:14:00 -0800522 public void onActiveDeviceChanged(boolean isActive, int bluetoothProfile) {
Pavlin Radoslavov1af33a12018-01-21 02:59:15 -0800523 boolean changed = false;
524 switch (bluetoothProfile) {
525 case BluetoothProfile.A2DP:
526 changed = (mIsActiveDeviceA2dp != isActive);
527 mIsActiveDeviceA2dp = isActive;
528 break;
529 case BluetoothProfile.HEADSET:
530 changed = (mIsActiveDeviceHeadset != isActive);
531 mIsActiveDeviceHeadset = isActive;
532 break;
Hansong Zhangd7b35912018-03-16 09:15:48 -0700533 case BluetoothProfile.HEARING_AID:
534 changed = (mIsActiveDeviceHearingAid != isActive);
535 mIsActiveDeviceHearingAid = isActive;
536 break;
Pavlin Radoslavov1af33a12018-01-21 02:59:15 -0800537 default:
Pavlin Radoslavovc285d552018-02-06 16:14:00 -0800538 Log.w(TAG, "onActiveDeviceChanged: unknown profile " + bluetoothProfile +
Pavlin Radoslavov1af33a12018-01-21 02:59:15 -0800539 " isActive " + isActive);
540 break;
541 }
542 if (changed) {
543 dispatchAttributesChanged();
544 }
545 }
546
Pavlin Radoslavovc285d552018-02-06 16:14:00 -0800547 /**
timhypengf0509322018-03-29 14:23:21 +0800548 * Update the profile audio state.
549 */
550 void onAudioModeChanged() {
551 dispatchAttributesChanged();
552 }
553 /**
Pavlin Radoslavovc285d552018-02-06 16:14:00 -0800554 * Get the device status as active or non-active per Bluetooth profile.
555 *
556 * @param bluetoothProfile the Bluetooth profile
557 * @return true if the device is active
558 */
559 @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
560 public boolean isActiveDevice(int bluetoothProfile) {
561 switch (bluetoothProfile) {
562 case BluetoothProfile.A2DP:
563 return mIsActiveDeviceA2dp;
564 case BluetoothProfile.HEADSET:
565 return mIsActiveDeviceHeadset;
Hansong Zhangd7b35912018-03-16 09:15:48 -0700566 case BluetoothProfile.HEARING_AID:
567 return mIsActiveDeviceHearingAid;
Pavlin Radoslavovc285d552018-02-06 16:14:00 -0800568 default:
569 Log.w(TAG, "getActiveDevice: unknown profile " + bluetoothProfile);
570 break;
571 }
572 return false;
573 }
574
Jason Monk7ce96b92015-02-02 11:27:58 -0500575 void setRssi(short rssi) {
576 if (mRssi != rssi) {
577 mRssi = rssi;
578 dispatchAttributesChanged();
579 }
580 }
581
582 /**
583 * Checks whether we are connected to this device (any profile counts).
584 *
585 * @return Whether it is connected.
586 */
587 public boolean isConnected() {
588 for (LocalBluetoothProfile profile : mProfiles) {
589 int status = getProfileConnectionState(profile);
590 if (status == BluetoothProfile.STATE_CONNECTED) {
591 return true;
592 }
593 }
594
595 return false;
596 }
597
598 public boolean isConnectedProfile(LocalBluetoothProfile profile) {
599 int status = getProfileConnectionState(profile);
600 return status == BluetoothProfile.STATE_CONNECTED;
601
602 }
603
604 public boolean isBusy() {
605 for (LocalBluetoothProfile profile : mProfiles) {
606 int status = getProfileConnectionState(profile);
607 if (status == BluetoothProfile.STATE_CONNECTING
608 || status == BluetoothProfile.STATE_DISCONNECTING) {
609 return true;
610 }
611 }
612 return getBondState() == BluetoothDevice.BOND_BONDING;
613 }
614
615 /**
616 * Fetches a new value for the cached BT class.
617 */
618 private void fetchBtClass() {
619 mBtClass = mDevice.getBluetoothClass();
620 }
621
622 private boolean updateProfiles() {
623 ParcelUuid[] uuids = mDevice.getUuids();
624 if (uuids == null) return false;
625
626 ParcelUuid[] localUuids = mLocalAdapter.getUuids();
627 if (localUuids == null) return false;
628
Jack Hec219bc92017-07-24 14:55:59 -0700629 /*
Jason Monk7ce96b92015-02-02 11:27:58 -0500630 * Now we know if the device supports PBAP, update permissions...
631 */
632 processPhonebookAccess();
633
634 mProfileManager.updateProfiles(uuids, localUuids, mProfiles, mRemovedProfiles,
635 mLocalNapRoleConnected, mDevice);
636
637 if (DEBUG) {
638 Log.e(TAG, "updating profiles for " + mDevice.getAliasName());
639 BluetoothClass bluetoothClass = mDevice.getBluetoothClass();
640
641 if (bluetoothClass != null) Log.v(TAG, "Class: " + bluetoothClass.toString());
642 Log.v(TAG, "UUID:");
643 for (ParcelUuid uuid : uuids) {
644 Log.v(TAG, " " + uuid);
645 }
646 }
647 return true;
648 }
649
Pavlin Radoslavov1af33a12018-01-21 02:59:15 -0800650 private void fetchActiveDevices() {
651 A2dpProfile a2dpProfile = mProfileManager.getA2dpProfile();
652 if (a2dpProfile != null) {
653 mIsActiveDeviceA2dp = mDevice.equals(a2dpProfile.getActiveDevice());
654 }
655 HeadsetProfile headsetProfile = mProfileManager.getHeadsetProfile();
656 if (headsetProfile != null) {
657 mIsActiveDeviceHeadset = mDevice.equals(headsetProfile.getActiveDevice());
658 }
Hansong Zhangd7b35912018-03-16 09:15:48 -0700659 HearingAidProfile hearingAidProfile = mProfileManager.getHearingAidProfile();
660 if (hearingAidProfile != null) {
Hansong Zhang3b8f09b2018-03-28 16:53:10 -0700661 mIsActiveDeviceHearingAid = hearingAidProfile.getActiveDevices().contains(mDevice);
Hansong Zhangd7b35912018-03-16 09:15:48 -0700662 }
Pavlin Radoslavov1af33a12018-01-21 02:59:15 -0800663 }
664
Jason Monk7ce96b92015-02-02 11:27:58 -0500665 /**
666 * Refreshes the UI for the BT class, including fetching the latest value
667 * for the class.
668 */
669 void refreshBtClass() {
670 fetchBtClass();
671 dispatchAttributesChanged();
672 }
673
674 /**
675 * Refreshes the UI when framework alerts us of a UUID change.
676 */
677 void onUuidChanged() {
678 updateProfiles();
Etan Cohen50d47612015-03-31 12:45:23 -0700679 ParcelUuid[] uuids = mDevice.getUuids();
Venkat Raghavan05e08c32015-04-06 16:26:11 -0700680
Etan Cohen50d47612015-03-31 12:45:23 -0700681 long timeout = MAX_UUID_DELAY_FOR_AUTO_CONNECT;
Venkat Raghavan05e08c32015-04-06 16:26:11 -0700682 if (BluetoothUuid.isUuidPresent(uuids, BluetoothUuid.Hogp)) {
683 timeout = MAX_HOGP_DELAY_FOR_AUTO_CONNECT;
684 }
Jason Monk7ce96b92015-02-02 11:27:58 -0500685
686 if (DEBUG) {
Etan Cohen50d47612015-03-31 12:45:23 -0700687 Log.d(TAG, "onUuidChanged: Time since last connect"
Jason Monk7ce96b92015-02-02 11:27:58 -0500688 + (SystemClock.elapsedRealtime() - mConnectAttempted));
689 }
690
691 /*
Venkat Raghavan05e08c32015-04-06 16:26:11 -0700692 * If a connect was attempted earlier without any UUID, we will do the connect now.
693 * Otherwise, allow the connect on UUID change.
Jason Monk7ce96b92015-02-02 11:27:58 -0500694 */
695 if (!mProfiles.isEmpty()
Andre Eisenbach7be83c52015-09-03 15:20:09 -0700696 && ((mConnectAttempted + timeout) > SystemClock.elapsedRealtime())) {
Jason Monk7ce96b92015-02-02 11:27:58 -0500697 connectWithoutResettingTimer(false);
698 }
Andre Eisenbach7be83c52015-09-03 15:20:09 -0700699
Jason Monk7ce96b92015-02-02 11:27:58 -0500700 dispatchAttributesChanged();
701 }
702
703 void onBondingStateChanged(int bondState) {
704 if (bondState == BluetoothDevice.BOND_NONE) {
705 mProfiles.clear();
Jason Monk7ce96b92015-02-02 11:27:58 -0500706 setPhonebookPermissionChoice(ACCESS_UNKNOWN);
707 setMessagePermissionChoice(ACCESS_UNKNOWN);
Casper Bonde424681e2015-05-04 22:07:45 -0700708 setSimPermissionChoice(ACCESS_UNKNOWN);
Jason Monk7ce96b92015-02-02 11:27:58 -0500709 mMessageRejectionCount = 0;
710 saveMessageRejectionCount();
711 }
712
713 refresh();
714
715 if (bondState == BluetoothDevice.BOND_BONDED) {
716 if (mDevice.isBluetoothDock()) {
717 onBondingDockConnect();
Jakub Pawlowski0278ab92016-07-20 11:55:48 -0700718 } else if (mDevice.isBondingInitiatedLocally()) {
Jason Monk7ce96b92015-02-02 11:27:58 -0500719 connect(false);
720 }
Jason Monk7ce96b92015-02-02 11:27:58 -0500721 }
722 }
723
724 void setBtClass(BluetoothClass btClass) {
725 if (btClass != null && mBtClass != btClass) {
726 mBtClass = btClass;
727 dispatchAttributesChanged();
728 }
729 }
730
731 public BluetoothClass getBtClass() {
732 return mBtClass;
733 }
734
735 public List<LocalBluetoothProfile> getProfiles() {
736 return Collections.unmodifiableList(mProfiles);
737 }
738
739 public List<LocalBluetoothProfile> getConnectableProfiles() {
740 List<LocalBluetoothProfile> connectableProfiles =
741 new ArrayList<LocalBluetoothProfile>();
742 for (LocalBluetoothProfile profile : mProfiles) {
Leon Liao3b5e6bd2018-09-18 12:45:45 +0800743 if (profile.accessProfileEnabled()) {
Jason Monk7ce96b92015-02-02 11:27:58 -0500744 connectableProfiles.add(profile);
745 }
746 }
747 return connectableProfiles;
748 }
749
750 public List<LocalBluetoothProfile> getRemovedProfiles() {
751 return mRemovedProfiles;
752 }
753
754 public void registerCallback(Callback callback) {
755 synchronized (mCallbacks) {
756 mCallbacks.add(callback);
757 }
758 }
759
760 public void unregisterCallback(Callback callback) {
761 synchronized (mCallbacks) {
762 mCallbacks.remove(callback);
763 }
764 }
765
766 private void dispatchAttributesChanged() {
767 synchronized (mCallbacks) {
768 for (Callback callback : mCallbacks) {
769 callback.onDeviceAttributesChanged();
770 }
771 }
772 }
773
774 @Override
775 public String toString() {
776 return mDevice.toString();
777 }
778
779 @Override
780 public boolean equals(Object o) {
781 if ((o == null) || !(o instanceof CachedBluetoothDevice)) {
782 return false;
783 }
784 return mDevice.equals(((CachedBluetoothDevice) o).mDevice);
785 }
786
787 @Override
788 public int hashCode() {
789 return mDevice.getAddress().hashCode();
790 }
791
792 // This comparison uses non-final fields so the sort order may change
793 // when device attributes change (such as bonding state). Settings
794 // will completely refresh the device list when this happens.
795 public int compareTo(CachedBluetoothDevice another) {
796 // Connected above not connected
797 int comparison = (another.isConnected() ? 1 : 0) - (isConnected() ? 1 : 0);
798 if (comparison != 0) return comparison;
799
800 // Paired above not paired
801 comparison = (another.getBondState() == BluetoothDevice.BOND_BONDED ? 1 : 0) -
802 (getBondState() == BluetoothDevice.BOND_BONDED ? 1 : 0);
803 if (comparison != 0) return comparison;
804
Jack He51520472017-07-24 12:30:08 -0700805 // Just discovered above discovered in the past
806 comparison = (another.mJustDiscovered ? 1 : 0) - (mJustDiscovered ? 1 : 0);
Jason Monk7ce96b92015-02-02 11:27:58 -0500807 if (comparison != 0) return comparison;
808
809 // Stronger signal above weaker signal
810 comparison = another.mRssi - mRssi;
811 if (comparison != 0) return comparison;
812
813 // Fallback on name
814 return mName.compareTo(another.mName);
815 }
816
817 public interface Callback {
818 void onDeviceAttributesChanged();
819 }
820
821 public int getPhonebookPermissionChoice() {
822 int permission = mDevice.getPhonebookAccessPermission();
823 if (permission == BluetoothDevice.ACCESS_ALLOWED) {
824 return ACCESS_ALLOWED;
825 } else if (permission == BluetoothDevice.ACCESS_REJECTED) {
826 return ACCESS_REJECTED;
827 }
828 return ACCESS_UNKNOWN;
829 }
830
831 public void setPhonebookPermissionChoice(int permissionChoice) {
832 int permission = BluetoothDevice.ACCESS_UNKNOWN;
833 if (permissionChoice == ACCESS_ALLOWED) {
834 permission = BluetoothDevice.ACCESS_ALLOWED;
835 } else if (permissionChoice == ACCESS_REJECTED) {
836 permission = BluetoothDevice.ACCESS_REJECTED;
837 }
838 mDevice.setPhonebookAccessPermission(permission);
839 }
840
841 // Migrates data from old data store (in Settings app's shared preferences) to new (in Bluetooth
842 // app's shared preferences).
843 private void migratePhonebookPermissionChoice() {
844 SharedPreferences preferences = mContext.getSharedPreferences(
845 "bluetooth_phonebook_permission", Context.MODE_PRIVATE);
846 if (!preferences.contains(mDevice.getAddress())) {
847 return;
848 }
849
850 if (mDevice.getPhonebookAccessPermission() == BluetoothDevice.ACCESS_UNKNOWN) {
851 int oldPermission = preferences.getInt(mDevice.getAddress(), ACCESS_UNKNOWN);
852 if (oldPermission == ACCESS_ALLOWED) {
853 mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_ALLOWED);
854 } else if (oldPermission == ACCESS_REJECTED) {
855 mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_REJECTED);
856 }
857 }
858
859 SharedPreferences.Editor editor = preferences.edit();
860 editor.remove(mDevice.getAddress());
861 editor.commit();
862 }
863
864 public int getMessagePermissionChoice() {
865 int permission = mDevice.getMessageAccessPermission();
866 if (permission == BluetoothDevice.ACCESS_ALLOWED) {
867 return ACCESS_ALLOWED;
868 } else if (permission == BluetoothDevice.ACCESS_REJECTED) {
869 return ACCESS_REJECTED;
870 }
871 return ACCESS_UNKNOWN;
872 }
873
874 public void setMessagePermissionChoice(int permissionChoice) {
875 int permission = BluetoothDevice.ACCESS_UNKNOWN;
876 if (permissionChoice == ACCESS_ALLOWED) {
877 permission = BluetoothDevice.ACCESS_ALLOWED;
878 } else if (permissionChoice == ACCESS_REJECTED) {
879 permission = BluetoothDevice.ACCESS_REJECTED;
880 }
881 mDevice.setMessageAccessPermission(permission);
882 }
883
Casper Bonde424681e2015-05-04 22:07:45 -0700884 public int getSimPermissionChoice() {
885 int permission = mDevice.getSimAccessPermission();
886 if (permission == BluetoothDevice.ACCESS_ALLOWED) {
887 return ACCESS_ALLOWED;
888 } else if (permission == BluetoothDevice.ACCESS_REJECTED) {
889 return ACCESS_REJECTED;
890 }
891 return ACCESS_UNKNOWN;
892 }
893
894 void setSimPermissionChoice(int permissionChoice) {
895 int permission = BluetoothDevice.ACCESS_UNKNOWN;
896 if (permissionChoice == ACCESS_ALLOWED) {
897 permission = BluetoothDevice.ACCESS_ALLOWED;
898 } else if (permissionChoice == ACCESS_REJECTED) {
899 permission = BluetoothDevice.ACCESS_REJECTED;
900 }
901 mDevice.setSimAccessPermission(permission);
902 }
903
Jason Monk7ce96b92015-02-02 11:27:58 -0500904 // Migrates data from old data store (in Settings app's shared preferences) to new (in Bluetooth
905 // app's shared preferences).
906 private void migrateMessagePermissionChoice() {
907 SharedPreferences preferences = mContext.getSharedPreferences(
908 "bluetooth_message_permission", Context.MODE_PRIVATE);
909 if (!preferences.contains(mDevice.getAddress())) {
910 return;
911 }
912
913 if (mDevice.getMessageAccessPermission() == BluetoothDevice.ACCESS_UNKNOWN) {
914 int oldPermission = preferences.getInt(mDevice.getAddress(), ACCESS_UNKNOWN);
915 if (oldPermission == ACCESS_ALLOWED) {
916 mDevice.setMessageAccessPermission(BluetoothDevice.ACCESS_ALLOWED);
917 } else if (oldPermission == ACCESS_REJECTED) {
918 mDevice.setMessageAccessPermission(BluetoothDevice.ACCESS_REJECTED);
919 }
920 }
921
922 SharedPreferences.Editor editor = preferences.edit();
923 editor.remove(mDevice.getAddress());
924 editor.commit();
925 }
926
927 /**
928 * @return Whether this rejection should persist.
929 */
930 public boolean checkAndIncreaseMessageRejectionCount() {
931 if (mMessageRejectionCount < MESSAGE_REJECTION_COUNT_LIMIT_TO_PERSIST) {
932 mMessageRejectionCount++;
933 saveMessageRejectionCount();
934 }
935 return mMessageRejectionCount >= MESSAGE_REJECTION_COUNT_LIMIT_TO_PERSIST;
936 }
937
938 private void fetchMessageRejectionCount() {
939 SharedPreferences preference = mContext.getSharedPreferences(
940 MESSAGE_REJECTION_COUNT_PREFS_NAME, Context.MODE_PRIVATE);
941 mMessageRejectionCount = preference.getInt(mDevice.getAddress(), 0);
942 }
943
944 private void saveMessageRejectionCount() {
945 SharedPreferences.Editor editor = mContext.getSharedPreferences(
946 MESSAGE_REJECTION_COUNT_PREFS_NAME, Context.MODE_PRIVATE).edit();
947 if (mMessageRejectionCount == 0) {
948 editor.remove(mDevice.getAddress());
949 } else {
950 editor.putInt(mDevice.getAddress(), mMessageRejectionCount);
951 }
952 editor.commit();
953 }
954
955 private void processPhonebookAccess() {
956 if (mDevice.getBondState() != BluetoothDevice.BOND_BONDED) return;
957
958 ParcelUuid[] uuids = mDevice.getUuids();
959 if (BluetoothUuid.containsAnyUuid(uuids, PbapServerProfile.PBAB_CLIENT_UUIDS)) {
960 // The pairing dialog now warns of phone-book access for paired devices.
961 // No separate prompt is displayed after pairing.
Sanket Padawe78c23762015-06-01 15:55:30 -0700962 if (getPhonebookPermissionChoice() == CachedBluetoothDevice.ACCESS_UNKNOWN) {
Sanket Padawe07533db2015-11-11 15:01:35 -0800963 if (mDevice.getBluetoothClass().getDeviceClass()
Hemant Guptab8d267a2016-06-07 12:53:59 +0530964 == BluetoothClass.Device.AUDIO_VIDEO_HANDSFREE ||
965 mDevice.getBluetoothClass().getDeviceClass()
966 == BluetoothClass.Device.AUDIO_VIDEO_WEARABLE_HEADSET) {
Sanket Padawe07533db2015-11-11 15:01:35 -0800967 setPhonebookPermissionChoice(CachedBluetoothDevice.ACCESS_ALLOWED);
968 } else {
969 setPhonebookPermissionChoice(CachedBluetoothDevice.ACCESS_REJECTED);
970 }
Sanket Padawe78c23762015-06-01 15:55:30 -0700971 }
Jason Monk7ce96b92015-02-02 11:27:58 -0500972 }
973 }
Jason Monkbe3c5db2015-02-04 13:00:55 -0500974
975 public int getMaxConnectionState() {
976 int maxState = BluetoothProfile.STATE_DISCONNECTED;
977 for (LocalBluetoothProfile profile : getProfiles()) {
978 int connectionStatus = getProfileConnectionState(profile);
979 if (connectionStatus > maxState) {
980 maxState = connectionStatus;
981 }
982 }
983 return maxState;
984 }
985
986 /**
987 * @return resource for string that discribes the connection state of this device.
timhypengf0509322018-03-29 14:23:21 +0800988 * case 1: idle or playing media, show "Active" on the only one A2DP active device.
989 * case 2: in phone call, show "Active" on the only one HFP active device
Jason Monkbe3c5db2015-02-04 13:00:55 -0500990 */
Jack He6258aae2017-06-29 17:01:23 -0700991 public String getConnectionSummary() {
timhypengf0509322018-03-29 14:23:21 +0800992 boolean profileConnected = false; // Updated as long as BluetoothProfile is connected
993 boolean a2dpConnected = true; // A2DP is connected
994 boolean hfpConnected = true; // HFP is connected
995 boolean hearingAidConnected = true; // Hearing Aid is connected
Jason Monkbe3c5db2015-02-04 13:00:55 -0500996
997 for (LocalBluetoothProfile profile : getProfiles()) {
998 int connectionStatus = getProfileConnectionState(profile);
999
1000 switch (connectionStatus) {
1001 case BluetoothProfile.STATE_CONNECTING:
1002 case BluetoothProfile.STATE_DISCONNECTING:
Jack He6258aae2017-06-29 17:01:23 -07001003 return mContext.getString(Utils.getConnectionStateSummary(connectionStatus));
Jason Monkbe3c5db2015-02-04 13:00:55 -05001004
1005 case BluetoothProfile.STATE_CONNECTED:
1006 profileConnected = true;
1007 break;
1008
1009 case BluetoothProfile.STATE_DISCONNECTED:
1010 if (profile.isProfileReady()) {
Sanket Agarwalf8a1c912016-01-26 20:12:52 -08001011 if ((profile instanceof A2dpProfile) ||
timhypengf0509322018-03-29 14:23:21 +08001012 (profile instanceof A2dpSinkProfile)) {
1013 a2dpConnected = false;
Sanket Agarwalf8a1c912016-01-26 20:12:52 -08001014 } else if ((profile instanceof HeadsetProfile) ||
timhypengf0509322018-03-29 14:23:21 +08001015 (profile instanceof HfpClientProfile)) {
1016 hfpConnected = false;
1017 } else if (profile instanceof HearingAidProfile) {
1018 hearingAidConnected = false;
Jason Monkbe3c5db2015-02-04 13:00:55 -05001019 }
1020 }
1021 break;
1022 }
1023 }
1024
Jack He6258aae2017-06-29 17:01:23 -07001025 String batteryLevelPercentageString = null;
1026 // Android framework should only set mBatteryLevel to valid range [0-100] or
1027 // BluetoothDevice.BATTERY_LEVEL_UNKNOWN, any other value should be a framework bug.
1028 // Thus assume here that if value is not BluetoothDevice.BATTERY_LEVEL_UNKNOWN, it must
1029 // be valid
1030 final int batteryLevel = getBatteryLevel();
1031 if (batteryLevel != BluetoothDevice.BATTERY_LEVEL_UNKNOWN) {
1032 // TODO: name com.android.settingslib.bluetooth.Utils something different
1033 batteryLevelPercentageString =
1034 com.android.settingslib.Utils.formatPercentage(batteryLevel);
1035 }
1036
timhypengf0509322018-03-29 14:23:21 +08001037 int stringRes = R.string.bluetooth_pairing;
1038 //when profile is connected, information would be available
Jason Monkbe3c5db2015-02-04 13:00:55 -05001039 if (profileConnected) {
timhypengf0509322018-03-29 14:23:21 +08001040 if (a2dpConnected || hfpConnected || hearingAidConnected) {
1041 //contain battery information
Jack He6258aae2017-06-29 17:01:23 -07001042 if (batteryLevelPercentageString != null) {
timhypengf0509322018-03-29 14:23:21 +08001043 //device is in phone call
1044 if (com.android.settingslib.Utils.isAudioModeOngoingCall(mContext)) {
1045 if (mIsActiveDeviceHeadset) {
1046 stringRes = R.string.bluetooth_active_battery_level;
1047 } else {
1048 stringRes = R.string.bluetooth_battery_level;
1049 }
1050 } else {//device is not in phone call(ex. idle or playing media)
1051 //need to check if A2DP and HearingAid are exclusive
1052 if (mIsActiveDeviceHearingAid || mIsActiveDeviceA2dp) {
1053 stringRes = R.string.bluetooth_active_battery_level;
1054 } else {
1055 stringRes = R.string.bluetooth_battery_level;
1056 }
1057 }
Jack He6258aae2017-06-29 17:01:23 -07001058 } else {
timhypengf0509322018-03-29 14:23:21 +08001059 //no battery information
1060 if (com.android.settingslib.Utils.isAudioModeOngoingCall(mContext)) {
1061 if (mIsActiveDeviceHeadset) {
1062 stringRes = R.string.bluetooth_active_no_battery_level;
1063 }
1064 } else {
1065 if (mIsActiveDeviceHearingAid || mIsActiveDeviceA2dp) {
1066 stringRes = R.string.bluetooth_active_no_battery_level;
1067 }
1068 }
Jack He6258aae2017-06-29 17:01:23 -07001069 }
timhypengf0509322018-03-29 14:23:21 +08001070 } else {//unknown profile with battery information
Jack He6258aae2017-06-29 17:01:23 -07001071 if (batteryLevelPercentageString != null) {
timhypengf0509322018-03-29 14:23:21 +08001072 stringRes = R.string.bluetooth_battery_level;
Jack He6258aae2017-06-29 17:01:23 -07001073 }
Jason Monkbe3c5db2015-02-04 13:00:55 -05001074 }
1075 }
1076
timhypengf0509322018-03-29 14:23:21 +08001077 return (stringRes != R.string.bluetooth_pairing
1078 || getBondState() == BluetoothDevice.BOND_BONDING)
1079 ? mContext.getString(stringRes, batteryLevelPercentageString)
1080 : null;
Jason Monkbe3c5db2015-02-04 13:00:55 -05001081 }
hughchen23b947e2018-03-31 17:32:53 +08001082
1083 /**
ryanywline26aecd2018-05-15 14:20:50 +08001084 * @return resource for android auto string that describes the connection state of this device.
1085 */
1086 public String getCarConnectionSummary() {
1087 boolean profileConnected = false; // at least one profile is connected
1088 boolean a2dpNotConnected = false; // A2DP is preferred but not connected
1089 boolean hfpNotConnected = false; // HFP is preferred but not connected
1090 boolean hearingAidNotConnected = false; // Hearing Aid is preferred but not connected
1091
1092 for (LocalBluetoothProfile profile : getProfiles()) {
1093 int connectionStatus = getProfileConnectionState(profile);
1094
1095 switch (connectionStatus) {
1096 case BluetoothProfile.STATE_CONNECTING:
1097 case BluetoothProfile.STATE_DISCONNECTING:
1098 return mContext.getString(Utils.getConnectionStateSummary(connectionStatus));
1099
1100 case BluetoothProfile.STATE_CONNECTED:
1101 profileConnected = true;
1102 break;
1103
1104 case BluetoothProfile.STATE_DISCONNECTED:
1105 if (profile.isProfileReady()) {
1106 if ((profile instanceof A2dpProfile) ||
1107 (profile instanceof A2dpSinkProfile)){
1108 a2dpNotConnected = true;
1109 } else if ((profile instanceof HeadsetProfile) ||
1110 (profile instanceof HfpClientProfile)) {
1111 hfpNotConnected = true;
1112 } else if (profile instanceof HearingAidProfile) {
1113 hearingAidNotConnected = true;
1114 }
1115 }
1116 break;
1117 }
1118 }
1119
1120 String batteryLevelPercentageString = null;
1121 // Android framework should only set mBatteryLevel to valid range [0-100] or
1122 // BluetoothDevice.BATTERY_LEVEL_UNKNOWN, any other value should be a framework bug.
1123 // Thus assume here that if value is not BluetoothDevice.BATTERY_LEVEL_UNKNOWN, it must
1124 // be valid
1125 final int batteryLevel = getBatteryLevel();
1126 if (batteryLevel != BluetoothDevice.BATTERY_LEVEL_UNKNOWN) {
1127 // TODO: name com.android.settingslib.bluetooth.Utils something different
1128 batteryLevelPercentageString =
1129 com.android.settingslib.Utils.formatPercentage(batteryLevel);
1130 }
1131
1132 // Prepare the string for the Active Device summary
1133 String[] activeDeviceStringsArray = mContext.getResources().getStringArray(
1134 R.array.bluetooth_audio_active_device_summaries);
1135 String activeDeviceString = activeDeviceStringsArray[0]; // Default value: not active
1136 if (mIsActiveDeviceA2dp && mIsActiveDeviceHeadset) {
1137 activeDeviceString = activeDeviceStringsArray[1]; // Active for Media and Phone
1138 } else {
1139 if (mIsActiveDeviceA2dp) {
1140 activeDeviceString = activeDeviceStringsArray[2]; // Active for Media only
1141 }
1142 if (mIsActiveDeviceHeadset) {
1143 activeDeviceString = activeDeviceStringsArray[3]; // Active for Phone only
1144 }
1145 }
1146 if (!hearingAidNotConnected && mIsActiveDeviceHearingAid) {
1147 activeDeviceString = activeDeviceStringsArray[1];
1148 return mContext.getString(R.string.bluetooth_connected, activeDeviceString);
1149 }
1150
1151 if (profileConnected) {
1152 if (a2dpNotConnected && hfpNotConnected) {
1153 if (batteryLevelPercentageString != null) {
1154 return mContext.getString(
1155 R.string.bluetooth_connected_no_headset_no_a2dp_battery_level,
1156 batteryLevelPercentageString, activeDeviceString);
1157 } else {
1158 return mContext.getString(R.string.bluetooth_connected_no_headset_no_a2dp,
1159 activeDeviceString);
1160 }
1161
1162 } else if (a2dpNotConnected) {
1163 if (batteryLevelPercentageString != null) {
1164 return mContext.getString(R.string.bluetooth_connected_no_a2dp_battery_level,
1165 batteryLevelPercentageString, activeDeviceString);
1166 } else {
1167 return mContext.getString(R.string.bluetooth_connected_no_a2dp,
1168 activeDeviceString);
1169 }
1170
1171 } else if (hfpNotConnected) {
1172 if (batteryLevelPercentageString != null) {
1173 return mContext.getString(R.string.bluetooth_connected_no_headset_battery_level,
1174 batteryLevelPercentageString, activeDeviceString);
1175 } else {
1176 return mContext.getString(R.string.bluetooth_connected_no_headset,
1177 activeDeviceString);
1178 }
1179 } else {
1180 if (batteryLevelPercentageString != null) {
1181 return mContext.getString(R.string.bluetooth_connected_battery_level,
1182 batteryLevelPercentageString, activeDeviceString);
1183 } else {
1184 return mContext.getString(R.string.bluetooth_connected, activeDeviceString);
1185 }
1186 }
1187 }
1188
1189 return getBondState() == BluetoothDevice.BOND_BONDING ?
1190 mContext.getString(R.string.bluetooth_pairing) : null;
1191 }
1192
1193 /**
hughchen23b947e2018-03-31 17:32:53 +08001194 * @return {@code true} if {@code cachedBluetoothDevice} is a2dp device
1195 */
1196 public boolean isA2dpDevice() {
Hansong Zhangc3ea0d32018-06-13 11:30:59 -07001197 A2dpProfile a2dpProfile = mProfileManager.getA2dpProfile();
1198 return a2dpProfile != null && a2dpProfile.getConnectionStatus(mDevice) ==
hughchen23b947e2018-03-31 17:32:53 +08001199 BluetoothProfile.STATE_CONNECTED;
1200 }
1201
1202 /**
1203 * @return {@code true} if {@code cachedBluetoothDevice} is HFP device
1204 */
1205 public boolean isHfpDevice() {
Hansong Zhangc3ea0d32018-06-13 11:30:59 -07001206 HeadsetProfile headsetProfile = mProfileManager.getHeadsetProfile();
1207 return headsetProfile != null && headsetProfile.getConnectionStatus(mDevice) ==
hughchen23b947e2018-03-31 17:32:53 +08001208 BluetoothProfile.STATE_CONNECTED;
1209 }
timhypeng1f24f522018-05-25 14:23:44 +08001210
1211 /**
1212 * @return {@code true} if {@code cachedBluetoothDevice} is Hearing Aid device
1213 */
1214 public boolean isConnectedHearingAidDevice() {
Hansong Zhangc3ea0d32018-06-13 11:30:59 -07001215 HearingAidProfile hearingAidProfile = mProfileManager.getHearingAidProfile();
1216 return hearingAidProfile != null && hearingAidProfile.getConnectionStatus(mDevice) ==
Jason Monk7ce96b92015-02-02 11:27:58 -05001217 BluetoothProfile.STATE_CONNECTED;
1218 }
1219}