blob: 8f164f1592d3104d0872a14f9ce8ea319d485513 [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 Zhangf7802ea2018-08-28 15:15:19 -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;
27import android.os.ParcelUuid;
28import android.os.SystemClock;
29import android.text.TextUtils;
Zongheng Wangefdcff22019-09-05 13:44:28 -070030import android.util.EventLog;
Jason Monk7ce96b92015-02-02 11:27:58 -050031import android.util.Log;
Fan Zhangf7802ea2018-08-28 15:15:19 -070032
timhypeng5c62ebb2018-08-28 09:59:03 +080033import androidx.annotation.VisibleForTesting;
34
Jason Monkbe3c5db2015-02-04 13:00:55 -050035import com.android.settingslib.R;
jackqdyulei1f9c1712019-03-05 16:42:57 -080036import com.android.settingslib.Utils;
Jason Monkbe3c5db2015-02-04 13:00:55 -050037
Jason Monk7ce96b92015-02-02 11:27:58 -050038import java.util.ArrayList;
39import java.util.Collection;
40import java.util.Collections;
Jason Monk7ce96b92015-02-02 11:27:58 -050041import 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";
Jason Monk7ce96b92015-02-02 11:27:58 -050051
jackqdyulei1751eaa2018-09-14 11:22:32 -070052 // See mConnectAttempted
53 private static final long MAX_UUID_DELAY_FOR_AUTO_CONNECT = 5000;
Stanley Tngca33f5b2019-05-02 10:36:40 -070054 // Some Hearing Aids (especially the 2nd device) needs more time to do service discovery
55 private static final long MAX_HEARING_AIDS_DELAY_FOR_AUTO_CONNECT = 15000;
jackqdyulei1751eaa2018-09-14 11:22:32 -070056 private static final long MAX_HOGP_DELAY_FOR_AUTO_CONNECT = 30000;
57
Jason Monk7ce96b92015-02-02 11:27:58 -050058 private final Context mContext;
timhypengf64f6902018-07-31 15:40:15 +080059 private final BluetoothAdapter mLocalAdapter;
Jason Monk7ce96b92015-02-02 11:27:58 -050060 private final LocalBluetoothProfileManager mProfileManager;
jackqdyulei1eb10ec2018-11-01 13:10:09 -070061 private final Object mProfileLock = new Object();
timhypeng5c62ebb2018-08-28 09:59:03 +080062 BluetoothDevice mDevice;
Isha Bobrac3d94132018-02-08 16:04:36 -080063 private long mHiSyncId;
Jack Hec219bc92017-07-24 14:55:59 -070064 // Need this since there is no method for getting RSSI
timhypeng5c62ebb2018-08-28 09:59:03 +080065 short mRssi;
66 // mProfiles and mRemovedProfiles does not do swap() between main and sub device. It is
67 // because current sub device is only for HearingAid and its profile is the same.
jackqdyulei1eb10ec2018-11-01 13:10:09 -070068 private final List<LocalBluetoothProfile> mProfiles = new ArrayList<>();
Jason Monk7ce96b92015-02-02 11:27:58 -050069
70 // List of profiles that were previously in mProfiles, but have been removed
jackqdyulei1eb10ec2018-11-01 13:10:09 -070071 private final List<LocalBluetoothProfile> mRemovedProfiles = new ArrayList<>();
Jason Monk7ce96b92015-02-02 11:27:58 -050072
73 // Device supports PANU but not NAP: remove PanProfile after device disconnects from NAP
74 private boolean mLocalNapRoleConnected;
75
timhypeng5c62ebb2018-08-28 09:59:03 +080076 boolean mJustDiscovered;
Jason Monk7ce96b92015-02-02 11:27:58 -050077
jackqdyulei1eb10ec2018-11-01 13:10:09 -070078 private final Collection<Callback> mCallbacks = new ArrayList<>();
Jason Monk7ce96b92015-02-02 11:27:58 -050079
Jason Monk7ce96b92015-02-02 11:27:58 -050080 /**
81 * Last time a bt profile auto-connect was attempted.
82 * If an ACTION_UUID intent comes in within
83 * MAX_UUID_DELAY_FOR_AUTO_CONNECT milliseconds, we will try auto-connect
84 * again with the new UUIDs
85 */
86 private long mConnectAttempted;
87
Pavlin Radoslavov1af33a12018-01-21 02:59:15 -080088 // Active device state
89 private boolean mIsActiveDeviceA2dp = false;
90 private boolean mIsActiveDeviceHeadset = false;
Hansong Zhangd7b35912018-03-16 09:15:48 -070091 private boolean mIsActiveDeviceHearingAid = false;
timhypeng5c62ebb2018-08-28 09:59:03 +080092 // Group second device for Hearing Aid
93 private CachedBluetoothDevice mSubDevice;
jackqdyulei1751eaa2018-09-14 11:22:32 -070094
95 CachedBluetoothDevice(Context context, LocalBluetoothProfileManager profileManager,
96 BluetoothDevice device) {
97 mContext = context;
98 mLocalAdapter = BluetoothAdapter.getDefaultAdapter();
99 mProfileManager = profileManager;
100 mDevice = device;
jackqdyulei1751eaa2018-09-14 11:22:32 -0700101 fillData();
102 mHiSyncId = BluetoothHearingAid.HI_SYNC_ID_INVALID;
103 }
104
Jason Monk7ce96b92015-02-02 11:27:58 -0500105 /**
106 * Describes the current device and profile for logging.
107 *
108 * @param profile Profile to describe
109 * @return Description of the device and profile
110 */
111 private String describe(LocalBluetoothProfile profile) {
112 StringBuilder sb = new StringBuilder();
113 sb.append("Address:").append(mDevice);
114 if (profile != null) {
115 sb.append(" Profile:").append(profile);
116 }
117
118 return sb.toString();
119 }
120
121 void onProfileStateChanged(LocalBluetoothProfile profile, int newProfileState) {
Kunhung Lie7b7cb82018-05-17 11:04:54 +0800122 if (BluetoothUtils.D) {
Chienyuan559b7f12018-12-17 15:30:20 +0800123 Log.d(TAG, "onProfileStateChanged: profile " + profile + ", device=" + mDevice
124 + ", newProfileState " + newProfileState);
Jason Monk7ce96b92015-02-02 11:27:58 -0500125 }
timhypengf64f6902018-07-31 15:40:15 +0800126 if (mLocalAdapter.getState() == BluetoothAdapter.STATE_TURNING_OFF)
Jason Monk7ce96b92015-02-02 11:27:58 -0500127 {
Kunhung Lie7b7cb82018-05-17 11:04:54 +0800128 if (BluetoothUtils.D) {
129 Log.d(TAG, " BT Turninig Off...Profile conn state change ignored...");
130 }
Jason Monk7ce96b92015-02-02 11:27:58 -0500131 return;
132 }
jackqdyulei1eb10ec2018-11-01 13:10:09 -0700133
134 synchronized (mProfileLock) {
135 if (newProfileState == BluetoothProfile.STATE_CONNECTED) {
136 if (profile instanceof MapProfile) {
137 profile.setPreferred(mDevice, true);
Jason Monk7ce96b92015-02-02 11:27:58 -0500138 }
jackqdyulei1eb10ec2018-11-01 13:10:09 -0700139 if (!mProfiles.contains(profile)) {
140 mRemovedProfiles.remove(profile);
141 mProfiles.add(profile);
142 if (profile instanceof PanProfile
143 && ((PanProfile) profile).isLocalRoleNap(mDevice)) {
144 // Device doesn't support NAP, so remove PanProfile on disconnect
145 mLocalNapRoleConnected = true;
146 }
147 }
148 } else if (profile instanceof MapProfile
149 && newProfileState == BluetoothProfile.STATE_DISCONNECTED) {
150 profile.setPreferred(mDevice, false);
151 } else if (mLocalNapRoleConnected && profile instanceof PanProfile
152 && ((PanProfile) profile).isLocalRoleNap(mDevice)
153 && newProfileState == BluetoothProfile.STATE_DISCONNECTED) {
154 Log.d(TAG, "Removing PanProfile from device after NAP disconnect");
155 mProfiles.remove(profile);
156 mRemovedProfiles.add(profile);
157 mLocalNapRoleConnected = false;
Jason Monk7ce96b92015-02-02 11:27:58 -0500158 }
Jason Monk7ce96b92015-02-02 11:27:58 -0500159 }
jackqdyulei1eb10ec2018-11-01 13:10:09 -0700160
Pavlin Radoslavov1af33a12018-01-21 02:59:15 -0800161 fetchActiveDevices();
Jason Monk7ce96b92015-02-02 11:27:58 -0500162 }
163
Jason Monk7ce96b92015-02-02 11:27:58 -0500164 public void disconnect() {
jackqdyulei1eb10ec2018-11-01 13:10:09 -0700165 synchronized (mProfileLock) {
166 for (LocalBluetoothProfile profile : mProfiles) {
167 disconnect(profile);
168 }
Jason Monk7ce96b92015-02-02 11:27:58 -0500169 }
170 // Disconnect PBAP server in case its connected
171 // This is to ensure all the profiles are disconnected as some CK/Hs do not
172 // disconnect PBAP connection when HF connection is brought down
173 PbapServerProfile PbapProfile = mProfileManager.getPbapProfile();
Joseph Pirozzocdbab122019-01-31 10:58:22 -0800174 if (PbapProfile != null && isConnectedProfile(PbapProfile))
Jason Monk7ce96b92015-02-02 11:27:58 -0500175 {
176 PbapProfile.disconnect(mDevice);
177 }
178 }
179
180 public void disconnect(LocalBluetoothProfile profile) {
181 if (profile.disconnect(mDevice)) {
Kunhung Lie7b7cb82018-05-17 11:04:54 +0800182 if (BluetoothUtils.D) {
Jason Monk7ce96b92015-02-02 11:27:58 -0500183 Log.d(TAG, "Command sent successfully:DISCONNECT " + describe(profile));
184 }
185 }
186 }
187
188 public void connect(boolean connectAllProfiles) {
189 if (!ensurePaired()) {
190 return;
191 }
192
193 mConnectAttempted = SystemClock.elapsedRealtime();
194 connectWithoutResettingTimer(connectAllProfiles);
195 }
196
jackqdyulei1751eaa2018-09-14 11:22:32 -0700197 public long getHiSyncId() {
198 return mHiSyncId;
199 }
200
201 public void setHiSyncId(long id) {
202 if (BluetoothUtils.D) {
203 Log.d(TAG, "setHiSyncId: mDevice " + mDevice + ", id " + id);
204 }
205 mHiSyncId = id;
206 }
207
Stanley Tngc1182942018-11-29 13:47:45 -0800208 public boolean isHearingAidDevice() {
209 return mHiSyncId != BluetoothHearingAid.HI_SYNC_ID_INVALID;
210 }
211
Jason Monk7ce96b92015-02-02 11:27:58 -0500212 void onBondingDockConnect() {
213 // Attempt to connect if UUIDs are available. Otherwise,
214 // we will connect when the ACTION_UUID intent arrives.
215 connect(false);
216 }
217
218 private void connectWithoutResettingTimer(boolean connectAllProfiles) {
jackqdyulei1eb10ec2018-11-01 13:10:09 -0700219 synchronized (mProfileLock) {
220 // Try to initialize the profiles if they were not.
221 if (mProfiles.isEmpty()) {
222 // if mProfiles is empty, then do not invoke updateProfiles. This causes a race
223 // condition with carkits during pairing, wherein RemoteDevice.UUIDs have been
224 // updated from bluetooth stack but ACTION.uuid is not sent yet.
225 // Eventually ACTION.uuid will be received which shall trigger the connection of the
226 // various profiles
227 // If UUIDs are not available yet, connect will be happen
228 // upon arrival of the ACTION_UUID intent.
Stanley Tngca33f5b2019-05-02 10:36:40 -0700229 Log.d(TAG, "No profiles. Maybe we will connect later for device " + mDevice);
jackqdyulei1eb10ec2018-11-01 13:10:09 -0700230 return;
231 }
Jason Monk7ce96b92015-02-02 11:27:58 -0500232
jackqdyulei1eb10ec2018-11-01 13:10:09 -0700233 int preferredProfiles = 0;
234 for (LocalBluetoothProfile profile : mProfiles) {
235 if (connectAllProfiles ? profile.accessProfileEnabled()
236 : profile.isAutoConnectable()) {
237 if (profile.isPreferred(mDevice)) {
238 ++preferredProfiles;
239 connectInt(profile);
240 }
Jason Monk7ce96b92015-02-02 11:27:58 -0500241 }
242 }
jackqdyulei1eb10ec2018-11-01 13:10:09 -0700243 if (BluetoothUtils.D) Log.d(TAG, "Preferred profiles = " + preferredProfiles);
Jason Monk7ce96b92015-02-02 11:27:58 -0500244
jackqdyulei1eb10ec2018-11-01 13:10:09 -0700245 if (preferredProfiles == 0) {
246 connectAutoConnectableProfiles();
247 }
Jason Monk7ce96b92015-02-02 11:27:58 -0500248 }
249 }
250
251 private void connectAutoConnectableProfiles() {
252 if (!ensurePaired()) {
253 return;
254 }
Jason Monk7ce96b92015-02-02 11:27:58 -0500255
jackqdyulei1eb10ec2018-11-01 13:10:09 -0700256 synchronized (mProfileLock) {
257 for (LocalBluetoothProfile profile : mProfiles) {
258 if (profile.isAutoConnectable()) {
259 profile.setPreferred(mDevice, true);
260 connectInt(profile);
261 }
Jason Monk7ce96b92015-02-02 11:27:58 -0500262 }
263 }
264 }
265
266 /**
267 * Connect this device to the specified profile.
268 *
269 * @param profile the profile to use with the remote device
270 */
271 public void connectProfile(LocalBluetoothProfile profile) {
272 mConnectAttempted = SystemClock.elapsedRealtime();
Jason Monk7ce96b92015-02-02 11:27:58 -0500273 connectInt(profile);
274 // Refresh the UI based on profile.connect() call
275 refresh();
276 }
277
278 synchronized void connectInt(LocalBluetoothProfile profile) {
279 if (!ensurePaired()) {
280 return;
281 }
282 if (profile.connect(mDevice)) {
Kunhung Lie7b7cb82018-05-17 11:04:54 +0800283 if (BluetoothUtils.D) {
Jason Monk7ce96b92015-02-02 11:27:58 -0500284 Log.d(TAG, "Command sent successfully:CONNECT " + describe(profile));
285 }
286 return;
287 }
hughchencb6e1972018-08-15 16:13:21 +0800288 Log.i(TAG, "Failed to connect " + profile.toString() + " to " + getName());
Jason Monk7ce96b92015-02-02 11:27:58 -0500289 }
290
291 private boolean ensurePaired() {
292 if (getBondState() == BluetoothDevice.BOND_NONE) {
293 startPairing();
294 return false;
295 } else {
296 return true;
297 }
298 }
299
300 public boolean startPairing() {
301 // Pairing is unreliable while scanning, so cancel discovery
302 if (mLocalAdapter.isDiscovering()) {
303 mLocalAdapter.cancelDiscovery();
304 }
305
306 if (!mDevice.createBond()) {
307 return false;
308 }
309
Jason Monk7ce96b92015-02-02 11:27:58 -0500310 return true;
311 }
312
Jason Monk7ce96b92015-02-02 11:27:58 -0500313 public void unpair() {
314 int state = getBondState();
315
316 if (state == BluetoothDevice.BOND_BONDING) {
317 mDevice.cancelBondProcess();
318 }
319
320 if (state != BluetoothDevice.BOND_NONE) {
321 final BluetoothDevice dev = mDevice;
322 if (dev != null) {
323 final boolean successful = dev.removeBond();
324 if (successful) {
Kunhung Lie7b7cb82018-05-17 11:04:54 +0800325 if (BluetoothUtils.D) {
Jason Monk7ce96b92015-02-02 11:27:58 -0500326 Log.d(TAG, "Command sent successfully:REMOVE_BOND " + describe(null));
327 }
Kunhung Lie7b7cb82018-05-17 11:04:54 +0800328 } else if (BluetoothUtils.V) {
Jason Monk7ce96b92015-02-02 11:27:58 -0500329 Log.v(TAG, "Framework rejected command immediately:REMOVE_BOND " +
Isha Bobrac3d94132018-02-08 16:04:36 -0800330 describe(null));
Jason Monk7ce96b92015-02-02 11:27:58 -0500331 }
332 }
333 }
334 }
335
336 public int getProfileConnectionState(LocalBluetoothProfile profile) {
jackqdyuleibf509052018-09-24 14:00:26 -0700337 return profile != null
338 ? profile.getConnectionStatus(mDevice)
339 : BluetoothProfile.STATE_DISCONNECTED;
Jason Monk7ce96b92015-02-02 11:27:58 -0500340 }
341
342 // TODO: do any of these need to run async on a background thread?
343 private void fillData() {
Jason Monk7ce96b92015-02-02 11:27:58 -0500344 updateProfiles();
Pavlin Radoslavov1af33a12018-01-21 02:59:15 -0800345 fetchActiveDevices();
Jason Monk7ce96b92015-02-02 11:27:58 -0500346 migratePhonebookPermissionChoice();
347 migrateMessagePermissionChoice();
Jason Monk7ce96b92015-02-02 11:27:58 -0500348
Jason Monk7ce96b92015-02-02 11:27:58 -0500349 dispatchAttributesChanged();
350 }
351
352 public BluetoothDevice getDevice() {
353 return mDevice;
354 }
355
Antony Sargent7ad051e2017-06-29 15:23:13 -0700356 /**
357 * Convenience method that can be mocked - it lets tests avoid having to call getDevice() which
358 * causes problems in tests since BluetoothDevice is final and cannot be mocked.
359 * @return the address of this device
360 */
361 public String getAddress() {
362 return mDevice.getAddress();
363 }
364
Jason Monk7ce96b92015-02-02 11:27:58 -0500365 /**
hughchencb6e1972018-08-15 16:13:21 +0800366 * Get name from remote device
367 * @return {@link BluetoothDevice#getAliasName()} if
368 * {@link BluetoothDevice#getAliasName()} is not null otherwise return
369 * {@link BluetoothDevice#getAddress()}
Jason Monk7ce96b92015-02-02 11:27:58 -0500370 */
hughchencb6e1972018-08-15 16:13:21 +0800371 public String getName() {
372 final String aliasName = mDevice.getAliasName();
373 return TextUtils.isEmpty(aliasName) ? getAddress() : aliasName;
Jason Monk7ce96b92015-02-02 11:27:58 -0500374 }
375
376 /**
Jack Hec219bc92017-07-24 14:55:59 -0700377 * User changes the device name
378 * @param name new alias name to be set, should never be null
Jason Monk7ce96b92015-02-02 11:27:58 -0500379 */
380 public void setName(String name) {
hughchencb6e1972018-08-15 16:13:21 +0800381 // Prevent getName() to be set to null if setName(null) is called
382 if (name != null && !TextUtils.equals(name, getName())) {
Jason Monk7ce96b92015-02-02 11:27:58 -0500383 mDevice.setAlias(name);
384 dispatchAttributesChanged();
385 }
386 }
387
Hansong Zhang6a416322018-03-19 18:20:38 -0700388 /**
389 * Set this device as active device
390 * @return true if at least one profile on this device is set to active, false otherwise
391 */
392 public boolean setActive() {
393 boolean result = false;
394 A2dpProfile a2dpProfile = mProfileManager.getA2dpProfile();
395 if (a2dpProfile != null && isConnectedProfile(a2dpProfile)) {
396 if (a2dpProfile.setActiveDevice(getDevice())) {
397 Log.i(TAG, "OnPreferenceClickListener: A2DP active device=" + this);
398 result = true;
399 }
400 }
401 HeadsetProfile headsetProfile = mProfileManager.getHeadsetProfile();
402 if ((headsetProfile != null) && isConnectedProfile(headsetProfile)) {
403 if (headsetProfile.setActiveDevice(getDevice())) {
404 Log.i(TAG, "OnPreferenceClickListener: Headset active device=" + this);
405 result = true;
406 }
407 }
Hansong Zhangd7b35912018-03-16 09:15:48 -0700408 HearingAidProfile hearingAidProfile = mProfileManager.getHearingAidProfile();
409 if ((hearingAidProfile != null) && isConnectedProfile(hearingAidProfile)) {
410 if (hearingAidProfile.setActiveDevice(getDevice())) {
411 Log.i(TAG, "OnPreferenceClickListener: Hearing Aid active device=" + this);
412 result = true;
413 }
414 }
Hansong Zhang6a416322018-03-19 18:20:38 -0700415 return result;
416 }
417
Jason Monk7ce96b92015-02-02 11:27:58 -0500418 void refreshName() {
hughchencb6e1972018-08-15 16:13:21 +0800419 if (BluetoothUtils.D) {
420 Log.d(TAG, "Device name: " + getName());
Jason Monk7ce96b92015-02-02 11:27:58 -0500421 }
hughchencb6e1972018-08-15 16:13:21 +0800422 dispatchAttributesChanged();
Jason Monk7ce96b92015-02-02 11:27:58 -0500423 }
424
Jack He6258aae2017-06-29 17:01:23 -0700425 /**
Jack Hec219bc92017-07-24 14:55:59 -0700426 * Checks if device has a human readable name besides MAC address
427 * @return true if device's alias name is not null nor empty, false otherwise
428 */
429 public boolean hasHumanReadableName() {
430 return !TextUtils.isEmpty(mDevice.getAliasName());
431 }
432
433 /**
Jack He6258aae2017-06-29 17:01:23 -0700434 * Get battery level from remote device
435 * @return battery level in percentage [0-100], or {@link BluetoothDevice#BATTERY_LEVEL_UNKNOWN}
436 */
437 public int getBatteryLevel() {
438 return mDevice.getBatteryLevel();
439 }
440
Jason Monk7ce96b92015-02-02 11:27:58 -0500441 void refresh() {
442 dispatchAttributesChanged();
443 }
444
Jack He51520472017-07-24 12:30:08 -0700445 public void setJustDiscovered(boolean justDiscovered) {
446 if (mJustDiscovered != justDiscovered) {
447 mJustDiscovered = justDiscovered;
Jason Monk7ce96b92015-02-02 11:27:58 -0500448 dispatchAttributesChanged();
449 }
450 }
451
452 public int getBondState() {
453 return mDevice.getBondState();
454 }
455
Pavlin Radoslavov1af33a12018-01-21 02:59:15 -0800456 /**
Pavlin Radoslavovc285d552018-02-06 16:14:00 -0800457 * Update the device status as active or non-active per Bluetooth profile.
Pavlin Radoslavov1af33a12018-01-21 02:59:15 -0800458 *
459 * @param isActive true if the device is active
460 * @param bluetoothProfile the Bluetooth profile
461 */
Pavlin Radoslavovc285d552018-02-06 16:14:00 -0800462 public void onActiveDeviceChanged(boolean isActive, int bluetoothProfile) {
Pavlin Radoslavov1af33a12018-01-21 02:59:15 -0800463 boolean changed = false;
464 switch (bluetoothProfile) {
465 case BluetoothProfile.A2DP:
466 changed = (mIsActiveDeviceA2dp != isActive);
467 mIsActiveDeviceA2dp = isActive;
468 break;
469 case BluetoothProfile.HEADSET:
470 changed = (mIsActiveDeviceHeadset != isActive);
471 mIsActiveDeviceHeadset = isActive;
472 break;
Hansong Zhangd7b35912018-03-16 09:15:48 -0700473 case BluetoothProfile.HEARING_AID:
474 changed = (mIsActiveDeviceHearingAid != isActive);
475 mIsActiveDeviceHearingAid = isActive;
476 break;
Pavlin Radoslavov1af33a12018-01-21 02:59:15 -0800477 default:
Pavlin Radoslavovc285d552018-02-06 16:14:00 -0800478 Log.w(TAG, "onActiveDeviceChanged: unknown profile " + bluetoothProfile +
Pavlin Radoslavov1af33a12018-01-21 02:59:15 -0800479 " isActive " + isActive);
480 break;
481 }
482 if (changed) {
483 dispatchAttributesChanged();
484 }
485 }
486
Pavlin Radoslavovc285d552018-02-06 16:14:00 -0800487 /**
timhypengf0509322018-03-29 14:23:21 +0800488 * Update the profile audio state.
489 */
490 void onAudioModeChanged() {
491 dispatchAttributesChanged();
492 }
493 /**
Pavlin Radoslavovc285d552018-02-06 16:14:00 -0800494 * Get the device status as active or non-active per Bluetooth profile.
495 *
496 * @param bluetoothProfile the Bluetooth profile
497 * @return true if the device is active
498 */
499 @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
500 public boolean isActiveDevice(int bluetoothProfile) {
501 switch (bluetoothProfile) {
502 case BluetoothProfile.A2DP:
503 return mIsActiveDeviceA2dp;
504 case BluetoothProfile.HEADSET:
505 return mIsActiveDeviceHeadset;
Hansong Zhangd7b35912018-03-16 09:15:48 -0700506 case BluetoothProfile.HEARING_AID:
507 return mIsActiveDeviceHearingAid;
Pavlin Radoslavovc285d552018-02-06 16:14:00 -0800508 default:
509 Log.w(TAG, "getActiveDevice: unknown profile " + bluetoothProfile);
510 break;
511 }
512 return false;
513 }
514
Jason Monk7ce96b92015-02-02 11:27:58 -0500515 void setRssi(short rssi) {
516 if (mRssi != rssi) {
517 mRssi = rssi;
518 dispatchAttributesChanged();
519 }
520 }
521
522 /**
523 * Checks whether we are connected to this device (any profile counts).
524 *
525 * @return Whether it is connected.
526 */
527 public boolean isConnected() {
jackqdyulei1eb10ec2018-11-01 13:10:09 -0700528 synchronized (mProfileLock) {
529 for (LocalBluetoothProfile profile : mProfiles) {
530 int status = getProfileConnectionState(profile);
531 if (status == BluetoothProfile.STATE_CONNECTED) {
532 return true;
533 }
Jason Monk7ce96b92015-02-02 11:27:58 -0500534 }
Jason Monk7ce96b92015-02-02 11:27:58 -0500535
jackqdyulei1eb10ec2018-11-01 13:10:09 -0700536 return false;
537 }
Jason Monk7ce96b92015-02-02 11:27:58 -0500538 }
539
540 public boolean isConnectedProfile(LocalBluetoothProfile profile) {
541 int status = getProfileConnectionState(profile);
542 return status == BluetoothProfile.STATE_CONNECTED;
543
544 }
545
546 public boolean isBusy() {
jackqdyulei1eb10ec2018-11-01 13:10:09 -0700547 synchronized (mProfileLock) {
548 for (LocalBluetoothProfile profile : mProfiles) {
549 int status = getProfileConnectionState(profile);
550 if (status == BluetoothProfile.STATE_CONNECTING
551 || status == BluetoothProfile.STATE_DISCONNECTING) {
552 return true;
553 }
Jason Monk7ce96b92015-02-02 11:27:58 -0500554 }
jackqdyulei1eb10ec2018-11-01 13:10:09 -0700555 return getBondState() == BluetoothDevice.BOND_BONDING;
Jason Monk7ce96b92015-02-02 11:27:58 -0500556 }
Jason Monk7ce96b92015-02-02 11:27:58 -0500557 }
558
Jason Monk7ce96b92015-02-02 11:27:58 -0500559 private boolean updateProfiles() {
560 ParcelUuid[] uuids = mDevice.getUuids();
561 if (uuids == null) return false;
562
563 ParcelUuid[] localUuids = mLocalAdapter.getUuids();
564 if (localUuids == null) return false;
565
Jack Hec219bc92017-07-24 14:55:59 -0700566 /*
Jason Monk7ce96b92015-02-02 11:27:58 -0500567 * Now we know if the device supports PBAP, update permissions...
568 */
569 processPhonebookAccess();
570
jackqdyulei1eb10ec2018-11-01 13:10:09 -0700571 synchronized (mProfileLock) {
572 mProfileManager.updateProfiles(uuids, localUuids, mProfiles, mRemovedProfiles,
573 mLocalNapRoleConnected, mDevice);
574 }
Jason Monk7ce96b92015-02-02 11:27:58 -0500575
Kunhung Lie7b7cb82018-05-17 11:04:54 +0800576 if (BluetoothUtils.D) {
Chienyuan559b7f12018-12-17 15:30:20 +0800577 Log.e(TAG, "updating profiles for " + mDevice.getAliasName() + ", " + mDevice);
Jason Monk7ce96b92015-02-02 11:27:58 -0500578 BluetoothClass bluetoothClass = mDevice.getBluetoothClass();
579
580 if (bluetoothClass != null) Log.v(TAG, "Class: " + bluetoothClass.toString());
581 Log.v(TAG, "UUID:");
582 for (ParcelUuid uuid : uuids) {
583 Log.v(TAG, " " + uuid);
584 }
585 }
586 return true;
587 }
588
Pavlin Radoslavov1af33a12018-01-21 02:59:15 -0800589 private void fetchActiveDevices() {
590 A2dpProfile a2dpProfile = mProfileManager.getA2dpProfile();
591 if (a2dpProfile != null) {
592 mIsActiveDeviceA2dp = mDevice.equals(a2dpProfile.getActiveDevice());
593 }
594 HeadsetProfile headsetProfile = mProfileManager.getHeadsetProfile();
595 if (headsetProfile != null) {
596 mIsActiveDeviceHeadset = mDevice.equals(headsetProfile.getActiveDevice());
597 }
Hansong Zhangd7b35912018-03-16 09:15:48 -0700598 HearingAidProfile hearingAidProfile = mProfileManager.getHearingAidProfile();
599 if (hearingAidProfile != null) {
Hansong Zhang3b8f09b2018-03-28 16:53:10 -0700600 mIsActiveDeviceHearingAid = hearingAidProfile.getActiveDevices().contains(mDevice);
Hansong Zhangd7b35912018-03-16 09:15:48 -0700601 }
Pavlin Radoslavov1af33a12018-01-21 02:59:15 -0800602 }
603
Jason Monk7ce96b92015-02-02 11:27:58 -0500604 /**
Jason Monk7ce96b92015-02-02 11:27:58 -0500605 * Refreshes the UI when framework alerts us of a UUID change.
606 */
607 void onUuidChanged() {
608 updateProfiles();
Etan Cohen50d47612015-03-31 12:45:23 -0700609 ParcelUuid[] uuids = mDevice.getUuids();
Venkat Raghavan05e08c32015-04-06 16:26:11 -0700610
Etan Cohen50d47612015-03-31 12:45:23 -0700611 long timeout = MAX_UUID_DELAY_FOR_AUTO_CONNECT;
Venkat Raghavan05e08c32015-04-06 16:26:11 -0700612 if (BluetoothUuid.isUuidPresent(uuids, BluetoothUuid.Hogp)) {
613 timeout = MAX_HOGP_DELAY_FOR_AUTO_CONNECT;
Stanley Tngca33f5b2019-05-02 10:36:40 -0700614 } else if (BluetoothUuid.isUuidPresent(uuids, BluetoothUuid.HearingAid)) {
615 timeout = MAX_HEARING_AIDS_DELAY_FOR_AUTO_CONNECT;
Venkat Raghavan05e08c32015-04-06 16:26:11 -0700616 }
Jason Monk7ce96b92015-02-02 11:27:58 -0500617
Kunhung Lie7b7cb82018-05-17 11:04:54 +0800618 if (BluetoothUtils.D) {
Stanley Tngca33f5b2019-05-02 10:36:40 -0700619 Log.d(TAG, "onUuidChanged: Time since last connect="
Jason Monk7ce96b92015-02-02 11:27:58 -0500620 + (SystemClock.elapsedRealtime() - mConnectAttempted));
621 }
622
623 /*
Venkat Raghavan05e08c32015-04-06 16:26:11 -0700624 * If a connect was attempted earlier without any UUID, we will do the connect now.
625 * Otherwise, allow the connect on UUID change.
Jason Monk7ce96b92015-02-02 11:27:58 -0500626 */
627 if (!mProfiles.isEmpty()
Andre Eisenbach7be83c52015-09-03 15:20:09 -0700628 && ((mConnectAttempted + timeout) > SystemClock.elapsedRealtime())) {
Jason Monk7ce96b92015-02-02 11:27:58 -0500629 connectWithoutResettingTimer(false);
630 }
Andre Eisenbach7be83c52015-09-03 15:20:09 -0700631
Jason Monk7ce96b92015-02-02 11:27:58 -0500632 dispatchAttributesChanged();
633 }
634
635 void onBondingStateChanged(int bondState) {
636 if (bondState == BluetoothDevice.BOND_NONE) {
jackqdyulei1eb10ec2018-11-01 13:10:09 -0700637 synchronized (mProfileLock) {
638 mProfiles.clear();
639 }
hughchen783a80b2018-08-20 17:04:56 +0800640 mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_UNKNOWN);
641 mDevice.setMessageAccessPermission(BluetoothDevice.ACCESS_UNKNOWN);
642 mDevice.setSimAccessPermission(BluetoothDevice.ACCESS_UNKNOWN);
Jason Monk7ce96b92015-02-02 11:27:58 -0500643 }
644
645 refresh();
646
647 if (bondState == BluetoothDevice.BOND_BONDED) {
648 if (mDevice.isBluetoothDock()) {
649 onBondingDockConnect();
Jakub Pawlowski0278ab92016-07-20 11:55:48 -0700650 } else if (mDevice.isBondingInitiatedLocally()) {
Jason Monk7ce96b92015-02-02 11:27:58 -0500651 connect(false);
652 }
Jason Monk7ce96b92015-02-02 11:27:58 -0500653 }
654 }
655
Jason Monk7ce96b92015-02-02 11:27:58 -0500656 public BluetoothClass getBtClass() {
hughchen8df81e32018-08-16 16:02:16 +0800657 return mDevice.getBluetoothClass();
Jason Monk7ce96b92015-02-02 11:27:58 -0500658 }
659
660 public List<LocalBluetoothProfile> getProfiles() {
661 return Collections.unmodifiableList(mProfiles);
662 }
663
664 public List<LocalBluetoothProfile> getConnectableProfiles() {
665 List<LocalBluetoothProfile> connectableProfiles =
666 new ArrayList<LocalBluetoothProfile>();
jackqdyulei1eb10ec2018-11-01 13:10:09 -0700667 synchronized (mProfileLock) {
668 for (LocalBluetoothProfile profile : mProfiles) {
669 if (profile.accessProfileEnabled()) {
670 connectableProfiles.add(profile);
671 }
Jason Monk7ce96b92015-02-02 11:27:58 -0500672 }
673 }
674 return connectableProfiles;
675 }
676
677 public List<LocalBluetoothProfile> getRemovedProfiles() {
678 return mRemovedProfiles;
679 }
680
681 public void registerCallback(Callback callback) {
682 synchronized (mCallbacks) {
683 mCallbacks.add(callback);
684 }
685 }
686
687 public void unregisterCallback(Callback callback) {
688 synchronized (mCallbacks) {
689 mCallbacks.remove(callback);
690 }
691 }
692
hughchen8df81e32018-08-16 16:02:16 +0800693 void dispatchAttributesChanged() {
Jason Monk7ce96b92015-02-02 11:27:58 -0500694 synchronized (mCallbacks) {
695 for (Callback callback : mCallbacks) {
696 callback.onDeviceAttributesChanged();
697 }
698 }
699 }
700
701 @Override
702 public String toString() {
703 return mDevice.toString();
704 }
705
706 @Override
707 public boolean equals(Object o) {
708 if ((o == null) || !(o instanceof CachedBluetoothDevice)) {
709 return false;
710 }
711 return mDevice.equals(((CachedBluetoothDevice) o).mDevice);
712 }
713
714 @Override
715 public int hashCode() {
716 return mDevice.getAddress().hashCode();
717 }
718
719 // This comparison uses non-final fields so the sort order may change
720 // when device attributes change (such as bonding state). Settings
721 // will completely refresh the device list when this happens.
722 public int compareTo(CachedBluetoothDevice another) {
723 // Connected above not connected
724 int comparison = (another.isConnected() ? 1 : 0) - (isConnected() ? 1 : 0);
725 if (comparison != 0) return comparison;
726
727 // Paired above not paired
728 comparison = (another.getBondState() == BluetoothDevice.BOND_BONDED ? 1 : 0) -
729 (getBondState() == BluetoothDevice.BOND_BONDED ? 1 : 0);
730 if (comparison != 0) return comparison;
731
Jack He51520472017-07-24 12:30:08 -0700732 // Just discovered above discovered in the past
733 comparison = (another.mJustDiscovered ? 1 : 0) - (mJustDiscovered ? 1 : 0);
Jason Monk7ce96b92015-02-02 11:27:58 -0500734 if (comparison != 0) return comparison;
735
736 // Stronger signal above weaker signal
737 comparison = another.mRssi - mRssi;
738 if (comparison != 0) return comparison;
739
740 // Fallback on name
hughchencb6e1972018-08-15 16:13:21 +0800741 return getName().compareTo(another.getName());
Jason Monk7ce96b92015-02-02 11:27:58 -0500742 }
743
744 public interface Callback {
745 void onDeviceAttributesChanged();
746 }
747
Jason Monk7ce96b92015-02-02 11:27:58 -0500748 // Migrates data from old data store (in Settings app's shared preferences) to new (in Bluetooth
749 // app's shared preferences).
750 private void migratePhonebookPermissionChoice() {
751 SharedPreferences preferences = mContext.getSharedPreferences(
752 "bluetooth_phonebook_permission", Context.MODE_PRIVATE);
753 if (!preferences.contains(mDevice.getAddress())) {
754 return;
755 }
756
757 if (mDevice.getPhonebookAccessPermission() == BluetoothDevice.ACCESS_UNKNOWN) {
hughchen783a80b2018-08-20 17:04:56 +0800758 int oldPermission =
759 preferences.getInt(mDevice.getAddress(), BluetoothDevice.ACCESS_UNKNOWN);
760 if (oldPermission == BluetoothDevice.ACCESS_ALLOWED) {
Jason Monk7ce96b92015-02-02 11:27:58 -0500761 mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_ALLOWED);
hughchen783a80b2018-08-20 17:04:56 +0800762 } else if (oldPermission == BluetoothDevice.ACCESS_REJECTED) {
Jason Monk7ce96b92015-02-02 11:27:58 -0500763 mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_REJECTED);
764 }
765 }
766
767 SharedPreferences.Editor editor = preferences.edit();
768 editor.remove(mDevice.getAddress());
769 editor.commit();
770 }
771
Jason Monk7ce96b92015-02-02 11:27:58 -0500772 // Migrates data from old data store (in Settings app's shared preferences) to new (in Bluetooth
773 // app's shared preferences).
774 private void migrateMessagePermissionChoice() {
775 SharedPreferences preferences = mContext.getSharedPreferences(
776 "bluetooth_message_permission", Context.MODE_PRIVATE);
777 if (!preferences.contains(mDevice.getAddress())) {
778 return;
779 }
780
781 if (mDevice.getMessageAccessPermission() == BluetoothDevice.ACCESS_UNKNOWN) {
hughchen783a80b2018-08-20 17:04:56 +0800782 int oldPermission =
783 preferences.getInt(mDevice.getAddress(), BluetoothDevice.ACCESS_UNKNOWN);
784 if (oldPermission == BluetoothDevice.ACCESS_ALLOWED) {
Jason Monk7ce96b92015-02-02 11:27:58 -0500785 mDevice.setMessageAccessPermission(BluetoothDevice.ACCESS_ALLOWED);
hughchen783a80b2018-08-20 17:04:56 +0800786 } else if (oldPermission == BluetoothDevice.ACCESS_REJECTED) {
Jason Monk7ce96b92015-02-02 11:27:58 -0500787 mDevice.setMessageAccessPermission(BluetoothDevice.ACCESS_REJECTED);
788 }
789 }
790
791 SharedPreferences.Editor editor = preferences.edit();
792 editor.remove(mDevice.getAddress());
793 editor.commit();
794 }
795
Jason Monk7ce96b92015-02-02 11:27:58 -0500796 private void processPhonebookAccess() {
797 if (mDevice.getBondState() != BluetoothDevice.BOND_BONDED) return;
798
799 ParcelUuid[] uuids = mDevice.getUuids();
800 if (BluetoothUuid.containsAnyUuid(uuids, PbapServerProfile.PBAB_CLIENT_UUIDS)) {
801 // The pairing dialog now warns of phone-book access for paired devices.
802 // No separate prompt is displayed after pairing.
hughchen783a80b2018-08-20 17:04:56 +0800803 if (mDevice.getPhonebookAccessPermission() == BluetoothDevice.ACCESS_UNKNOWN) {
Sanket Padawe07533db2015-11-11 15:01:35 -0800804 if (mDevice.getBluetoothClass().getDeviceClass()
Hemant Guptab8d267a2016-06-07 12:53:59 +0530805 == BluetoothClass.Device.AUDIO_VIDEO_HANDSFREE ||
806 mDevice.getBluetoothClass().getDeviceClass()
807 == BluetoothClass.Device.AUDIO_VIDEO_WEARABLE_HEADSET) {
Zongheng Wangefdcff22019-09-05 13:44:28 -0700808 EventLog.writeEvent(0x534e4554, "138529441", -1, "");
Sanket Padawe07533db2015-11-11 15:01:35 -0800809 }
Zongheng Wangefdcff22019-09-05 13:44:28 -0700810 mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_REJECTED);
Sanket Padawe78c23762015-06-01 15:55:30 -0700811 }
Jason Monk7ce96b92015-02-02 11:27:58 -0500812 }
813 }
Jason Monkbe3c5db2015-02-04 13:00:55 -0500814
815 public int getMaxConnectionState() {
816 int maxState = BluetoothProfile.STATE_DISCONNECTED;
jackqdyulei1eb10ec2018-11-01 13:10:09 -0700817 synchronized (mProfileLock) {
818 for (LocalBluetoothProfile profile : getProfiles()) {
819 int connectionStatus = getProfileConnectionState(profile);
820 if (connectionStatus > maxState) {
821 maxState = connectionStatus;
822 }
Jason Monkbe3c5db2015-02-04 13:00:55 -0500823 }
824 }
825 return maxState;
826 }
827
828 /**
Lei Yue63ca1b2019-05-13 16:07:57 -0700829 * Return full summary that describes connection state of this device
830 *
831 * @see #getConnectionSummary(boolean shortSummary)
Jason Monkbe3c5db2015-02-04 13:00:55 -0500832 */
Jack He6258aae2017-06-29 17:01:23 -0700833 public String getConnectionSummary() {
Lei Yue63ca1b2019-05-13 16:07:57 -0700834 return getConnectionSummary(false /* shortSummary */);
835 }
836
837 /**
838 * Return summary that describes connection state of this device. Summary depends on:
839 * 1. Whether device has battery info
840 * 2. Whether device is in active usage(or in phone call)
841 *
842 * @param shortSummary {@code true} if need to return short version summary
843 */
844 public String getConnectionSummary(boolean shortSummary) {
timhypengf0509322018-03-29 14:23:21 +0800845 boolean profileConnected = false; // Updated as long as BluetoothProfile is connected
846 boolean a2dpConnected = true; // A2DP is connected
847 boolean hfpConnected = true; // HFP is connected
848 boolean hearingAidConnected = true; // Hearing Aid is connected
jackqdyulei1f9c1712019-03-05 16:42:57 -0800849 int leftBattery = -1;
850 int rightBattery = -1;
Jason Monkbe3c5db2015-02-04 13:00:55 -0500851
jackqdyulei1eb10ec2018-11-01 13:10:09 -0700852 synchronized (mProfileLock) {
853 for (LocalBluetoothProfile profile : getProfiles()) {
854 int connectionStatus = getProfileConnectionState(profile);
Jason Monkbe3c5db2015-02-04 13:00:55 -0500855
jackqdyulei1eb10ec2018-11-01 13:10:09 -0700856 switch (connectionStatus) {
857 case BluetoothProfile.STATE_CONNECTING:
858 case BluetoothProfile.STATE_DISCONNECTING:
859 return mContext.getString(
860 BluetoothUtils.getConnectionStateSummary(connectionStatus));
Jason Monkbe3c5db2015-02-04 13:00:55 -0500861
jackqdyulei1eb10ec2018-11-01 13:10:09 -0700862 case BluetoothProfile.STATE_CONNECTED:
863 profileConnected = true;
864 break;
Jason Monkbe3c5db2015-02-04 13:00:55 -0500865
jackqdyulei1eb10ec2018-11-01 13:10:09 -0700866 case BluetoothProfile.STATE_DISCONNECTED:
867 if (profile.isProfileReady()) {
868 if (profile instanceof A2dpProfile
869 || profile instanceof A2dpSinkProfile) {
870 a2dpConnected = false;
871 } else if (profile instanceof HeadsetProfile
872 || profile instanceof HfpClientProfile) {
873 hfpConnected = false;
874 } else if (profile instanceof HearingAidProfile) {
875 hearingAidConnected = false;
876 }
Jason Monkbe3c5db2015-02-04 13:00:55 -0500877 }
jackqdyulei1eb10ec2018-11-01 13:10:09 -0700878 break;
879 }
Jason Monkbe3c5db2015-02-04 13:00:55 -0500880 }
881 }
882
Jack He6258aae2017-06-29 17:01:23 -0700883 String batteryLevelPercentageString = null;
884 // Android framework should only set mBatteryLevel to valid range [0-100] or
885 // BluetoothDevice.BATTERY_LEVEL_UNKNOWN, any other value should be a framework bug.
886 // Thus assume here that if value is not BluetoothDevice.BATTERY_LEVEL_UNKNOWN, it must
887 // be valid
888 final int batteryLevel = getBatteryLevel();
889 if (batteryLevel != BluetoothDevice.BATTERY_LEVEL_UNKNOWN) {
890 // TODO: name com.android.settingslib.bluetooth.Utils something different
891 batteryLevelPercentageString =
892 com.android.settingslib.Utils.formatPercentage(batteryLevel);
893 }
894
timhypengf0509322018-03-29 14:23:21 +0800895 int stringRes = R.string.bluetooth_pairing;
896 //when profile is connected, information would be available
Jason Monkbe3c5db2015-02-04 13:00:55 -0500897 if (profileConnected) {
jackqdyulei1f9c1712019-03-05 16:42:57 -0800898 // Update Meta data for connected device
Ugo Yu73c387d2019-03-20 19:50:58 +0800899 if (BluetoothUtils.getBooleanMetaData(
900 mDevice, BluetoothDevice.METADATA_IS_UNTETHERED_HEADSET)) {
901 leftBattery = BluetoothUtils.getIntMetaData(mDevice,
902 BluetoothDevice.METADATA_UNTETHERED_LEFT_BATTERY);
903 rightBattery = BluetoothUtils.getIntMetaData(mDevice,
904 BluetoothDevice.METADATA_UNTETHERED_RIGHT_BATTERY);
jackqdyulei1f9c1712019-03-05 16:42:57 -0800905 }
906
wengsu6d263c22018-11-15 16:51:45 +0800907 // Set default string with battery level in device connected situation.
jackqdyulei1f9c1712019-03-05 16:42:57 -0800908 if (isTwsBatteryAvailable(leftBattery, rightBattery)) {
909 stringRes = R.string.bluetooth_battery_level_untethered;
910 } else if (batteryLevelPercentageString != null) {
wengsu6d263c22018-11-15 16:51:45 +0800911 stringRes = R.string.bluetooth_battery_level;
912 }
913
914 // Set active string in following device connected situation.
915 // 1. Hearing Aid device active.
916 // 2. Headset device active with in-calling state.
917 // 3. A2DP device active without in-calling state.
timhypengf0509322018-03-29 14:23:21 +0800918 if (a2dpConnected || hfpConnected || hearingAidConnected) {
jackqdyulei1f9c1712019-03-05 16:42:57 -0800919 final boolean isOnCall = Utils.isAudioModeOngoingCall(mContext);
wengsu6d263c22018-11-15 16:51:45 +0800920 if ((mIsActiveDeviceHearingAid)
921 || (mIsActiveDeviceHeadset && isOnCall)
922 || (mIsActiveDeviceA2dp && !isOnCall)) {
Lei Yue63ca1b2019-05-13 16:07:57 -0700923 if (isTwsBatteryAvailable(leftBattery, rightBattery) && !shortSummary) {
jackqdyulei1f9c1712019-03-05 16:42:57 -0800924 stringRes = R.string.bluetooth_active_battery_level_untethered;
Lei Yue63ca1b2019-05-13 16:07:57 -0700925 } else if (batteryLevelPercentageString != null && !shortSummary) {
jackqdyulei1f9c1712019-03-05 16:42:57 -0800926 stringRes = R.string.bluetooth_active_battery_level;
927 } else {
928 stringRes = R.string.bluetooth_active_no_battery_level;
929 }
Jack He6258aae2017-06-29 17:01:23 -0700930 }
Jason Monkbe3c5db2015-02-04 13:00:55 -0500931 }
932 }
933
jackqdyulei1f9c1712019-03-05 16:42:57 -0800934 if (stringRes != R.string.bluetooth_pairing
935 || getBondState() == BluetoothDevice.BOND_BONDING) {
936 if (isTwsBatteryAvailable(leftBattery, rightBattery)) {
937 return mContext.getString(stringRes, Utils.formatPercentage(leftBattery),
938 Utils.formatPercentage(rightBattery));
939 } else {
940 return mContext.getString(stringRes, batteryLevelPercentageString);
941 }
942 } else {
943 return null;
944 }
945 }
946
947 private boolean isTwsBatteryAvailable(int leftBattery, int rightBattery) {
948 return leftBattery >= 0 && rightBattery >= 0;
Jason Monkbe3c5db2015-02-04 13:00:55 -0500949 }
hughchen23b947e2018-03-31 17:32:53 +0800950
951 /**
ryanywline26aecd2018-05-15 14:20:50 +0800952 * @return resource for android auto string that describes the connection state of this device.
953 */
954 public String getCarConnectionSummary() {
955 boolean profileConnected = false; // at least one profile is connected
956 boolean a2dpNotConnected = false; // A2DP is preferred but not connected
957 boolean hfpNotConnected = false; // HFP is preferred but not connected
958 boolean hearingAidNotConnected = false; // Hearing Aid is preferred but not connected
959
jackqdyulei1eb10ec2018-11-01 13:10:09 -0700960 synchronized (mProfileLock) {
961 for (LocalBluetoothProfile profile : getProfiles()) {
962 int connectionStatus = getProfileConnectionState(profile);
ryanywline26aecd2018-05-15 14:20:50 +0800963
jackqdyulei1eb10ec2018-11-01 13:10:09 -0700964 switch (connectionStatus) {
965 case BluetoothProfile.STATE_CONNECTING:
966 case BluetoothProfile.STATE_DISCONNECTING:
967 return mContext.getString(
968 BluetoothUtils.getConnectionStateSummary(connectionStatus));
ryanywline26aecd2018-05-15 14:20:50 +0800969
jackqdyulei1eb10ec2018-11-01 13:10:09 -0700970 case BluetoothProfile.STATE_CONNECTED:
971 profileConnected = true;
972 break;
ryanywline26aecd2018-05-15 14:20:50 +0800973
jackqdyulei1eb10ec2018-11-01 13:10:09 -0700974 case BluetoothProfile.STATE_DISCONNECTED:
975 if (profile.isProfileReady()) {
976 if (profile instanceof A2dpProfile
977 || profile instanceof A2dpSinkProfile) {
978 a2dpNotConnected = true;
979 } else if (profile instanceof HeadsetProfile
980 || profile instanceof HfpClientProfile) {
981 hfpNotConnected = true;
982 } else if (profile instanceof HearingAidProfile) {
983 hearingAidNotConnected = true;
984 }
ryanywline26aecd2018-05-15 14:20:50 +0800985 }
jackqdyulei1eb10ec2018-11-01 13:10:09 -0700986 break;
987 }
ryanywline26aecd2018-05-15 14:20:50 +0800988 }
989 }
990
991 String batteryLevelPercentageString = null;
992 // Android framework should only set mBatteryLevel to valid range [0-100] or
993 // BluetoothDevice.BATTERY_LEVEL_UNKNOWN, any other value should be a framework bug.
994 // Thus assume here that if value is not BluetoothDevice.BATTERY_LEVEL_UNKNOWN, it must
995 // be valid
996 final int batteryLevel = getBatteryLevel();
997 if (batteryLevel != BluetoothDevice.BATTERY_LEVEL_UNKNOWN) {
998 // TODO: name com.android.settingslib.bluetooth.Utils something different
999 batteryLevelPercentageString =
1000 com.android.settingslib.Utils.formatPercentage(batteryLevel);
1001 }
1002
1003 // Prepare the string for the Active Device summary
1004 String[] activeDeviceStringsArray = mContext.getResources().getStringArray(
1005 R.array.bluetooth_audio_active_device_summaries);
1006 String activeDeviceString = activeDeviceStringsArray[0]; // Default value: not active
1007 if (mIsActiveDeviceA2dp && mIsActiveDeviceHeadset) {
1008 activeDeviceString = activeDeviceStringsArray[1]; // Active for Media and Phone
1009 } else {
1010 if (mIsActiveDeviceA2dp) {
1011 activeDeviceString = activeDeviceStringsArray[2]; // Active for Media only
1012 }
1013 if (mIsActiveDeviceHeadset) {
1014 activeDeviceString = activeDeviceStringsArray[3]; // Active for Phone only
1015 }
1016 }
1017 if (!hearingAidNotConnected && mIsActiveDeviceHearingAid) {
1018 activeDeviceString = activeDeviceStringsArray[1];
1019 return mContext.getString(R.string.bluetooth_connected, activeDeviceString);
1020 }
1021
1022 if (profileConnected) {
1023 if (a2dpNotConnected && hfpNotConnected) {
1024 if (batteryLevelPercentageString != null) {
1025 return mContext.getString(
1026 R.string.bluetooth_connected_no_headset_no_a2dp_battery_level,
1027 batteryLevelPercentageString, activeDeviceString);
1028 } else {
1029 return mContext.getString(R.string.bluetooth_connected_no_headset_no_a2dp,
1030 activeDeviceString);
1031 }
1032
1033 } else if (a2dpNotConnected) {
1034 if (batteryLevelPercentageString != null) {
1035 return mContext.getString(R.string.bluetooth_connected_no_a2dp_battery_level,
1036 batteryLevelPercentageString, activeDeviceString);
1037 } else {
1038 return mContext.getString(R.string.bluetooth_connected_no_a2dp,
1039 activeDeviceString);
1040 }
1041
1042 } else if (hfpNotConnected) {
1043 if (batteryLevelPercentageString != null) {
1044 return mContext.getString(R.string.bluetooth_connected_no_headset_battery_level,
1045 batteryLevelPercentageString, activeDeviceString);
1046 } else {
1047 return mContext.getString(R.string.bluetooth_connected_no_headset,
1048 activeDeviceString);
1049 }
1050 } else {
1051 if (batteryLevelPercentageString != null) {
1052 return mContext.getString(R.string.bluetooth_connected_battery_level,
1053 batteryLevelPercentageString, activeDeviceString);
1054 } else {
1055 return mContext.getString(R.string.bluetooth_connected, activeDeviceString);
1056 }
1057 }
1058 }
1059
1060 return getBondState() == BluetoothDevice.BOND_BONDING ?
1061 mContext.getString(R.string.bluetooth_pairing) : null;
1062 }
1063
1064 /**
hughchen23b947e2018-03-31 17:32:53 +08001065 * @return {@code true} if {@code cachedBluetoothDevice} is a2dp device
1066 */
timhypeng0cd2ecc2018-08-20 15:51:04 +08001067 public boolean isConnectedA2dpDevice() {
Hansong Zhang28963cd2018-06-13 11:30:59 -07001068 A2dpProfile a2dpProfile = mProfileManager.getA2dpProfile();
1069 return a2dpProfile != null && a2dpProfile.getConnectionStatus(mDevice) ==
hughchen23b947e2018-03-31 17:32:53 +08001070 BluetoothProfile.STATE_CONNECTED;
1071 }
1072
1073 /**
1074 * @return {@code true} if {@code cachedBluetoothDevice} is HFP device
1075 */
timhypenga025ad32018-08-20 16:01:16 +08001076 public boolean isConnectedHfpDevice() {
Hansong Zhang28963cd2018-06-13 11:30:59 -07001077 HeadsetProfile headsetProfile = mProfileManager.getHeadsetProfile();
1078 return headsetProfile != null && headsetProfile.getConnectionStatus(mDevice) ==
hughchen23b947e2018-03-31 17:32:53 +08001079 BluetoothProfile.STATE_CONNECTED;
1080 }
timhypengc509a652018-05-25 14:23:44 +08001081
1082 /**
1083 * @return {@code true} if {@code cachedBluetoothDevice} is Hearing Aid device
1084 */
1085 public boolean isConnectedHearingAidDevice() {
Hansong Zhang28963cd2018-06-13 11:30:59 -07001086 HearingAidProfile hearingAidProfile = mProfileManager.getHearingAidProfile();
1087 return hearingAidProfile != null && hearingAidProfile.getConnectionStatus(mDevice) ==
timhypengc509a652018-05-25 14:23:44 +08001088 BluetoothProfile.STATE_CONNECTED;
1089 }
timhypeng5c62ebb2018-08-28 09:59:03 +08001090
1091 public CachedBluetoothDevice getSubDevice() {
1092 return mSubDevice;
1093 }
1094
1095 public void setSubDevice(CachedBluetoothDevice subDevice) {
1096 mSubDevice = subDevice;
1097 }
1098
1099 public void switchSubDeviceContent() {
1100 // Backup from main device
1101 BluetoothDevice tmpDevice = mDevice;
1102 short tmpRssi = mRssi;
1103 boolean tmpJustDiscovered = mJustDiscovered;
1104 // Set main device from sub device
1105 mDevice = mSubDevice.mDevice;
1106 mRssi = mSubDevice.mRssi;
1107 mJustDiscovered = mSubDevice.mJustDiscovered;
1108 // Set sub device from backup
1109 mSubDevice.mDevice = tmpDevice;
1110 mSubDevice.mRssi = tmpRssi;
1111 mSubDevice.mJustDiscovered = tmpJustDiscovered;
1112 fetchActiveDevices();
1113 }
Jason Monk7ce96b92015-02-02 11:27:58 -05001114}