| /* |
| * Copyright (C) 2023 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License |
| */ |
| |
| package com.android.server.telecom; |
| |
| import static com.android.server.telecom.CallAudioRouteAdapter.BT_AUDIO_CONNECTED; |
| import static com.android.server.telecom.CallAudioRouteAdapter.BT_AUDIO_DISCONNECTED; |
| import static com.android.server.telecom.CallAudioRouteAdapter.PENDING_ROUTE_FAILED; |
| import static com.android.server.telecom.CallAudioRouteAdapter.SPEAKER_OFF; |
| import static com.android.server.telecom.CallAudioRouteAdapter.SPEAKER_ON; |
| |
| import android.annotation.IntDef; |
| import android.bluetooth.BluetoothDevice; |
| import android.bluetooth.BluetoothHeadset; |
| import android.bluetooth.BluetoothStatusCodes; |
| import android.media.AudioDeviceInfo; |
| import android.media.AudioManager; |
| import android.sysprop.BluetoothProperties; |
| import android.telecom.Log; |
| import android.util.Pair; |
| |
| import com.android.internal.annotations.VisibleForTesting; |
| import com.android.server.telecom.bluetooth.BluetoothRouteManager; |
| |
| import java.lang.annotation.Retention; |
| import java.lang.annotation.RetentionPolicy; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Objects; |
| import java.util.Set; |
| import java.util.concurrent.CompletableFuture; |
| import java.util.concurrent.ExecutionException; |
| import java.util.concurrent.RejectedExecutionException; |
| import java.util.concurrent.ScheduledExecutorService; |
| import java.util.concurrent.ScheduledThreadPoolExecutor; |
| import java.util.concurrent.TimeUnit; |
| |
| public class AudioRoute { |
| public static class Factory { |
| private final ScheduledExecutorService mScheduledExecutorService = |
| new ScheduledThreadPoolExecutor(1); |
| private CompletableFuture<AudioRoute> mAudioRouteFuture; |
| public AudioRoute create(@AudioRouteType int type, String bluetoothAddress, |
| AudioManager audioManager, boolean scoManagedByAudio) throws RuntimeException { |
| mAudioRouteFuture = new CompletableFuture(); |
| createRetry(type, bluetoothAddress, audioManager, scoManagedByAudio, |
| MAX_CONNECTION_RETRIES); |
| try { |
| return mAudioRouteFuture.get(); |
| } catch (InterruptedException | ExecutionException e) { |
| throw new RuntimeException("Error when creating requested audio route"); |
| } |
| } |
| private void createRetry(@AudioRouteType int type, String bluetoothAddress, |
| AudioManager audioManager, boolean scoManagedByAudio, int retryCount) { |
| // Early exit if exceeded max number of retries (and complete the future). |
| if (retryCount == 0) { |
| mAudioRouteFuture.complete(null); |
| return; |
| } |
| |
| Log.i(this, "createRetry; type=%s, address=%s, retryCount=%d", |
| DEVICE_TYPE_STRINGS.get(type), bluetoothAddress, retryCount); |
| AudioDeviceInfo routeInfo = null; |
| List<AudioDeviceInfo> infos = audioManager.getAvailableCommunicationDevices(); |
| List<Integer> possibleInfoTypes = AUDIO_ROUTE_TYPE_TO_DEVICE_INFO_TYPE.get(type); |
| for (AudioDeviceInfo info : infos) { |
| Log.i(this, "type: " + info.getType()); |
| if (possibleInfoTypes != null && possibleInfoTypes.contains(info.getType())) { |
| if (BT_AUDIO_ROUTE_TYPES.contains(type)) { |
| if (bluetoothAddress.equals(info.getAddress())) { |
| routeInfo = info; |
| break; |
| } |
| } else { |
| routeInfo = info; |
| break; |
| } |
| } |
| } |
| // Try connecting BT device anyway (to handle wearables not showing as available |
| // communication device or LE device not showing up since it may not be the lead |
| // device). |
| if (routeInfo == null && bluetoothAddress == null) { |
| try { |
| mScheduledExecutorService.schedule( |
| () -> createRetry(type, bluetoothAddress, audioManager, |
| scoManagedByAudio, retryCount - 1), |
| RETRY_TIME_DELAY, TimeUnit.MILLISECONDS); |
| } catch (RejectedExecutionException e) { |
| Log.e(this, e, "Could not schedule retry for audio routing."); |
| } |
| } else { |
| mAudioRouteFuture.complete(new AudioRoute(type, bluetoothAddress, routeInfo, |
| scoManagedByAudio)); |
| } |
| } |
| } |
| |
| private static final long RETRY_TIME_DELAY = 500L; |
| private static final int MAX_CONNECTION_RETRIES = 2; |
| public static final int TYPE_INVALID = 0; |
| public static final int TYPE_EARPIECE = 1; |
| public static final int TYPE_WIRED = 2; |
| public static final int TYPE_SPEAKER = 3; |
| public static final int TYPE_DOCK = 4; |
| public static final int TYPE_BLUETOOTH_SCO = 5; |
| public static final int TYPE_BLUETOOTH_HA = 6; |
| public static final int TYPE_BLUETOOTH_LE = 7; |
| public static final int TYPE_STREAMING = 8; |
| // Used by auto |
| public static final int TYPE_BUS = 9; |
| @IntDef(prefix = "TYPE", value = { |
| TYPE_INVALID, |
| TYPE_EARPIECE, |
| TYPE_WIRED, |
| TYPE_SPEAKER, |
| TYPE_DOCK, |
| TYPE_BLUETOOTH_SCO, |
| TYPE_BLUETOOTH_HA, |
| TYPE_BLUETOOTH_LE, |
| TYPE_STREAMING, |
| TYPE_BUS |
| }) |
| @Retention(RetentionPolicy.SOURCE) |
| public @interface AudioRouteType {} |
| |
| private @AudioRouteType int mAudioRouteType; |
| private String mBluetoothAddress; |
| private BluetoothDevice mBluetoothHaPairDevice; |
| private AudioDeviceInfo mInfo; |
| private boolean mIsDestRouteForWatch; |
| private boolean mIsScoManagedByAudio; |
| public static final Set<Integer> BT_AUDIO_DEVICE_INFO_TYPES = new HashSet<>(Arrays.asList( |
| AudioDeviceInfo.TYPE_BLE_HEADSET, |
| AudioDeviceInfo.TYPE_BLE_SPEAKER, |
| AudioDeviceInfo.TYPE_BLE_BROADCAST, |
| AudioDeviceInfo.TYPE_HEARING_AID, |
| AudioDeviceInfo.TYPE_BLUETOOTH_SCO |
| )); |
| |
| public static final Set<Integer> BT_AUDIO_ROUTE_TYPES = Set.of( |
| AudioRoute.TYPE_BLUETOOTH_SCO, |
| AudioRoute.TYPE_BLUETOOTH_HA, |
| AudioRoute.TYPE_BLUETOOTH_LE |
| ); |
| |
| public static final HashMap<Integer, String> DEVICE_TYPE_STRINGS; |
| static { |
| DEVICE_TYPE_STRINGS = new HashMap<>(); |
| DEVICE_TYPE_STRINGS.put(TYPE_EARPIECE, "TYPE_EARPIECE"); |
| DEVICE_TYPE_STRINGS.put(TYPE_WIRED, "TYPE_WIRED_HEADSET"); |
| DEVICE_TYPE_STRINGS.put(TYPE_SPEAKER, "TYPE_SPEAKER"); |
| DEVICE_TYPE_STRINGS.put(TYPE_DOCK, "TYPE_DOCK"); |
| DEVICE_TYPE_STRINGS.put(TYPE_BUS, "TYPE_BUS"); |
| DEVICE_TYPE_STRINGS.put(TYPE_BLUETOOTH_SCO, "TYPE_BLUETOOTH_SCO"); |
| DEVICE_TYPE_STRINGS.put(TYPE_BLUETOOTH_HA, "TYPE_BLUETOOTH_HA"); |
| DEVICE_TYPE_STRINGS.put(TYPE_BLUETOOTH_LE, "TYPE_BLUETOOTH_LE"); |
| DEVICE_TYPE_STRINGS.put(TYPE_STREAMING, "TYPE_STREAMING"); |
| } |
| |
| public static final HashMap<Integer, Integer> DEVICE_INFO_TYPE_TO_AUDIO_ROUTE_TYPE; |
| static { |
| DEVICE_INFO_TYPE_TO_AUDIO_ROUTE_TYPE = new HashMap<>(); |
| DEVICE_INFO_TYPE_TO_AUDIO_ROUTE_TYPE.put(AudioDeviceInfo.TYPE_BUILTIN_EARPIECE, |
| TYPE_EARPIECE); |
| DEVICE_INFO_TYPE_TO_AUDIO_ROUTE_TYPE.put(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER, |
| TYPE_SPEAKER); |
| DEVICE_INFO_TYPE_TO_AUDIO_ROUTE_TYPE.put(AudioDeviceInfo.TYPE_WIRED_HEADSET, TYPE_WIRED); |
| DEVICE_INFO_TYPE_TO_AUDIO_ROUTE_TYPE.put(AudioDeviceInfo.TYPE_WIRED_HEADPHONES, TYPE_WIRED); |
| DEVICE_INFO_TYPE_TO_AUDIO_ROUTE_TYPE.put(AudioDeviceInfo.TYPE_BLUETOOTH_SCO, |
| TYPE_BLUETOOTH_SCO); |
| DEVICE_INFO_TYPE_TO_AUDIO_ROUTE_TYPE.put(AudioDeviceInfo.TYPE_USB_DEVICE, TYPE_WIRED); |
| DEVICE_INFO_TYPE_TO_AUDIO_ROUTE_TYPE.put(AudioDeviceInfo.TYPE_USB_ACCESSORY, TYPE_WIRED); |
| DEVICE_INFO_TYPE_TO_AUDIO_ROUTE_TYPE.put(AudioDeviceInfo.TYPE_DOCK, TYPE_DOCK); |
| DEVICE_INFO_TYPE_TO_AUDIO_ROUTE_TYPE.put(AudioDeviceInfo.TYPE_USB_HEADSET, TYPE_WIRED); |
| DEVICE_INFO_TYPE_TO_AUDIO_ROUTE_TYPE.put(AudioDeviceInfo.TYPE_HEARING_AID, |
| TYPE_BLUETOOTH_HA); |
| DEVICE_INFO_TYPE_TO_AUDIO_ROUTE_TYPE.put(AudioDeviceInfo.TYPE_BLE_HEADSET, |
| TYPE_BLUETOOTH_LE); |
| DEVICE_INFO_TYPE_TO_AUDIO_ROUTE_TYPE.put(AudioDeviceInfo.TYPE_BLE_SPEAKER, |
| TYPE_BLUETOOTH_LE); |
| DEVICE_INFO_TYPE_TO_AUDIO_ROUTE_TYPE.put(AudioDeviceInfo.TYPE_BLE_BROADCAST, |
| TYPE_BLUETOOTH_LE); |
| DEVICE_INFO_TYPE_TO_AUDIO_ROUTE_TYPE.put(AudioDeviceInfo.TYPE_LINE_ANALOG, TYPE_WIRED); |
| DEVICE_INFO_TYPE_TO_AUDIO_ROUTE_TYPE.put(AudioDeviceInfo.TYPE_DOCK_ANALOG, TYPE_DOCK); |
| DEVICE_INFO_TYPE_TO_AUDIO_ROUTE_TYPE.put(AudioDeviceInfo.TYPE_BUS, TYPE_BUS); |
| } |
| |
| public static final HashMap<Integer, List<Integer>> AUDIO_ROUTE_TYPE_TO_DEVICE_INFO_TYPE; |
| static { |
| AUDIO_ROUTE_TYPE_TO_DEVICE_INFO_TYPE = new HashMap<>(); |
| List<Integer> earpieceDeviceInfoTypes = new ArrayList<>(); |
| earpieceDeviceInfoTypes.add(AudioDeviceInfo.TYPE_BUILTIN_EARPIECE); |
| AUDIO_ROUTE_TYPE_TO_DEVICE_INFO_TYPE.put(TYPE_EARPIECE, earpieceDeviceInfoTypes); |
| |
| List<Integer> wiredDeviceInfoTypes = new ArrayList<>(); |
| wiredDeviceInfoTypes.add(AudioDeviceInfo.TYPE_WIRED_HEADSET); |
| wiredDeviceInfoTypes.add(AudioDeviceInfo.TYPE_WIRED_HEADPHONES); |
| wiredDeviceInfoTypes.add(AudioDeviceInfo.TYPE_USB_DEVICE); |
| wiredDeviceInfoTypes.add(AudioDeviceInfo.TYPE_USB_ACCESSORY); |
| wiredDeviceInfoTypes.add(AudioDeviceInfo.TYPE_USB_HEADSET); |
| AUDIO_ROUTE_TYPE_TO_DEVICE_INFO_TYPE.put(TYPE_WIRED, wiredDeviceInfoTypes); |
| |
| List<Integer> speakerDeviceInfoTypes = new ArrayList<>(); |
| speakerDeviceInfoTypes.add(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER); |
| AUDIO_ROUTE_TYPE_TO_DEVICE_INFO_TYPE.put(TYPE_SPEAKER, speakerDeviceInfoTypes); |
| |
| List<Integer> dockDeviceInfoTypes = new ArrayList<>(); |
| dockDeviceInfoTypes.add(AudioDeviceInfo.TYPE_DOCK); |
| dockDeviceInfoTypes.add(AudioDeviceInfo.TYPE_DOCK_ANALOG); |
| AUDIO_ROUTE_TYPE_TO_DEVICE_INFO_TYPE.put(TYPE_DOCK, dockDeviceInfoTypes); |
| |
| List<Integer> busDeviceInfoTypes = new ArrayList<>(); |
| busDeviceInfoTypes.add(AudioDeviceInfo.TYPE_BUS); |
| AUDIO_ROUTE_TYPE_TO_DEVICE_INFO_TYPE.put(TYPE_BUS, busDeviceInfoTypes); |
| |
| List<Integer> bluetoothScoDeviceInfoTypes = new ArrayList<>(); |
| bluetoothScoDeviceInfoTypes.add(AudioDeviceInfo.TYPE_BLUETOOTH_A2DP); |
| bluetoothScoDeviceInfoTypes.add(AudioDeviceInfo.TYPE_BLUETOOTH_SCO); |
| AUDIO_ROUTE_TYPE_TO_DEVICE_INFO_TYPE.put(TYPE_BLUETOOTH_SCO, bluetoothScoDeviceInfoTypes); |
| |
| List<Integer> bluetoothHearingAidDeviceInfoTypes = new ArrayList<>(); |
| bluetoothHearingAidDeviceInfoTypes.add(AudioDeviceInfo.TYPE_HEARING_AID); |
| AUDIO_ROUTE_TYPE_TO_DEVICE_INFO_TYPE.put(TYPE_BLUETOOTH_HA, |
| bluetoothHearingAidDeviceInfoTypes); |
| |
| List<Integer> bluetoothLeDeviceInfoTypes = new ArrayList<>(); |
| bluetoothLeDeviceInfoTypes.add(AudioDeviceInfo.TYPE_BLE_HEADSET); |
| bluetoothLeDeviceInfoTypes.add(AudioDeviceInfo.TYPE_BLE_SPEAKER); |
| bluetoothLeDeviceInfoTypes.add(AudioDeviceInfo.TYPE_BLE_BROADCAST); |
| AUDIO_ROUTE_TYPE_TO_DEVICE_INFO_TYPE.put(TYPE_BLUETOOTH_LE, bluetoothLeDeviceInfoTypes); |
| } |
| |
| public int getType() { |
| return mAudioRouteType; |
| } |
| |
| public AudioDeviceInfo getInfo() { |
| return mInfo; |
| } |
| |
| public boolean isWatch() { |
| return mIsDestRouteForWatch; |
| } |
| |
| public void setBluetoothAddress(String address) { |
| mBluetoothAddress = address; |
| } |
| |
| public String getBluetoothAddress() { |
| return mBluetoothAddress; |
| } |
| |
| public void setBluetoothHaPairDevice(BluetoothDevice device) { |
| mBluetoothHaPairDevice = device; |
| } |
| |
| public BluetoothDevice getBluetoothHaPairDevice() { |
| return mBluetoothHaPairDevice; |
| } |
| |
| // Invoked when entered pending route whose dest route is this route |
| void onDestRouteAsPendingRoute(boolean active, PendingAudioRoute pendingAudioRoute, |
| BluetoothDevice device, AudioManager audioManager, |
| BluetoothRouteManager bluetoothRouteManager, boolean isScoAlreadyConnected) { |
| Log.i(this, "onDestRouteAsPendingRoute: active (%b), type (%s), isScoAlreadyConnected(%s)", |
| active, DEVICE_TYPE_STRINGS.get(mAudioRouteType), isScoAlreadyConnected); |
| if (pendingAudioRoute.isActive() && !active) { |
| clearCommunicationDevice(pendingAudioRoute, bluetoothRouteManager, audioManager); |
| } else if (active) { |
| // Handle BT routing case. |
| if (BT_AUDIO_ROUTE_TYPES.contains(mAudioRouteType)) { |
| // Check if the communication device was set for the device, even if |
| // BluetoothHeadset#connectAudio reports that the SCO connection wasn't |
| // successfully established. |
| boolean connectedBtAudio = connectBtAudio(pendingAudioRoute, device, |
| audioManager, bluetoothRouteManager, isScoAlreadyConnected); |
| // Special handling for SCO case. |
| if (!mIsScoManagedByAudio && mAudioRouteType == TYPE_BLUETOOTH_SCO) { |
| // Set whether the dest route is for the watch |
| mIsDestRouteForWatch = bluetoothRouteManager.isWatch(device); |
| if (connectedBtAudio || isScoAlreadyConnected) { |
| pendingAudioRoute.setCommunicationDeviceType(mAudioRouteType); |
| if (!isScoAlreadyConnected) { |
| pendingAudioRoute.addMessage(BT_AUDIO_CONNECTED, mBluetoothAddress); |
| } |
| } else { |
| pendingAudioRoute.onMessageReceived(new Pair<>(PENDING_ROUTE_FAILED, |
| mBluetoothAddress), mBluetoothAddress); |
| } |
| return; |
| } |
| } else if (mAudioRouteType == TYPE_SPEAKER && !this.equals( |
| pendingAudioRoute.getOrigRoute())) { |
| pendingAudioRoute.addMessage(SPEAKER_ON, null); |
| } |
| |
| boolean result = false; |
| List<AudioDeviceInfo> devices = audioManager.getAvailableCommunicationDevices(); |
| for (AudioDeviceInfo deviceInfo : devices) { |
| // It's possible for the AudioDeviceInfo to be updated for the BT device so adjust |
| // mInfo accordingly. |
| // Note: we need to check the device type as well since a dual mode (LE and HFP) BT |
| // device can change type during a call if the user toggles LE for the device. |
| boolean isSameDeviceType = mAudioRouteType |
| == DEVICE_INFO_TYPE_TO_AUDIO_ROUTE_TYPE.getOrDefault( |
| deviceInfo.getType(), TYPE_INVALID); |
| boolean isHearingAidPairConnected = mBluetoothHaPairDevice != null |
| && Objects.equals(deviceInfo.getAddress(), |
| mBluetoothHaPairDevice.getAddress()); |
| if (BT_AUDIO_ROUTE_TYPES.contains(mAudioRouteType) |
| && (mBluetoothAddress.equals(deviceInfo.getAddress()) |
| || isHearingAidPairConnected) |
| && isSameDeviceType) { |
| mInfo = deviceInfo; |
| } |
| // Handle wired headset device address changes to ensure that we choose the right |
| // available device to set the communication device to. |
| if (mAudioRouteType == TYPE_WIRED && isSameDeviceType |
| && !deviceInfo.equals(mInfo)) { |
| Log.i(this, "onDestRouteAsPendingRoute: wired headset device changed, " |
| + "update mInfo from %s to %s", |
| mInfo == null ? "null" : audioDeviceTypeToString(mInfo.getType()), |
| audioDeviceTypeToString(deviceInfo.getType())); |
| mInfo = deviceInfo; |
| } |
| if (deviceInfo.equals(mInfo)) { |
| if (!com.android.internal.telecom.flags.Flags.callAudioRouteRf()) { |
| result = audioManager.setCommunicationDevice(mInfo); |
| Log.i(this, "onDestRouteAsPendingRoute: route=%s, " |
| + "AudioManager#setCommunicationDevice(%s)=%b", this, |
| audioDeviceTypeToString(mInfo.getType()), result); |
| } else { |
| result = true; |
| } |
| if (result) { |
| pendingAudioRoute.setCommunicationDeviceType(mAudioRouteType); |
| if (mAudioRouteType == TYPE_BLUETOOTH_SCO |
| && !isScoAlreadyConnected |
| && mIsScoManagedByAudio) { |
| pendingAudioRoute.addMessage(BT_AUDIO_CONNECTED, mBluetoothAddress); |
| } |
| } |
| break; |
| } |
| } |
| |
| // It's possible that BluetoothStateReceiver needs to report that the device is active |
| // before being able to successfully set the communication device. Refrain from sending |
| // pending route failed message for BT route until the second attempt fails. |
| if (!result && !BT_AUDIO_ROUTE_TYPES.contains(mAudioRouteType)) { |
| pendingAudioRoute.onMessageReceived(new Pair<>(PENDING_ROUTE_FAILED, null), null); |
| } |
| } |
| } |
| |
| /** |
| * Takes care of cleaning up original audio route (i.e. clearCommunicationDevice, |
| * sending SPEAKER_OFF, or disconnecting SCO). |
| * @param wasActive Was the origin route active or not. |
| * @param pendingAudioRoute The pending audio route change we're performing. |
| * @param audioManager Good 'ol audio manager. |
| * @param bluetoothRouteManager The BT route manager. |
| */ |
| void onOrigRouteAsPendingRoute(boolean wasActive, PendingAudioRoute pendingAudioRoute, |
| AudioManager audioManager, BluetoothRouteManager bluetoothRouteManager, |
| boolean isScoAlreadyConnected) { |
| Log.i(this, "onOrigRouteAsPendingRoute: wasActive (%b), type (%s), pending(%s)," |
| + "isScoAlreadyConnected(%s)", wasActive, DEVICE_TYPE_STRINGS.get(mAudioRouteType), |
| pendingAudioRoute, isScoAlreadyConnected); |
| if (wasActive && !isScoAlreadyConnected) { |
| int result = clearCommunicationDevice(pendingAudioRoute, bluetoothRouteManager, |
| audioManager); |
| if (mAudioRouteType == TYPE_SPEAKER) { |
| pendingAudioRoute.addMessage(SPEAKER_OFF, null); |
| } else if (mAudioRouteType == TYPE_BLUETOOTH_SCO |
| && result == BluetoothStatusCodes.SUCCESS) { |
| // Only send BT_AUDIO_DISCONNECTED for SCO if disconnect was successful. |
| pendingAudioRoute.addMessage(BT_AUDIO_DISCONNECTED, mBluetoothAddress); |
| } |
| } |
| } |
| |
| @VisibleForTesting |
| public AudioRoute(@AudioRouteType int type, String bluetoothAddress, AudioDeviceInfo info, |
| boolean isScoManagedByAudio) { |
| mAudioRouteType = type; |
| mBluetoothAddress = bluetoothAddress; |
| mInfo = info; |
| // Indication that SCO is managed by audio (i.e. supports setCommunicationDevice). |
| mIsScoManagedByAudio = isScoManagedByAudio; |
| } |
| |
| @Override |
| public boolean equals(Object obj) { |
| if (obj == null) { |
| return false; |
| } |
| if (!(obj instanceof AudioRoute otherRoute)) { |
| return false; |
| } |
| if (mAudioRouteType != otherRoute.getType()) { |
| return false; |
| } |
| String deviceAddress = null; |
| String otherDeviceAddress = null; |
| if (mBluetoothHaPairDevice != null) { |
| deviceAddress = mBluetoothHaPairDevice.getAddress(); |
| } |
| if (otherRoute.getBluetoothHaPairDevice() != null) { |
| otherDeviceAddress = otherRoute.getBluetoothHaPairDevice().getAddress(); |
| } |
| return !BT_AUDIO_ROUTE_TYPES.contains(mAudioRouteType) || (mBluetoothAddress.equals( |
| otherRoute.getBluetoothAddress()) |
| && Objects.equals(deviceAddress, otherDeviceAddress)); |
| } |
| |
| @Override |
| public int hashCode() { |
| return Objects.hash(mAudioRouteType, mBluetoothAddress); |
| } |
| |
| @Override |
| public String toString() { |
| return getClass().getSimpleName() + "[Type=" + DEVICE_TYPE_STRINGS.get(mAudioRouteType) |
| + ", Address=" + ((mBluetoothAddress != null) ? mBluetoothAddress : "invalid") |
| + ", HA Pair Device=" + (mBluetoothHaPairDevice != null |
| ? mBluetoothHaPairDevice.getAddress() : "invalid") + "]"; |
| } |
| |
| private boolean connectBtAudio(PendingAudioRoute pendingAudioRoute, BluetoothDevice device, |
| AudioManager audioManager, BluetoothRouteManager bluetoothRouteManager, |
| boolean isScoAlreadyConnected) { |
| // Ensure that if another BT device was set, it is disconnected before connecting |
| // the new one. |
| AudioRoute currentRoute = pendingAudioRoute.getOrigRoute(); |
| if (!isScoAlreadyConnected && currentRoute.getBluetoothAddress() != null && |
| !currentRoute.getBluetoothAddress().equals(device.getAddress())) { |
| clearCommunicationDevice(pendingAudioRoute, bluetoothRouteManager, audioManager); |
| } |
| |
| // Connect to the device (explicit handling for HFP devices). |
| // TODO: b/494671714 This is not needed with audio mode session API |
| boolean success = false; |
| if (device != null) { |
| success = bluetoothRouteManager.getDeviceManager() |
| .connectAudio(device, mAudioRouteType, mIsScoManagedByAudio); |
| } |
| |
| Log.i(this, "connectBtAudio: routeToConnectTo = %s, successful = %b", |
| this, success); |
| return success; |
| } |
| |
| /** |
| * Clears the communication device; this takes into account the fact that SCO devices require |
| * us to call {@link BluetoothHeadset#disconnectAudio()} rather than |
| * {@link AudioManager#clearCommunicationDevice()}. |
| * As a general rule, if we are transitioning from an active route to another active route, we |
| * do NOT need to call {@link AudioManager#clearCommunicationDevice()}, but if the device is a |
| * legacy SCO device we WILL need to call {@link BluetoothHeadset#disconnectAudio()}. We rely |
| * on the {@link PendingAudioRoute#isActive()} indicator to tell us if the destination route |
| * is going to be active or not. |
| * @param pendingAudioRoute The pending audio route transition we're implementing. |
| * @param bluetoothRouteManager The BT route manager. |
| * @param audioManager The audio manager. |
| * @return -1 if nothing was done, or the result code from the BT SCO disconnect. |
| */ |
| int clearCommunicationDevice(PendingAudioRoute pendingAudioRoute, |
| BluetoothRouteManager bluetoothRouteManager, AudioManager audioManager) { |
| // Only clear communication device if the destination route will be inactive; route to |
| // route transitions do not require clearing the communication device. |
| if (!pendingAudioRoute.isActive()) { |
| if (!com.android.internal.telecom.flags.Flags.callAudioRouteRf()) { |
| Log.i(this, "clearCommunicationDevice: AudioManager#clearCommunicationDevice," |
| + " type=" + DEVICE_TYPE_STRINGS.get( |
| pendingAudioRoute.getCommunicationDeviceType())); |
| audioManager.clearCommunicationDevice(); |
| } |
| } |
| |
| // Try to see if there's a previously set device for communication that should be cleared. |
| // This only serves to help in the SCO case to ensure that we disconnect the headset. |
| if (pendingAudioRoute.getCommunicationDeviceType() == AudioRoute.TYPE_INVALID) { |
| return -1; |
| } |
| |
| int result = BluetoothStatusCodes.SUCCESS; |
| boolean shouldDisconnectSco = !mIsScoManagedByAudio |
| && pendingAudioRoute.getCommunicationDeviceType() == TYPE_BLUETOOTH_SCO; |
| if (shouldDisconnectSco) { |
| Log.i(this, "Disconnecting SCO device via BluetoothHeadset."); |
| result = bluetoothRouteManager.getDeviceManager().disconnectSco(); |
| } |
| |
| if (result == BluetoothStatusCodes.SUCCESS) { |
| maybeClearConnectedPendingMessages(pendingAudioRoute); |
| pendingAudioRoute.setCommunicationDeviceType(AudioRoute.TYPE_INVALID); |
| } |
| return result; |
| } |
| |
| private void maybeClearConnectedPendingMessages(PendingAudioRoute pendingAudioRoute) { |
| // If we're still waiting on BT_AUDIO_CONNECTED/SPEAKER_ON but have routed out of it |
| // since and disconnected the device, then remove that message so we aren't waiting for |
| // it in the message queue. |
| if (mAudioRouteType == TYPE_BLUETOOTH_SCO) { |
| Log.i(this, "clearCommunicationDevice: Clearing pending " |
| + "BT_AUDIO_CONNECTED messages."); |
| pendingAudioRoute.clearPendingMessage( |
| new Pair<>(BT_AUDIO_CONNECTED, mBluetoothAddress)); |
| } else if (mAudioRouteType == TYPE_SPEAKER) { |
| Log.i(this, "clearCommunicationDevice: Clearing pending SPEAKER_ON messages."); |
| pendingAudioRoute.clearPendingMessage(new Pair<>(SPEAKER_ON, null)); |
| } |
| } |
| |
| /** |
| * Get a human readable (for logs) version of an an audio device type. |
| * @param type the device type |
| * @return the human readable string |
| */ |
| private static String audioDeviceTypeToString(int type) { |
| return switch (type) { |
| case AudioDeviceInfo.TYPE_BUILTIN_EARPIECE -> "earpiece"; |
| case AudioDeviceInfo.TYPE_BUILTIN_SPEAKER -> "speaker"; |
| case AudioDeviceInfo.TYPE_BUS -> "bus(auto speaker)"; |
| case AudioDeviceInfo.TYPE_BLUETOOTH_SCO -> "bt sco"; |
| case AudioDeviceInfo.TYPE_BLE_HEADSET -> "bt le"; |
| case AudioDeviceInfo.TYPE_HEARING_AID -> "bt hearing aid"; |
| case AudioDeviceInfo.TYPE_USB_HEADSET -> "usb headset"; |
| case AudioDeviceInfo.TYPE_WIRED_HEADSET -> "wired headset"; |
| default -> Integer.toString(type); |
| }; |
| } |
| |
| public void setScoManagedByAudio(boolean isScoManagedByAudio) { |
| mIsScoManagedByAudio = isScoManagedByAudio; |
| } |
| } |