blob: 833c4ac07633d85296d92bf2110295e231fe036d [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;
hughchen00ce0252019-10-28 17:03:04 +080027import android.os.Handler;
28import android.os.Looper;
29import android.os.Message;
Jason Monk7ce96b92015-02-02 11:27:58 -050030import android.os.ParcelUuid;
31import android.os.SystemClock;
32import android.text.TextUtils;
Zongheng Wang810c6d22019-09-05 13:44:28 -070033import android.util.EventLog;
Jason Monk7ce96b92015-02-02 11:27:58 -050034import android.util.Log;
Fan Zhangf7802ea2018-08-28 15:15:19 -070035
timhypeng5c62ebb2018-08-28 09:59:03 +080036import androidx.annotation.VisibleForTesting;
37
Jason Monkbe3c5db2015-02-04 13:00:55 -050038import com.android.settingslib.R;
jackqdyulei1f9c1712019-03-05 16:42:57 -080039import com.android.settingslib.Utils;
Jason Monkbe3c5db2015-02-04 13:00:55 -050040
Jason Monk7ce96b92015-02-02 11:27:58 -050041import java.util.ArrayList;
42import java.util.Collection;
43import java.util.Collections;
Jason Monk7ce96b92015-02-02 11:27:58 -050044import java.util.List;
davidln0bc952a2019-06-17 14:48:11 -070045import java.util.concurrent.CopyOnWriteArrayList;
Jason Monk7ce96b92015-02-02 11:27:58 -050046
47/**
48 * CachedBluetoothDevice represents a remote Bluetooth device. It contains
49 * attributes of the device (such as the address, name, RSSI, etc.) and
50 * functionality that can be performed on the device (connect, pair, disconnect,
51 * etc.).
52 */
Fan Zhang82dd3b02016-12-27 13:13:00 -080053public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> {
Jason Monk7ce96b92015-02-02 11:27:58 -050054 private static final String TAG = "CachedBluetoothDevice";
Jason Monk7ce96b92015-02-02 11:27:58 -050055
jackqdyulei1751eaa2018-09-14 11:22:32 -070056 // See mConnectAttempted
57 private static final long MAX_UUID_DELAY_FOR_AUTO_CONNECT = 5000;
Stanley Tngca33f5b2019-05-02 10:36:40 -070058 // Some Hearing Aids (especially the 2nd device) needs more time to do service discovery
59 private static final long MAX_HEARING_AIDS_DELAY_FOR_AUTO_CONNECT = 15000;
jackqdyulei1751eaa2018-09-14 11:22:32 -070060 private static final long MAX_HOGP_DELAY_FOR_AUTO_CONNECT = 30000;
hughchen00ce0252019-10-28 17:03:04 +080061 private static final long MAX_MEDIA_PROFILE_CONNECT_DELAY = 60000;
jackqdyulei1751eaa2018-09-14 11:22:32 -070062
Jason Monk7ce96b92015-02-02 11:27:58 -050063 private final Context mContext;
timhypengf64f6902018-07-31 15:40:15 +080064 private final BluetoothAdapter mLocalAdapter;
Jason Monk7ce96b92015-02-02 11:27:58 -050065 private final LocalBluetoothProfileManager mProfileManager;
jackqdyulei1eb10ec2018-11-01 13:10:09 -070066 private final Object mProfileLock = new Object();
timhypeng5c62ebb2018-08-28 09:59:03 +080067 BluetoothDevice mDevice;
Isha Bobrac3d94132018-02-08 16:04:36 -080068 private long mHiSyncId;
Jack Hec219bc92017-07-24 14:55:59 -070069 // Need this since there is no method for getting RSSI
timhypeng5c62ebb2018-08-28 09:59:03 +080070 short mRssi;
71 // mProfiles and mRemovedProfiles does not do swap() between main and sub device. It is
72 // because current sub device is only for HearingAid and its profile is the same.
jackqdyulei1eb10ec2018-11-01 13:10:09 -070073 private final List<LocalBluetoothProfile> mProfiles = new ArrayList<>();
Jason Monk7ce96b92015-02-02 11:27:58 -050074
75 // List of profiles that were previously in mProfiles, but have been removed
jackqdyulei1eb10ec2018-11-01 13:10:09 -070076 private final List<LocalBluetoothProfile> mRemovedProfiles = new ArrayList<>();
Jason Monk7ce96b92015-02-02 11:27:58 -050077
78 // Device supports PANU but not NAP: remove PanProfile after device disconnects from NAP
79 private boolean mLocalNapRoleConnected;
80
timhypeng5c62ebb2018-08-28 09:59:03 +080081 boolean mJustDiscovered;
Jason Monk7ce96b92015-02-02 11:27:58 -050082
davidln0bc952a2019-06-17 14:48:11 -070083 private final Collection<Callback> mCallbacks = new CopyOnWriteArrayList<>();
Jason Monk7ce96b92015-02-02 11:27:58 -050084
Jason Monk7ce96b92015-02-02 11:27:58 -050085 /**
86 * Last time a bt profile auto-connect was attempted.
87 * If an ACTION_UUID intent comes in within
88 * MAX_UUID_DELAY_FOR_AUTO_CONNECT milliseconds, we will try auto-connect
89 * again with the new UUIDs
90 */
91 private long mConnectAttempted;
92
Pavlin Radoslavov1af33a12018-01-21 02:59:15 -080093 // Active device state
94 private boolean mIsActiveDeviceA2dp = false;
95 private boolean mIsActiveDeviceHeadset = false;
Hansong Zhangd7b35912018-03-16 09:15:48 -070096 private boolean mIsActiveDeviceHearingAid = false;
hughchen00ce0252019-10-28 17:03:04 +080097 // Media profile connect state
98 private boolean mIsA2dpProfileConnectedFail = false;
99 private boolean mIsHeadsetProfileConnectedFail = false;
100 private boolean mIsHearingAidProfileConnectedFail = false;
timhypeng5c62ebb2018-08-28 09:59:03 +0800101 // Group second device for Hearing Aid
102 private CachedBluetoothDevice mSubDevice;
jackqdyulei1751eaa2018-09-14 11:22:32 -0700103
hughchen00ce0252019-10-28 17:03:04 +0800104 private final Handler mHandler = new Handler(Looper.getMainLooper()) {
105 @Override
106 public void handleMessage(Message msg) {
107 switch (msg.what) {
108 case BluetoothProfile.A2DP:
109 mIsA2dpProfileConnectedFail = true;
110 break;
111 case BluetoothProfile.HEADSET:
112 mIsHeadsetProfileConnectedFail = true;
113 break;
114 case BluetoothProfile.HEARING_AID:
115 mIsHearingAidProfileConnectedFail = true;
116 break;
117 default:
118 Log.w(TAG, "handleMessage(): unknown message : " + msg.what);
119 break;
120 }
121 Log.w(TAG, "Connect to profile : " + msg.what + " timeout, show error message !");
122 refresh();
123 }
124 };
125
jackqdyulei1751eaa2018-09-14 11:22:32 -0700126 CachedBluetoothDevice(Context context, LocalBluetoothProfileManager profileManager,
127 BluetoothDevice device) {
128 mContext = context;
129 mLocalAdapter = BluetoothAdapter.getDefaultAdapter();
130 mProfileManager = profileManager;
131 mDevice = device;
jackqdyulei1751eaa2018-09-14 11:22:32 -0700132 fillData();
133 mHiSyncId = BluetoothHearingAid.HI_SYNC_ID_INVALID;
134 }
135
Jason Monk7ce96b92015-02-02 11:27:58 -0500136 /**
137 * Describes the current device and profile for logging.
138 *
139 * @param profile Profile to describe
140 * @return Description of the device and profile
141 */
142 private String describe(LocalBluetoothProfile profile) {
143 StringBuilder sb = new StringBuilder();
144 sb.append("Address:").append(mDevice);
145 if (profile != null) {
146 sb.append(" Profile:").append(profile);
147 }
148
149 return sb.toString();
150 }
151
152 void onProfileStateChanged(LocalBluetoothProfile profile, int newProfileState) {
Kunhung Lie7b7cb82018-05-17 11:04:54 +0800153 if (BluetoothUtils.D) {
Chienyuan559b7f12018-12-17 15:30:20 +0800154 Log.d(TAG, "onProfileStateChanged: profile " + profile + ", device=" + mDevice
155 + ", newProfileState " + newProfileState);
Jason Monk7ce96b92015-02-02 11:27:58 -0500156 }
timhypengf64f6902018-07-31 15:40:15 +0800157 if (mLocalAdapter.getState() == BluetoothAdapter.STATE_TURNING_OFF)
Jason Monk7ce96b92015-02-02 11:27:58 -0500158 {
Kunhung Lie7b7cb82018-05-17 11:04:54 +0800159 if (BluetoothUtils.D) {
160 Log.d(TAG, " BT Turninig Off...Profile conn state change ignored...");
161 }
Jason Monk7ce96b92015-02-02 11:27:58 -0500162 return;
163 }
jackqdyulei1eb10ec2018-11-01 13:10:09 -0700164
165 synchronized (mProfileLock) {
hughchen00ce0252019-10-28 17:03:04 +0800166 if (profile instanceof A2dpProfile || profile instanceof HeadsetProfile
167 || profile instanceof HearingAidProfile) {
168 setProfileConnectedStatus(profile.getProfileId(), false);
169 switch (newProfileState) {
170 case BluetoothProfile.STATE_CONNECTED:
171 mHandler.removeMessages(profile.getProfileId());
172 break;
173 case BluetoothProfile.STATE_CONNECTING:
174 mHandler.sendEmptyMessageDelayed(profile.getProfileId(),
175 MAX_MEDIA_PROFILE_CONNECT_DELAY);
176 break;
177 case BluetoothProfile.STATE_DISCONNECTING:
178 if (mHandler.hasMessages(profile.getProfileId())) {
179 mHandler.removeMessages(profile.getProfileId());
180 }
181 break;
182 case BluetoothProfile.STATE_DISCONNECTED:
183 if (mHandler.hasMessages(profile.getProfileId())) {
184 mHandler.removeMessages(profile.getProfileId());
185 setProfileConnectedStatus(profile.getProfileId(), true);
186 }
187 break;
188 default:
189 Log.w(TAG, "onProfileStateChanged(): unknown profile state : "
190 + newProfileState);
191 break;
192 }
193 }
194
jackqdyulei1eb10ec2018-11-01 13:10:09 -0700195 if (newProfileState == BluetoothProfile.STATE_CONNECTED) {
196 if (profile instanceof MapProfile) {
197 profile.setPreferred(mDevice, true);
Jason Monk7ce96b92015-02-02 11:27:58 -0500198 }
jackqdyulei1eb10ec2018-11-01 13:10:09 -0700199 if (!mProfiles.contains(profile)) {
200 mRemovedProfiles.remove(profile);
201 mProfiles.add(profile);
202 if (profile instanceof PanProfile
203 && ((PanProfile) profile).isLocalRoleNap(mDevice)) {
204 // Device doesn't support NAP, so remove PanProfile on disconnect
205 mLocalNapRoleConnected = true;
206 }
207 }
208 } else if (profile instanceof MapProfile
209 && newProfileState == BluetoothProfile.STATE_DISCONNECTED) {
210 profile.setPreferred(mDevice, false);
211 } else if (mLocalNapRoleConnected && profile instanceof PanProfile
212 && ((PanProfile) profile).isLocalRoleNap(mDevice)
213 && newProfileState == BluetoothProfile.STATE_DISCONNECTED) {
214 Log.d(TAG, "Removing PanProfile from device after NAP disconnect");
215 mProfiles.remove(profile);
216 mRemovedProfiles.add(profile);
217 mLocalNapRoleConnected = false;
Jason Monk7ce96b92015-02-02 11:27:58 -0500218 }
Jason Monk7ce96b92015-02-02 11:27:58 -0500219 }
jackqdyulei1eb10ec2018-11-01 13:10:09 -0700220
Pavlin Radoslavov1af33a12018-01-21 02:59:15 -0800221 fetchActiveDevices();
Jason Monk7ce96b92015-02-02 11:27:58 -0500222 }
223
hughchen00ce0252019-10-28 17:03:04 +0800224 @VisibleForTesting
225 void setProfileConnectedStatus(int profileId, boolean isFailed) {
226 switch (profileId) {
227 case BluetoothProfile.A2DP:
228 mIsA2dpProfileConnectedFail = isFailed;
229 break;
230 case BluetoothProfile.HEADSET:
231 mIsHeadsetProfileConnectedFail = isFailed;
232 break;
233 case BluetoothProfile.HEARING_AID:
234 mIsHearingAidProfileConnectedFail = isFailed;
235 break;
236 default:
237 Log.w(TAG, "setProfileConnectedStatus(): unknown profile id : " + profileId);
238 break;
239 }
240 }
241
Jason Monk7ce96b92015-02-02 11:27:58 -0500242 public void disconnect() {
jackqdyulei1eb10ec2018-11-01 13:10:09 -0700243 synchronized (mProfileLock) {
hughchenfb0c7ad2019-11-25 14:48:46 +0800244 mLocalAdapter.disconnectAllEnabledProfiles(mDevice);
Jason Monk7ce96b92015-02-02 11:27:58 -0500245 }
246 // Disconnect PBAP server in case its connected
247 // This is to ensure all the profiles are disconnected as some CK/Hs do not
248 // disconnect PBAP connection when HF connection is brought down
249 PbapServerProfile PbapProfile = mProfileManager.getPbapProfile();
Joseph Pirozzocdbab122019-01-31 10:58:22 -0800250 if (PbapProfile != null && isConnectedProfile(PbapProfile))
Jason Monk7ce96b92015-02-02 11:27:58 -0500251 {
252 PbapProfile.disconnect(mDevice);
253 }
254 }
255
256 public void disconnect(LocalBluetoothProfile profile) {
257 if (profile.disconnect(mDevice)) {
Kunhung Lie7b7cb82018-05-17 11:04:54 +0800258 if (BluetoothUtils.D) {
Jason Monk7ce96b92015-02-02 11:27:58 -0500259 Log.d(TAG, "Command sent successfully:DISCONNECT " + describe(profile));
260 }
261 }
262 }
263
264 public void connect(boolean connectAllProfiles) {
265 if (!ensurePaired()) {
266 return;
267 }
268
269 mConnectAttempted = SystemClock.elapsedRealtime();
270 connectWithoutResettingTimer(connectAllProfiles);
271 }
272
jackqdyulei1751eaa2018-09-14 11:22:32 -0700273 public long getHiSyncId() {
274 return mHiSyncId;
275 }
276
277 public void setHiSyncId(long id) {
278 if (BluetoothUtils.D) {
279 Log.d(TAG, "setHiSyncId: mDevice " + mDevice + ", id " + id);
280 }
281 mHiSyncId = id;
282 }
283
Stanley Tngc1182942018-11-29 13:47:45 -0800284 public boolean isHearingAidDevice() {
285 return mHiSyncId != BluetoothHearingAid.HI_SYNC_ID_INVALID;
286 }
287
Jason Monk7ce96b92015-02-02 11:27:58 -0500288 void onBondingDockConnect() {
289 // Attempt to connect if UUIDs are available. Otherwise,
290 // we will connect when the ACTION_UUID intent arrives.
291 connect(false);
292 }
293
294 private void connectWithoutResettingTimer(boolean connectAllProfiles) {
jackqdyulei1eb10ec2018-11-01 13:10:09 -0700295 synchronized (mProfileLock) {
296 // Try to initialize the profiles if they were not.
297 if (mProfiles.isEmpty()) {
298 // if mProfiles is empty, then do not invoke updateProfiles. This causes a race
299 // condition with carkits during pairing, wherein RemoteDevice.UUIDs have been
300 // updated from bluetooth stack but ACTION.uuid is not sent yet.
301 // Eventually ACTION.uuid will be received which shall trigger the connection of the
302 // various profiles
303 // If UUIDs are not available yet, connect will be happen
304 // upon arrival of the ACTION_UUID intent.
Stanley Tngca33f5b2019-05-02 10:36:40 -0700305 Log.d(TAG, "No profiles. Maybe we will connect later for device " + mDevice);
jackqdyulei1eb10ec2018-11-01 13:10:09 -0700306 return;
307 }
Jason Monk7ce96b92015-02-02 11:27:58 -0500308
jackqdyulei1eb10ec2018-11-01 13:10:09 -0700309 int preferredProfiles = 0;
310 for (LocalBluetoothProfile profile : mProfiles) {
311 if (connectAllProfiles ? profile.accessProfileEnabled()
312 : profile.isAutoConnectable()) {
313 if (profile.isPreferred(mDevice)) {
314 ++preferredProfiles;
315 connectInt(profile);
316 }
Jason Monk7ce96b92015-02-02 11:27:58 -0500317 }
318 }
jackqdyulei1eb10ec2018-11-01 13:10:09 -0700319 if (BluetoothUtils.D) Log.d(TAG, "Preferred profiles = " + preferredProfiles);
Jason Monk7ce96b92015-02-02 11:27:58 -0500320
jackqdyulei1eb10ec2018-11-01 13:10:09 -0700321 if (preferredProfiles == 0) {
322 connectAutoConnectableProfiles();
323 }
Jason Monk7ce96b92015-02-02 11:27:58 -0500324 }
325 }
326
327 private void connectAutoConnectableProfiles() {
328 if (!ensurePaired()) {
329 return;
330 }
Jason Monk7ce96b92015-02-02 11:27:58 -0500331
jackqdyulei1eb10ec2018-11-01 13:10:09 -0700332 synchronized (mProfileLock) {
333 for (LocalBluetoothProfile profile : mProfiles) {
334 if (profile.isAutoConnectable()) {
335 profile.setPreferred(mDevice, true);
336 connectInt(profile);
337 }
Jason Monk7ce96b92015-02-02 11:27:58 -0500338 }
339 }
340 }
341
342 /**
343 * Connect this device to the specified profile.
344 *
345 * @param profile the profile to use with the remote device
346 */
347 public void connectProfile(LocalBluetoothProfile profile) {
348 mConnectAttempted = SystemClock.elapsedRealtime();
Jason Monk7ce96b92015-02-02 11:27:58 -0500349 connectInt(profile);
350 // Refresh the UI based on profile.connect() call
351 refresh();
352 }
353
354 synchronized void connectInt(LocalBluetoothProfile profile) {
355 if (!ensurePaired()) {
356 return;
357 }
358 if (profile.connect(mDevice)) {
Kunhung Lie7b7cb82018-05-17 11:04:54 +0800359 if (BluetoothUtils.D) {
Jason Monk7ce96b92015-02-02 11:27:58 -0500360 Log.d(TAG, "Command sent successfully:CONNECT " + describe(profile));
361 }
362 return;
363 }
hughchencb6e1972018-08-15 16:13:21 +0800364 Log.i(TAG, "Failed to connect " + profile.toString() + " to " + getName());
Jason Monk7ce96b92015-02-02 11:27:58 -0500365 }
366
367 private boolean ensurePaired() {
368 if (getBondState() == BluetoothDevice.BOND_NONE) {
369 startPairing();
370 return false;
371 } else {
372 return true;
373 }
374 }
375
376 public boolean startPairing() {
377 // Pairing is unreliable while scanning, so cancel discovery
378 if (mLocalAdapter.isDiscovering()) {
379 mLocalAdapter.cancelDiscovery();
380 }
381
382 if (!mDevice.createBond()) {
383 return false;
384 }
385
Jason Monk7ce96b92015-02-02 11:27:58 -0500386 return true;
387 }
388
Jason Monk7ce96b92015-02-02 11:27:58 -0500389 public void unpair() {
390 int state = getBondState();
391
392 if (state == BluetoothDevice.BOND_BONDING) {
393 mDevice.cancelBondProcess();
394 }
395
396 if (state != BluetoothDevice.BOND_NONE) {
397 final BluetoothDevice dev = mDevice;
398 if (dev != null) {
399 final boolean successful = dev.removeBond();
400 if (successful) {
Kunhung Lie7b7cb82018-05-17 11:04:54 +0800401 if (BluetoothUtils.D) {
Jason Monk7ce96b92015-02-02 11:27:58 -0500402 Log.d(TAG, "Command sent successfully:REMOVE_BOND " + describe(null));
403 }
Kunhung Lie7b7cb82018-05-17 11:04:54 +0800404 } else if (BluetoothUtils.V) {
Jason Monk7ce96b92015-02-02 11:27:58 -0500405 Log.v(TAG, "Framework rejected command immediately:REMOVE_BOND " +
Isha Bobrac3d94132018-02-08 16:04:36 -0800406 describe(null));
Jason Monk7ce96b92015-02-02 11:27:58 -0500407 }
408 }
409 }
410 }
411
412 public int getProfileConnectionState(LocalBluetoothProfile profile) {
jackqdyuleibf509052018-09-24 14:00:26 -0700413 return profile != null
414 ? profile.getConnectionStatus(mDevice)
415 : BluetoothProfile.STATE_DISCONNECTED;
Jason Monk7ce96b92015-02-02 11:27:58 -0500416 }
417
418 // TODO: do any of these need to run async on a background thread?
419 private void fillData() {
Jason Monk7ce96b92015-02-02 11:27:58 -0500420 updateProfiles();
Pavlin Radoslavov1af33a12018-01-21 02:59:15 -0800421 fetchActiveDevices();
Jason Monk7ce96b92015-02-02 11:27:58 -0500422 migratePhonebookPermissionChoice();
423 migrateMessagePermissionChoice();
Jason Monk7ce96b92015-02-02 11:27:58 -0500424
Jason Monk7ce96b92015-02-02 11:27:58 -0500425 dispatchAttributesChanged();
426 }
427
428 public BluetoothDevice getDevice() {
429 return mDevice;
430 }
431
Antony Sargent7ad051e2017-06-29 15:23:13 -0700432 /**
433 * Convenience method that can be mocked - it lets tests avoid having to call getDevice() which
434 * causes problems in tests since BluetoothDevice is final and cannot be mocked.
435 * @return the address of this device
436 */
437 public String getAddress() {
438 return mDevice.getAddress();
439 }
440
Jason Monk7ce96b92015-02-02 11:27:58 -0500441 /**
hughchencb6e1972018-08-15 16:13:21 +0800442 * Get name from remote device
Rahul Sabnis24ad4b52019-10-22 13:37:29 -0700443 * @return {@link BluetoothDevice#getAlias()} if
444 * {@link BluetoothDevice#getAlias()} is not null otherwise return
hughchencb6e1972018-08-15 16:13:21 +0800445 * {@link BluetoothDevice#getAddress()}
Jason Monk7ce96b92015-02-02 11:27:58 -0500446 */
hughchencb6e1972018-08-15 16:13:21 +0800447 public String getName() {
Rahul Sabnis24ad4b52019-10-22 13:37:29 -0700448 final String aliasName = mDevice.getAlias();
hughchencb6e1972018-08-15 16:13:21 +0800449 return TextUtils.isEmpty(aliasName) ? getAddress() : aliasName;
Jason Monk7ce96b92015-02-02 11:27:58 -0500450 }
451
452 /**
Jack Hec219bc92017-07-24 14:55:59 -0700453 * User changes the device name
454 * @param name new alias name to be set, should never be null
Jason Monk7ce96b92015-02-02 11:27:58 -0500455 */
456 public void setName(String name) {
hughchencb6e1972018-08-15 16:13:21 +0800457 // Prevent getName() to be set to null if setName(null) is called
458 if (name != null && !TextUtils.equals(name, getName())) {
Jason Monk7ce96b92015-02-02 11:27:58 -0500459 mDevice.setAlias(name);
460 dispatchAttributesChanged();
461 }
462 }
463
Hansong Zhang6a416322018-03-19 18:20:38 -0700464 /**
465 * Set this device as active device
466 * @return true if at least one profile on this device is set to active, false otherwise
467 */
468 public boolean setActive() {
469 boolean result = false;
470 A2dpProfile a2dpProfile = mProfileManager.getA2dpProfile();
471 if (a2dpProfile != null && isConnectedProfile(a2dpProfile)) {
472 if (a2dpProfile.setActiveDevice(getDevice())) {
473 Log.i(TAG, "OnPreferenceClickListener: A2DP active device=" + this);
474 result = true;
475 }
476 }
477 HeadsetProfile headsetProfile = mProfileManager.getHeadsetProfile();
478 if ((headsetProfile != null) && isConnectedProfile(headsetProfile)) {
479 if (headsetProfile.setActiveDevice(getDevice())) {
480 Log.i(TAG, "OnPreferenceClickListener: Headset active device=" + this);
481 result = true;
482 }
483 }
Hansong Zhangd7b35912018-03-16 09:15:48 -0700484 HearingAidProfile hearingAidProfile = mProfileManager.getHearingAidProfile();
485 if ((hearingAidProfile != null) && isConnectedProfile(hearingAidProfile)) {
486 if (hearingAidProfile.setActiveDevice(getDevice())) {
487 Log.i(TAG, "OnPreferenceClickListener: Hearing Aid active device=" + this);
488 result = true;
489 }
490 }
Hansong Zhang6a416322018-03-19 18:20:38 -0700491 return result;
492 }
493
Jason Monk7ce96b92015-02-02 11:27:58 -0500494 void refreshName() {
hughchencb6e1972018-08-15 16:13:21 +0800495 if (BluetoothUtils.D) {
496 Log.d(TAG, "Device name: " + getName());
Jason Monk7ce96b92015-02-02 11:27:58 -0500497 }
hughchencb6e1972018-08-15 16:13:21 +0800498 dispatchAttributesChanged();
Jason Monk7ce96b92015-02-02 11:27:58 -0500499 }
500
Jack He6258aae2017-06-29 17:01:23 -0700501 /**
Jack Hec219bc92017-07-24 14:55:59 -0700502 * Checks if device has a human readable name besides MAC address
503 * @return true if device's alias name is not null nor empty, false otherwise
504 */
505 public boolean hasHumanReadableName() {
Rahul Sabnis24ad4b52019-10-22 13:37:29 -0700506 return !TextUtils.isEmpty(mDevice.getAlias());
Jack Hec219bc92017-07-24 14:55:59 -0700507 }
508
509 /**
Jack He6258aae2017-06-29 17:01:23 -0700510 * Get battery level from remote device
511 * @return battery level in percentage [0-100], or {@link BluetoothDevice#BATTERY_LEVEL_UNKNOWN}
512 */
513 public int getBatteryLevel() {
514 return mDevice.getBatteryLevel();
515 }
516
Jason Monk7ce96b92015-02-02 11:27:58 -0500517 void refresh() {
518 dispatchAttributesChanged();
519 }
520
Jack He51520472017-07-24 12:30:08 -0700521 public void setJustDiscovered(boolean justDiscovered) {
522 if (mJustDiscovered != justDiscovered) {
523 mJustDiscovered = justDiscovered;
Jason Monk7ce96b92015-02-02 11:27:58 -0500524 dispatchAttributesChanged();
525 }
526 }
527
528 public int getBondState() {
529 return mDevice.getBondState();
530 }
531
Pavlin Radoslavov1af33a12018-01-21 02:59:15 -0800532 /**
Pavlin Radoslavovc285d552018-02-06 16:14:00 -0800533 * Update the device status as active or non-active per Bluetooth profile.
Pavlin Radoslavov1af33a12018-01-21 02:59:15 -0800534 *
535 * @param isActive true if the device is active
536 * @param bluetoothProfile the Bluetooth profile
537 */
Pavlin Radoslavovc285d552018-02-06 16:14:00 -0800538 public void onActiveDeviceChanged(boolean isActive, int bluetoothProfile) {
Pavlin Radoslavov1af33a12018-01-21 02:59:15 -0800539 boolean changed = false;
540 switch (bluetoothProfile) {
541 case BluetoothProfile.A2DP:
542 changed = (mIsActiveDeviceA2dp != isActive);
543 mIsActiveDeviceA2dp = isActive;
544 break;
545 case BluetoothProfile.HEADSET:
546 changed = (mIsActiveDeviceHeadset != isActive);
547 mIsActiveDeviceHeadset = isActive;
548 break;
Hansong Zhangd7b35912018-03-16 09:15:48 -0700549 case BluetoothProfile.HEARING_AID:
550 changed = (mIsActiveDeviceHearingAid != isActive);
551 mIsActiveDeviceHearingAid = isActive;
552 break;
Pavlin Radoslavov1af33a12018-01-21 02:59:15 -0800553 default:
Pavlin Radoslavovc285d552018-02-06 16:14:00 -0800554 Log.w(TAG, "onActiveDeviceChanged: unknown profile " + bluetoothProfile +
Pavlin Radoslavov1af33a12018-01-21 02:59:15 -0800555 " isActive " + isActive);
556 break;
557 }
558 if (changed) {
559 dispatchAttributesChanged();
560 }
561 }
562
Pavlin Radoslavovc285d552018-02-06 16:14:00 -0800563 /**
timhypengf0509322018-03-29 14:23:21 +0800564 * Update the profile audio state.
565 */
566 void onAudioModeChanged() {
567 dispatchAttributesChanged();
568 }
569 /**
Pavlin Radoslavovc285d552018-02-06 16:14:00 -0800570 * Get the device status as active or non-active per Bluetooth profile.
571 *
572 * @param bluetoothProfile the Bluetooth profile
573 * @return true if the device is active
574 */
575 @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
576 public boolean isActiveDevice(int bluetoothProfile) {
577 switch (bluetoothProfile) {
578 case BluetoothProfile.A2DP:
579 return mIsActiveDeviceA2dp;
580 case BluetoothProfile.HEADSET:
581 return mIsActiveDeviceHeadset;
Hansong Zhangd7b35912018-03-16 09:15:48 -0700582 case BluetoothProfile.HEARING_AID:
583 return mIsActiveDeviceHearingAid;
Pavlin Radoslavovc285d552018-02-06 16:14:00 -0800584 default:
585 Log.w(TAG, "getActiveDevice: unknown profile " + bluetoothProfile);
586 break;
587 }
588 return false;
589 }
590
Jason Monk7ce96b92015-02-02 11:27:58 -0500591 void setRssi(short rssi) {
592 if (mRssi != rssi) {
593 mRssi = rssi;
594 dispatchAttributesChanged();
595 }
596 }
597
598 /**
599 * Checks whether we are connected to this device (any profile counts).
600 *
601 * @return Whether it is connected.
602 */
603 public boolean isConnected() {
jackqdyulei1eb10ec2018-11-01 13:10:09 -0700604 synchronized (mProfileLock) {
605 for (LocalBluetoothProfile profile : mProfiles) {
606 int status = getProfileConnectionState(profile);
607 if (status == BluetoothProfile.STATE_CONNECTED) {
608 return true;
609 }
Jason Monk7ce96b92015-02-02 11:27:58 -0500610 }
Jason Monk7ce96b92015-02-02 11:27:58 -0500611
jackqdyulei1eb10ec2018-11-01 13:10:09 -0700612 return false;
613 }
Jason Monk7ce96b92015-02-02 11:27:58 -0500614 }
615
616 public boolean isConnectedProfile(LocalBluetoothProfile profile) {
617 int status = getProfileConnectionState(profile);
618 return status == BluetoothProfile.STATE_CONNECTED;
619
620 }
621
622 public boolean isBusy() {
jackqdyulei1eb10ec2018-11-01 13:10:09 -0700623 synchronized (mProfileLock) {
624 for (LocalBluetoothProfile profile : mProfiles) {
625 int status = getProfileConnectionState(profile);
626 if (status == BluetoothProfile.STATE_CONNECTING
627 || status == BluetoothProfile.STATE_DISCONNECTING) {
628 return true;
629 }
Jason Monk7ce96b92015-02-02 11:27:58 -0500630 }
jackqdyulei1eb10ec2018-11-01 13:10:09 -0700631 return getBondState() == BluetoothDevice.BOND_BONDING;
Jason Monk7ce96b92015-02-02 11:27:58 -0500632 }
Jason Monk7ce96b92015-02-02 11:27:58 -0500633 }
634
Jason Monk7ce96b92015-02-02 11:27:58 -0500635 private boolean updateProfiles() {
636 ParcelUuid[] uuids = mDevice.getUuids();
637 if (uuids == null) return false;
638
639 ParcelUuid[] localUuids = mLocalAdapter.getUuids();
640 if (localUuids == null) return false;
641
Jack Hec219bc92017-07-24 14:55:59 -0700642 /*
Jason Monk7ce96b92015-02-02 11:27:58 -0500643 * Now we know if the device supports PBAP, update permissions...
644 */
645 processPhonebookAccess();
646
jackqdyulei1eb10ec2018-11-01 13:10:09 -0700647 synchronized (mProfileLock) {
648 mProfileManager.updateProfiles(uuids, localUuids, mProfiles, mRemovedProfiles,
649 mLocalNapRoleConnected, mDevice);
650 }
Jason Monk7ce96b92015-02-02 11:27:58 -0500651
Kunhung Lie7b7cb82018-05-17 11:04:54 +0800652 if (BluetoothUtils.D) {
Rahul Sabnis24ad4b52019-10-22 13:37:29 -0700653 Log.e(TAG, "updating profiles for " + mDevice.getAlias() + ", " + mDevice);
Jason Monk7ce96b92015-02-02 11:27:58 -0500654 BluetoothClass bluetoothClass = mDevice.getBluetoothClass();
655
656 if (bluetoothClass != null) Log.v(TAG, "Class: " + bluetoothClass.toString());
657 Log.v(TAG, "UUID:");
658 for (ParcelUuid uuid : uuids) {
659 Log.v(TAG, " " + uuid);
660 }
661 }
662 return true;
663 }
664
Pavlin Radoslavov1af33a12018-01-21 02:59:15 -0800665 private void fetchActiveDevices() {
666 A2dpProfile a2dpProfile = mProfileManager.getA2dpProfile();
667 if (a2dpProfile != null) {
668 mIsActiveDeviceA2dp = mDevice.equals(a2dpProfile.getActiveDevice());
669 }
670 HeadsetProfile headsetProfile = mProfileManager.getHeadsetProfile();
671 if (headsetProfile != null) {
672 mIsActiveDeviceHeadset = mDevice.equals(headsetProfile.getActiveDevice());
673 }
Hansong Zhangd7b35912018-03-16 09:15:48 -0700674 HearingAidProfile hearingAidProfile = mProfileManager.getHearingAidProfile();
675 if (hearingAidProfile != null) {
Hansong Zhang3b8f09b2018-03-28 16:53:10 -0700676 mIsActiveDeviceHearingAid = hearingAidProfile.getActiveDevices().contains(mDevice);
Hansong Zhangd7b35912018-03-16 09:15:48 -0700677 }
Pavlin Radoslavov1af33a12018-01-21 02:59:15 -0800678 }
679
Jason Monk7ce96b92015-02-02 11:27:58 -0500680 /**
Jason Monk7ce96b92015-02-02 11:27:58 -0500681 * Refreshes the UI when framework alerts us of a UUID change.
682 */
683 void onUuidChanged() {
684 updateProfiles();
Etan Cohen50d47612015-03-31 12:45:23 -0700685 ParcelUuid[] uuids = mDevice.getUuids();
Venkat Raghavan05e08c32015-04-06 16:26:11 -0700686
Etan Cohen50d47612015-03-31 12:45:23 -0700687 long timeout = MAX_UUID_DELAY_FOR_AUTO_CONNECT;
Venkat Raghavan05e08c32015-04-06 16:26:11 -0700688 if (BluetoothUuid.isUuidPresent(uuids, BluetoothUuid.Hogp)) {
689 timeout = MAX_HOGP_DELAY_FOR_AUTO_CONNECT;
Stanley Tngca33f5b2019-05-02 10:36:40 -0700690 } else if (BluetoothUuid.isUuidPresent(uuids, BluetoothUuid.HearingAid)) {
691 timeout = MAX_HEARING_AIDS_DELAY_FOR_AUTO_CONNECT;
Venkat Raghavan05e08c32015-04-06 16:26:11 -0700692 }
Jason Monk7ce96b92015-02-02 11:27:58 -0500693
Kunhung Lie7b7cb82018-05-17 11:04:54 +0800694 if (BluetoothUtils.D) {
Stanley Tngca33f5b2019-05-02 10:36:40 -0700695 Log.d(TAG, "onUuidChanged: Time since last connect="
Jason Monk7ce96b92015-02-02 11:27:58 -0500696 + (SystemClock.elapsedRealtime() - mConnectAttempted));
697 }
698
699 /*
Venkat Raghavan05e08c32015-04-06 16:26:11 -0700700 * If a connect was attempted earlier without any UUID, we will do the connect now.
701 * Otherwise, allow the connect on UUID change.
Jason Monk7ce96b92015-02-02 11:27:58 -0500702 */
703 if (!mProfiles.isEmpty()
Andre Eisenbach7be83c52015-09-03 15:20:09 -0700704 && ((mConnectAttempted + timeout) > SystemClock.elapsedRealtime())) {
Jason Monk7ce96b92015-02-02 11:27:58 -0500705 connectWithoutResettingTimer(false);
706 }
Andre Eisenbach7be83c52015-09-03 15:20:09 -0700707
Jason Monk7ce96b92015-02-02 11:27:58 -0500708 dispatchAttributesChanged();
709 }
710
711 void onBondingStateChanged(int bondState) {
712 if (bondState == BluetoothDevice.BOND_NONE) {
jackqdyulei1eb10ec2018-11-01 13:10:09 -0700713 synchronized (mProfileLock) {
714 mProfiles.clear();
715 }
hughchen783a80b2018-08-20 17:04:56 +0800716 mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_UNKNOWN);
717 mDevice.setMessageAccessPermission(BluetoothDevice.ACCESS_UNKNOWN);
718 mDevice.setSimAccessPermission(BluetoothDevice.ACCESS_UNKNOWN);
Jason Monk7ce96b92015-02-02 11:27:58 -0500719 }
720
721 refresh();
722
723 if (bondState == BluetoothDevice.BOND_BONDED) {
724 if (mDevice.isBluetoothDock()) {
725 onBondingDockConnect();
Jakub Pawlowski0278ab92016-07-20 11:55:48 -0700726 } else if (mDevice.isBondingInitiatedLocally()) {
Jason Monk7ce96b92015-02-02 11:27:58 -0500727 connect(false);
728 }
Jason Monk7ce96b92015-02-02 11:27:58 -0500729 }
730 }
731
Jason Monk7ce96b92015-02-02 11:27:58 -0500732 public BluetoothClass getBtClass() {
hughchen8df81e32018-08-16 16:02:16 +0800733 return mDevice.getBluetoothClass();
Jason Monk7ce96b92015-02-02 11:27:58 -0500734 }
735
736 public List<LocalBluetoothProfile> getProfiles() {
737 return Collections.unmodifiableList(mProfiles);
738 }
739
740 public List<LocalBluetoothProfile> getConnectableProfiles() {
741 List<LocalBluetoothProfile> connectableProfiles =
742 new ArrayList<LocalBluetoothProfile>();
jackqdyulei1eb10ec2018-11-01 13:10:09 -0700743 synchronized (mProfileLock) {
744 for (LocalBluetoothProfile profile : mProfiles) {
745 if (profile.accessProfileEnabled()) {
746 connectableProfiles.add(profile);
747 }
Jason Monk7ce96b92015-02-02 11:27:58 -0500748 }
749 }
750 return connectableProfiles;
751 }
752
753 public List<LocalBluetoothProfile> getRemovedProfiles() {
754 return mRemovedProfiles;
755 }
756
757 public void registerCallback(Callback callback) {
davidln0bc952a2019-06-17 14:48:11 -0700758 mCallbacks.add(callback);
Jason Monk7ce96b92015-02-02 11:27:58 -0500759 }
760
761 public void unregisterCallback(Callback callback) {
davidln0bc952a2019-06-17 14:48:11 -0700762 mCallbacks.remove(callback);
Jason Monk7ce96b92015-02-02 11:27:58 -0500763 }
764
hughchen8df81e32018-08-16 16:02:16 +0800765 void dispatchAttributesChanged() {
davidln0bc952a2019-06-17 14:48:11 -0700766 for (Callback callback : mCallbacks) {
767 callback.onDeviceAttributesChanged();
Jason Monk7ce96b92015-02-02 11:27:58 -0500768 }
769 }
770
771 @Override
772 public String toString() {
773 return mDevice.toString();
774 }
775
776 @Override
777 public boolean equals(Object o) {
778 if ((o == null) || !(o instanceof CachedBluetoothDevice)) {
779 return false;
780 }
781 return mDevice.equals(((CachedBluetoothDevice) o).mDevice);
782 }
783
784 @Override
785 public int hashCode() {
786 return mDevice.getAddress().hashCode();
787 }
788
789 // This comparison uses non-final fields so the sort order may change
790 // when device attributes change (such as bonding state). Settings
791 // will completely refresh the device list when this happens.
792 public int compareTo(CachedBluetoothDevice another) {
793 // Connected above not connected
794 int comparison = (another.isConnected() ? 1 : 0) - (isConnected() ? 1 : 0);
795 if (comparison != 0) return comparison;
796
797 // Paired above not paired
798 comparison = (another.getBondState() == BluetoothDevice.BOND_BONDED ? 1 : 0) -
799 (getBondState() == BluetoothDevice.BOND_BONDED ? 1 : 0);
800 if (comparison != 0) return comparison;
801
Jack He51520472017-07-24 12:30:08 -0700802 // Just discovered above discovered in the past
803 comparison = (another.mJustDiscovered ? 1 : 0) - (mJustDiscovered ? 1 : 0);
Jason Monk7ce96b92015-02-02 11:27:58 -0500804 if (comparison != 0) return comparison;
805
806 // Stronger signal above weaker signal
807 comparison = another.mRssi - mRssi;
808 if (comparison != 0) return comparison;
809
810 // Fallback on name
hughchencb6e1972018-08-15 16:13:21 +0800811 return getName().compareTo(another.getName());
Jason Monk7ce96b92015-02-02 11:27:58 -0500812 }
813
814 public interface Callback {
815 void onDeviceAttributesChanged();
816 }
817
Jason Monk7ce96b92015-02-02 11:27:58 -0500818 // Migrates data from old data store (in Settings app's shared preferences) to new (in Bluetooth
819 // app's shared preferences).
820 private void migratePhonebookPermissionChoice() {
821 SharedPreferences preferences = mContext.getSharedPreferences(
822 "bluetooth_phonebook_permission", Context.MODE_PRIVATE);
823 if (!preferences.contains(mDevice.getAddress())) {
824 return;
825 }
826
827 if (mDevice.getPhonebookAccessPermission() == BluetoothDevice.ACCESS_UNKNOWN) {
hughchen783a80b2018-08-20 17:04:56 +0800828 int oldPermission =
829 preferences.getInt(mDevice.getAddress(), BluetoothDevice.ACCESS_UNKNOWN);
830 if (oldPermission == BluetoothDevice.ACCESS_ALLOWED) {
Jason Monk7ce96b92015-02-02 11:27:58 -0500831 mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_ALLOWED);
hughchen783a80b2018-08-20 17:04:56 +0800832 } else if (oldPermission == BluetoothDevice.ACCESS_REJECTED) {
Jason Monk7ce96b92015-02-02 11:27:58 -0500833 mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_REJECTED);
834 }
835 }
836
837 SharedPreferences.Editor editor = preferences.edit();
838 editor.remove(mDevice.getAddress());
839 editor.commit();
840 }
841
Jason Monk7ce96b92015-02-02 11:27:58 -0500842 // Migrates data from old data store (in Settings app's shared preferences) to new (in Bluetooth
843 // app's shared preferences).
844 private void migrateMessagePermissionChoice() {
845 SharedPreferences preferences = mContext.getSharedPreferences(
846 "bluetooth_message_permission", Context.MODE_PRIVATE);
847 if (!preferences.contains(mDevice.getAddress())) {
848 return;
849 }
850
851 if (mDevice.getMessageAccessPermission() == BluetoothDevice.ACCESS_UNKNOWN) {
hughchen783a80b2018-08-20 17:04:56 +0800852 int oldPermission =
853 preferences.getInt(mDevice.getAddress(), BluetoothDevice.ACCESS_UNKNOWN);
854 if (oldPermission == BluetoothDevice.ACCESS_ALLOWED) {
Jason Monk7ce96b92015-02-02 11:27:58 -0500855 mDevice.setMessageAccessPermission(BluetoothDevice.ACCESS_ALLOWED);
hughchen783a80b2018-08-20 17:04:56 +0800856 } else if (oldPermission == BluetoothDevice.ACCESS_REJECTED) {
Jason Monk7ce96b92015-02-02 11:27:58 -0500857 mDevice.setMessageAccessPermission(BluetoothDevice.ACCESS_REJECTED);
858 }
859 }
860
861 SharedPreferences.Editor editor = preferences.edit();
862 editor.remove(mDevice.getAddress());
863 editor.commit();
864 }
865
Jason Monk7ce96b92015-02-02 11:27:58 -0500866 private void processPhonebookAccess() {
867 if (mDevice.getBondState() != BluetoothDevice.BOND_BONDED) return;
868
869 ParcelUuid[] uuids = mDevice.getUuids();
870 if (BluetoothUuid.containsAnyUuid(uuids, PbapServerProfile.PBAB_CLIENT_UUIDS)) {
871 // The pairing dialog now warns of phone-book access for paired devices.
872 // No separate prompt is displayed after pairing.
hughchen783a80b2018-08-20 17:04:56 +0800873 if (mDevice.getPhonebookAccessPermission() == BluetoothDevice.ACCESS_UNKNOWN) {
Sanket Padawe07533db2015-11-11 15:01:35 -0800874 if (mDevice.getBluetoothClass().getDeviceClass()
Hemant Guptab8d267a2016-06-07 12:53:59 +0530875 == BluetoothClass.Device.AUDIO_VIDEO_HANDSFREE ||
876 mDevice.getBluetoothClass().getDeviceClass()
877 == BluetoothClass.Device.AUDIO_VIDEO_WEARABLE_HEADSET) {
Zongheng Wang810c6d22019-09-05 13:44:28 -0700878 EventLog.writeEvent(0x534e4554, "138529441", -1, "");
Sanket Padawe07533db2015-11-11 15:01:35 -0800879 }
Zongheng Wang810c6d22019-09-05 13:44:28 -0700880 mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_REJECTED);
Sanket Padawe78c23762015-06-01 15:55:30 -0700881 }
Jason Monk7ce96b92015-02-02 11:27:58 -0500882 }
883 }
Jason Monkbe3c5db2015-02-04 13:00:55 -0500884
885 public int getMaxConnectionState() {
886 int maxState = BluetoothProfile.STATE_DISCONNECTED;
jackqdyulei1eb10ec2018-11-01 13:10:09 -0700887 synchronized (mProfileLock) {
888 for (LocalBluetoothProfile profile : getProfiles()) {
889 int connectionStatus = getProfileConnectionState(profile);
890 if (connectionStatus > maxState) {
891 maxState = connectionStatus;
892 }
Jason Monkbe3c5db2015-02-04 13:00:55 -0500893 }
894 }
895 return maxState;
896 }
897
898 /**
Lei Yue63ca1b2019-05-13 16:07:57 -0700899 * Return full summary that describes connection state of this device
900 *
901 * @see #getConnectionSummary(boolean shortSummary)
Jason Monkbe3c5db2015-02-04 13:00:55 -0500902 */
Jack He6258aae2017-06-29 17:01:23 -0700903 public String getConnectionSummary() {
Lei Yue63ca1b2019-05-13 16:07:57 -0700904 return getConnectionSummary(false /* shortSummary */);
905 }
906
907 /**
908 * Return summary that describes connection state of this device. Summary depends on:
909 * 1. Whether device has battery info
910 * 2. Whether device is in active usage(or in phone call)
911 *
912 * @param shortSummary {@code true} if need to return short version summary
913 */
914 public String getConnectionSummary(boolean shortSummary) {
timhypengf0509322018-03-29 14:23:21 +0800915 boolean profileConnected = false; // Updated as long as BluetoothProfile is connected
916 boolean a2dpConnected = true; // A2DP is connected
917 boolean hfpConnected = true; // HFP is connected
918 boolean hearingAidConnected = true; // Hearing Aid is connected
jackqdyulei1f9c1712019-03-05 16:42:57 -0800919 int leftBattery = -1;
920 int rightBattery = -1;
Jason Monkbe3c5db2015-02-04 13:00:55 -0500921
hughchen00ce0252019-10-28 17:03:04 +0800922 if (isProfileConnectedFail() && isConnected()) {
923 return mContext.getString(R.string.profile_connect_timeout_subtext);
924 }
925
jackqdyulei1eb10ec2018-11-01 13:10:09 -0700926 synchronized (mProfileLock) {
927 for (LocalBluetoothProfile profile : getProfiles()) {
928 int connectionStatus = getProfileConnectionState(profile);
Jason Monkbe3c5db2015-02-04 13:00:55 -0500929
jackqdyulei1eb10ec2018-11-01 13:10:09 -0700930 switch (connectionStatus) {
931 case BluetoothProfile.STATE_CONNECTING:
932 case BluetoothProfile.STATE_DISCONNECTING:
933 return mContext.getString(
934 BluetoothUtils.getConnectionStateSummary(connectionStatus));
Jason Monkbe3c5db2015-02-04 13:00:55 -0500935
jackqdyulei1eb10ec2018-11-01 13:10:09 -0700936 case BluetoothProfile.STATE_CONNECTED:
937 profileConnected = true;
938 break;
Jason Monkbe3c5db2015-02-04 13:00:55 -0500939
jackqdyulei1eb10ec2018-11-01 13:10:09 -0700940 case BluetoothProfile.STATE_DISCONNECTED:
941 if (profile.isProfileReady()) {
942 if (profile instanceof A2dpProfile
943 || profile instanceof A2dpSinkProfile) {
944 a2dpConnected = false;
945 } else if (profile instanceof HeadsetProfile
946 || profile instanceof HfpClientProfile) {
947 hfpConnected = false;
948 } else if (profile instanceof HearingAidProfile) {
949 hearingAidConnected = false;
950 }
Jason Monkbe3c5db2015-02-04 13:00:55 -0500951 }
jackqdyulei1eb10ec2018-11-01 13:10:09 -0700952 break;
953 }
Jason Monkbe3c5db2015-02-04 13:00:55 -0500954 }
955 }
956
Jack He6258aae2017-06-29 17:01:23 -0700957 String batteryLevelPercentageString = null;
958 // Android framework should only set mBatteryLevel to valid range [0-100] or
959 // BluetoothDevice.BATTERY_LEVEL_UNKNOWN, any other value should be a framework bug.
960 // Thus assume here that if value is not BluetoothDevice.BATTERY_LEVEL_UNKNOWN, it must
961 // be valid
962 final int batteryLevel = getBatteryLevel();
963 if (batteryLevel != BluetoothDevice.BATTERY_LEVEL_UNKNOWN) {
964 // TODO: name com.android.settingslib.bluetooth.Utils something different
965 batteryLevelPercentageString =
966 com.android.settingslib.Utils.formatPercentage(batteryLevel);
967 }
968
timhypengf0509322018-03-29 14:23:21 +0800969 int stringRes = R.string.bluetooth_pairing;
970 //when profile is connected, information would be available
Jason Monkbe3c5db2015-02-04 13:00:55 -0500971 if (profileConnected) {
jackqdyulei1f9c1712019-03-05 16:42:57 -0800972 // Update Meta data for connected device
Ugo Yu73c387d2019-03-20 19:50:58 +0800973 if (BluetoothUtils.getBooleanMetaData(
974 mDevice, BluetoothDevice.METADATA_IS_UNTETHERED_HEADSET)) {
975 leftBattery = BluetoothUtils.getIntMetaData(mDevice,
976 BluetoothDevice.METADATA_UNTETHERED_LEFT_BATTERY);
977 rightBattery = BluetoothUtils.getIntMetaData(mDevice,
978 BluetoothDevice.METADATA_UNTETHERED_RIGHT_BATTERY);
jackqdyulei1f9c1712019-03-05 16:42:57 -0800979 }
980
wengsu6d263c22018-11-15 16:51:45 +0800981 // Set default string with battery level in device connected situation.
jackqdyulei1f9c1712019-03-05 16:42:57 -0800982 if (isTwsBatteryAvailable(leftBattery, rightBattery)) {
983 stringRes = R.string.bluetooth_battery_level_untethered;
984 } else if (batteryLevelPercentageString != null) {
wengsu6d263c22018-11-15 16:51:45 +0800985 stringRes = R.string.bluetooth_battery_level;
986 }
987
988 // Set active string in following device connected situation.
989 // 1. Hearing Aid device active.
990 // 2. Headset device active with in-calling state.
991 // 3. A2DP device active without in-calling state.
timhypengf0509322018-03-29 14:23:21 +0800992 if (a2dpConnected || hfpConnected || hearingAidConnected) {
jackqdyulei1f9c1712019-03-05 16:42:57 -0800993 final boolean isOnCall = Utils.isAudioModeOngoingCall(mContext);
wengsu6d263c22018-11-15 16:51:45 +0800994 if ((mIsActiveDeviceHearingAid)
995 || (mIsActiveDeviceHeadset && isOnCall)
996 || (mIsActiveDeviceA2dp && !isOnCall)) {
Lei Yue63ca1b2019-05-13 16:07:57 -0700997 if (isTwsBatteryAvailable(leftBattery, rightBattery) && !shortSummary) {
jackqdyulei1f9c1712019-03-05 16:42:57 -0800998 stringRes = R.string.bluetooth_active_battery_level_untethered;
Lei Yue63ca1b2019-05-13 16:07:57 -0700999 } else if (batteryLevelPercentageString != null && !shortSummary) {
jackqdyulei1f9c1712019-03-05 16:42:57 -08001000 stringRes = R.string.bluetooth_active_battery_level;
1001 } else {
1002 stringRes = R.string.bluetooth_active_no_battery_level;
1003 }
Jack He6258aae2017-06-29 17:01:23 -07001004 }
Jason Monkbe3c5db2015-02-04 13:00:55 -05001005 }
1006 }
1007
jackqdyulei1f9c1712019-03-05 16:42:57 -08001008 if (stringRes != R.string.bluetooth_pairing
1009 || getBondState() == BluetoothDevice.BOND_BONDING) {
1010 if (isTwsBatteryAvailable(leftBattery, rightBattery)) {
1011 return mContext.getString(stringRes, Utils.formatPercentage(leftBattery),
1012 Utils.formatPercentage(rightBattery));
1013 } else {
1014 return mContext.getString(stringRes, batteryLevelPercentageString);
1015 }
1016 } else {
1017 return null;
1018 }
1019 }
1020
1021 private boolean isTwsBatteryAvailable(int leftBattery, int rightBattery) {
1022 return leftBattery >= 0 && rightBattery >= 0;
Jason Monkbe3c5db2015-02-04 13:00:55 -05001023 }
hughchen23b947e2018-03-31 17:32:53 +08001024
hughchen00ce0252019-10-28 17:03:04 +08001025 private boolean isProfileConnectedFail() {
1026 return mIsA2dpProfileConnectedFail || mIsHearingAidProfileConnectedFail
1027 || mIsHeadsetProfileConnectedFail;
1028 }
1029
hughchen23b947e2018-03-31 17:32:53 +08001030 /**
ryanywline26aecd2018-05-15 14:20:50 +08001031 * @return resource for android auto string that describes the connection state of this device.
1032 */
1033 public String getCarConnectionSummary() {
1034 boolean profileConnected = false; // at least one profile is connected
1035 boolean a2dpNotConnected = false; // A2DP is preferred but not connected
1036 boolean hfpNotConnected = false; // HFP is preferred but not connected
1037 boolean hearingAidNotConnected = false; // Hearing Aid is preferred but not connected
1038
jackqdyulei1eb10ec2018-11-01 13:10:09 -07001039 synchronized (mProfileLock) {
1040 for (LocalBluetoothProfile profile : getProfiles()) {
1041 int connectionStatus = getProfileConnectionState(profile);
ryanywline26aecd2018-05-15 14:20:50 +08001042
jackqdyulei1eb10ec2018-11-01 13:10:09 -07001043 switch (connectionStatus) {
1044 case BluetoothProfile.STATE_CONNECTING:
1045 case BluetoothProfile.STATE_DISCONNECTING:
1046 return mContext.getString(
1047 BluetoothUtils.getConnectionStateSummary(connectionStatus));
ryanywline26aecd2018-05-15 14:20:50 +08001048
jackqdyulei1eb10ec2018-11-01 13:10:09 -07001049 case BluetoothProfile.STATE_CONNECTED:
1050 profileConnected = true;
1051 break;
ryanywline26aecd2018-05-15 14:20:50 +08001052
jackqdyulei1eb10ec2018-11-01 13:10:09 -07001053 case BluetoothProfile.STATE_DISCONNECTED:
1054 if (profile.isProfileReady()) {
1055 if (profile instanceof A2dpProfile
1056 || profile instanceof A2dpSinkProfile) {
1057 a2dpNotConnected = true;
1058 } else if (profile instanceof HeadsetProfile
1059 || profile instanceof HfpClientProfile) {
1060 hfpNotConnected = true;
1061 } else if (profile instanceof HearingAidProfile) {
1062 hearingAidNotConnected = true;
1063 }
ryanywline26aecd2018-05-15 14:20:50 +08001064 }
jackqdyulei1eb10ec2018-11-01 13:10:09 -07001065 break;
1066 }
ryanywline26aecd2018-05-15 14:20:50 +08001067 }
1068 }
1069
1070 String batteryLevelPercentageString = null;
1071 // Android framework should only set mBatteryLevel to valid range [0-100] or
1072 // BluetoothDevice.BATTERY_LEVEL_UNKNOWN, any other value should be a framework bug.
1073 // Thus assume here that if value is not BluetoothDevice.BATTERY_LEVEL_UNKNOWN, it must
1074 // be valid
1075 final int batteryLevel = getBatteryLevel();
1076 if (batteryLevel != BluetoothDevice.BATTERY_LEVEL_UNKNOWN) {
1077 // TODO: name com.android.settingslib.bluetooth.Utils something different
1078 batteryLevelPercentageString =
1079 com.android.settingslib.Utils.formatPercentage(batteryLevel);
1080 }
1081
1082 // Prepare the string for the Active Device summary
1083 String[] activeDeviceStringsArray = mContext.getResources().getStringArray(
1084 R.array.bluetooth_audio_active_device_summaries);
1085 String activeDeviceString = activeDeviceStringsArray[0]; // Default value: not active
1086 if (mIsActiveDeviceA2dp && mIsActiveDeviceHeadset) {
1087 activeDeviceString = activeDeviceStringsArray[1]; // Active for Media and Phone
1088 } else {
1089 if (mIsActiveDeviceA2dp) {
1090 activeDeviceString = activeDeviceStringsArray[2]; // Active for Media only
1091 }
1092 if (mIsActiveDeviceHeadset) {
1093 activeDeviceString = activeDeviceStringsArray[3]; // Active for Phone only
1094 }
1095 }
1096 if (!hearingAidNotConnected && mIsActiveDeviceHearingAid) {
1097 activeDeviceString = activeDeviceStringsArray[1];
1098 return mContext.getString(R.string.bluetooth_connected, activeDeviceString);
1099 }
1100
1101 if (profileConnected) {
1102 if (a2dpNotConnected && hfpNotConnected) {
1103 if (batteryLevelPercentageString != null) {
1104 return mContext.getString(
1105 R.string.bluetooth_connected_no_headset_no_a2dp_battery_level,
1106 batteryLevelPercentageString, activeDeviceString);
1107 } else {
1108 return mContext.getString(R.string.bluetooth_connected_no_headset_no_a2dp,
1109 activeDeviceString);
1110 }
1111
1112 } else if (a2dpNotConnected) {
1113 if (batteryLevelPercentageString != null) {
1114 return mContext.getString(R.string.bluetooth_connected_no_a2dp_battery_level,
1115 batteryLevelPercentageString, activeDeviceString);
1116 } else {
1117 return mContext.getString(R.string.bluetooth_connected_no_a2dp,
1118 activeDeviceString);
1119 }
1120
1121 } else if (hfpNotConnected) {
1122 if (batteryLevelPercentageString != null) {
1123 return mContext.getString(R.string.bluetooth_connected_no_headset_battery_level,
1124 batteryLevelPercentageString, activeDeviceString);
1125 } else {
1126 return mContext.getString(R.string.bluetooth_connected_no_headset,
1127 activeDeviceString);
1128 }
1129 } else {
1130 if (batteryLevelPercentageString != null) {
1131 return mContext.getString(R.string.bluetooth_connected_battery_level,
1132 batteryLevelPercentageString, activeDeviceString);
1133 } else {
1134 return mContext.getString(R.string.bluetooth_connected, activeDeviceString);
1135 }
1136 }
1137 }
1138
1139 return getBondState() == BluetoothDevice.BOND_BONDING ?
1140 mContext.getString(R.string.bluetooth_pairing) : null;
1141 }
1142
1143 /**
hughchen23b947e2018-03-31 17:32:53 +08001144 * @return {@code true} if {@code cachedBluetoothDevice} is a2dp device
1145 */
timhypeng0cd2ecc2018-08-20 15:51:04 +08001146 public boolean isConnectedA2dpDevice() {
Hansong Zhang28963cd2018-06-13 11:30:59 -07001147 A2dpProfile a2dpProfile = mProfileManager.getA2dpProfile();
1148 return a2dpProfile != null && a2dpProfile.getConnectionStatus(mDevice) ==
hughchen23b947e2018-03-31 17:32:53 +08001149 BluetoothProfile.STATE_CONNECTED;
1150 }
1151
1152 /**
1153 * @return {@code true} if {@code cachedBluetoothDevice} is HFP device
1154 */
timhypenga025ad32018-08-20 16:01:16 +08001155 public boolean isConnectedHfpDevice() {
Hansong Zhang28963cd2018-06-13 11:30:59 -07001156 HeadsetProfile headsetProfile = mProfileManager.getHeadsetProfile();
1157 return headsetProfile != null && headsetProfile.getConnectionStatus(mDevice) ==
hughchen23b947e2018-03-31 17:32:53 +08001158 BluetoothProfile.STATE_CONNECTED;
1159 }
timhypengc509a652018-05-25 14:23:44 +08001160
1161 /**
1162 * @return {@code true} if {@code cachedBluetoothDevice} is Hearing Aid device
1163 */
1164 public boolean isConnectedHearingAidDevice() {
Hansong Zhang28963cd2018-06-13 11:30:59 -07001165 HearingAidProfile hearingAidProfile = mProfileManager.getHearingAidProfile();
1166 return hearingAidProfile != null && hearingAidProfile.getConnectionStatus(mDevice) ==
timhypengc509a652018-05-25 14:23:44 +08001167 BluetoothProfile.STATE_CONNECTED;
1168 }
timhypeng5c62ebb2018-08-28 09:59:03 +08001169
1170 public CachedBluetoothDevice getSubDevice() {
1171 return mSubDevice;
1172 }
1173
1174 public void setSubDevice(CachedBluetoothDevice subDevice) {
1175 mSubDevice = subDevice;
1176 }
1177
1178 public void switchSubDeviceContent() {
1179 // Backup from main device
1180 BluetoothDevice tmpDevice = mDevice;
1181 short tmpRssi = mRssi;
1182 boolean tmpJustDiscovered = mJustDiscovered;
1183 // Set main device from sub device
1184 mDevice = mSubDevice.mDevice;
1185 mRssi = mSubDevice.mRssi;
1186 mJustDiscovered = mSubDevice.mJustDiscovered;
1187 // Set sub device from backup
1188 mSubDevice.mDevice = tmpDevice;
1189 mSubDevice.mRssi = tmpRssi;
1190 mSubDevice.mJustDiscovered = tmpJustDiscovered;
1191 fetchActiveDevices();
1192 }
Jason Monk7ce96b92015-02-02 11:27:58 -05001193}