1 /* 2 * Copyright (C) 2018 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 package com.android.settingslib.bluetooth; 17 18 import static android.bluetooth.BluetoothDevice.BOND_BONDED; 19 20 import android.annotation.CallbackExecutor; 21 import android.bluetooth.BluetoothCsipSetCoordinator; 22 import android.bluetooth.BluetoothDevice; 23 import android.bluetooth.BluetoothHapClient; 24 import android.bluetooth.BluetoothHearingAid; 25 import android.bluetooth.BluetoothProfile; 26 import android.bluetooth.BluetoothUuid; 27 import android.bluetooth.le.ScanFilter; 28 import android.content.ContentResolver; 29 import android.content.Context; 30 import android.media.AudioDeviceAttributes; 31 import android.media.audiopolicy.AudioProductStrategy; 32 import android.os.ParcelUuid; 33 import android.provider.Settings; 34 import android.util.FeatureFlagUtils; 35 import android.util.Log; 36 37 import androidx.annotation.IntDef; 38 import androidx.annotation.NonNull; 39 import androidx.collection.ArraySet; 40 41 import com.android.internal.annotations.VisibleForTesting; 42 import com.android.settingslib.bluetooth.HearingAidAudioRoutingConstants.RoutingValue; 43 44 import java.lang.annotation.Retention; 45 import java.lang.annotation.RetentionPolicy; 46 import java.util.HashSet; 47 import java.util.List; 48 import java.util.Map; 49 import java.util.Set; 50 import java.util.concurrent.ConcurrentHashMap; 51 import java.util.concurrent.Executor; 52 import java.util.stream.Collectors; 53 54 /** 55 * HearingAidDeviceManager manages the set of remote bluetooth hearing devices. 56 */ 57 public class HearingAidDeviceManager { 58 private static final String TAG = "HearingAidDeviceManager"; 59 private static final boolean DEBUG = BluetoothUtils.D; 60 61 private final ContentResolver mContentResolver; 62 private final Context mContext; 63 private final LocalBluetoothManager mBtManager; 64 private final List<CachedBluetoothDevice> mCachedDevices; 65 private final HearingAidAudioRoutingHelper mRoutingHelper; 66 private static final Map<ConnectionStatusListener, Executor> 67 mConnectionStatusListeners = new ConcurrentHashMap<>(); 68 @ConnectionStatus 69 private int mDevicesConnectionStatus = ConnectionStatus.NO_DEVICE_BONDED; 70 private boolean mInitialDevicesConnectionStatusUpdate = false; 71 HearingAidDeviceManager(Context context, LocalBluetoothManager localBtManager, List<CachedBluetoothDevice> CachedDevices)72 HearingAidDeviceManager(Context context, LocalBluetoothManager localBtManager, 73 List<CachedBluetoothDevice> CachedDevices) { 74 mContext = context; 75 mContentResolver = context.getContentResolver(); 76 mBtManager = localBtManager; 77 mCachedDevices = CachedDevices; 78 mRoutingHelper = new HearingAidAudioRoutingHelper(context); 79 } 80 81 @VisibleForTesting HearingAidDeviceManager(Context context, LocalBluetoothManager localBtManager, List<CachedBluetoothDevice> cachedDevices, HearingAidAudioRoutingHelper routingHelper)82 HearingAidDeviceManager(Context context, LocalBluetoothManager localBtManager, 83 List<CachedBluetoothDevice> cachedDevices, HearingAidAudioRoutingHelper routingHelper) { 84 mContext = context; 85 mContentResolver = context.getContentResolver(); 86 mBtManager = localBtManager; 87 mCachedDevices = cachedDevices; 88 mRoutingHelper = routingHelper; 89 } 90 91 /** 92 * Defines the connection status for hearing devices. 93 */ 94 @Retention(RetentionPolicy.SOURCE) 95 @IntDef({ 96 ConnectionStatus.NO_DEVICE_BONDED, 97 ConnectionStatus.DISCONNECTED, 98 ConnectionStatus.CONNECTED, 99 ConnectionStatus.CONNECTING_OR_DISCONNECTING, 100 ConnectionStatus.ACTIVE 101 }) 102 public @interface ConnectionStatus { 103 int NO_DEVICE_BONDED = -1; 104 int DISCONNECTED = 0; 105 int CONNECTED = 1; 106 int CONNECTING_OR_DISCONNECTING = 2; 107 int ACTIVE = 3; 108 } 109 /** 110 * Interface for connection status listener. 111 */ 112 public interface ConnectionStatusListener { 113 /** 114 * Callback when hearing devices connection status change. 115 * 116 * <p>devices here means singular device or binaural device. 117 * E.g. One of hearing device is in CONNECTED status and another is in DISCONNECTED, 118 * it will callback CONNECTED status. 119 * 120 * @param status Updated {@link ConnectionStatus} 121 */ onDevicesConnectionStatusChanged(@onnectionStatus int status)122 void onDevicesConnectionStatusChanged(@ConnectionStatus int status); 123 } 124 125 /** 126 * Registers a listener to be notified of connection status changes. 127 * 128 * @param listener The listener to register. 129 * @param executor The executor on which the listener's callback will be run. 130 */ registerConnectionStatusListener( @onNull ConnectionStatusListener listener, @NonNull @CallbackExecutor Executor executor)131 public void registerConnectionStatusListener( 132 @NonNull ConnectionStatusListener listener, 133 @NonNull @CallbackExecutor Executor executor) { 134 mConnectionStatusListeners.put(listener, executor); 135 } 136 137 /** 138 * Unregisters a listener previously registered with 139 * {@link #registerConnectionStatusListener(ConnectionStatusListener, Executor)}. 140 * 141 * @param listener The listener to unregister. 142 */ unregisterConnectionStatusListener( @onNull ConnectionStatusListener listener)143 public void unregisterConnectionStatusListener( 144 @NonNull ConnectionStatusListener listener) { 145 mConnectionStatusListeners.remove(listener); 146 } 147 notifyDevicesConnectionStatusChanged(int status)148 private void notifyDevicesConnectionStatusChanged(int status) { 149 mConnectionStatusListeners.forEach((listener, executor) -> 150 executor.execute(() -> listener.onDevicesConnectionStatusChanged(status))); 151 } 152 153 /** 154 * Updates the connection status of the hearing devices based on the currently bonded 155 * hearing aid devices. 156 */ notifyDevicesConnectionStatusChanged()157 synchronized void notifyDevicesConnectionStatusChanged() { 158 final int prevVal = mDevicesConnectionStatus; 159 updateDevicesConnectionStatus(); 160 if (mDevicesConnectionStatus != prevVal) { 161 notifyDevicesConnectionStatusChanged(mDevicesConnectionStatus); 162 } 163 } 164 updateDevicesConnectionStatus()165 private void updateDevicesConnectionStatus() { 166 mInitialDevicesConnectionStatusUpdate = true; 167 // Add all hearing devices including sub and member into a set. 168 Set<CachedBluetoothDevice> allHearingDevices = mCachedDevices.stream() 169 .filter(d -> d.getBondState() == BluetoothDevice.BOND_BONDED 170 && d.isHearingDevice()) 171 .flatMap(d -> getAssociatedCachedDevice(d).stream()) 172 .collect(Collectors.toSet()); 173 174 // Status sequence matters here. If one of the hearing devices is in previous 175 // ConnectionStatus, we will treat whole hearing devices is in this status. 176 // E.g. One of hearing device is in CONNECTED status and another is in DISCONNECTED 177 // status, the hearing devices connection status will notify CONNECTED status. 178 if (isConnectingOrDisconnectingConnectionStatus(allHearingDevices)) { 179 mDevicesConnectionStatus = ConnectionStatus.CONNECTING_OR_DISCONNECTING; 180 } else if (isActiveConnectionStatus(allHearingDevices)) { 181 mDevicesConnectionStatus = ConnectionStatus.ACTIVE; 182 } else if (isConnectedStatus(allHearingDevices)) { 183 mDevicesConnectionStatus = ConnectionStatus.CONNECTED; 184 } else if (isDisconnectedStatus(allHearingDevices)) { 185 mDevicesConnectionStatus = ConnectionStatus.DISCONNECTED; 186 } else { 187 mDevicesConnectionStatus = ConnectionStatus.NO_DEVICE_BONDED; 188 } 189 190 if (DEBUG) { 191 Log.d(TAG, "updateDevicesConnectionStatus: " + mDevicesConnectionStatus); 192 } 193 } 194 195 /** 196 * @return all the related CachedBluetoothDevices for this device. 197 */ 198 @NonNull getAssociatedCachedDevice( @onNull CachedBluetoothDevice device)199 public Set<CachedBluetoothDevice> getAssociatedCachedDevice( 200 @NonNull CachedBluetoothDevice device) { 201 ArraySet<CachedBluetoothDevice> cachedDeviceSet = new ArraySet<>(); 202 cachedDeviceSet.add(device); 203 // Associated device should be added into memberDevice if it support CSIP profile. 204 Set<CachedBluetoothDevice> memberDevices = device.getMemberDevice(); 205 if (!memberDevices.isEmpty()) { 206 cachedDeviceSet.addAll(memberDevices); 207 return cachedDeviceSet; 208 } 209 // If not support CSIP profile, it should be ASHA hearing device and added into subDevice. 210 CachedBluetoothDevice subDevice = device.getSubDevice(); 211 if (subDevice != null) { 212 cachedDeviceSet.add(subDevice); 213 return cachedDeviceSet; 214 } 215 216 return cachedDeviceSet; 217 } 218 isConnectingOrDisconnectingConnectionStatus( Set<CachedBluetoothDevice> devices)219 private boolean isConnectingOrDisconnectingConnectionStatus( 220 Set<CachedBluetoothDevice> devices) { 221 HearingAidProfile hearingAidProfile = mBtManager.getProfileManager().getHearingAidProfile(); 222 HapClientProfile hapClientProfile = mBtManager.getProfileManager().getHapClientProfile(); 223 224 for (CachedBluetoothDevice device : devices) { 225 if (hearingAidProfile != null) { 226 int status = device.getProfileConnectionState(hearingAidProfile); 227 if (status == BluetoothProfile.STATE_DISCONNECTING 228 || status == BluetoothProfile.STATE_CONNECTING) { 229 return true; 230 } 231 } 232 if (hapClientProfile != null) { 233 int status = device.getProfileConnectionState(hapClientProfile); 234 if (status == BluetoothProfile.STATE_DISCONNECTING 235 || status == BluetoothProfile.STATE_CONNECTING) { 236 return true; 237 } 238 } 239 } 240 return false; 241 } 242 isActiveConnectionStatus(Set<CachedBluetoothDevice> devices)243 private boolean isActiveConnectionStatus(Set<CachedBluetoothDevice> devices) { 244 for (CachedBluetoothDevice device : devices) { 245 if ((device.isActiveDevice(BluetoothProfile.HEARING_AID) 246 && device.isConnectedProfile(BluetoothProfile.HEARING_AID)) 247 || (device.isActiveDevice(BluetoothProfile.LE_AUDIO) 248 && device.isConnectedProfile(BluetoothProfile.LE_AUDIO))) { 249 return true; 250 } 251 } 252 return false; 253 } 254 isConnectedStatus(Set<CachedBluetoothDevice> devices)255 private boolean isConnectedStatus(Set<CachedBluetoothDevice> devices) { 256 return devices.stream().anyMatch(CachedBluetoothDevice::isConnected); 257 } 258 isDisconnectedStatus(Set<CachedBluetoothDevice> devices)259 private boolean isDisconnectedStatus(Set<CachedBluetoothDevice> devices) { 260 return devices.stream().anyMatch( 261 d -> (!d.isConnected() && d.getBondState() == BOND_BONDED)); 262 } 263 264 /** 265 * Gets the connection status for hearing device set. Will update connection status first if 266 * never updated. 267 */ 268 @ConnectionStatus getDevicesConnectionStatus()269 public int getDevicesConnectionStatus() { 270 if (!mInitialDevicesConnectionStatusUpdate) { 271 updateDevicesConnectionStatus(); 272 } 273 return mDevicesConnectionStatus; 274 } 275 initHearingAidDeviceIfNeeded(CachedBluetoothDevice newDevice, List<ScanFilter> leScanFilters)276 void initHearingAidDeviceIfNeeded(CachedBluetoothDevice newDevice, 277 List<ScanFilter> leScanFilters) { 278 HearingAidInfo info = generateHearingAidInfo(newDevice); 279 if (info != null) { 280 newDevice.setHearingAidInfo(info); 281 } else if (leScanFilters != null && !newDevice.isHearingAidDevice()) { 282 // If the device is added with hearing aid scan filter during pairing, set an empty 283 // hearing aid info to indicate it's a hearing aid device. The info will be updated 284 // when corresponding profiles connected. 285 for (ScanFilter leScanFilter: leScanFilters) { 286 final ParcelUuid serviceUuid = leScanFilter.getServiceUuid(); 287 final ParcelUuid serviceDataUuid = leScanFilter.getServiceDataUuid(); 288 if (BluetoothUuid.HEARING_AID.equals(serviceUuid) 289 || BluetoothUuid.HAS.equals(serviceUuid) 290 || BluetoothUuid.HEARING_AID.equals(serviceDataUuid) 291 || BluetoothUuid.HAS.equals(serviceDataUuid)) { 292 newDevice.setHearingAidInfo(new HearingAidInfo.Builder().build()); 293 break; 294 } 295 } 296 } 297 } 298 setSubDeviceIfNeeded(CachedBluetoothDevice newDevice)299 boolean setSubDeviceIfNeeded(CachedBluetoothDevice newDevice) { 300 final long hiSyncId = newDevice.getHiSyncId(); 301 if (isValidHiSyncId(hiSyncId)) { 302 // The remote device supports CSIP, the other ear should be processed as a member 303 // device. Ignore hiSyncId grouping from ASHA here. 304 if (newDevice.getProfiles().stream().anyMatch( 305 profile -> profile instanceof CsipSetCoordinatorProfile)) { 306 Log.w(TAG, "Skip ASHA grouping since this device supports CSIP"); 307 return false; 308 } 309 310 final CachedBluetoothDevice hearingAidDevice = getCachedDevice(hiSyncId); 311 // Just add one of the hearing aids from a pair in the list that is shown in the UI. 312 // Once there is another device with the same hiSyncId, to add new device as sub 313 // device. 314 if (hearingAidDevice != null) { 315 hearingAidDevice.setSubDevice(newDevice); 316 newDevice.setName(hearingAidDevice.getName()); 317 return true; 318 } 319 } 320 return false; 321 } 322 isValidHiSyncId(long hiSyncId)323 private boolean isValidHiSyncId(long hiSyncId) { 324 return hiSyncId != BluetoothHearingAid.HI_SYNC_ID_INVALID; 325 } 326 isValidGroupId(int groupId)327 private boolean isValidGroupId(int groupId) { 328 return groupId != BluetoothCsipSetCoordinator.GROUP_ID_INVALID; 329 } 330 getCachedDevice(long hiSyncId)331 private CachedBluetoothDevice getCachedDevice(long hiSyncId) { 332 for (int i = mCachedDevices.size() - 1; i >= 0; i--) { 333 CachedBluetoothDevice cachedDevice = mCachedDevices.get(i); 334 if (cachedDevice.getHiSyncId() == hiSyncId) { 335 return cachedDevice; 336 } 337 } 338 return null; 339 } 340 341 // To collect all HearingAid devices and call #onHiSyncIdChanged to group device by HiSyncId updateHearingAidsDevices()342 void updateHearingAidsDevices() { 343 final Set<Long> newSyncIdSet = new HashSet<>(); 344 for (CachedBluetoothDevice cachedDevice : mCachedDevices) { 345 // Do nothing if HiSyncId has been assigned 346 if (isValidHiSyncId(cachedDevice.getHiSyncId())) { 347 continue; 348 } 349 HearingAidInfo info = generateHearingAidInfo(cachedDevice); 350 if (info != null) { 351 cachedDevice.setHearingAidInfo(info); 352 if (isValidHiSyncId(info.getHiSyncId())) { 353 newSyncIdSet.add(info.getHiSyncId()); 354 } 355 } 356 } 357 for (Long syncId : newSyncIdSet) { 358 onHiSyncIdChanged(syncId); 359 } 360 } 361 362 // Group devices by hiSyncId 363 @VisibleForTesting onHiSyncIdChanged(long hiSyncId)364 void onHiSyncIdChanged(long hiSyncId) { 365 int firstMatchedIndex = -1; 366 367 for (int i = mCachedDevices.size() - 1; i >= 0; i--) { 368 CachedBluetoothDevice cachedDevice = mCachedDevices.get(i); 369 if (cachedDevice.getHiSyncId() != hiSyncId) { 370 continue; 371 } 372 373 // The remote device supports CSIP, the other ear should be processed as a member 374 // device. Ignore hiSyncId grouping from ASHA here. 375 if (cachedDevice.getProfiles().stream().anyMatch( 376 profile -> profile instanceof CsipSetCoordinatorProfile)) { 377 Log.w(TAG, "Skip ASHA grouping since this device supports CSIP"); 378 continue; 379 } 380 381 if (firstMatchedIndex == -1) { 382 // Found the first one 383 firstMatchedIndex = i; 384 continue; 385 } 386 // Found the second one 387 int indexToRemoveFromUi; 388 CachedBluetoothDevice subDevice; 389 CachedBluetoothDevice mainDevice; 390 // Since the hiSyncIds have been updated for a connected pair of hearing aids, 391 // we remove the entry of one the hearing aids from the UI. Unless the 392 // hiSyncId get updated, the system does not know it is a hearing aid, so we add 393 // both the hearing aids as separate entries in the UI first, then remove one 394 // of them after the hiSyncId is populated. We will choose the device that 395 // is not connected to be removed. 396 if (cachedDevice.isConnected()) { 397 mainDevice = cachedDevice; 398 indexToRemoveFromUi = firstMatchedIndex; 399 subDevice = mCachedDevices.get(firstMatchedIndex); 400 } else { 401 mainDevice = mCachedDevices.get(firstMatchedIndex); 402 indexToRemoveFromUi = i; 403 subDevice = cachedDevice; 404 } 405 406 mainDevice.setSubDevice(subDevice); 407 mCachedDevices.remove(indexToRemoveFromUi); 408 log("onHiSyncIdChanged: removed from UI device =" + subDevice 409 + ", with hiSyncId=" + hiSyncId); 410 mBtManager.getEventManager().dispatchDeviceRemoved(subDevice); 411 break; 412 } 413 } 414 415 // @return {@code true}, the event is processed inside the method. It is for updating 416 // hearing aid device on main-sub relationship when receiving connected or disconnected. 417 // @return {@code false}, it is not hearing aid device or to process it same as other profiles onProfileConnectionStateChangedIfProcessed(CachedBluetoothDevice cachedDevice, int state)418 boolean onProfileConnectionStateChangedIfProcessed(CachedBluetoothDevice cachedDevice, 419 int state) { 420 switch (state) { 421 case BluetoothProfile.STATE_CONNECTED: 422 onHiSyncIdChanged(cachedDevice.getHiSyncId()); 423 CachedBluetoothDevice mainDevice = findMainDevice(cachedDevice); 424 if (mainDevice != null) { 425 if (mainDevice.isConnected()) { 426 // Sub/member device is connected and main device is connected 427 // To refresh main device UI 428 mainDevice.refresh(); 429 } else { 430 // Sub/member device is connected and main device is disconnected 431 // To switch content and dispatch to notify UI change 432 switchDeviceContent(mainDevice, cachedDevice); 433 } 434 return true; 435 } 436 break; 437 case BluetoothProfile.STATE_DISCONNECTED: 438 if (cachedDevice.getUnpairing()) { 439 return true; 440 } 441 mainDevice = findMainDevice(cachedDevice); 442 if (mainDevice != null) { 443 // Sub/member device is disconnected and main device exists 444 // To update main device UI 445 mainDevice.refresh(); 446 return true; 447 } 448 CachedBluetoothDevice connectedSecondaryDevice = getConnectedSecondaryDevice( 449 cachedDevice); 450 if (connectedSecondaryDevice != null) { 451 // Main device is disconnected and sub/member device is connected 452 // To switch content and dispatch to notify UI change 453 switchDeviceContent(cachedDevice, connectedSecondaryDevice); 454 return true; 455 } 456 break; 457 } 458 return false; 459 } 460 switchDeviceContent(CachedBluetoothDevice mainDevice, CachedBluetoothDevice secondaryDevice)461 private void switchDeviceContent(CachedBluetoothDevice mainDevice, 462 CachedBluetoothDevice secondaryDevice) { 463 mBtManager.getEventManager().dispatchDeviceRemoved(mainDevice); 464 if (mainDevice.getSubDevice() != null 465 && mainDevice.getSubDevice().equals(secondaryDevice)) { 466 mainDevice.switchSubDeviceContent(); 467 } else { 468 mainDevice.switchMemberDeviceContent(secondaryDevice); 469 } 470 mainDevice.refresh(); 471 // It is necessary to do remove and add for updating the mapping on 472 // preference and device 473 mBtManager.getEventManager().dispatchDeviceAdded(mainDevice); 474 } 475 getConnectedSecondaryDevice(CachedBluetoothDevice cachedDevice)476 private CachedBluetoothDevice getConnectedSecondaryDevice(CachedBluetoothDevice cachedDevice) { 477 if (cachedDevice.getSubDevice() != null && cachedDevice.getSubDevice().isConnected()) { 478 return cachedDevice.getSubDevice(); 479 } 480 return cachedDevice.getMemberDevice().stream().filter( 481 CachedBluetoothDevice::isConnected).findAny().orElse(null); 482 } 483 onActiveDeviceChanged(CachedBluetoothDevice device)484 void onActiveDeviceChanged(CachedBluetoothDevice device) { 485 if (FeatureFlagUtils.isEnabled(mContext, FeatureFlagUtils.SETTINGS_AUDIO_ROUTING)) { 486 if (device.isConnectedHearingAidDevice() 487 && (device.isActiveDevice(BluetoothProfile.HEARING_AID) 488 || device.isActiveDevice(BluetoothProfile.LE_AUDIO))) { 489 setAudioRoutingConfig(device); 490 } else { 491 clearAudioRoutingConfig(); 492 } 493 } 494 if (com.android.settingslib.flags.Flags.hearingDevicesInputRoutingControl()) { 495 if (device.isConnectedHearingAidDevice() 496 && (device.isActiveDevice(BluetoothProfile.HEARING_AID) 497 || device.isActiveDevice(BluetoothProfile.LE_AUDIO))) { 498 setMicrophoneForCalls(device); 499 } else { 500 clearMicrophoneForCalls(); 501 } 502 } 503 } 504 syncDeviceIfNeeded(CachedBluetoothDevice device)505 void syncDeviceIfNeeded(CachedBluetoothDevice device) { 506 final LocalBluetoothProfileManager profileManager = mBtManager.getProfileManager(); 507 final HapClientProfile hap = profileManager.getHapClientProfile(); 508 // Sync preset if device doesn't support synchronization on the remote side 509 if (hap != null && !hap.supportsSynchronizedPresets(device.getDevice())) { 510 final CachedBluetoothDevice mainDevice = findMainDevice(device); 511 if (mainDevice != null) { 512 int mainPresetIndex = hap.getActivePresetIndex(mainDevice.getDevice()); 513 int presetIndex = hap.getActivePresetIndex(device.getDevice()); 514 if (mainPresetIndex != BluetoothHapClient.PRESET_INDEX_UNAVAILABLE 515 && mainPresetIndex != presetIndex) { 516 if (DEBUG) { 517 Log.d(TAG, "syncing preset from " + presetIndex + "->" 518 + mainPresetIndex + ", device=" + device); 519 } 520 hap.selectPreset(device.getDevice(), mainPresetIndex); 521 } 522 } 523 } 524 } 525 clearLocalDataIfNeeded(CachedBluetoothDevice device)526 void clearLocalDataIfNeeded(CachedBluetoothDevice device) { 527 HearingDeviceLocalDataManager.clear(mContext, device.getDevice()); 528 } 529 setMicrophoneForCalls(CachedBluetoothDevice device)530 private void setMicrophoneForCalls(CachedBluetoothDevice device) { 531 boolean useRemoteMicrophone = device.getDevice().isMicrophonePreferredForCalls(); 532 boolean status = mRoutingHelper.setPreferredInputDeviceForCalls(device, 533 useRemoteMicrophone ? RoutingValue.AUTO : RoutingValue.BUILTIN_DEVICE); 534 if (!status) { 535 Log.d(TAG, "Fail to configure setPreferredInputDeviceForCalls"); 536 } 537 } 538 clearMicrophoneForCalls()539 private void clearMicrophoneForCalls() { 540 boolean status = mRoutingHelper.clearPreferredInputDeviceForCalls(); 541 if (!status) { 542 Log.d(TAG, "Fail to configure clearMicrophoneForCalls"); 543 } 544 } 545 setAudioRoutingConfig(CachedBluetoothDevice device)546 private void setAudioRoutingConfig(CachedBluetoothDevice device) { 547 AudioDeviceAttributes hearingDeviceAttributes = 548 mRoutingHelper.getMatchedHearingDeviceAttributesForOutput(device); 549 if (hearingDeviceAttributes == null) { 550 Log.w(TAG, "Can not find expected AudioDeviceAttributes for hearing device: " 551 + device.getDevice().getAnonymizedAddress()); 552 return; 553 } 554 555 final int callRoutingValue = Settings.Secure.getInt(mContentResolver, 556 Settings.Secure.HEARING_AID_CALL_ROUTING, RoutingValue.AUTO); 557 final int mediaRoutingValue = Settings.Secure.getInt(mContentResolver, 558 Settings.Secure.HEARING_AID_MEDIA_ROUTING, RoutingValue.AUTO); 559 final int ringtoneRoutingValue = Settings.Secure.getInt(mContentResolver, 560 Settings.Secure.HEARING_AID_RINGTONE_ROUTING, RoutingValue.AUTO); 561 final int systemSoundsRoutingValue = Settings.Secure.getInt(mContentResolver, 562 Settings.Secure.HEARING_AID_NOTIFICATION_ROUTING, RoutingValue.AUTO); 563 564 setPreferredDeviceRoutingStrategies( 565 HearingAidAudioRoutingConstants.CALL_ROUTING_ATTRIBUTES, 566 hearingDeviceAttributes, callRoutingValue); 567 setPreferredDeviceRoutingStrategies( 568 HearingAidAudioRoutingConstants.MEDIA_ROUTING_ATTRIBUTES, 569 hearingDeviceAttributes, mediaRoutingValue); 570 setPreferredDeviceRoutingStrategies( 571 HearingAidAudioRoutingConstants.RINGTONE_ROUTING_ATTRIBUTES, 572 hearingDeviceAttributes, ringtoneRoutingValue); 573 setPreferredDeviceRoutingStrategies( 574 HearingAidAudioRoutingConstants.NOTIFICATION_ROUTING_ATTRIBUTES, 575 hearingDeviceAttributes, systemSoundsRoutingValue); 576 } 577 clearAudioRoutingConfig()578 private void clearAudioRoutingConfig() { 579 // Don't need to pass hearingDevice when we want to reset it (set to AUTO). 580 setPreferredDeviceRoutingStrategies( 581 HearingAidAudioRoutingConstants.CALL_ROUTING_ATTRIBUTES, 582 /* hearingDevice = */ null, RoutingValue.AUTO); 583 setPreferredDeviceRoutingStrategies( 584 HearingAidAudioRoutingConstants.MEDIA_ROUTING_ATTRIBUTES, 585 /* hearingDevice = */ null, RoutingValue.AUTO); 586 setPreferredDeviceRoutingStrategies( 587 HearingAidAudioRoutingConstants.RINGTONE_ROUTING_ATTRIBUTES, 588 /* hearingDevice = */ null, RoutingValue.AUTO); 589 setPreferredDeviceRoutingStrategies( 590 HearingAidAudioRoutingConstants.NOTIFICATION_ROUTING_ATTRIBUTES, 591 /* hearingDevice = */ null, RoutingValue.AUTO); 592 } 593 setPreferredDeviceRoutingStrategies(int[] attributeSdkUsageList, AudioDeviceAttributes hearingDevice, @RoutingValue int routingValue)594 private void setPreferredDeviceRoutingStrategies(int[] attributeSdkUsageList, 595 AudioDeviceAttributes hearingDevice, 596 @RoutingValue int routingValue) { 597 final List<AudioProductStrategy> supportedStrategies = 598 mRoutingHelper.getSupportedStrategies(attributeSdkUsageList); 599 600 final boolean status = mRoutingHelper.setPreferredDeviceRoutingStrategies( 601 supportedStrategies, hearingDevice, routingValue); 602 603 if (!status) { 604 Log.w(TAG, "routingStrategies: " + supportedStrategies.toString() + "routingValue: " 605 + routingValue + " fail to configure AudioProductStrategy"); 606 } 607 } 608 findMainDevice(CachedBluetoothDevice device)609 CachedBluetoothDevice findMainDevice(CachedBluetoothDevice device) { 610 if (device == null || mCachedDevices == null) { 611 return null; 612 } 613 614 for (CachedBluetoothDevice cachedDevice : mCachedDevices) { 615 if (isValidGroupId(cachedDevice.getGroupId())) { 616 Set<CachedBluetoothDevice> memberSet = cachedDevice.getMemberDevice(); 617 for (CachedBluetoothDevice memberDevice : memberSet) { 618 if (memberDevice != null && memberDevice.equals(device)) { 619 return cachedDevice; 620 } 621 } 622 } 623 if (isValidHiSyncId(cachedDevice.getHiSyncId())) { 624 CachedBluetoothDevice subDevice = cachedDevice.getSubDevice(); 625 if (subDevice != null && subDevice.equals(device)) { 626 return cachedDevice; 627 } 628 } 629 } 630 return null; 631 } 632 generateHearingAidInfo(CachedBluetoothDevice cachedDevice)633 private HearingAidInfo generateHearingAidInfo(CachedBluetoothDevice cachedDevice) { 634 final LocalBluetoothProfileManager profileManager = mBtManager.getProfileManager(); 635 636 final HearingAidProfile asha = profileManager.getHearingAidProfile(); 637 if (asha == null) { 638 Log.w(TAG, "HearingAidProfile is not supported on this device"); 639 } else { 640 long hiSyncId = asha.getHiSyncId(cachedDevice.getDevice()); 641 if (isValidHiSyncId(hiSyncId)) { 642 final HearingAidInfo info = new HearingAidInfo.Builder() 643 .setAshaDeviceSide(asha.getDeviceSide(cachedDevice.getDevice())) 644 .setAshaDeviceMode(asha.getDeviceMode(cachedDevice.getDevice())) 645 .setHiSyncId(hiSyncId) 646 .build(); 647 if (DEBUG) { 648 Log.d(TAG, "generateHearingAidInfo, " + cachedDevice + ", info=" + info); 649 } 650 return info; 651 } 652 } 653 654 final HapClientProfile hapClientProfile = profileManager.getHapClientProfile(); 655 final LeAudioProfile leAudioProfile = profileManager.getLeAudioProfile(); 656 if (hapClientProfile == null || leAudioProfile == null) { 657 Log.w(TAG, "HapClientProfile or LeAudioProfile is not supported on this device"); 658 } else if (cachedDevice.getProfiles().stream().anyMatch( 659 p -> p instanceof HapClientProfile)) { 660 int audioLocation = leAudioProfile.getAudioLocation(cachedDevice.getDevice()); 661 int hearingAidType = hapClientProfile.getHearingAidType(cachedDevice.getDevice()); 662 if (hearingAidType != HapClientProfile.HearingAidType.TYPE_INVALID) { 663 final HearingAidInfo info = new HearingAidInfo.Builder() 664 .setLeAudioLocation(audioLocation) 665 .setHapDeviceType(hearingAidType) 666 .build(); 667 if (DEBUG) { 668 Log.d(TAG, "generateHearingAidInfo, " + cachedDevice + ", info=" + info); 669 } 670 return info; 671 } 672 } 673 674 return null; 675 } 676 log(String msg)677 private void log(String msg) { 678 if (DEBUG) { 679 Log.d(TAG, msg); 680 } 681 } 682 }