| /* |
| * 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.AudioRoute.AUDIO_ROUTE_TYPE_TO_DEVICE_INFO_TYPE; |
| import static com.android.server.telecom.AudioRoute.BT_AUDIO_DEVICE_INFO_TYPES; |
| import static com.android.server.telecom.AudioRoute.BT_AUDIO_ROUTE_TYPES; |
| import static com.android.server.telecom.AudioRoute.DEVICE_INFO_TYPE_TO_AUDIO_ROUTE_TYPE; |
| import static com.android.server.telecom.AudioRoute.TYPE_BLUETOOTH_HA; |
| import static com.android.server.telecom.AudioRoute.TYPE_BLUETOOTH_SCO; |
| import static com.android.server.telecom.AudioRoute.TYPE_EARPIECE; |
| import static com.android.server.telecom.AudioRoute.TYPE_INVALID; |
| import static com.android.server.telecom.AudioRoute.TYPE_SPEAKER; |
| import static com.android.server.telecom.AudioRoute.TYPE_WIRED; |
| |
| import android.annotation.SuppressLint; |
| import android.bluetooth.BluetoothAdapter; |
| import android.bluetooth.BluetoothDevice; |
| import android.bluetooth.BluetoothLeAudio; |
| import android.bluetooth.BluetoothProfile; |
| import android.content.BroadcastReceiver; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.IntentFilter; |
| import android.content.pm.PackageManager; |
| import android.media.AudioAttributes; |
| import android.media.AudioDeviceAttributes; |
| import android.media.AudioDeviceCallback; |
| import android.media.AudioDeviceInfo; |
| import android.media.AudioManager; |
| import android.media.AudioModeSession; |
| import android.os.Handler; |
| import android.os.HandlerThread; |
| import android.os.Message; |
| import android.telecom.CallAudioState; |
| import android.telecom.Log; |
| import android.telecom.Logging.Session; |
| import android.telecom.VideoProfile; |
| import android.util.ArrayMap; |
| import android.util.IndentingPrintWriter; |
| import android.util.Pair; |
| |
| import androidx.annotation.NonNull; |
| import androidx.annotation.Nullable; |
| |
| import com.android.internal.annotations.VisibleForTesting; |
| import com.android.internal.os.SomeArgs; |
| import com.android.server.telecom.bluetooth.BluetoothRouteManager; |
| import com.android.server.telecom.flags.FeatureFlags; |
| import com.android.server.telecom.metrics.ErrorStats; |
| import com.android.server.telecom.metrics.TelecomMetricsController; |
| |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.LinkedHashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Objects; |
| import java.util.Set; |
| import java.util.UUID; |
| import java.util.concurrent.ConcurrentHashMap; |
| import java.util.concurrent.CountDownLatch; |
| import java.util.concurrent.ExecutorService; |
| import java.util.concurrent.Executors; |
| |
| @SuppressLint("NewApi") |
| public class CallAudioRouteController extends CallsManagerListenerBase |
| implements CallAudioRouteAdapter { |
| public static class Factory { |
| public CallAudioRouteController create( |
| Context context, |
| CallsManager callsManager, |
| AudioRoute.Factory audioRouteFactory, |
| WiredHeadsetManager wiredHeadsetManager, |
| BluetoothRouteManager bluetoothRouteManager, |
| StatusBarNotifier notifier, |
| FeatureFlags featureFlags, |
| TelecomMetricsController metricsController, |
| AsyncRingtonePlayer ringtonePlayer, |
| AnomalyReporterAdapter anomalyReporterAdapter) { |
| return new CallAudioRouteController( |
| context, |
| callsManager, |
| audioRouteFactory, |
| wiredHeadsetManager, |
| bluetoothRouteManager, |
| notifier, |
| featureFlags, |
| metricsController, |
| ringtonePlayer, |
| anomalyReporterAdapter); |
| } |
| } |
| |
| // TODO(b/469161729) - this is used to know when the ringing volume is increased from a muted |
| // to a non-muted state to start playing the ringtone again. We need an alternative. |
| public static final String STREAM_MUTE_CHANGED_ACTION = |
| "android.media.STREAM_MUTE_CHANGED_ACTION"; |
| // TODO(b/469161729) - this is used to know when the ringing volume is increased from a muted |
| // to a non-muted state to start playing the ringtone again. We need an alternative. |
| public static final String EXTRA_STREAM_VOLUME_MUTED = |
| "android.media.EXTRA_STREAM_VOLUME_MUTED"; |
| |
| private static final AudioRoute DUMMY_ROUTE = new AudioRoute(TYPE_INVALID, null, null, false); |
| private static final UUID AUDIO_ROUTING_EXTERNAL_CHANGE_UUID = |
| UUID.fromString("d9b38771-ff36-417b-8723-2363a870c702"); |
| private static final String AUDIO_ROUTING_EXTERNAL_CHANGE_MSG = |
| "Call audio routing was changed externally by another app via the AudioManager APIs." |
| + "This should only be handled by Telecom."; |
| private static final Map<Integer, Integer> ROUTE_MAP; |
| private static final Set<Integer> USER_REQUESTED_MESSAGES; |
| |
| static { |
| ROUTE_MAP = new ArrayMap<>(); |
| ROUTE_MAP.put(TYPE_INVALID, 0); |
| ROUTE_MAP.put(AudioRoute.TYPE_EARPIECE, CallAudioState.ROUTE_EARPIECE); |
| ROUTE_MAP.put(AudioRoute.TYPE_WIRED, CallAudioState.ROUTE_WIRED_HEADSET); |
| ROUTE_MAP.put(AudioRoute.TYPE_SPEAKER, CallAudioState.ROUTE_SPEAKER); |
| ROUTE_MAP.put(AudioRoute.TYPE_DOCK, CallAudioState.ROUTE_SPEAKER); |
| ROUTE_MAP.put(AudioRoute.TYPE_BUS, CallAudioState.ROUTE_SPEAKER); |
| ROUTE_MAP.put(AudioRoute.TYPE_BLUETOOTH_SCO, CallAudioState.ROUTE_BLUETOOTH); |
| ROUTE_MAP.put(AudioRoute.TYPE_BLUETOOTH_HA, CallAudioState.ROUTE_BLUETOOTH); |
| ROUTE_MAP.put(AudioRoute.TYPE_BLUETOOTH_LE, CallAudioState.ROUTE_BLUETOOTH); |
| ROUTE_MAP.put(AudioRoute.TYPE_STREAMING, CallAudioState.ROUTE_STREAMING); |
| USER_REQUESTED_MESSAGES = new HashSet<>(); |
| USER_REQUESTED_MESSAGES.add(USER_SWITCH_EARPIECE); |
| USER_REQUESTED_MESSAGES.add(USER_SWITCH_BLUETOOTH); |
| USER_REQUESTED_MESSAGES.add(USER_SWITCH_HEADSET); |
| USER_REQUESTED_MESSAGES.add(USER_SWITCH_SPEAKER); |
| USER_REQUESTED_MESSAGES.add(USER_SWITCH_BASELINE_ROUTE); |
| } |
| |
| /** Valid values for the first argument for SWITCH_BASELINE_ROUTE */ |
| public static final int NO_INCLUDE_BLUETOOTH_IN_BASELINE = 0; |
| |
| public static final int INCLUDE_BLUETOOTH_IN_BASELINE = 1; |
| |
| private final CallsManager mCallsManager; |
| private final Context mContext; |
| private AudioManager mAudioManager; |
| private CallAudioManager mCallAudioManager; |
| private final BluetoothRouteManager mBluetoothRouteManager; |
| private final Handler mHandler; |
| private final WiredHeadsetManager mWiredHeadsetManager; |
| private final AsyncRingtonePlayer mRingtonePlayer; |
| private Set<AudioRoute> mAvailableRoutes; |
| private Set<AudioRoute> mCallSupportedRoutes; |
| private AudioRoute mCurrentRoute; |
| private AudioRoute mEarpieceWiredRoute; |
| private AudioRoute mSpeakerDockRoute; |
| private AudioRoute mStreamingRoute; |
| private Set<AudioRoute> mStreamingRoutes; |
| private Map<AudioRoute, BluetoothDevice> mBluetoothRoutes; |
| private Pair<Integer, String> mActiveBluetoothDevice; |
| |
| /** |
| * Caches the previously and currently active Bluetooth device addresses for each {@link |
| * AudioRoute.AudioRouteType}. The key is the {@link AudioRoute.AudioRouteType} (e.g., |
| * TYPE_BLUETOOTH_SCO, TYPE_BLUETOOTH_HA, TYPE_BLUETOOTH_LE), and the value is a {@link |
| * Pair<String, String>} where the first element is the address of the previously active device |
| * and the second element is the address of the currently active device for that type. |
| */ |
| private Map<Integer, Pair<String, String>> mActiveDeviceCache; |
| |
| private String mBluetoothAddressForRinging; |
| private Map<Integer, AudioRoute> mTypeRoutes; |
| private Map<Call, Boolean> mCallsActiveFocusSwitch; |
| private PendingAudioRoute mPendingAudioRoute; |
| private AudioRoute.Factory mAudioRouteFactory; |
| private StatusBarNotifier mStatusBarNotifier; |
| private AudioManager.OnCommunicationDeviceChangedListener mCommunicationDeviceListener; |
| private ExecutorService mAudioManagerListenerExecutor; |
| private AudioManager.OnPreferredDevicesForStrategyChangedListener mPreferredDeviceListener; |
| private AudioRoute mPreferredDeviceRoute; |
| private FeatureFlags mFeatureFlags; |
| private int mFocusType; |
| private int mCallSupportedRouteMask = -1; |
| private BluetoothDevice mScoAudioConnectedDevice; |
| private BluetoothDevice mLastScoDisconnectedDevice; |
| private boolean mAvailableRoutesUpdated; |
| private boolean mUsePreferredDeviceStrategy; |
| private boolean mIsLastRequestedRouteUserSwitch; |
| private AudioDeviceInfo mCurrentCommunicationDevice; |
| private int mMode = AudioManager.MODE_NORMAL; |
| private AudioModeSession mAudioModeSession; |
| private final AudioModeSession.Callback mAudioModeSessionCallback = |
| new AudioModeSession.Callback() { |
| @Override |
| public void onAvailableRoutesChanged( |
| @NonNull List<AudioModeSession.AudioRoute> routes) { |
| Log.i(CallAudioRouteController.this, "onAvailableRoutesChanged: %s", routes); |
| if (com.android.internal.telecom.flags.Flags.callAudioRouteRf()) { |
| mHandler.post( |
| () -> { |
| handleAvailableRoutesChanged(routes); |
| }); |
| } |
| } |
| |
| @Override |
| public void onExternalRequestedRouteChanged( |
| @Nullable AudioModeSession.AudioRoute newRoute, int requestId) { |
| Log.i(CallAudioRouteController.this, |
| "onExternalRequestedRouteChanged: %s, id=%d", newRoute, requestId); |
| if (com.android.internal.telecom.flags.Flags.callAudioRouteRf()) { |
| mHandler.post( |
| () -> { |
| AudioRoute telecomRoute = |
| mapAudioModeRouteToTelecomRoute(newRoute); |
| if (telecomRoute != null) { |
| Log.i(CallAudioRouteController.this, |
| "External route change to %s", telecomRoute); |
| mIsPending = true; |
| mPendingAudioRoute.overrideDestRoute(telecomRoute); |
| mPendingAudioRoute.setActive(mIsActive); |
| setCurrentCommunicationDevice(telecomRoute.getInfo()); |
| handleExitPendingRoute(); |
| } |
| }); |
| } |
| } |
| |
| @Override |
| public void onPaused() { |
| // Not used for now. It is needed to support session per call |
| Log.i(CallAudioRouteController.this, "onPaused"); |
| } |
| |
| @Override |
| public void onResumed(int requestId) { |
| // Not used for now. It is needed to support session per call |
| Log.i(CallAudioRouteController.this, "onResumed: requestId=%d", requestId); |
| } |
| |
| @Override |
| public void onClosed() { |
| Log.i(CallAudioRouteController.this, "onClosed"); |
| mAudioModeSession = null; |
| } |
| |
| @Override |
| public void onRoutingResult( |
| int requestId, @Nullable AudioModeSession.AudioRoute route, int status) { |
| Log.i(CallAudioRouteController.this, |
| "onRoutingResult: id=%d, route=%s, status=%d", |
| requestId, route, status); |
| if (com.android.internal.telecom.flags.Flags.callAudioRouteRf()) { |
| mHandler.post( |
| () -> { |
| if (status == AudioModeSession.ROUTING_RESULT_SUCCESSFUL) { |
| AudioRoute telecomRoute = |
| mapAudioModeRouteToTelecomRoute(route); |
| if (telecomRoute != null) { |
| getPendingAudioRoute().overrideDestRoute(telecomRoute); |
| getPendingAudioRoute().setActive(mIsActive); |
| setCurrentCommunicationDevice(telecomRoute.getInfo()); |
| } |
| handleExitPendingRoute(); |
| } else { |
| Log.w(CallAudioRouteController.this, |
| "Routing failed: %d", status); |
| if (mIsActive) { |
| fallBack(null); |
| } else { |
| mIsPending = false; |
| mPendingAudioRoute.clearPendingMessages(); |
| } |
| } |
| }); |
| } |
| } |
| }; |
| private final Object mLock = new Object(); |
| private final TelecomSystem.SyncRoot mTelecomLock; |
| private CountDownLatch mAudioOperationsCompleteLatch; |
| private CountDownLatch mAudioActiveCompleteLatch; |
| private AnomalyReporterAdapter mAnomalyReporterAdapter; |
| private boolean mIsScoManagedByAudio; |
| |
| /** Receiver for added/removed device outputs that are reported by the audio fwk */ |
| public class AudioRoutesCallback extends AudioDeviceCallback { |
| @Override |
| public void onAudioDevicesAdded(AudioDeviceInfo[] addedDevices) { |
| Log.startSession("ARC.oADA"); |
| try { |
| updateAudioRoutes(addedDevices, true); |
| } finally { |
| Log.endSession(); |
| } |
| } |
| |
| @Override |
| public void onAudioDevicesRemoved(AudioDeviceInfo[] devices) { |
| Log.startSession("ARC.oADR"); |
| try { |
| updateAudioRoutes(devices, false); |
| } finally { |
| Log.endSession(); |
| } |
| } |
| |
| private void updateAudioRoutes(AudioDeviceInfo[] devices, boolean addDevices) { |
| Log.i(this, "updateAudioRoutes: add devices? %b", addDevices); |
| for (AudioDeviceInfo deviceInfo : devices) { |
| int audioRouteType = getAudioType(deviceInfo); |
| Log.i(this, "updateAudioRoutes: audioDeviceInfo: %s, audioRouteType: %d", |
| deviceInfo, audioRouteType); |
| if (!deviceInfo.isSink()) { |
| Log.i(this, "Ignore non sink device."); |
| continue; |
| } |
| // We should really only worry about handling earpiece and speaker. Bluetooth and |
| // wired headset routes are already dynamically updated. This logic can be updated |
| // once we support call audio route centralization. |
| // SCO needs to check here as well when the SCO refactor feature is enabled. |
| if ((!mIsScoManagedByAudio || audioRouteType != AudioRoute.TYPE_BLUETOOTH_SCO) |
| && (audioRouteType == TYPE_INVALID |
| || audioRouteType == AudioRoute.TYPE_WIRED |
| || BT_AUDIO_ROUTE_TYPES.contains(audioRouteType))) { |
| Log.i(this, "updateAudioRoutes: skipping route."); |
| continue; |
| } |
| if (addDevices) { |
| switch (audioRouteType) { |
| case AudioRoute.TYPE_SPEAKER -> createSpeakerRoute(); |
| case AudioRoute.TYPE_EARPIECE -> createEarpieceRoute(); |
| case AudioRoute.TYPE_BLUETOOTH_SCO -> { |
| if (mIsScoManagedByAudio && mCallAudioManager != null) { |
| mCallAudioManager |
| .getBluetoothStateReceiver() |
| .handleActiveDeviceChanged( |
| audioRouteType, deviceInfo.getAddress()); |
| } |
| } |
| } |
| } else { |
| AudioRoute route = mTypeRoutes.remove(audioRouteType); |
| updateAvailableRoutes(route, false); |
| if (audioRouteType == AudioRoute.TYPE_BLUETOOTH_SCO && mIsScoManagedByAudio |
| && mCallAudioManager != null) { |
| mCallAudioManager |
| .getBluetoothStateReceiver() |
| .handleActiveDeviceChanged(audioRouteType, null); |
| } |
| } |
| } |
| } |
| } |
| |
| private final BroadcastReceiver mMuteChangeReceiver = |
| new BroadcastReceiver() { |
| @Override |
| public void onReceive(Context context, Intent intent) { |
| Log.startSession("CARC.mCR"); |
| try { |
| if (AudioManager.ACTION_MICROPHONE_MUTE_CHANGED.equals( |
| intent.getAction())) { |
| if (mCallsManager.isInEmergencyCall()) { |
| Log.i(this, |
| "Mute was externally changed when there's an emergency" |
| + " call. Forcing mute back off."); |
| sendMessageWithSessionInfo(MUTE_OFF); |
| } else { |
| sendMessageWithSessionInfo(MUTE_EXTERNALLY_CHANGED); |
| } |
| } else if (STREAM_MUTE_CHANGED_ACTION.equals(intent.getAction())) { |
| int streamType = |
| intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, -1); |
| boolean isStreamMuted = |
| intent.getBooleanExtra(EXTRA_STREAM_VOLUME_MUTED, false); |
| |
| if (streamType == AudioManager.STREAM_RING |
| && !isStreamMuted |
| && mCallAudioManager != null) { |
| Log.i(this, "Ring stream was un-muted."); |
| // clear the silenced calls if device is un-muted |
| mCallAudioManager.clearSilencedCalls(); |
| mCallAudioManager.onRingerModeChange(); |
| } |
| } else { |
| Log.w(this, "Received non-mute-change intent"); |
| } |
| } finally { |
| Log.endSession(); |
| } |
| } |
| }; |
| private CallAudioState mCallAudioState; |
| private boolean mIsMute; |
| private boolean mIsPending; |
| private boolean mIsActive; |
| private boolean mWasOnSpeaker; |
| private AudioRoutesCallback mAudioRoutesCallback; |
| private final TelecomMetricsController mMetricsController; |
| |
| public CallAudioRouteController( |
| Context context, |
| CallsManager callsManager, |
| AudioRoute.Factory audioRouteFactory, |
| WiredHeadsetManager wiredHeadsetManager, |
| BluetoothRouteManager bluetoothRouteManager, |
| StatusBarNotifier statusBarNotifier, |
| FeatureFlags featureFlags, |
| TelecomMetricsController metricsController, |
| AsyncRingtonePlayer ringtonePlayer, |
| AnomalyReporterAdapter anomalyReporterAdapter) { |
| mContext = context; |
| mCallsManager = callsManager; |
| mAudioManager = context.getSystemService(AudioManager.class); |
| mAudioRouteFactory = audioRouteFactory; |
| mWiredHeadsetManager = wiredHeadsetManager; |
| mIsMute = false; |
| mBluetoothRouteManager = bluetoothRouteManager; |
| mStatusBarNotifier = statusBarNotifier; |
| mFeatureFlags = featureFlags; |
| mMetricsController = metricsController; |
| mRingtonePlayer = ringtonePlayer; |
| mAnomalyReporterAdapter = anomalyReporterAdapter; |
| mFocusType = NO_FOCUS; |
| mScoAudioConnectedDevice = null; |
| mUsePreferredDeviceStrategy = true; |
| mWasOnSpeaker = false; |
| setCurrentCommunicationDevice(null); |
| mPreferredDeviceRoute = DUMMY_ROUTE; |
| // Add support for TYPE_BLE_HEARING_AID |
| if (android.media.audio.Flags.bleHearingAidDevice()) { |
| BT_AUDIO_DEVICE_INFO_TYPES.add(AudioDeviceInfo.TYPE_BLE_HEARING_AID); |
| DEVICE_INFO_TYPE_TO_AUDIO_ROUTE_TYPE.put(AudioDeviceInfo.TYPE_BLE_HEARING_AID, |
| AudioRoute.TYPE_BLUETOOTH_LE); |
| List<Integer> bluetoothLeDeviceInfoTypes = AUDIO_ROUTE_TYPE_TO_DEVICE_INFO_TYPE |
| .get(AudioRoute.TYPE_BLUETOOTH_LE); |
| if (!bluetoothLeDeviceInfoTypes.contains(AudioDeviceInfo.TYPE_BLE_HEARING_AID)) { |
| bluetoothLeDeviceInfoTypes.add(AudioDeviceInfo.TYPE_BLE_HEARING_AID); |
| } |
| } else { |
| // This is mainly in place for unit testing. |
| BT_AUDIO_DEVICE_INFO_TYPES.remove(AudioDeviceInfo.TYPE_BLE_HEARING_AID); |
| DEVICE_INFO_TYPE_TO_AUDIO_ROUTE_TYPE.remove(AudioDeviceInfo.TYPE_BLE_HEARING_AID); |
| List<Integer> bluetoothLeDeviceInfoTypes = AUDIO_ROUTE_TYPE_TO_DEVICE_INFO_TYPE |
| .get(AudioRoute.TYPE_BLUETOOTH_LE); |
| bluetoothLeDeviceInfoTypes.removeIf( |
| device -> device.equals(AudioDeviceInfo.TYPE_BLE_HEARING_AID)); |
| } |
| |
| mTelecomLock = callsManager.getLock(); |
| HandlerThread handlerThread = new HandlerThread(this.getClass().getSimpleName()); |
| handlerThread.start(); |
| |
| IntentFilter micMuteChangedFilter = |
| new IntentFilter(AudioManager.ACTION_MICROPHONE_MUTE_CHANGED); |
| micMuteChangedFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY); |
| context.registerReceiver(mMuteChangeReceiver, micMuteChangedFilter); |
| |
| IntentFilter muteChangedFilter = new IntentFilter(STREAM_MUTE_CHANGED_ACTION); |
| muteChangedFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY); |
| context.registerReceiver(mMuteChangeReceiver, muteChangedFilter); |
| |
| // Register AudioManager#onCommunicationDeviceChangedListener listener to receive updates |
| // to communication device (via AudioManager#setCommunicationDevice). This is a replacement |
| // to using broadcasts in the hopes of improving performance. |
| mAudioManagerListenerExecutor = Executors.newSingleThreadExecutor(); |
| mCommunicationDeviceListener = |
| new AudioManager.OnCommunicationDeviceChangedListener() { |
| @Override |
| public void onCommunicationDeviceChanged(AudioDeviceInfo device) { |
| try { |
| Log.startSession("CARC.oCDC"); |
| if (device == null) { |
| Log.i(this, "It should not happen as device is null"); |
| } else { |
| @AudioRoute.AudioRouteType int audioType = getAudioType(device); |
| // Get the previous communication device for handling |
| // SCO disconnected for HFP devices |
| AudioDeviceInfo previousDevice = getCurrentCommunicationDevice(); |
| setCurrentCommunicationDevice(device); |
| Log.i(this, "onCommunicationDeviceChanged: previous device (%s)," |
| + " current device (%s), audioType (%d)", |
| previousDevice, device, audioType); |
| // Todo: Update to account for all device types once we support |
| // audio route centralization. |
| handleCommunicationDeviceChanged(audioType, device, previousDevice); |
| } |
| } finally { |
| Log.endSession(); |
| } |
| } |
| }; |
| |
| if (android.media.audio.Flags.amscoAvailableApi() && mAudioManager != null) { |
| mIsScoManagedByAudio = mAudioManager.isScoManagedByAudio(); |
| } |
| |
| // Register the AudioManager. OnPreferredDevicesForStrategyChangedListener listener to |
| // receive updates for the communication device. This is a replacement to directly querying |
| // the preferred device via AudioManager#getPreferredDeviceForStrategy, which was known |
| // to hold up the invoking thread. |
| mPreferredDeviceListener = |
| (strategy, devices) -> { |
| try { |
| Log.startSession("CARC.oPDFSCL"); |
| final AudioAttributes attr = |
| new AudioAttributes.Builder() |
| .setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION) |
| .build(); |
| if (!devices.isEmpty() && strategy.supportsAudioAttributes(attr)) { |
| AudioRoute audioRoute = |
| getPreferredDeviceAudioRoute(devices.getFirst()); |
| Log.i(this, |
| "OnPreferredDevicesForStrategyChangedListener: preferred device" |
| + " was updated to %s", audioRoute); |
| // Get the first device listed |
| setPreferredDeviceRoute(audioRoute); |
| } |
| } finally { |
| Log.endSession(); |
| } |
| }; |
| |
| // Create handler |
| mHandler = new Handler(handlerThread.getLooper()) { |
| @Override |
| public void handleMessage(@NonNull Message msg) { |
| synchronized (this) { |
| preHandleMessage(msg); |
| String address; |
| BluetoothDevice bluetoothDevice; |
| int focus; |
| int handleEndTone; |
| @AudioRoute.AudioRouteType int type; |
| AudioRoute currentPendingRoute = getCurrentOrPendingRoute(); |
| switch (msg.what) { |
| case CONNECT_WIRED_HEADSET -> handleWiredHeadsetConnected(); |
| case DISCONNECT_WIRED_HEADSET -> handleWiredHeadsetDisconnected(); |
| case CONNECT_DOCK -> handleDockConnected(); |
| case DISCONNECT_DOCK -> handleDockDisconnected(); |
| case BLUETOOTH_DEVICE_LIST_CHANGED -> {} |
| case BT_ACTIVE_DEVICE_PRESENT -> { |
| type = msg.arg1; |
| address = (String) ((SomeArgs) msg.obj).arg2; |
| handleBtActiveDevicePresent(type, address); |
| } |
| case BT_ACTIVE_DEVICE_GONE -> { |
| type = msg.arg1; |
| handleBtActiveDeviceGone(type); |
| } |
| case BT_DEVICE_ADDED -> { |
| type = msg.arg1; |
| bluetoothDevice = (BluetoothDevice) ((SomeArgs) msg.obj).arg2; |
| handleBtConnected(type, bluetoothDevice); |
| } |
| case BT_DEVICE_REMOVED -> { |
| type = msg.arg1; |
| bluetoothDevice = (BluetoothDevice) ((SomeArgs) msg.obj).arg2; |
| handleBtDisconnected(type, bluetoothDevice); |
| } |
| case SWITCH_EARPIECE, USER_SWITCH_EARPIECE -> handleSwitchEarpiece( |
| msg.what == USER_SWITCH_EARPIECE); |
| case SWITCH_BLUETOOTH, USER_SWITCH_BLUETOOTH -> { |
| address = (String) ((SomeArgs) msg.obj).arg2; |
| handleSwitchBluetooth( |
| address, msg.what == USER_SWITCH_BLUETOOTH); |
| } |
| case SWITCH_HEADSET, USER_SWITCH_HEADSET -> |
| handleSwitchHeadset(msg.what == USER_SWITCH_HEADSET); |
| case SWITCH_SPEAKER, USER_SWITCH_SPEAKER -> |
| handleSwitchSpeaker(msg.what == USER_SWITCH_SPEAKER); |
| case SWITCH_BASELINE_ROUTE -> { |
| address = (String) ((SomeArgs) msg.obj).arg2; |
| handleSwitchBaselineRoute( |
| false, |
| msg.arg1 == INCLUDE_BLUETOOTH_IN_BASELINE, |
| address); |
| } |
| case USER_SWITCH_BASELINE_ROUTE -> handleSwitchBaselineRoute( |
| true, msg.arg1 == INCLUDE_BLUETOOTH_IN_BASELINE, null); |
| case SPEAKER_ON -> handleSpeakerOn(); |
| case SPEAKER_OFF -> handleSpeakerOff(); |
| case STREAMING_FORCE_ENABLED -> handleStreamingEnabled(); |
| case STREAMING_FORCE_DISABLED -> handleStreamingDisabled(); |
| case BT_AUDIO_CONNECTED -> { |
| bluetoothDevice = (BluetoothDevice) ((SomeArgs) msg.obj).arg2; |
| handleBtAudioActive(bluetoothDevice); |
| } |
| case BT_AUDIO_DISCONNECTED -> { |
| bluetoothDevice = (BluetoothDevice) ((SomeArgs) msg.obj).arg2; |
| handleBtAudioInactive(bluetoothDevice); |
| } |
| case MUTE_ON -> handleMuteChanged(true, false /* isExternal */); |
| case MUTE_OFF -> handleMuteChanged(false, false /* isExternal */); |
| case MUTE_EXTERNALLY_CHANGED -> handleMuteChanged( |
| mAudioManager.isMicrophoneMute(), true /* isExternal */); |
| case TOGGLE_MUTE -> handleMuteChanged(!mIsMute, false /* isExternal */); |
| case SWITCH_FOCUS -> { |
| focus = msg.arg1; |
| handleEndTone = (int) ((SomeArgs) msg.obj).arg2; |
| handleSwitchFocus(focus, handleEndTone); |
| } |
| case EXIT_PENDING_ROUTE -> handleExitPendingRoute(); |
| case UPDATE_SYSTEM_AUDIO_ROUTE -> { |
| // Based on the available routes for foreground call, adjust |
| // routing. |
| updateRouteForForeground(); |
| // Force update to notify all ICS/CS. |
| updateCallAudioState( |
| new CallAudioState( |
| mIsMute, |
| mCallAudioState.getRoute(), |
| mCallAudioState.getSupportedRouteMask(), |
| mCallAudioState.getActiveBluetoothDevice(), |
| mCallAudioState |
| .getSupportedBluetoothDevices())); |
| } |
| case ON_CALL_ADDED -> createAudioModeSession(); |
| case ON_CALL_REMOVED -> { |
| if (!mCallsManager.hasAnyCalls()) { |
| closeAudioModeSession(); |
| } |
| } |
| case VIDEO_STATE_CHANGED -> { |
| int videoState = msg.arg1; |
| if (com.android.internal.telecom.flags.Flags.callAudioRouteRf() |
| && mAudioModeSession != null) { |
| Log.i(this, "AudioModeSession#setDisplayActiveUseCase %b", |
| VideoProfile.isVideo(videoState)); |
| mAudioModeSession.setDisplayActiveUseCase( |
| VideoProfile.isVideo(videoState)); |
| } |
| } |
| case SET_AUDIO_MODE -> setAudioMode(msg.arg1); |
| } |
| postHandleMessage(msg, currentPendingRoute); |
| } |
| } |
| }; |
| } |
| |
| @Override |
| public void initialize() { |
| mAvailableRoutes = new HashSet<>(); |
| mCallSupportedRoutes = new HashSet<>(); |
| mBluetoothRoutes = Collections.synchronizedMap(new LinkedHashMap<>()); |
| mActiveDeviceCache = new HashMap<>(); |
| mActiveDeviceCache.put(AudioRoute.TYPE_BLUETOOTH_SCO, null); |
| mActiveDeviceCache.put(AudioRoute.TYPE_BLUETOOTH_HA, null); |
| mActiveDeviceCache.put(AudioRoute.TYPE_BLUETOOTH_LE, null); |
| mActiveBluetoothDevice = null; |
| mTypeRoutes = new ArrayMap<>(); |
| mCallsActiveFocusSwitch = new ConcurrentHashMap<>(); |
| mStreamingRoutes = new HashSet<>(); |
| mPendingAudioRoute = |
| new PendingAudioRoute(this, mAudioManager, mBluetoothRouteManager, mFeatureFlags); |
| mStreamingRoute = |
| new AudioRoute(AudioRoute.TYPE_STREAMING, null, null, mIsScoManagedByAudio); |
| DUMMY_ROUTE.setScoManagedByAudio(mIsScoManagedByAudio); |
| mStreamingRoutes.add(mStreamingRoute); |
| |
| int supportMask = calculateSupportedRouteMaskInit(); |
| if ((supportMask & CallAudioState.ROUTE_SPEAKER) != 0) { |
| int audioRouteType = AudioRoute.TYPE_SPEAKER; |
| createSpeakerRoute(); |
| } |
| |
| if ((supportMask & CallAudioState.ROUTE_WIRED_HEADSET) != 0) { |
| // Create wired headset routes |
| mEarpieceWiredRoute = |
| mAudioRouteFactory.create( |
| AudioRoute.TYPE_WIRED, null, mAudioManager, mIsScoManagedByAudio); |
| if (mEarpieceWiredRoute == null) { |
| Log.w(this, "Can't find available audio device info for route TYPE_WIRED_HEADSET"); |
| } else { |
| mTypeRoutes.put(AudioRoute.TYPE_WIRED, mEarpieceWiredRoute); |
| updateAvailableRoutes(mEarpieceWiredRoute, true); |
| } |
| } else if ((supportMask & CallAudioState.ROUTE_EARPIECE) != 0) { |
| createEarpieceRoute(); |
| } |
| |
| // set current route |
| if (mEarpieceWiredRoute != null) { |
| mCurrentRoute = mEarpieceWiredRoute; |
| } else if (mSpeakerDockRoute != null) { |
| mCurrentRoute = mSpeakerDockRoute; |
| } else { |
| mCurrentRoute = DUMMY_ROUTE; |
| } |
| // Audio ops will only ever be completed if there's a call placed and it gains |
| // ACTIVE/RINGING focus, hence why the initial value is 0. |
| mAudioOperationsCompleteLatch = new CountDownLatch(0); |
| // This latch will be count down when ACTIVE/RINGING focus is gained. This is determined |
| // when the routing goes active. |
| mAudioActiveCompleteLatch = new CountDownLatch(1); |
| mIsActive = false; |
| mCallAudioState = new CallAudioState(mIsMute, ROUTE_MAP.get(mCurrentRoute.getType()), |
| supportMask, null, new HashSet<>()); |
| if (!com.android.internal.telecom.flags.Flags.callAudioRouteRf()) { |
| mAudioManager.addOnCommunicationDeviceChangedListener( |
| mAudioManagerListenerExecutor, mCommunicationDeviceListener); |
| mAudioManager.addOnPreferredDevicesForStrategyChangedListener( |
| mAudioManagerListenerExecutor, mPreferredDeviceListener); |
| mAudioRoutesCallback = new AudioRoutesCallback(); |
| mAudioManager.registerAudioDeviceCallback(mAudioRoutesCallback, mHandler); |
| } |
| mIsLastRequestedRouteUserSwitch = false; |
| } |
| |
| @Override |
| public void sendMessageWithSessionInfo(int message) { |
| sendMessageWithSessionInfo(message, 0, (String) null); |
| } |
| |
| @Override |
| public void sendMessageWithSessionInfo(int message, int arg) { |
| sendMessageWithSessionInfo(message, arg, (String) null); |
| } |
| |
| @Override |
| public void sendMessageWithSessionInfo(int message, int arg, String data) { |
| SomeArgs args = SomeArgs.obtain(); |
| args.arg1 = Log.createSubsession(); |
| args.arg2 = data; |
| sendMessage(message, arg, 0, args); |
| } |
| |
| @Override |
| public void sendMessageWithSessionInfo(int message, int arg, int data) { |
| SomeArgs args = SomeArgs.obtain(); |
| args.arg1 = Log.createSubsession(); |
| args.arg2 = data; |
| sendMessage(message, arg, 0, args); |
| } |
| |
| @Override |
| public void sendMessageWithSessionInfoAtFront(int message, int arg, int data) { |
| SomeArgs args = SomeArgs.obtain(); |
| args.arg1 = Log.createSubsession(); |
| args.arg2 = data; |
| mHandler.sendMessageAtFrontOfQueue(Message.obtain(mHandler, message, arg, 0, args)); |
| } |
| |
| @Override |
| public void sendMessageWithSessionInfo(int message, int arg, BluetoothDevice bluetoothDevice) { |
| SomeArgs args = SomeArgs.obtain(); |
| args.arg1 = Log.createSubsession(); |
| args.arg2 = bluetoothDevice; |
| sendMessage(message, arg, 0, args); |
| } |
| |
| @Override |
| public void sendMessage(int message, Runnable r) { |
| r.run(); |
| } |
| |
| private void sendMessage(int what, int arg1, int arg2, Object obj) { |
| mHandler.sendMessage(Message.obtain(mHandler, what, arg1, arg2, obj)); |
| } |
| |
| @Override |
| public void setCallAudioManager(CallAudioManager callAudioManager) { |
| mCallAudioManager = callAudioManager; |
| } |
| |
| @Override |
| public CallAudioState getCurrentCallAudioState() { |
| return mCallAudioState; |
| } |
| |
| @Override |
| public boolean isHfpDeviceAvailable() { |
| return !mBluetoothRoutes.isEmpty(); |
| } |
| |
| @Override |
| public Handler getAdapterHandler() { |
| return mHandler; |
| } |
| |
| @Override |
| public PendingAudioRoute getPendingAudioRoute() { |
| return mPendingAudioRoute; |
| } |
| |
| @Override |
| public void dump(IndentingPrintWriter pw) {} |
| |
| @Override |
| public void onCallAdded(Call call) { |
| Log.i(this, "onCallAdded(%s)", call); |
| // Tracks the added call internally and the latest audio route request type (whether it's |
| // a user request or not) |
| mCallsActiveFocusSwitch.put(call, false); |
| } |
| |
| @Override |
| public void onCallRemoved(Call call) { |
| Log.i(this, "onCallRemoved(%s)", call); |
| mCallsActiveFocusSwitch.remove(call); |
| // Reset the mute state if all external and non-external calls have been removed. This |
| // should only be performed for the watch case. |
| if (mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH) |
| && mFocusType == NO_FOCUS && !mCallsManager.hasAnyCalls() |
| && !mCallsManager.hasExternalCalls()) { |
| Log.i(this, "Last call removed in NO_FOCUS, resetting mute."); |
| handleMuteChanged(false, false); |
| } |
| } |
| |
| private boolean shouldHandleRouteForVideoCall() { |
| // Determine if we need to handle initial audio routing for a video call. The idea is that |
| // upon the first ACTIVE_FOCUS switch (CONNECTING/DIALING for outgoing calls and ACTIVE for |
| // incoming calls), we should trigger a recalculation if all the following conditions |
| // are met: |
| // (1) The call is a video call |
| // (2) We're processing the first active focus switch for the foreground call. |
| // (3) The last processed request isn't a user request. |
| Call foregroundCall = mCallsManager.getForegroundCall(); |
| boolean isVideo = false; |
| boolean isActiveFocusAlreadySet = true; |
| if (foregroundCall != null) { |
| isVideo = VideoProfile.isVideo(foregroundCall.getVideoState()); |
| if (mCallsActiveFocusSwitch.containsKey(foregroundCall)) { |
| isActiveFocusAlreadySet = mCallsActiveFocusSwitch.get(foregroundCall); |
| if (!isActiveFocusAlreadySet) { |
| mCallsActiveFocusSwitch.put(foregroundCall, true); |
| } |
| } |
| } |
| Log.i(this, "shouldHandleRouteForVideoCall: fgCall=%s, isVideo=%b, " |
| + "isActiveFocusAlreadySet=%b, " + "mIsLastRequestedRouteUserSwitch=%b", |
| foregroundCall, isVideo, isActiveFocusAlreadySet, mIsLastRequestedRouteUserSwitch); |
| return isVideo && !isActiveFocusAlreadySet && !mIsLastRequestedRouteUserSwitch; |
| } |
| |
| private void preHandleMessage(Message msg) { |
| if (msg.obj instanceof SomeArgs) { |
| Session session = (Session) ((SomeArgs) msg.obj).arg1; |
| String messageCodeName = MESSAGE_CODE_TO_NAME.get(msg.what, "unknown"); |
| Log.continueSession(session, "CARC.pM_" + messageCodeName); |
| Log.i(this, "Message received: %s=%d, arg1=%d", messageCodeName, msg.what, msg.arg1); |
| } |
| } |
| |
| private void postHandleMessage(Message msg, AudioRoute previousRoute) { |
| AudioRoute newRoute = getCurrentOrPendingRoute(); |
| if (USER_REQUESTED_MESSAGES.contains(msg.what)) { |
| mIsLastRequestedRouteUserSwitch = true; |
| } else if (!Objects.equals(previousRoute, newRoute)) { |
| // If it's not a user switch, and it resulted in the route changing, only then should we |
| // reset. Consider the following: USER_SWITCH_EARPIECE -> RINGING_FOCUS -> ACTIVE_FOCUS: |
| // We shouldn't reset on the ringing focus switch if it doesn't result in the route |
| // changing. |
| mIsLastRequestedRouteUserSwitch = false; |
| } |
| Log.endSession(); |
| if (msg.obj instanceof SomeArgs) { |
| ((SomeArgs) msg.obj).recycle(); |
| } |
| } |
| |
| public boolean isActive() { |
| return mIsActive; |
| } |
| |
| public boolean isPending() { |
| return mIsPending; |
| } |
| |
| private void routeTo(boolean isDestRouteActive, AudioRoute destRoute, boolean isUserRequest) { |
| if (destRoute == null |
| || (!destRoute.equals(mStreamingRoute) |
| && !getCallSupportedRoutes().contains(destRoute))) { |
| Log.i(this, "Ignore routing to unavailable route: %s", destRoute); |
| mMetricsController |
| .getErrorStats() |
| .log(ErrorStats.SUB_CALL_AUDIO, ErrorStats.ERROR_AUDIO_ROUTE_UNAVAILABLE); |
| return; |
| } |
| |
| if (com.android.internal.telecom.flags.Flags.callAudioRouteRf()) { |
| if (mAudioModeSession != null) { |
| // Handle the user request first |
| if (isUserRequest) { |
| Log.i(this, "routeTo: requesting route by AudioModeSession#setRequestedRoute " |
| + "with %s", destRoute); |
| mAudioModeSession.setRequestedRoute(mapTelecomRouteToAudioModeRoute(destRoute)); |
| } else if (!isDestRouteActive) { |
| Log.i(this, "routeTo: moving to inactive route, clearing request " |
| + "by AudioModeSession#setRequestedRoute with null"); |
| // This will take the default route by audio framework |
| mAudioModeSession.setRequestedRoute(null); |
| } else { |
| Log.i(this, "routeTo: skip requesting route for non-user request to %s", |
| destRoute); |
| Call foregroundCall = mCallsManager.getForegroundCall(); |
| if (foregroundCall != null) { |
| Log.i(this, "AudioModeSession#setDisplayActiveUseCase %b", |
| VideoProfile.isVideo(foregroundCall.getVideoState())); |
| mAudioModeSession.setDisplayActiveUseCase( |
| VideoProfile.isVideo(foregroundCall.getVideoState())); |
| } |
| |
| } |
| |
| mIsActive = isDestRouteActive; |
| mIsPending = true; |
| return; |
| } else { |
| // should not happen |
| Log.i(this, "no active session, skip route to " + destRoute); |
| return; |
| } |
| } |
| |
| // If another BT device connects during RINGING_FOCUS, in-band ringing will be disabled by |
| // default. In this case, we should adjust the active routing value so that we don't try |
| // to connect to the BT device as it will fail. |
| isDestRouteActive = maybeAdjustActiveRouting(destRoute, isDestRouteActive); |
| // Determine if the destination BT device's SCO is already connected. |
| boolean isScoDeviceAlreadyConnected = |
| mScoAudioConnectedDevice != null |
| && isDestRouteActive |
| && Objects.equals( |
| mScoAudioConnectedDevice, mBluetoothRoutes.get(destRoute)); |
| |
| // Determine the origin route for this audio switch. |
| AudioRoute originRoute = mIsPending ? mPendingAudioRoute.getDestRoute() : mCurrentRoute; |
| // Check if the origin BT device has already been disconnected by the stack, |
| // which can happen due to race conditions during BT device switching. |
| boolean isOriginAlreadyDisconnected = |
| mLastScoDisconnectedDevice != null |
| && Objects.equals( |
| mLastScoDisconnectedDevice, mBluetoothRoutes.get(originRoute)); |
| // The last-disconnected state is transient and only relevant for this specific |
| // routing operation. Clear it immediately after use to prevent stale state issues. |
| if (mLastScoDisconnectedDevice != null) { |
| mLastScoDisconnectedDevice = null; |
| } |
| // Decide whether to skip sending a SCO disconnect command. |
| boolean shouldAvoidBtDisconnect = |
| shouldAvoidBluetoothDisconnect( |
| isScoDeviceAlreadyConnected, isOriginAlreadyDisconnected); |
| // Override shouldAvoidBtDisconnect if we're moving to inactive routing. We will never run |
| // fully through the destination routing logic but in the original routing logic, we should |
| // ensure that we always call AudioManager#clearCommunicationDevice at the end of the call. |
| // It's possible that isOriginAlreadyDisconnected is true by the time we process the |
| // UNFOCUSED switch, in which case, we would end up not clearing the communication device. |
| if (mIsActive && !isDestRouteActive) { |
| shouldAvoidBtDisconnect = false; |
| } |
| |
| // If the destination route is already the currently reported communication device from the |
| // audio fwk, then we should only reflect the route change in the UI and not try to set/ |
| // clear the communication device. For routing centralization, we will improve this to only |
| // update the UI when the requested route change matches up. |
| boolean isDestRouteCommunicationDevice = isCurrentCommunicationDevice(destRoute); |
| if (mIsPending) { |
| if (destRoute.equals(mPendingAudioRoute.getDestRoute()) |
| && (mIsActive == isDestRouteActive)) { |
| return; |
| } |
| Log.i(this, "Override current pending route from %s(active=%b) to " |
| + "%s(active=%b). isOriginAlreadyDisconnected=[%b]", |
| mPendingAudioRoute.getDestRoute(), mIsActive, destRoute, |
| isDestRouteActive, isOriginAlreadyDisconnected); |
| |
| // Override the pending route destination. |
| mPendingAudioRoute.setOrigRoute( |
| mIsActive /* origin */, |
| mPendingAudioRoute.getDestRoute(), |
| isDestRouteActive /* dest */, |
| shouldAvoidBtDisconnect, |
| isDestRouteCommunicationDevice); |
| } else { |
| Log.i(this, "Enter pending route, orig=%s(active=%b), dest=%s(active=%b)", |
| mCurrentRoute, mIsActive, destRoute, isDestRouteActive); |
| // Set the original route for the pending state. |
| if (getCallSupportedRoutes().contains(mCurrentRoute)) { |
| mPendingAudioRoute.setOrigRoute( |
| mIsActive /* origin */, |
| mCurrentRoute, |
| isDestRouteActive /* dest */, |
| shouldAvoidBtDisconnect, |
| isDestRouteCommunicationDevice); |
| } else { |
| // Avoid waiting for messages for an unavailable route. |
| mPendingAudioRoute.setOrigRoute( |
| mIsActive /* origin */, |
| DUMMY_ROUTE, |
| isDestRouteActive /* dest */, |
| shouldAvoidBtDisconnect, |
| isDestRouteCommunicationDevice); |
| } |
| mIsPending = true; |
| } |
| boolean isMovingToActiveRouting = mIsActive != isDestRouteActive && isDestRouteActive; |
| mPendingAudioRoute.setDestRoute( |
| isDestRouteActive, |
| destRoute, |
| mBluetoothRoutes.get(destRoute), |
| shouldAvoidBtDisconnect, |
| isDestRouteCommunicationDevice, |
| isMovingToActiveRouting); |
| mIsActive = isDestRouteActive; |
| maybeClearPendingMessage(); |
| mPendingAudioRoute.evaluatePendingState(); |
| mMetricsController.getAudioRouteStats().onRouteEnter(mPendingAudioRoute); |
| } |
| |
| /** |
| * Ensure we clear the pending message if the pending destination route is pointing to the |
| * current communication device (provided by the onCommunicationDeviceChanged listener). |
| */ |
| private void maybeClearPendingMessage() { |
| AudioRoute pendingDestRoute = mPendingAudioRoute.getDestRoute(); |
| if (pendingDestRoute != null) { |
| // If the pending destination route is already the current communication device or if |
| // it's not BT and speaker, then clear all pending connecting messages (speaker + BT). |
| if (isCurrentCommunicationDevice(pendingDestRoute) || (pendingDestRoute.getType() |
| != TYPE_BLUETOOTH_SCO && pendingDestRoute.getType() != TYPE_SPEAKER)) { |
| Log.i(this, "maybeClearPendingMessage: Clearing all BT_AUDIO_CONNECTED " |
| + "+ SPEAKER_ON pending messages."); |
| // Clear all BT_AUDIO_CONNECTED messages |
| for (Pair<Integer, String> pendingMessage : mPendingAudioRoute |
| .getPendingMessages()) { |
| if (pendingMessage != null && pendingMessage.first == BT_AUDIO_CONNECTED) { |
| mPendingAudioRoute.clearPendingMessage(pendingMessage); |
| } |
| } |
| mPendingAudioRoute.clearPendingMessage(new Pair<>(SPEAKER_ON, null)); |
| } else if (pendingDestRoute.getType() == TYPE_BLUETOOTH_SCO) { |
| Log.i(this, "maybeClearPendingMessage: Clearing SPEAKER_ON pending message."); |
| // Don't wait for SPEAKER_ON msg if pending route is not SPEAKER. |
| mPendingAudioRoute.clearPendingMessage(new Pair<>(SPEAKER_ON, null)); |
| } else if (pendingDestRoute.getType() == TYPE_SPEAKER) { |
| Log.i(this, "maybeClearPendingMessage: Clearing all BT_AUDIO_CONNECTED " |
| + "pending messages."); |
| // Don't wait for BT_AUDIO_CONNECTED msgs if pending route is not BT_SCO. |
| for (Pair<Integer, String> pendingMessage : |
| mPendingAudioRoute.getPendingMessages()) { |
| if (pendingMessage != null && pendingMessage.first == BT_AUDIO_CONNECTED) { |
| mPendingAudioRoute.clearPendingMessage(pendingMessage); |
| } |
| } |
| } |
| } |
| } |
| |
| private boolean shouldAvoidBluetoothDisconnect( |
| boolean isScoDeviceAlreadyConnected, boolean isOriginAlreadyDisconnected) { |
| Log.i(this, "Evaluating BT disconnect: isScoConnected=%b, isOriginDisconnected=%b", |
| isScoDeviceAlreadyConnected, isOriginAlreadyDisconnected); |
| |
| // Avoids sending a disconnect command if the new device is already connected |
| // or if the old device has already reported its disconnection. |
| return isScoDeviceAlreadyConnected || isOriginAlreadyDisconnected; |
| } |
| |
| private boolean isCurrentCommunicationDevice(AudioRoute destRoute) { |
| AudioDeviceInfo currentCommunicationDevice = getCurrentCommunicationDevice(); |
| if (currentCommunicationDevice == null || destRoute == null) { |
| return false; |
| } |
| if (destRoute.getBluetoothAddress() != null) { |
| return Objects.equals( |
| currentCommunicationDevice.getAddress(), destRoute.getBluetoothAddress()); |
| } else { |
| int audioRouteType = getAudioType(currentCommunicationDevice); |
| return destRoute.getType() == audioRouteType; |
| } |
| } |
| |
| private void handleWiredHeadsetConnected() { |
| if (shouldIgnoreActionForCrs("handleWiredHeadsetConnected")) return; |
| |
| AudioRoute wiredHeadsetRoute = null; |
| try { |
| wiredHeadsetRoute = |
| mAudioRouteFactory.create( |
| AudioRoute.TYPE_WIRED, null, mAudioManager, mIsScoManagedByAudio); |
| } catch (IllegalArgumentException e) { |
| mMetricsController |
| .getErrorStats() |
| .log(ErrorStats.SUB_CALL_AUDIO, ErrorStats.ERROR_EXTERNAL_EXCEPTION); |
| Log.e(this, e, "Can't find available audio device info for route type:" |
| + AudioRoute.DEVICE_TYPE_STRINGS.get(AudioRoute.TYPE_WIRED)); |
| } |
| |
| if (wiredHeadsetRoute != null) { |
| updateAvailableRoutes(wiredHeadsetRoute, true); |
| updateAvailableRoutes(mEarpieceWiredRoute, false); |
| mTypeRoutes.put(AudioRoute.TYPE_WIRED, wiredHeadsetRoute); |
| mEarpieceWiredRoute = wiredHeadsetRoute; |
| routeTo(mIsActive, wiredHeadsetRoute, false); |
| onAvailableRoutesChanged(); |
| } |
| } |
| |
| public void handleWiredHeadsetDisconnected() { |
| if (shouldIgnoreActionForCrs("handleWiredHeadsetDisconnected")) return; |
| |
| // Update audio route states |
| AudioRoute wiredHeadsetRoute = mTypeRoutes.remove(AudioRoute.TYPE_WIRED); |
| if (wiredHeadsetRoute != null) { |
| updateAvailableRoutes(wiredHeadsetRoute, false); |
| mEarpieceWiredRoute = null; |
| } |
| AudioRoute earpieceRoute = null; |
| try { |
| earpieceRoute = |
| mTypeRoutes.get(AudioRoute.TYPE_EARPIECE) == null |
| ? mAudioRouteFactory.create( |
| AudioRoute.TYPE_EARPIECE, |
| null, |
| mAudioManager, |
| mIsScoManagedByAudio) |
| : mTypeRoutes.get(AudioRoute.TYPE_EARPIECE); |
| } catch (IllegalArgumentException e) { |
| mMetricsController |
| .getErrorStats() |
| .log(ErrorStats.SUB_CALL_AUDIO, ErrorStats.ERROR_EXTERNAL_EXCEPTION); |
| Log.e(this, e, "Can't find available audio device info for route type:" |
| + AudioRoute.DEVICE_TYPE_STRINGS.get(AudioRoute.TYPE_EARPIECE)); |
| } |
| if (earpieceRoute != null) { |
| updateAvailableRoutes(earpieceRoute, true); |
| mEarpieceWiredRoute = earpieceRoute; |
| // In the case that the route was never created, ensure that we update the map. |
| mTypeRoutes.putIfAbsent(AudioRoute.TYPE_EARPIECE, mEarpieceWiredRoute); |
| } |
| onAvailableRoutesChanged(); |
| |
| // Route to expected state |
| if (mCurrentRoute.equals(wiredHeadsetRoute)) { |
| // Preserve speaker routing if it was the last audio routing path when the wired headset |
| // disconnects. Ignore this special cased routing when the route isn't active |
| // (in other words, when we're not in a call). |
| AudioRoute route = mWasOnSpeaker && mIsActive && mSpeakerDockRoute != null |
| && mSpeakerDockRoute.getType() == AudioRoute.TYPE_SPEAKER |
| ? mSpeakerDockRoute |
| : getBaseRoute(true, null); |
| routeTo(mIsActive, route, false); |
| } |
| } |
| |
| private void handleDockConnected() { |
| AudioRoute dockRoute = null; |
| try { |
| dockRoute = mAudioRouteFactory.create( |
| AudioRoute.TYPE_DOCK, null, mAudioManager, mIsScoManagedByAudio); |
| } catch (IllegalArgumentException e) { |
| mMetricsController |
| .getErrorStats() |
| .log(ErrorStats.SUB_CALL_AUDIO, ErrorStats.ERROR_EXTERNAL_EXCEPTION); |
| Log.e(this, e, "Can't find available audio device info for route type:" |
| + AudioRoute.DEVICE_TYPE_STRINGS.get(AudioRoute.TYPE_WIRED)); |
| } |
| |
| if (dockRoute != null) { |
| updateAvailableRoutes(dockRoute, true); |
| updateAvailableRoutes(mSpeakerDockRoute, false); |
| mTypeRoutes.put(AudioRoute.TYPE_DOCK, dockRoute); |
| mSpeakerDockRoute = dockRoute; |
| routeTo(mIsActive, dockRoute, false); |
| onAvailableRoutesChanged(); |
| } |
| } |
| |
| public void handleDockDisconnected() { |
| // Update audio route states |
| AudioRoute dockRoute = mTypeRoutes.get(AudioRoute.TYPE_DOCK); |
| if (dockRoute != null) { |
| updateAvailableRoutes(dockRoute, false); |
| mSpeakerDockRoute = null; |
| } |
| AudioRoute speakerRoute = mTypeRoutes.get(AudioRoute.TYPE_SPEAKER); |
| if (speakerRoute != null) { |
| updateAvailableRoutes(speakerRoute, true); |
| mSpeakerDockRoute = speakerRoute; |
| } |
| onAvailableRoutesChanged(); |
| |
| // Route to baseline |
| if (isDockRoute(dockRoute)) { |
| routeTo(mIsActive, getBaseRoute(true, null), false); |
| } |
| } |
| |
| private boolean isDockRoute(AudioRoute dockRoute) { |
| return mCurrentRoute.equals(dockRoute) || mCurrentRoute.equals(mSpeakerDockRoute); |
| } |
| |
| private void handleStreamingEnabled() { |
| if (!mCurrentRoute.equals(mStreamingRoute)) { |
| routeTo(mIsActive, mStreamingRoute, false); |
| } else { |
| Log.i(this, "ignore enable streaming, already in streaming"); |
| } |
| } |
| |
| private void handleStreamingDisabled() { |
| if (mCurrentRoute.equals(mStreamingRoute)) { |
| mCurrentRoute = DUMMY_ROUTE; |
| onAvailableRoutesChanged(); |
| routeTo(mIsActive, getBaseRoute(true, null), false); |
| } else { |
| Log.i(this, "ignore disable streaming, not in streaming"); |
| } |
| } |
| |
| /** |
| * Handles the case when SCO audio is connected for the BT headset. This follows shortly after |
| * the BT device has been established as an active device (BT_ACTIVE_DEVICE_PRESENT) and doesn't |
| * apply to other BT device types. In this case, the pending audio route will process the |
| * BT_AUDIO_CONNECTED message that will trigger routing to the pending destination audio route; |
| * otherwise, routing will be ignored if there aren't pending routes to be processed. |
| * |
| * <p>Message being handled: BT_AUDIO_CONNECTED |
| */ |
| private void handleBtAudioActive(BluetoothDevice bluetoothDevice) { |
| if (shouldIgnoreActionForCrs("handleBtAudioActive")) return; |
| |
| if (mIsPending && bluetoothDevice != null) { |
| Log.i(this, "handleBtAudioActive: is pending path"); |
| AudioRoute btRoute = |
| getBluetoothRoute(TYPE_BLUETOOTH_SCO, bluetoothDevice.getAddress()); |
| // If the connected HFP device isn't the current communication device, then don't |
| // continue processing the message. We will wait until the audio fwk reports that |
| // the communication device has been updated accordingly instead of relying on |
| // what the BT broadcasts are telling us. |
| if (!isCurrentCommunicationDevice(btRoute)) { |
| Log.i(this, "handleBtAudioActive: %s is not the current " |
| + "communication device yet. Ignoring message.", btRoute); |
| return; |
| } |
| // Ensure we aren't keeping track of pending speaker off and SCO audio disconnected |
| // messages for this device if BT stack indicates that SCO audio is connected. |
| mPendingAudioRoute.clearPendingMessage( |
| new Pair<>(BT_AUDIO_DISCONNECTED, bluetoothDevice.getAddress())); |
| mPendingAudioRoute.clearPendingMessage(new Pair<>(SPEAKER_OFF, null)); |
| // Maybe turn off speaker from notification bar. This will be a no-op if the enabled |
| // status is already off. |
| mStatusBarNotifier.notifySpeakerphone(false); |
| if (Objects.equals( |
| mPendingAudioRoute.getDestRoute().getBluetoothAddress(), |
| bluetoothDevice.getAddress())) { |
| mPendingAudioRoute.onMessageReceived( |
| new Pair<>(BT_AUDIO_CONNECTED, bluetoothDevice.getAddress()), null); |
| } |
| } |
| } |
| |
| /** |
| * Handles the case when SCO audio is disconnected for the BT headset. In this case, the pending |
| * audio route will process the BT_AUDIO_DISCONNECTED message which will trigger routing to the |
| * pending destination audio route; otherwise, routing will be ignored if there aren't any |
| * pending routes to be processed. |
| * |
| * <p>Message being handled: BT_AUDIO_DISCONNECTED |
| */ |
| private void handleBtAudioInactive(BluetoothDevice bluetoothDevice) { |
| if (shouldIgnoreActionForCrs("handleBtAudioInactive")) return; |
| |
| if (mIsPending && bluetoothDevice != null) { |
| Log.i(this, "handleBtAudioInactive: is pending path"); |
| AudioRoute btRoute = |
| getBluetoothRoute(TYPE_BLUETOOTH_SCO, bluetoothDevice.getAddress()); |
| // If the disconnected HFP device isn't reflected in current communication device, |
| // then don't continue processing the disconnect message. We will wait until the |
| // audio fwk reports that the communication device has changed first instead of |
| // relying on what the BT broadcasts are telling us. |
| if (isCurrentCommunicationDevice(btRoute)) { |
| Log.i(this, "handleBtAudioInactive: %s is still the current" |
| + " communication device. Ignoring message.", btRoute); |
| return; |
| } |
| // Ensure we aren't keeping track of pending s SCO audio connected messages for this |
| // device if the BT stack has indicated that SCO audio has disconnected. |
| mPendingAudioRoute.clearPendingMessage( |
| new Pair<>(BT_AUDIO_CONNECTED, bluetoothDevice.getAddress())); |
| // Always ensure that we process the BT_AUDIO_DISCONNECTED message if the device is |
| // no longer the communication device. |
| mPendingAudioRoute.onMessageReceived( |
| new Pair<>(BT_AUDIO_DISCONNECTED, bluetoothDevice.getAddress()), null); |
| } |
| } |
| |
| /** |
| * This particular routing occurs when the BT device is trying to establish itself as a |
| * connected device (refer to BluetoothStateReceiver#handleConnectionStateChanged). The device |
| * is included as an available route and cached into the current BT routes. |
| * |
| * <p>Message being handled: BT_DEVICE_ADDED |
| */ |
| private void handleBtConnected( |
| @AudioRoute.AudioRouteType int type, BluetoothDevice bluetoothDevice) { |
| if (shouldIgnoreActionForCrs("handleBtConnected")) return; |
| |
| if (containsHearingAidPair(type, bluetoothDevice)) { |
| return; |
| } |
| |
| AudioRoute bluetoothRoute = |
| mAudioRouteFactory.create( |
| type, bluetoothDevice.getAddress(), mAudioManager, mIsScoManagedByAudio); |
| if (bluetoothRoute == null) { |
| Log.w(this, "Can't find available audio device info for route type:" |
| + AudioRoute.DEVICE_TYPE_STRINGS.get(type)); |
| } else { |
| Log.i(this, "bluetooth route added: " + bluetoothRoute); |
| updateAvailableRoutes(bluetoothRoute, true); |
| mBluetoothRoutes.put(bluetoothRoute, bluetoothDevice); |
| mMetricsController |
| .getCallEndpointStats() |
| .updateBluetoothDevices(new HashMap<>(mBluetoothRoutes)); |
| onAvailableRoutesChanged(); |
| } |
| } |
| |
| /** |
| * Handles the case when the BT device is in a disconnecting/disconnected state. In this case, |
| * the audio route for the specified device is removed from the available BT routes and the |
| * audio is routed to an available route if the current route is pointing to the device which |
| * got disconnected. |
| * |
| * <p>Message being handled: BT_DEVICE_REMOVED |
| */ |
| private void handleBtDisconnected( |
| @AudioRoute.AudioRouteType int type, BluetoothDevice bluetoothDevice) { |
| if (shouldIgnoreActionForCrs("handleBtDisconnected")) return; |
| |
| // Clean up unavailable routes |
| AudioRoute bluetoothRoute = getBluetoothRoute(type, bluetoothDevice.getAddress()); |
| // Get the potentially modified route for if a hearing aid was removed. |
| AudioRoute adjustedHaRoute = |
| maybeAdjustHearingAidRoute(type, bluetoothDevice, bluetoothRoute); |
| if (bluetoothRoute != null) { |
| // Remove the audio route for the passed in BT device |
| if (adjustedHaRoute == null) { |
| Log.i(this, "bluetooth route removed: " + bluetoothRoute); |
| mBluetoothRoutes.remove(bluetoothRoute); |
| updateAvailableRoutes(bluetoothRoute, false); |
| } else { |
| // If the route was updated for the HA case, then ensure that we update this |
| // new state in the available routes. |
| updateAvailableRoutes(adjustedHaRoute, true); |
| } |
| mMetricsController |
| .getCallEndpointStats() |
| .updateBluetoothDevices(new HashMap<>(mBluetoothRoutes)); |
| onAvailableRoutesChanged(); |
| } |
| |
| // Fallback to an available route |
| if (Objects.equals(getCurrentOrPendingRoute(), bluetoothRoute)) { |
| routeTo(mIsActive, getBaseRoute(true, null), false); |
| } |
| } |
| |
| /** |
| * This particular routing occurs when the specified bluetooth device is marked as the active |
| * device (refer to BluetoothStateReceiver#handleActiveDeviceChanged). This takes care of moving |
| * the call audio route to the bluetooth route. |
| * |
| * <p>Message being handled: BT_ACTIVE_DEVICE_PRESENT |
| */ |
| private void handleBtActiveDevicePresent( |
| @AudioRoute.AudioRouteType int type, String deviceAddress) { |
| if (shouldIgnoreActionForCrs("handleBtActiveDevicePresent")) return; |
| |
| AudioRoute bluetoothRoute = getBluetoothRoute(type, deviceAddress); |
| boolean isBtDeviceCurrentActive = |
| Objects.equals(bluetoothRoute, getArbitraryBluetoothDevice()); |
| if (bluetoothRoute != null && isBtDeviceCurrentActive) { |
| Log.i(this, "request to route to bluetooth route: %s (active=%b)", |
| bluetoothRoute, mIsActive); |
| routeTo(mIsActive, bluetoothRoute, false); |
| } else { |
| Log.i(this, "request to route to unavailable bluetooth route or the route isn't the " |
| + "currently active device - type (%s), address (%s)", type, deviceAddress); |
| } |
| } |
| |
| /** |
| * Handles routing for when the active BT device is removed for a given audio route type. In |
| * this case, the audio is routed to another available route if the current route hasn't been |
| * adjusted yet or there is a pending destination route associated with the device type that |
| * went inactive. Note that BT_DEVICE_REMOVED will be processed first in this case, which will |
| * handle removing the BT route for the device that went inactive as well as falling back to an |
| * available route. |
| * |
| * <p>Message being handled: BT_ACTIVE_DEVICE_GONE |
| */ |
| private void handleBtActiveDeviceGone(@AudioRoute.AudioRouteType int type) { |
| if (shouldIgnoreActionForCrs("handleBtActiveDeviceGone")) return; |
| |
| // Determine what the active device for the BT audio type was so that we can exclude this |
| // device from being used when calculating the base route. |
| String previouslyActiveDeviceAddress = getPreviouslyActiveBtDeviceAddress(type); |
| // It's possible that the dest route hasn't been set yet when the controller is first |
| // initialized. |
| boolean pendingRouteNeedsUpdate = |
| mPendingAudioRoute.getDestRoute() != null |
| && mPendingAudioRoute.getDestRoute().getType() == type; |
| boolean currentRouteNeedsUpdate = mCurrentRoute.getType() == type; |
| if (pendingRouteNeedsUpdate) { |
| pendingRouteNeedsUpdate = |
| mPendingAudioRoute |
| .getDestRoute() |
| .getBluetoothAddress() |
| .equals(previouslyActiveDeviceAddress); |
| } |
| if (currentRouteNeedsUpdate) { |
| currentRouteNeedsUpdate = |
| mCurrentRoute.getBluetoothAddress().equals(previouslyActiveDeviceAddress); |
| } |
| if ((mIsPending && pendingRouteNeedsUpdate) || (!mIsPending && currentRouteNeedsUpdate)) { |
| maybeDisableWasOnSpeaker(true); |
| // Fallback to an available route excluding the previously active device. |
| routeTo(mIsActive, getBaseRoute(true, previouslyActiveDeviceAddress), false); |
| } |
| } |
| |
| private void handleMuteChanged(boolean mute, boolean isExternal) { |
| mIsMute = mute; |
| // Allow the mute state to be set for an internal request even if the routing isn't |
| // active. If there are calls in progress, we should always allow the mute state to be set. |
| if (mIsMute != mAudioManager.isMicrophoneMute() && (mIsActive || !isExternal)) { |
| Context userContext = |
| mContext.createContextAsUser(mCallsManager.getCurrentUserHandle(), 0); |
| AudioManager userAudioManager = userContext.getSystemService(AudioManager.class); |
| Log.i(this, "changing microphone mute state to: %b " + "[userAudioManagerIsNull=%b]", |
| mute, userAudioManager == null); |
| if (userAudioManager != null) { |
| userAudioManager.setMicrophoneMute(mute); |
| } |
| } |
| processOnMuteStateChanged(mIsMute); |
| } |
| |
| private void handleSwitchFocus(int focus, int handleEndTone) { |
| Log.i(this, "handleSwitchFocus: focus (%s)", focus); |
| mFocusType = focus; |
| switch (focus) { |
| case NO_FOCUS -> { |
| mWasOnSpeaker = false; |
| // Reset mute state after call ends. This should remain unaffected if audio routing |
| // never went active. Only reset if there are no external calls. Ensure that this |
| // conditional gate only applies to watches, otherwise, always reset as per existing |
| // behavior. |
| if (!mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH) |
| || !mCallsManager.hasExternalCalls()) { |
| handleMuteChanged(false, false /* isExternal */); |
| } |
| // Ensure we reset call audio state at the end of the call (i.e. if we're on |
| // speaker, route back to earpiece). If we're on BT, remain on BT if it's still |
| // connected. |
| AudioRoute route = calculateBaselineRoute(false, true, null); |
| routeTo(false, route, false); |
| // Clear pending messages |
| mPendingAudioRoute.clearPendingMessages(); |
| clearRingingBluetoothAddress(); |
| mUsePreferredDeviceStrategy = true; |
| if (com.android.internal.telecom.flags.Flags.callAudioRouteRf() |
| && !mCallsManager.hasAnyCalls()) { |
| closeAudioModeSession(); |
| } |
| // Notify the CallAudioModeStateMachine that audio operations are complete so |
| // that we can relinquish audio focus. |
| mCallAudioManager.notifyAudioOperationsComplete(); |
| } |
| case ACTIVE_FOCUS -> { |
| // Route to the current route if routing was already active. This should |
| // preserve the audio routing state when a call is held/unheld. Otherwise, we |
| // should calculate the base routing. |
| boolean useRingingBluetoothDevice = mBluetoothAddressForRinging != null |
| && mBluetoothAddressForRinging.equals( |
| mCurrentRoute.getBluetoothAddress()); |
| AudioRoute route = useRingingBluetoothDevice |
| ? mCurrentRoute |
| : getBaseRoute(true, null); |
| // Unless the preferred device selection (reported by the audio fwk) is set or the |
| // initial audio routing needs to be explicitly handled for video calls, the route |
| // should remain on the current route. We don't want to override the selection if |
| // the user had explicitly requested route. |
| if (!isPreferredDeviceSet() && !shouldHandleRouteForVideoCall()) { |
| Log.i(this, "Using current route for audio"); |
| route = getCurrentOrPendingRoute(); |
| } |
| routeTo(true, route, false); |
| // Once we have processed active focus once during the call, we can ignore |
| // using the preferred device strategy. |
| mUsePreferredDeviceStrategy = false; |
| clearRingingBluetoothAddress(); |
| } |
| case RINGING_FOCUS -> { |
| if (!mIsActive) { |
| AudioRoute route = getBaseRoute(true, null); |
| // Use the current route for handling ringing focus when the flag is enabled |
| // unless the preferred device route is set as indicated by the audio fwk. We |
| // don't want to override this selection if the user had set a default audio |
| // route for calls. |
| if (!isPreferredDeviceSet()) { |
| route = getCurrentOrPendingRoute(); |
| } |
| BluetoothDevice device = mBluetoothRoutes.get(route); |
| // Check if in-band ringtone is enabled for the device; if it isn't, move to |
| // inactive route. |
| if (device != null |
| && !mBluetoothRouteManager.isInbandRingEnabled( |
| route.getType(), device)) { |
| routeTo(false, route, false); |
| } else { |
| routeTo(true, route, false); |
| } |
| } else { |
| // Route is already active. |
| BluetoothDevice device = mBluetoothRoutes.get(mCurrentRoute); |
| if (device != null |
| && !mBluetoothRouteManager.isInbandRingEnabled( |
| mCurrentRoute.getType(), device)) { |
| routeTo(false, mCurrentRoute, false); |
| } |
| } |
| } |
| } |
| } |
| |
| public void handleSwitchEarpiece(boolean isUserRequest) { |
| AudioRoute earpieceRoute = mTypeRoutes.get(AudioRoute.TYPE_EARPIECE); |
| if (earpieceRoute != null && getCallSupportedRoutes().contains(earpieceRoute)) { |
| maybeDisableWasOnSpeaker(isUserRequest); |
| routeTo(mIsActive, earpieceRoute, isUserRequest); |
| } else { |
| Log.i(this, "ignore switch earpiece request"); |
| } |
| } |
| |
| private void handleSwitchBluetooth(String address, boolean isUserRequest) { |
| Log.i(this, "handle switch to bluetooth with address %s", address); |
| if (shouldIgnoreActionForCrs("handleSwitchBluetooth")) return; |
| |
| AudioRoute bluetoothRoute = null; |
| BluetoothDevice bluetoothDevice = null; |
| if (address == null) { |
| bluetoothRoute = getArbitraryBluetoothDevice(); |
| bluetoothDevice = mBluetoothRoutes.get(bluetoothRoute); |
| } else { |
| for (AudioRoute route : getCallSupportedRoutes()) { |
| if (Objects.equals(address, route.getBluetoothAddress())) { |
| bluetoothRoute = route; |
| bluetoothDevice = mBluetoothRoutes.get(route); |
| break; |
| } |
| } |
| } |
| |
| if (bluetoothRoute != null && bluetoothDevice != null) { |
| maybeDisableWasOnSpeaker(isUserRequest); |
| if (mFocusType == RINGING_FOCUS) { |
| routeTo( |
| mBluetoothRouteManager.isInbandRingEnabled( |
| bluetoothRoute.getType(), bluetoothDevice) |
| && mIsActive, |
| bluetoothRoute, |
| isUserRequest); |
| mBluetoothAddressForRinging = bluetoothDevice.getAddress(); |
| } else { |
| routeTo(mIsActive, bluetoothRoute, isUserRequest); |
| } |
| } else { |
| Log.i(this, "ignore switch bluetooth request to unavailable address"); |
| } |
| } |
| |
| /** |
| * Retrieve the active BT device, if available, otherwise return the most recently tracked |
| * active device, or null if none are available. |
| * |
| * @return {@link AudioRoute} of the BT device. |
| */ |
| private AudioRoute getArbitraryBluetoothDevice() { |
| synchronized (mLock) { |
| if (mActiveBluetoothDevice != null) { |
| return getBluetoothRoute( |
| mActiveBluetoothDevice.first, mActiveBluetoothDevice.second); |
| } else if (!mBluetoothRoutes.isEmpty()) { |
| return mBluetoothRoutes.keySet().stream().toList().get(mBluetoothRoutes.size() - 1); |
| } |
| return null; |
| } |
| } |
| |
| private void handleSwitchHeadset(boolean isUserRequest) { |
| if (shouldIgnoreActionForCrs("handleSwitchHeadset")) return; |
| AudioRoute headsetRoute = mTypeRoutes.get(AudioRoute.TYPE_WIRED); |
| if (headsetRoute != null && getCallSupportedRoutes().contains(headsetRoute)) { |
| maybeDisableWasOnSpeaker(isUserRequest); |
| routeTo(mIsActive, headsetRoute, isUserRequest); |
| } else { |
| Log.i(this, "ignore switch headset request"); |
| } |
| } |
| |
| private void handleSwitchSpeaker(boolean isUserRequest) { |
| if (mSpeakerDockRoute != null |
| && getCallSupportedRoutes().contains(mSpeakerDockRoute) |
| && mSpeakerDockRoute.getType() == AudioRoute.TYPE_SPEAKER) { |
| routeTo(mIsActive, mSpeakerDockRoute, isUserRequest); |
| } else { |
| Log.i(this, "ignore switch speaker request"); |
| } |
| } |
| |
| private void handleSwitchBaselineRoute( |
| boolean isExplicitUserRequest, boolean includeBluetooth, String btAddressToExclude) { |
| Log.i(this, "handleSwitchBaselineRoute: includeBluetooth: %b, " + "btAddressToExclude: %s", |
| includeBluetooth, btAddressToExclude); |
| AudioRoute pendingDestRoute = mPendingAudioRoute.getDestRoute(); |
| boolean areExcludedBtAndDestBtSame = |
| btAddressToExclude != null |
| && pendingDestRoute != null |
| && Objects.equals( |
| btAddressToExclude, pendingDestRoute.getBluetoothAddress()); |
| Pair<Integer, String> btDevicePendingMsg = |
| new Pair<>(BT_AUDIO_CONNECTED, btAddressToExclude); |
| |
| // If SCO is once again connected or there's a pending message for BT_AUDIO_CONNECTED, then |
| // we know that the device has reconnected or is in the middle of connecting. Ignore routing |
| // out of this BT device. |
| boolean isExcludedDeviceConnectingOrConnected = |
| areExcludedBtAndDestBtSame |
| && (Objects.equals( |
| mBluetoothRoutes.get(pendingDestRoute), |
| mScoAudioConnectedDevice) |
| || mPendingAudioRoute |
| .getPendingMessages() |
| .contains(btDevicePendingMsg)); |
| // Check if the pending audio route or current route is already different from the route |
| // including the BT device that should be excluded from route selection. |
| boolean isCurrentOrDestRouteDifferent = |
| btAddressToExclude != null |
| && ((mIsPending |
| && !btAddressToExclude.equals( |
| mPendingAudioRoute |
| .getDestRoute() |
| .getBluetoothAddress())) |
| || (!mIsPending |
| && !btAddressToExclude.equals( |
| mCurrentRoute.getBluetoothAddress()))); |
| if (isExcludedDeviceConnectingOrConnected) { |
| Log.i(this, "BT device with address (%s) is currently connecting/connected. " |
| + "Ignoring route switch.", btAddressToExclude); |
| return; |
| } else if (isCurrentOrDestRouteDifferent) { |
| Log.i(this, "Current or pending audio route isn't routed to device with address " |
| + "(%s). Ignoring route switch.", btAddressToExclude); |
| return; |
| } |
| maybeDisableWasOnSpeaker(isExplicitUserRequest); |
| routeTo(mIsActive, |
| calculateBaselineRoute(isExplicitUserRequest, includeBluetooth, btAddressToExclude), |
| isExplicitUserRequest); |
| } |
| |
| private void handleSpeakerOn() { |
| if (isPending()) { |
| Log.i(this, "handleSpeakerOn: sending SPEAKER_ON to pending audio route"); |
| // Clear any pending speaker off message as the speaker has been explicitly turned on as |
| // indicated by the audio fwk. |
| mPendingAudioRoute.clearPendingMessage(new Pair<>(SPEAKER_OFF, null)); |
| // Clear any pending BT_AUDIO_DISCONNECTED messages for connected BT devices if speaker |
| // has explicitly been turned on. |
| for (BluetoothDevice device : mBluetoothRoutes.values()) { |
| mPendingAudioRoute.clearPendingMessage( |
| new Pair<>(BT_AUDIO_DISCONNECTED, device.getAddress())); |
| } |
| mPendingAudioRoute.onMessageReceived(new Pair<>(SPEAKER_ON, null), null); |
| // Update status bar notification if we are in a call. |
| mStatusBarNotifier.notifySpeakerphone(mCallsManager.hasAnyCalls()); |
| } else { |
| if (mSpeakerDockRoute != null |
| && getCallSupportedRoutes().contains(mSpeakerDockRoute) |
| && mSpeakerDockRoute.getType() == AudioRoute.TYPE_SPEAKER |
| && mCurrentRoute.getType() != AudioRoute.TYPE_SPEAKER) { |
| // This path should not be hit. This means that some other application manipulated |
| // the audio routing to turn speaker on. Telecom will always attempt to route to |
| // speaker via USER_SWITCH_SPEAKER and change the routing, which would create a |
| // pending route change. Generate an anomaly report if this happens. |
| mAnomalyReporterAdapter.reportAnomaly( |
| AUDIO_ROUTING_EXTERNAL_CHANGE_UUID, AUDIO_ROUTING_EXTERNAL_CHANGE_MSG); |
| routeTo(mIsActive, mSpeakerDockRoute, false); |
| // Since the route switching triggered by this message, we need to manually send it |
| // again so that we won't stuck in the pending route |
| if (mIsActive) { |
| sendMessageWithSessionInfo(SPEAKER_ON); |
| } |
| } |
| } |
| } |
| |
| private void handleSpeakerOff() { |
| if (isPending()) { |
| Log.i(this, "handleSpeakerOff - sending SPEAKER_OFF to pending audio route"); |
| // Clear any pending speaker on message as the speaker has been explicitly turned off as |
| // indicated by the audio fwk. |
| mPendingAudioRoute.clearPendingMessage(new Pair<>(SPEAKER_ON, null)); |
| mPendingAudioRoute.onMessageReceived(new Pair<>(SPEAKER_OFF, null), null); |
| // Update status bar notification |
| mStatusBarNotifier.notifySpeakerphone(false); |
| } else if (mCurrentRoute.getType() == AudioRoute.TYPE_SPEAKER) { |
| // This path should not be hit. This means that some other application manipulated the |
| // audio routing to turn speaker off. Telecom will always attempt to route out of |
| // speaker via user switches (I.e. USER_SWITCH_EARPIECE) and change the routing, which |
| // would create a pending route change. Generate an anomaly report if this happens. |
| mAnomalyReporterAdapter.reportAnomaly( |
| AUDIO_ROUTING_EXTERNAL_CHANGE_UUID, AUDIO_ROUTING_EXTERNAL_CHANGE_MSG); |
| AudioRoute newRoute = getBaseRoute(true, null); |
| // Route to whatever the current communication device is as reported by the audio fwk. |
| AudioRoute communicationDeviceRoute = |
| getAudioRouteForAudioDeviceInfo(getCurrentCommunicationDevice()); |
| if (isValidRoute(communicationDeviceRoute)) { |
| newRoute = communicationDeviceRoute; |
| } |
| routeTo(mIsActive, newRoute, false); |
| // Since the route switching triggered by this message, we need to manually send it |
| // again so that we won't stuck in the pending route. Do not send the additional |
| // SPEAKER_OFF msg if we find that audio wasn't routed out of speaker. This would have |
| // the potential to cause an infinite loop if routing doesn't change. |
| if (mIsActive && newRoute.getType() != TYPE_SPEAKER) { |
| sendMessageWithSessionInfo(SPEAKER_OFF); |
| } |
| onAvailableRoutesChanged(); |
| } |
| } |
| |
| /** |
| * This is invoked when there are no more pending audio routes to be processed, which signals a |
| * change for the current audio route and the call audio state to be updated accordingly. |
| */ |
| public void handleExitPendingRoute() { |
| if (shouldIgnoreActionForCrs("handleExitPendingRoute")) return; |
| |
| if (mIsPending) { |
| mCurrentRoute = mPendingAudioRoute.getDestRoute(); |
| Log.addEvent(mCallsManager.getForegroundCall(), LogUtils.Events.AUDIO_ROUTE, |
| "Entering audio route: " + mCurrentRoute + " (active=" + mIsActive + ")"); |
| mIsPending = false; |
| mPendingAudioRoute.clearPendingMessages(); |
| onCurrentRouteChanged(); |
| if (mIsActive) { |
| // Only set mWasOnSpeaker if the routing was active. We don't want to consider this |
| // selection outside of a call. |
| if (mCurrentRoute.getType() == TYPE_SPEAKER) { |
| mWasOnSpeaker = true; |
| } |
| // Reinitialize the audio ops complete latch since the routing went active. We |
| // should always expect operations to complete after this point. |
| if (mAudioOperationsCompleteLatch.getCount() == 0) { |
| mAudioOperationsCompleteLatch = new CountDownLatch(1); |
| } |
| mAudioActiveCompleteLatch.countDown(); |
| } else { |
| // Reinitialize the active routing latch when audio ops are complete so that it can |
| // once again be processed when a new call is placed/received. |
| if (mAudioActiveCompleteLatch.getCount() == 0) { |
| mAudioActiveCompleteLatch = new CountDownLatch(1); |
| } |
| mAudioOperationsCompleteLatch.countDown(); |
| } |
| mMetricsController.getAudioRouteStats().onRouteExit(mPendingAudioRoute, true); |
| } |
| } |
| |
| private void onCurrentRouteChanged() { |
| synchronized (mLock) { |
| BluetoothDevice activeBluetoothDevice = null; |
| int route = ROUTE_MAP.get(mCurrentRoute.getType()); |
| if (route == CallAudioState.ROUTE_STREAMING) { |
| updateCallAudioState(new CallAudioState(mIsMute, route, route)); |
| return; |
| } |
| if (route == CallAudioState.ROUTE_BLUETOOTH) { |
| activeBluetoothDevice = mBluetoothRoutes.get(mCurrentRoute); |
| } |
| updateCallAudioState( |
| new CallAudioState( |
| mIsMute, |
| route, |
| mCallAudioState.getRawSupportedRouteMask(), |
| activeBluetoothDevice, |
| mCallAudioState.getSupportedBluetoothDevices())); |
| } |
| } |
| |
| private void onAvailableRoutesChanged() { |
| synchronized (mLock) { |
| int routeMask = 0; |
| Set<BluetoothDevice> availableBluetoothDevices = new HashSet<>(); |
| boolean isCurrentRouteOnHa = getCurrentRoute().getType() == TYPE_BLUETOOTH_HA; |
| BluetoothDevice haDevice = null; |
| for (AudioRoute route : getCallSupportedRoutes()) { |
| routeMask |= ROUTE_MAP.get(route.getType()); |
| if (BT_AUDIO_ROUTE_TYPES.contains(route.getType())) { |
| BluetoothDevice deviceToAdd = mBluetoothRoutes.get(route); |
| // Only include the lead device for LE audio (otherwise, the routes will show |
| // two separate devices in the UI). |
| if (deviceToAdd != null |
| && route.getType() == AudioRoute.TYPE_BLUETOOTH_LE |
| && getLeAudioService() != null) { |
| int groupId = getLeAudioService().getGroupId(deviceToAdd); |
| if (groupId != BluetoothLeAudio.GROUP_ID_INVALID) { |
| deviceToAdd = getLeAudioService().getConnectedGroupLeadDevice(groupId); |
| } |
| } |
| // This will only ever be null when the lead device (LE) is disconnected and |
| // try to obtain the lead device for the 2nd bud. |
| if (deviceToAdd != null) { |
| availableBluetoothDevices.add(deviceToAdd); |
| if (route.getType() == TYPE_BLUETOOTH_HA) { |
| haDevice = deviceToAdd; |
| } |
| } |
| } |
| } |
| // We may have to change which active device is displayed if a hearing aid pair was |
| // removed and replaced as the main active device. |
| BluetoothDevice activeDevice = mCallAudioState.getActiveBluetoothDevice(); |
| if (isCurrentRouteOnHa |
| && activeDevice != null |
| && haDevice != null |
| && !Objects.equals(haDevice.getAddress(), activeDevice.getAddress())) { |
| activeDevice = haDevice; |
| } |
| |
| updateCallAudioState( |
| new CallAudioState( |
| mIsMute, |
| mCallAudioState.getRoute(), |
| routeMask, |
| activeDevice, |
| availableBluetoothDevices)); |
| } |
| } |
| |
| private void processOnMuteStateChanged(boolean mute) { |
| updateCallAudioState(new CallAudioState(mute, mCallAudioState.getRoute(), |
| mCallAudioState.getSupportedRouteMask(), mCallAudioState.getActiveBluetoothDevice(), |
| mCallAudioState.getSupportedBluetoothDevices())); |
| } |
| |
| /** |
| * Retrieves the current call's supported audio route and adjusts the audio routing if the |
| * current route isn't supported. |
| */ |
| private void updateRouteForForeground() { |
| boolean updatedRouteForCall = updateCallSupportedAudioRoutes(); |
| // Ensure that current call audio state has updated routes for current call. |
| if (updatedRouteForCall) { |
| mCallAudioState = |
| new CallAudioState( |
| mIsMute, |
| mCallAudioState.getRoute(), |
| mCallSupportedRouteMask, |
| mCallAudioState.getActiveBluetoothDevice(), |
| mCallAudioState.getSupportedBluetoothDevices()); |
| // Update audio route when the foreground call changes. This ensures, for example, that |
| // if we have an ongoing video call + new PSTN call that the PSTN call doesn't get |
| // placed on the speaker. Don't recalculate the route if the foreground call is not |
| // defined (e.g. when a call is disconnected). This ensures that we don't change the |
| // route when playing the disconnect tone. |
| // Todo: b/455395635 - Routing recalculation should be based on phone accounts. We |
| // should only update the route for calls on different phone accounts. |
| routeTo(mIsActive, getBaseRoute(true, null), false); |
| } |
| } |
| |
| /** Update supported audio routes for the foreground call if present. */ |
| private boolean updateCallSupportedAudioRoutes() { |
| int availableRouteMask = 0; |
| Call foregroundCall = mCallsManager.getForegroundCall(); |
| mCallSupportedRoutes.clear(); |
| if (foregroundCall != null) { |
| int foregroundCallSupportedRouteMask = foregroundCall.getSupportedAudioRoutes(); |
| for (AudioRoute route : getAvailableRoutes()) { |
| int routeType = ROUTE_MAP.get(route.getType()); |
| availableRouteMask |= routeType; |
| if ((routeType & foregroundCallSupportedRouteMask) == routeType) { |
| mCallSupportedRoutes.add(route); |
| } |
| } |
| mCallSupportedRouteMask = availableRouteMask & foregroundCallSupportedRouteMask; |
| return true; |
| } else { |
| mCallSupportedRouteMask = -1; |
| return false; |
| } |
| } |
| |
| private void updateCallAudioState(CallAudioState newCallAudioState) { |
| synchronized (mTelecomLock) { |
| Log.i(this, "updateCallAudioState: updating call audio state to %s", newCallAudioState); |
| CallAudioState oldState = mCallAudioState; |
| mCallAudioState = newCallAudioState; |
| // Update status bar notification |
| mStatusBarNotifier.notifyMute(newCallAudioState.isMuted()); |
| mCallsManager.onCallAudioStateChanged(oldState, mCallAudioState); |
| updateAudioStateForTrackedCalls(mCallAudioState); |
| } |
| } |
| |
| private void updateAudioStateForTrackedCalls(CallAudioState newCallAudioState) { |
| List<Call> calls = new ArrayList<>(mCallsManager.getTrackedCalls()); |
| for (Call call : calls) { |
| if (call != null && call.getConnectionService() != null) { |
| call.getConnectionService().onCallAudioStateChanged(call, newCallAudioState); |
| } |
| } |
| } |
| |
| private AudioRoute getPreferredDeviceAudioRoute(AudioDeviceAttributes deviceAttr) { |
| Log.i(this, "getPreferredAudioRouteFromStrategy: preferred device is %s", deviceAttr); |
| if (deviceAttr == null) { |
| return DUMMY_ROUTE; |
| } |
| |
| // Get corresponding audio route |
| @AudioRoute.AudioRouteType |
| int type = DEVICE_INFO_TYPE_TO_AUDIO_ROUTE_TYPE.get(deviceAttr.getType()); |
| AudioDeviceInfo currentCommunicationDevice = getCurrentCommunicationDevice(); |
| // We will default to TYPE_INVALID if the currentCommunicationDevice is null or the type |
| // cannot be resolved from the given audio device info. |
| int communicationDeviceAudioType = getAudioType(currentCommunicationDevice); |
| // Sync the preferred device strategy with the current communication device if there's a |
| // valid audio device output set as the preferred device strategy. This will address timing |
| // issues between updates made to the preferred device strategy. From the audio fwk |
| // standpoint, updates to the communication device take precedent to changes in the |
| // preferred device strategy so the former should be used as the source of truth. |
| if (type != TYPE_INVALID |
| && communicationDeviceAudioType != TYPE_INVALID |
| && communicationDeviceAudioType != type) { |
| type = communicationDeviceAudioType; |
| } |
| if (BT_AUDIO_ROUTE_TYPES.contains(type)) { |
| return getBluetoothRoute(type, deviceAttr.getAddress()); |
| } else { |
| return mTypeRoutes.get(type); |
| } |
| } |
| |
| private AudioRoute getPreferredAudioRouteFromDefault( |
| boolean isExplicitUserRequest, boolean includeBluetooth, String btAddressToExclude) { |
| if (shouldIgnoreActionForCrs("getPreferredAudioRouteFromDefault")) return mSpeakerDockRoute; |
| boolean skipEarpiece = false; |
| Call foregroundCall = mCallAudioManager.getForegroundCall(); |
| if (!isExplicitUserRequest) { |
| synchronized (mTelecomLock) { |
| skipEarpiece = |
| foregroundCall != null |
| && foregroundCall.isActiveFocus() |
| && VideoProfile.isVideo(foregroundCall.getVideoState()); |
| Log.i(this, "skipEarpiece for video call?" + skipEarpiece); |
| } |
| } |
| // Route to earpiece, wired, or speaker route if there are not bluetooth routes or if there |
| // are only wearables available. |
| AudioRoute activeWatchOrNonWatchDeviceRoute = |
| getActiveWatchOrNonWatchDeviceRoute(btAddressToExclude); |
| if ((!mCallSupportedRoutes.isEmpty() |
| && (mCallSupportedRouteMask & CallAudioState.ROUTE_BLUETOOTH) == 0) |
| || mBluetoothRoutes.isEmpty() |
| || !includeBluetooth |
| || activeWatchOrNonWatchDeviceRoute == null) { |
| Log.i(this, "getPreferredAudioRouteFromDefault: Audio routing defaulting to " |
| + "available non-BT route."); |
| boolean callSupportsEarpieceWiredRoute = |
| mCallSupportedRoutes.isEmpty() |
| || mCallSupportedRoutes.contains(mEarpieceWiredRoute); |
| // If call supported route doesn't contain earpiece/wired/BT, it should have speaker |
| // enabled. Otherwise, no routes would be supported for the call which should never be |
| // the case. |
| AudioRoute defaultRoute = |
| mEarpieceWiredRoute != null && callSupportsEarpieceWiredRoute |
| ? mEarpieceWiredRoute |
| : mSpeakerDockRoute; |
| if (skipEarpiece |
| && defaultRoute != null |
| && defaultRoute.getType() == AudioRoute.TYPE_EARPIECE) { |
| Log.i(this, "getPreferredAudioRouteFromDefault: Audio routing defaulting to " |
| + "speaker route for (video) call."); |
| defaultRoute = mSpeakerDockRoute; |
| } |
| return defaultRoute; |
| } else { |
| // Most recent active route will always be the last in the array (ensure that we don't |
| // auto route to a wearable device unless it's already active). |
| String autoRoutingToWatchExcerpt = " (except inactive watch)"; |
| Log.i(this, "getPreferredAudioRouteFromDefault: Audio routing defaulting to " |
| + "most recently active BT route" + autoRoutingToWatchExcerpt + "."); |
| return activeWatchOrNonWatchDeviceRoute; |
| } |
| } |
| |
| private boolean shouldIgnoreActionForCrs(String actionDescription) { |
| if (mCallAudioManager == null) { |
| Log.i(this, "CallAudioManager is null"); |
| return false; |
| } |
| CrsAudioController crsAudioController = mCallAudioManager.getCrsAudioController(); |
| if (crsAudioController == null) { |
| Log.d(this, "crsAudioController is null"); |
| return false; |
| } |
| |
| final boolean isCrsControlledByHal = crsAudioController.shouldControlCrsWithParameters(); |
| final boolean isCrsModeActive = mCallAudioManager.isCrsInCallMode(); |
| |
| if (isCrsModeActive && !isCrsControlledByHal) { |
| Log.i(this, "Ignoring %s. Not allowed during CRS call.", actionDescription); |
| return true; |
| } |
| return false; |
| } |
| |
| private int calculateSupportedRouteMaskInit() { |
| Log.i(this, "calculateSupportedRouteMaskInit: is wired headset plugged in - %s", |
| mWiredHeadsetManager.isPluggedIn()); |
| int routeMask = CallAudioState.ROUTE_SPEAKER; |
| |
| if (mWiredHeadsetManager.isPluggedIn()) { |
| routeMask |= CallAudioState.ROUTE_WIRED_HEADSET; |
| } else { |
| AudioDeviceInfo[] deviceList = |
| mAudioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS); |
| // For debugging purposes in cases where the device list returned by the API fwk is |
| // empty and we don't end up adding the earpiece route upon init. |
| Log.i(this, "calculateSupportedRouteMaskInit: is device list size: %d", |
| deviceList.length); |
| for (AudioDeviceInfo device : deviceList) { |
| Log.i(this, "calculateSupportedRouteMaskInit: audio route type from audio " |
| + "device info: %d", device != null |
| ? DEVICE_INFO_TYPE_TO_AUDIO_ROUTE_TYPE.getOrDefault( |
| device.getType(), TYPE_INVALID) |
| : TYPE_INVALID); |
| if (device != null && device.getType() == AudioDeviceInfo.TYPE_BUILTIN_EARPIECE) { |
| routeMask |= CallAudioState.ROUTE_EARPIECE; |
| break; |
| } |
| } |
| } |
| return routeMask; |
| } |
| |
| @VisibleForTesting |
| public Set<AudioRoute> getAvailableRoutes() { |
| if (mCurrentRoute.equals(mStreamingRoute)) { |
| return mStreamingRoutes; |
| } else { |
| return mAvailableRoutes; |
| } |
| } |
| |
| public Set<AudioRoute> getCallSupportedRoutes() { |
| if (mCurrentRoute.equals(mStreamingRoute)) { |
| return mStreamingRoutes; |
| } else { |
| if (mAvailableRoutesUpdated) { |
| updateCallSupportedAudioRoutes(); |
| mAvailableRoutesUpdated = false; |
| } |
| return mCallSupportedRoutes.isEmpty() ? mAvailableRoutes : mCallSupportedRoutes; |
| } |
| } |
| |
| /* Only used for testing purposes */ |
| @VisibleForTesting |
| public AudioRoute getCurrentRoute() { |
| return mCurrentRoute; |
| } |
| |
| /** |
| * This should be used to determine what the most up to date route is. This is either the |
| * current route or the pending destination route if there's a pending audio route change. |
| */ |
| public AudioRoute getCurrentOrPendingRoute() { |
| return mIsPending ? mPendingAudioRoute.getDestRoute() : mCurrentRoute; |
| } |
| |
| public AudioRoute getBluetoothRoute( |
| @AudioRoute.AudioRouteType int audioRouteType, String address) { |
| // Don't proceed if the passed in address is null. Every BT route should be pointing to a |
| // valid address. |
| if (address == null) { |
| return null; |
| } |
| for (AudioRoute route : mBluetoothRoutes.keySet()) { |
| boolean checkHearingAidPair = |
| audioRouteType == AudioRoute.TYPE_BLUETOOTH_HA |
| && route.getBluetoothHaPairDevice() != null |
| && Objects.equals( |
| address, route.getBluetoothHaPairDevice().getAddress()); |
| if (route.getType() == audioRouteType |
| && (route.getBluetoothAddress().equals(address) || checkHearingAidPair)) { |
| return route; |
| } |
| } |
| return null; |
| } |
| |
| public AudioRoute getBaseRoute(boolean includeBluetooth, String btAddressToExclude) { |
| // Catch-all case for all invocations to this method where we shouldn't be using |
| // getPreferredAudioRouteFromStrategy |
| if (!mUsePreferredDeviceStrategy) { |
| return calculateBaselineRoute(false, includeBluetooth, btAddressToExclude); |
| } |
| // Get the preferred device value cached from the listener |
| AudioRoute destRoute = getPreferredDeviceRoute(); |
| boolean isPreferredDeviceSet = isPreferredDeviceSet(); |
| if (!isPreferredDeviceSet) { |
| Log.i(this, "getBaseRoute: preferred audio route is not reported by " |
| + "AudioManager; telecom to determine"); |
| } else { |
| Log.i(this, "getBaseRoute: preferred audio route is %s", destRoute); |
| } |
| if (!isPreferredDeviceSet |
| || (destRoute.getBluetoothAddress() != null |
| && (!includeBluetooth |
| || destRoute.getBluetoothAddress().equals(btAddressToExclude)))) { |
| destRoute = |
| getPreferredAudioRouteFromDefault(false, includeBluetooth, btAddressToExclude); |
| } |
| if (destRoute != null && !getCallSupportedRoutes().contains(destRoute)) { |
| destRoute = null; |
| } |
| Log.i(this, "getBaseRoute - audio routing to %s", destRoute); |
| return destRoute; |
| } |
| |
| private AudioRoute calculateBaselineRoute( |
| boolean isExplicitUserRequest, boolean includeBluetooth, String btAddressToExclude) { |
| AudioRoute destRoute = |
| getPreferredAudioRouteFromDefault( |
| isExplicitUserRequest, includeBluetooth, btAddressToExclude); |
| if (destRoute != null && !getCallSupportedRoutes().contains(destRoute)) { |
| destRoute = null; |
| } |
| Log.i(this, "getBaseRoute - audio routing to %s", destRoute); |
| return destRoute; |
| } |
| |
| /** |
| * Don't add additional AudioRoute when a hearing aid pair is detected. The devices have |
| * separate addresses, so we need to perform explicit handling to ensure we don't treat them as |
| * two separate devices. |
| */ |
| private boolean containsHearingAidPair( |
| @AudioRoute.AudioRouteType int type, BluetoothDevice bluetoothDevice) { |
| // Check if it is a hearing aid pair and skip connecting to the other device in this case. |
| // Traverse mBluetoothRoutes backwards as the most recently active device will be inserted |
| // last. |
| String existingHearingAidAddress = null; |
| AudioRoute existingHearingAidRoute = null; |
| List<AudioRoute> bluetoothRoutes = mBluetoothRoutes.keySet().stream().toList(); |
| for (int i = bluetoothRoutes.size() - 1; i >= 0; i--) { |
| AudioRoute audioRoute = bluetoothRoutes.get(i); |
| if (audioRoute.getType() == AudioRoute.TYPE_BLUETOOTH_HA) { |
| existingHearingAidRoute = audioRoute; |
| existingHearingAidAddress = audioRoute.getBluetoothAddress(); |
| break; |
| } |
| } |
| |
| // Check that route is for hearing aid and that there exists another hearing aid route |
| // created for the first device (of the pair) that was connected. |
| if (type == AudioRoute.TYPE_BLUETOOTH_HA && existingHearingAidAddress != null) { |
| BluetoothAdapter bluetoothAdapter = |
| mBluetoothRouteManager.getDeviceManager().getBluetoothAdapter(); |
| if (bluetoothAdapter != null) { |
| List<BluetoothDevice> activeHearingAids = |
| bluetoothAdapter.getActiveDevices(BluetoothProfile.HEARING_AID); |
| for (BluetoothDevice hearingAid : activeHearingAids) { |
| if (hearingAid != null && hearingAid.getAddress() != null) { |
| String address = hearingAid.getAddress(); |
| if (address.equals(bluetoothDevice.getAddress()) |
| || address.equals(existingHearingAidAddress)) { |
| Log.i(this, "containsHearingAidPair: Detected a hearing aid " |
| + "pair, ignoring creating a new AudioRoute."); |
| // Track the pair as part of the existing HA audio route. |
| trackHearingAidPair(existingHearingAidRoute, bluetoothDevice); |
| return true; |
| } |
| } |
| } |
| } |
| } |
| return false; |
| } |
| |
| private void trackHearingAidPair(AudioRoute existingHaRoute, BluetoothDevice newHaDevice) { |
| if (newHaDevice == null |
| || existingHaRoute == null |
| || existingHaRoute.getType() != AudioRoute.TYPE_BLUETOOTH_HA) { |
| return; |
| } |
| if (newHaDevice.getAddress().equals(existingHaRoute.getBluetoothAddress())) { |
| return; |
| } |
| // This is critical to avoid an inconsistent hash state. |
| updateAvailableRoutes(existingHaRoute, false); |
| existingHaRoute.setBluetoothHaPairDevice(newHaDevice); |
| // Add the new modified route back into the available routes. |
| updateAvailableRoutes(existingHaRoute, true); |
| Log.i(this, "trackHearingAidPair: tracking hearing aid pair (%s) in existing route. " |
| + "New route: %s", newHaDevice.getAddress(), existingHaRoute); |
| } |
| |
| // Returns the modified bluetooth route if a hearing aid device (pair) was removed or null if |
| // no modifications were made. |
| private AudioRoute maybeAdjustHearingAidRoute( |
| @AudioRoute.AudioRouteType int type, |
| BluetoothDevice bluetoothDevice, |
| AudioRoute existingRoute) { |
| if (type != AudioRoute.TYPE_BLUETOOTH_HA |
| || bluetoothDevice == null |
| || existingRoute == null) { |
| return null; |
| } |
| String removedDeviceAddress = bluetoothDevice.getAddress(); |
| BluetoothDevice remainingDevice = existingRoute.getBluetoothHaPairDevice(); |
| // The device removed is either being tracked as a route in Telecom or we are storing the |
| // address as part of AudioRoute#mBluetoothHaPair. Update the route information accordingly. |
| if (Objects.equals(existingRoute.getBluetoothAddress(), removedDeviceAddress) |
| && remainingDevice != null) { |
| // If the primary route's BT address got removed, move the stored HA pair address as |
| // the primary BT address. |
| String mainHaAddress = existingRoute.getBluetoothAddress(); |
| String haPairAddress = remainingDevice.getAddress(); |
| // Replace the existing route's BT device mapping to the new device |
| mBluetoothRoutes.remove(existingRoute); |
| updateAvailableRoutes(existingRoute, false); |
| existingRoute.setBluetoothAddress(haPairAddress); |
| existingRoute.setBluetoothHaPairDevice(null); |
| mBluetoothRoutes.put(existingRoute, remainingDevice); |
| Log.i(this, |
| "maybeAdjustHearingAidRoute: Replacing removed device (address: %s) with the" |
| + " pair (address: %s). Updated route: %s with new device mapping in" |
| + " mBluetoothRoutes is %s", |
| mainHaAddress, |
| haPairAddress, |
| existingRoute, |
| mBluetoothRoutes.get(existingRoute)); |
| return existingRoute; |
| } else if (remainingDevice != null |
| && Objects.equals( |
| existingRoute.getBluetoothHaPairDevice().getAddress(), |
| removedDeviceAddress)) { |
| // If the HA pair was the device that got disconnected, all we need to do is reset |
| // the stored HA pair address. |
| String haPairAddress = remainingDevice.getAddress(); |
| updateAvailableRoutes(existingRoute, false); |
| existingRoute.setBluetoothHaPairDevice(null); |
| // Replace the existing route's BT device mapping to the new device |
| Log.i(this, "maybeAdjustHearingAidRoute: Removing tracked HA pair (%s) from existing " |
| + "route. Updated route: %s", haPairAddress, existingRoute); |
| return existingRoute; |
| } |
| return null; |
| } |
| |
| /** |
| * Prevent auto routing to a wearable device when calculating the default bluetooth audio route |
| * to move to. This function ensures that the most recently active non-wearable device is |
| * selected for routing unless a wearable device has already been identified as an active |
| * device. |
| */ |
| private AudioRoute getActiveWatchOrNonWatchDeviceRoute(String btAddressToExclude) { |
| List<AudioRoute> bluetoothRoutes = getAvailableBluetoothDevicesForRouting(); |
| // Traverse the routes from the most recently active recorded devices first. |
| AudioRoute nonWatchDeviceRoute = null; |
| for (int i = bluetoothRoutes.size() - 1; i >= 0; i--) { |
| AudioRoute route = bluetoothRoutes.get(i); |
| BluetoothDevice device = mBluetoothRoutes.get(route); |
| // Skip excluded BT address and LE audio if it's not the lead device. |
| if (route.getBluetoothAddress().equals(btAddressToExclude) |
| || isLeAudioNonLeadDeviceOrServiceUnavailable(route.getType(), device) |
| || device == null) { |
| continue; |
| } |
| // Check if the most recently active device is a watch device. |
| boolean isActiveDevice; |
| synchronized (mLock) { |
| isActiveDevice = |
| mActiveBluetoothDevice != null |
| && device.getAddress().equals(mActiveBluetoothDevice.second); |
| } |
| if (i == (bluetoothRoutes.size() - 1) |
| && mBluetoothRouteManager.isWatch(device) |
| && (device.equals(mCallAudioState.getActiveBluetoothDevice()) |
| || isActiveDevice)) { |
| Log.i(this, "getActiveWatchOrNonWatchDeviceRoute: Routing to active watch - %s", |
| bluetoothRoutes.get(bluetoothRoutes.size() - 1)); |
| return bluetoothRoutes.get(bluetoothRoutes.size() - 1); |
| } |
| // Record the first occurrence of a non-watch device route if found. |
| if (!mBluetoothRouteManager.isWatch(device)) { |
| nonWatchDeviceRoute = route; |
| break; |
| } |
| } |
| |
| Log.i(this, "Routing to a non-watch device - %s", nonWatchDeviceRoute); |
| return nonWatchDeviceRoute; |
| } |
| |
| private List<AudioRoute> getAvailableBluetoothDevicesForRouting() { |
| List<AudioRoute> bluetoothRoutes = new ArrayList<>(mBluetoothRoutes.keySet()); |
| // Consider the active device (BT_ACTIVE_DEVICE_PRESENT) if it exists first. |
| AudioRoute activeDeviceRoute = getArbitraryBluetoothDevice(); |
| if (activeDeviceRoute != null |
| && (bluetoothRoutes.isEmpty() |
| || !bluetoothRoutes |
| .get(bluetoothRoutes.size() - 1) |
| .equals(activeDeviceRoute))) { |
| Log.i(this, "getActiveWatchOrNonWatchDeviceRoute: active BT device (%s) present." |
| + "Considering this device for selection first.", activeDeviceRoute); |
| bluetoothRoutes.add(activeDeviceRoute); |
| } |
| return bluetoothRoutes; |
| } |
| |
| private boolean isLeAudioNonLeadDeviceOrServiceUnavailable( |
| @AudioRoute.AudioRouteType int type, BluetoothDevice device) { |
| BluetoothLeAudio leAudioService = getLeAudioService(); |
| if (type != AudioRoute.TYPE_BLUETOOTH_LE) { |
| return false; |
| } else if (leAudioService == null) { |
| return true; |
| } |
| |
| int groupId = leAudioService.getGroupId(device); |
| if (groupId != BluetoothLeAudio.GROUP_ID_INVALID) { |
| BluetoothDevice leadDevice = leAudioService.getConnectedGroupLeadDevice(groupId); |
| Log.i(this, "Lead device for device (%s) is %s.", device, leadDevice); |
| return leadDevice == null || !device.getAddress().equals(leadDevice.getAddress()); |
| } |
| return false; |
| } |
| |
| private BluetoothLeAudio getLeAudioService() { |
| return mBluetoothRouteManager.getDeviceManager().getLeAudioService(); |
| } |
| |
| @VisibleForTesting |
| public void setAudioManager(AudioManager audioManager) { |
| mAudioManager = audioManager; |
| } |
| |
| @VisibleForTesting |
| public void setAudioRouteFactory(AudioRoute.Factory audioRouteFactory) { |
| mAudioRouteFactory = audioRouteFactory; |
| } |
| |
| public Map<AudioRoute, BluetoothDevice> getBluetoothRoutes() { |
| return mBluetoothRoutes; |
| } |
| |
| public void overrideIsPending(boolean isPending) { |
| mIsPending = isPending; |
| } |
| |
| @VisibleForTesting |
| public void setScoAudioConnectedDevice(BluetoothDevice device) { |
| mScoAudioConnectedDevice = device; |
| } |
| |
| @VisibleForTesting |
| public void setLastScoDisconnectedDevice(BluetoothDevice device) { |
| mLastScoDisconnectedDevice = device; |
| } |
| |
| private void clearRingingBluetoothAddress() { |
| mBluetoothAddressForRinging = null; |
| } |
| |
| /** |
| * Update the active bluetooth device being tracked (as well as for individual profiles). We |
| * need to keep track of active devices for individual profiles because of potential |
| * inconsistencies found in BluetoothStateReceiver#handleActiveDeviceChanged. When multiple |
| * profiles are paired, we could have a scenario where an active device A is replaced with an |
| * active device B (from a different profile), which is then removed as an active device shortly |
| * after, causing device A to be reactive. It's possible that the active device changed intent |
| * is never received again for device A so an active device cache is necessary to track these |
| * devices at a profile level. |
| * |
| * @param device {@link Pair} containing the BT audio route type (i.e. SCO/HA/LE) and the |
| * address of the device. |
| */ |
| public void updateActiveBluetoothDevice(Pair<Integer, String> device) { |
| synchronized (mLock) { |
| // Get what's currently stored in the active device cache accessing the device's audio |
| // route type (device.first). |
| Pair<String, String> prevAndCurrentDevices = |
| mActiveDeviceCache.computeIfAbsent(device.first, k -> new Pair<>(null, null)); |
| // Store the old current active device as the previous device. The new active device is |
| // stored in the passed device (device.second). |
| String previousDevice = prevAndCurrentDevices.second; |
| mActiveDeviceCache.put(device.first, new Pair<>(previousDevice, device.second)); |
| // Update most recently active device if address isn't null (meaning |
| // some device is active). |
| if (device.second != null) { |
| mActiveBluetoothDevice = device; |
| } else { |
| // If a device was removed, check to ensure that no other device is |
| // still considered active. |
| boolean hasActiveDevice = false; |
| List<Map.Entry<Integer, Pair<String, String>>> activeBtDevices = |
| new ArrayList<>(mActiveDeviceCache.entrySet()); |
| for (Map.Entry<Integer, Pair<String, String>> activeDeviceInfo : activeBtDevices) { |
| Integer btAudioType = activeDeviceInfo.getKey(); |
| prevAndCurrentDevices = activeDeviceInfo.getValue(); |
| // This is the new active device for the specified audio route type. |
| String activeDeviceAddress = |
| prevAndCurrentDevices != null ? prevAndCurrentDevices.second : null; |
| if (activeDeviceAddress != null) { |
| hasActiveDevice = true; |
| mActiveBluetoothDevice = new Pair<>(btAudioType, activeDeviceAddress); |
| break; |
| } |
| } |
| if (!hasActiveDevice) { |
| mActiveBluetoothDevice = null; |
| } |
| } |
| } |
| } |
| |
| private String getPreviouslyActiveBtDeviceAddress(int audioRouteType) { |
| synchronized (mLock) { |
| Pair<String, String> prevAndCurrentDevices = |
| mActiveDeviceCache.computeIfAbsent(audioRouteType, k -> new Pair<>(null, null)); |
| return prevAndCurrentDevices.first; |
| } |
| } |
| |
| private void updateAvailableRoutes(AudioRoute route, boolean includeRoute) { |
| if (route == null) { |
| return; |
| } |
| if (includeRoute) { |
| mAvailableRoutes.add(route); |
| } else { |
| mAvailableRoutes.remove(route); |
| } |
| mAvailableRoutesUpdated = true; |
| } |
| |
| @VisibleForTesting |
| public void setActive(boolean active) { |
| if (active) { |
| mFocusType = ACTIVE_FOCUS; |
| } else { |
| mFocusType = NO_FOCUS; |
| } |
| mIsActive = active; |
| } |
| |
| void fallBack(String btAddressToExclude) { |
| mMetricsController.getAudioRouteStats().onRouteExit(mPendingAudioRoute, false); |
| sendMessageWithSessionInfo( |
| SWITCH_BASELINE_ROUTE, INCLUDE_BLUETOOTH_IN_BASELINE, btAddressToExclude); |
| } |
| |
| public CountDownLatch getAudioOperationsCompleteLatch() { |
| return mAudioOperationsCompleteLatch; |
| } |
| |
| public CountDownLatch getAudioActiveCompleteLatch() { |
| return mAudioActiveCompleteLatch; |
| } |
| |
| private @AudioRoute.AudioRouteType int getAudioType(AudioDeviceInfo device) { |
| if (device == null) { |
| return TYPE_INVALID; |
| } |
| int type; |
| try { |
| type = device.getType(); |
| } catch (NullPointerException npe) { |
| Log.w(this, "getAudioType: device.getType() threw NPEs"); |
| return TYPE_INVALID; |
| } |
| return DEVICE_INFO_TYPE_TO_AUDIO_ROUTE_TYPE.getOrDefault(type, TYPE_INVALID); |
| } |
| |
| @VisibleForTesting |
| public boolean getUsePreferredDeviceStrategy() { |
| return mUsePreferredDeviceStrategy; |
| } |
| |
| @VisibleForTesting |
| public void setCurrentCommunicationDevice(AudioDeviceInfo device) { |
| synchronized (mLock) { |
| mCurrentCommunicationDevice = device; |
| } |
| } |
| |
| public AudioDeviceInfo getCurrentCommunicationDevice() { |
| synchronized (mLock) { |
| return mCurrentCommunicationDevice; |
| } |
| } |
| |
| public void setPreferredDeviceRoute(AudioRoute route) { |
| synchronized (mLock) { |
| mPreferredDeviceRoute = route; |
| } |
| } |
| |
| public AudioRoute getPreferredDeviceRoute() { |
| synchronized (mLock) { |
| return mPreferredDeviceRoute; |
| } |
| } |
| |
| /** |
| * Determines if there's a non-empty {@link AudioDeviceInfo} set by the audio fwk to signal that |
| * the user has set a preferred audio route for placing/taking calls. |
| * |
| * <p>Note: This is set via {@link AudioManager.OnPreferredDevicesForStrategyChangedListener} |
| * but this API needs to be improved as we have seen instances where this is updated without any |
| * user intervention as well. |
| */ |
| private boolean isPreferredDeviceSet() { |
| synchronized (mLock) { |
| AudioRoute preferredDevice = getPreferredDeviceRoute(); |
| return preferredDevice != null && !preferredDevice.equals(DUMMY_ROUTE); |
| } |
| } |
| |
| private void maybeDisableWasOnSpeaker(boolean isUserRequest) { |
| if (isUserRequest) { |
| mWasOnSpeaker = false; |
| } |
| } |
| |
| /* |
| * Adjusts routing to go inactive if we're active in the case that we're processing |
| * RINGING_FOCUS and another BT headset is connected which causes in-band ringing to get |
| * disabled. If we stay in active routing, Telecom will send requests to connect to these BT |
| * devices while the call is ringing and each of these requests will fail at the BT stack side. |
| * By default, in-band ringtone is disabled when more than one BT device is paired. Instead, |
| * ringtone is played using the headset's default ringtone. |
| */ |
| private boolean maybeAdjustActiveRouting(AudioRoute destRoute, boolean isDestRouteActive) { |
| BluetoothDevice device = mBluetoothRoutes.get(destRoute); |
| // If routing is active and in-band ringing is disabled while the call is ringing, move to |
| // inactive routing. |
| if (isDestRouteActive |
| && mFocusType == RINGING_FOCUS |
| && device != null |
| && !mBluetoothRouteManager.isInbandRingEnabled(destRoute.getType(), device)) { |
| return false; |
| } else if (!isDestRouteActive |
| && mFocusType == RINGING_FOCUS |
| && (device == null |
| || mBluetoothRouteManager.isInbandRingEnabled( |
| destRoute.getType(), device))) { |
| // If the routing is inactive while the call is ringing and we re-evaluate this to find |
| // that we're routing to a non-BT device or a BT device that does support in-band |
| // ringing, then re-enable active routing (i.e. second HFP headset is disconnected |
| // while call is ringing). |
| return true; |
| } |
| return isDestRouteActive; |
| } |
| |
| private void createSpeakerRoute() { |
| int audioRouteType = TYPE_SPEAKER; |
| if (mSpeakerDockRoute == null) { |
| // create type speaker |
| mSpeakerDockRoute = |
| mAudioRouteFactory.create( |
| audioRouteType, null, mAudioManager, mIsScoManagedByAudio); |
| // If speaker route couldn't be instantiated, try for TYPE_BUS |
| if (mSpeakerDockRoute == null) { |
| Log.i(this, "createSpeakerRoute: Can't find available audio device info for " |
| + "route TYPE_SPEAKER, trying for TYPE_BUS"); |
| mSpeakerDockRoute = |
| mAudioRouteFactory.create( |
| AudioRoute.TYPE_BUS, null, mAudioManager, mIsScoManagedByAudio); |
| audioRouteType = AudioRoute.TYPE_BUS; |
| } |
| if (mSpeakerDockRoute == null) { |
| Log.w(this, "createSpeakerRoute: Can't find available audio device info " |
| + "for route TYPE_SPEAKER or TYPE_BUS."); |
| } else { |
| // Update available routes |
| mTypeRoutes.put(audioRouteType, mSpeakerDockRoute); |
| updateAvailableRoutes(mSpeakerDockRoute, true); |
| } |
| } else { |
| Log.i(this, "createSpeakerRoute: route already created. Skipping."); |
| } |
| } |
| |
| private void createEarpieceRoute() { |
| // Create earpiece route |
| if (mEarpieceWiredRoute != null) { |
| Log.i(this, "createEarpieceRoute: route already created. Skipping."); |
| return; |
| } |
| mEarpieceWiredRoute = |
| mAudioRouteFactory.create( |
| AudioRoute.TYPE_EARPIECE, null, mAudioManager, mIsScoManagedByAudio); |
| if (mEarpieceWiredRoute == null) { |
| Log.w(this, "createEarpieceRoute: Can't find available audio device info for " |
| + "route TYPE_EARPIECE"); |
| } else { |
| mTypeRoutes.put(AudioRoute.TYPE_EARPIECE, mEarpieceWiredRoute); |
| updateAvailableRoutes(mEarpieceWiredRoute, true); |
| } |
| } |
| |
| @VisibleForTesting |
| public AudioRoute getAudioRouteForTesting(int audioRouteType) { |
| return switch (audioRouteType) { |
| case AudioRoute.TYPE_EARPIECE, AudioRoute.TYPE_WIRED -> mEarpieceWiredRoute; |
| case AudioRoute.TYPE_SPEAKER -> mSpeakerDockRoute; |
| default -> DUMMY_ROUTE; |
| }; |
| } |
| |
| @VisibleForTesting |
| public AudioRoutesCallback getAudioRoutesCallback() { |
| return mAudioRoutesCallback; |
| } |
| |
| /** |
| * This method processes the communication device change updates in two steps given that the new |
| * communication device either doesn't correspond to the current audio route tracked in Telecom |
| * or in the case of the same BT profiles that the addresses are different. Telecom: 1. We will |
| * first handle cleanup related to the current (or source) route. For speaker + SCO, this |
| * entails handling the pending messages for BT_AUDIO_DISCONNECTED or SPEAKER_OFF. 2. We will |
| * then handle UI routing to what the new communication device is. We will first adjust the |
| * routing if needed. This may be needed in cases where audio fwk sends intermediate |
| * communication device updates that may alter the pending destination audio route (i.e. HFP A |
| * -> HFP B may result in an intermediary communication update to speaker). There is also no |
| * need for us to set the communication device again or disconnect SCO (for the legacy path done |
| * via BluetoothHeadset). This logic is already accounted for in #routeTo with |
| * "isDestRouteCommunicationDevice". |
| * |
| * @param newAudioType The new audio route type for the new communication device reported by the |
| * audio fwk. |
| * @param newCommunicationDevice The new communication device update received from the audio fwk |
| * signaling where audio is currently routed to. |
| * @param previousCommunicationDevice The previous communication device stored in Telecom before |
| * the new communication device update was received from the audio fwk. |
| */ |
| @VisibleForTesting |
| public void handleCommunicationDeviceChanged( |
| int newAudioType, |
| AudioDeviceInfo newCommunicationDevice, |
| AudioDeviceInfo previousCommunicationDevice) { |
| if (!mIsActive) { |
| Log.i(this, "handleCommunicationDeviceChanged: Not active, skipping. New audio type: " |
| + "%d, new communication device: %s, previous communication device: %s", |
| newAudioType, newCommunicationDevice, previousCommunicationDevice); |
| return; |
| } |
| int currentAudioType = mCurrentRoute.getType(); |
| boolean areAudioTypesSame = newAudioType == currentAudioType; |
| boolean areBluetoothAddressesSame = |
| BT_AUDIO_ROUTE_TYPES.contains(newAudioType) |
| && Objects.equals( |
| mCurrentRoute.getBluetoothAddress(), |
| newCommunicationDevice.getAddress()); |
| boolean areAudioRoutesSame = areAudioTypesSame && areBluetoothAddressesSame; |
| boolean areCommunicationDevicesSame = Objects.equals(previousCommunicationDevice, |
| newCommunicationDevice); |
| |
| // Ensure that if the BT device is removed via BT_DEVICE_REMOVED and the SCO route no longer |
| // exists, that we still clear the pending SCO disconnect message and recalculate the route. |
| // Note that if the route doesn't exist, then we must be in a pending route change to move |
| // out of the current (removed) route. |
| if (currentAudioType == TYPE_BLUETOOTH_SCO && previousCommunicationDevice != null |
| && previousCommunicationDevice.getAddress() != null && getBluetoothRoute( |
| TYPE_BLUETOOTH_SCO, previousCommunicationDevice.getAddress()) == null) { |
| // Use onMessageReceived here to ensure that the pending route change is fulfilled if |
| // there aren't anymore pending messages. |
| mPendingAudioRoute.onMessageReceived(new Pair<>(BT_AUDIO_DISCONNECTED, |
| previousCommunicationDevice.getAddress()), null); |
| } |
| |
| // We need to perform an update if the current communication device type is different from |
| // whatever the current route is and we should also account for multiple BT devices of the |
| // same type. It's also possible that the previous communication device is null in which |
| // case we should also perform an update. |
| if (!areAudioRoutesSame || !areCommunicationDevicesSame) { |
| // SOURCE ROUTING HANDLING: |
| // Handle clean-up for source route first before handling where audio should be |
| // routed to. These are sent to handle any pending SPEAKER_OFF or BT_AUDIO_DISCONNECTED |
| // messages. The pending messages are only sent if Telecom is pending a route change so |
| // we can skip this if the audio routes are the same. |
| if (!areAudioRoutesSame) { |
| if (currentAudioType == TYPE_SPEAKER) { |
| sendMessageWithSessionInfo(SPEAKER_OFF); |
| } else if (previousCommunicationDevice != null |
| && currentAudioType == TYPE_BLUETOOTH_SCO |
| && Objects.equals( |
| mCurrentRoute.getBluetoothAddress(), |
| previousCommunicationDevice.getAddress())) { |
| handleBtConnectionStateChanged( |
| previousCommunicationDevice.getAddress(), false /* isScoConnected */); |
| mRingtonePlayer.updateBtActiveState(false); |
| } |
| } |
| |
| // DESTINATION ROUTING HANDLING: |
| // Now we can handle the changes for where routing will go to after. |
| if (newAudioType == TYPE_SPEAKER) { |
| // Maybe handle switch to speaker first if needed (i.e. if there's an intermediary |
| // switch from the audio fwk for the communication device) before it is updated to |
| // speaker |
| handleSwitchSpeaker(false); |
| // Signal SPEAKER_ON to handle routing for the UI. |
| sendMessageWithSessionInfo(SPEAKER_ON); |
| } else if (newAudioType == TYPE_BLUETOOTH_SCO) { |
| // Handle switch to BT in the case that the UI isn't already reflected |
| sendMessageWithSessionInfo(SWITCH_BLUETOOTH, 0, |
| newCommunicationDevice.getAddress()); |
| // Signal BT_AUDIO_CONNECTED if needed |
| handleBtConnectionStateChanged( |
| newCommunicationDevice.getAddress(), true /* isScoConnected */); |
| mRingtonePlayer.updateBtActiveState(true); |
| } |
| } |
| } |
| |
| /** |
| * This logic is duplicated from what's being handled in the {@link |
| * com.android.server.telecom.bluetooth.BluetoothStateReceiver} class. Instead of triggering the |
| * logic from the BT broadcast signals, we will do it via the communication device updates |
| * provided by {@link AudioManager.OnCommunicationDeviceChangedListener}. We have seen cases |
| * where the BT broadcast signals may not be aligned with what audio fwk reports. Ultimately, |
| * those broadcasts are also relying on the audio fwk for signaling so we can avoid the extra |
| * latency by listening to AudioManager directly. |
| */ |
| private void handleBtConnectionStateChanged(String address, boolean isScoConnected) { |
| AudioRoute btRoute = getBluetoothRoute(TYPE_BLUETOOTH_SCO, address); |
| if (btRoute == null) { |
| Log.w(this, "handleBtConnectionStateChanged: Audio route is undefined for " |
| + "address (%s)", address); |
| return; |
| } |
| BluetoothDevice device = mBluetoothRoutes.get(btRoute); |
| if (device == null) { |
| Log.w(this, "handleBtConnectionStateChanged: Bluetooth device is undefined " |
| + "for the given route (%s)", btRoute); |
| return; |
| } |
| Log.i(this, "handleBtConnectionStateChanged: SCO connected(%b) for address %s", |
| isScoConnected, address); |
| // BT_AUDIO_CONNECTED |
| if (isScoConnected) { |
| setScoAudioConnectedDevice(device); |
| if (isPending() && Objects.equals(getPendingAudioRoute().getDestRoute(), btRoute)) { |
| sendMessageWithSessionInfo(BT_AUDIO_CONNECTED, 0, device); |
| } else { |
| // It's possible that the initial BT connection fails but BT_AUDIO_CONNECTED |
| // is sent later, indicating that SCO audio is on. We should route |
| // appropriately in order for the UI to reflect this state. |
| getPendingAudioRoute().overrideDestRoute(btRoute); |
| overrideIsPending(true); |
| getPendingAudioRoute().setCommunicationDeviceType(AudioRoute.TYPE_BLUETOOTH_SCO); |
| sendMessageWithSessionInfo(EXIT_PENDING_ROUTE); |
| } |
| } else { // // BT_AUDIO_DISCONNECTED |
| setLastScoDisconnectedDevice(device); |
| setScoAudioConnectedDevice(null); |
| if (isPending()) { |
| sendMessageWithSessionInfo(BT_AUDIO_DISCONNECTED, 0, device); |
| } else { |
| // Handle case where BT stack signals SCO disconnected but Telecom isn't |
| // processing any pending routes. This explicitly addresses cf instances |
| // where a remote device disconnects SCO. Telecom should ensure that audio |
| // is properly routed in the UI. Instead of calculating the baseline, we can just |
| // route to whatever the audio fwk says the new communication device has changed to. |
| // Ignore the rerouting if we're not in a call and the BT device hasn't been |
| // unpaired from the device, we can just stay in inactive routing. We may get a |
| // communication device update signaling that it moved away from SCO if, for |
| // instance, the BT stack is using the set/clear communication device APIs. |
| int audioType = getAudioType(getCurrentCommunicationDevice()); |
| getPendingAudioRoute().setCommunicationDeviceType(audioType); |
| if (mIsActive) { |
| routeTo( |
| true, |
| getAudioRouteForAudioDeviceInfo(getCurrentCommunicationDevice()), |
| false); |
| } |
| } |
| } |
| } |
| |
| private AudioRoute getAudioRouteForAudioDeviceInfo(AudioDeviceInfo deviceInfo) { |
| if (deviceInfo == null) { |
| Log.w(this, "getAudioRouteForAudioDeviceInfo: device info is undefined"); |
| return DUMMY_ROUTE; |
| } |
| int audioType = getAudioType(deviceInfo); |
| if (audioType == TYPE_INVALID) { |
| Log.w(this, "getAudioRouteForAudioDeviceInfo: unable to resolve audio type for %s", |
| deviceInfo); |
| return DUMMY_ROUTE; |
| } |
| if (BT_AUDIO_ROUTE_TYPES.contains(audioType)) { |
| return getBluetoothRoute(audioType, deviceInfo.getAddress()); |
| } else { |
| return mTypeRoutes.get(audioType); |
| } |
| } |
| |
| public void setAudioMode(int mode) { |
| mMode = mode; |
| if (com.android.internal.telecom.flags.Flags.callAudioRouteRf()) { |
| if (mAudioModeSession != null) { |
| Log.i(this, "setAudioMode: requesting mode %d", mode); |
| mAudioModeSession.setMode(mode); |
| } else { |
| Log.i(this, "no active session, skip setAudioMode: requesting mode %d", mode); |
| } |
| } |
| } |
| |
| private void createAudioModeSession() { |
| if (!com.android.internal.telecom.flags.Flags.callAudioRouteRf() |
| || mAudioModeSession != null) { |
| return; |
| } |
| |
| Call foregroundCall = mCallsManager.getForegroundCall(); |
| boolean isVideo = |
| foregroundCall != null && VideoProfile.isVideo(foregroundCall.getVideoState()); |
| |
| Log.i(this, "createAudioModeSession: creating new session, mode = %d, isVideo=%b", |
| mMode, isVideo); |
| AudioModeSession.Request request = |
| new AudioModeSession.Request.Builder() |
| .setInitialMode(mMode) |
| .setDisplayActiveUseCase(isVideo) |
| .build(); |
| mAudioModeSession = |
| mAudioManager.createAudioModeSession( |
| request, mHandler::post, mAudioModeSessionCallback); |
| Log.i(this, "new AudioModeSession:" + mAudioModeSession + " is created"); |
| } |
| |
| private void closeAudioModeSession() { |
| if (mAudioModeSession != null) { |
| Log.i(this, "closeAudioModeSession: closing session"); |
| mAudioModeSession.close(); |
| mAudioModeSession = null; |
| } |
| } |
| |
| private AudioRoute mapAudioModeRouteToTelecomRoute(AudioModeSession.AudioRoute audioRoute) { |
| if (audioRoute == null || audioRoute.getPrimaryDevice() == null) { |
| return null; |
| } |
| AudioDeviceAttributes primary = audioRoute.getPrimaryDevice(); |
| @AudioRoute.AudioRouteType |
| int type = |
| DEVICE_INFO_TYPE_TO_AUDIO_ROUTE_TYPE.getOrDefault(primary.getType(), TYPE_INVALID); |
| |
| if (BT_AUDIO_ROUTE_TYPES.contains(type)) { |
| return getBluetoothRoute(type, primary.getAddress()); |
| } else { |
| return mTypeRoutes.get(type); |
| } |
| } |
| |
| private AudioModeSession.AudioRoute mapTelecomRouteToAudioModeRoute(AudioRoute telecomRoute) { |
| if (telecomRoute == null || telecomRoute.getType() == TYPE_INVALID) { |
| return null; |
| } |
| |
| AudioDeviceInfo info = telecomRoute.getInfo(); |
| if (info == null) { |
| // Try to find a suitable AudioDeviceInfo if not present in AudioRoute |
| List<AudioDeviceInfo> devices = mAudioManager.getAvailableCommunicationDevices(); |
| for (AudioDeviceInfo device : devices) { |
| @AudioRoute.AudioRouteType |
| int type = |
| DEVICE_INFO_TYPE_TO_AUDIO_ROUTE_TYPE.getOrDefault( |
| device.getType(), TYPE_INVALID); |
| if (type == telecomRoute.getType()) { |
| if (telecomRoute.getBluetoothAddress() != null) { |
| if (telecomRoute.getBluetoothAddress().equals(device.getAddress())) { |
| info = device; |
| break; |
| } |
| } else { |
| info = device; |
| break; |
| } |
| } |
| } |
| } |
| |
| if (info != null) { |
| return new AudioModeSession.AudioRoute.Builder(new AudioDeviceAttributes(info)).build(); |
| } |
| return null; |
| } |
| |
| private boolean isValidRoute(AudioRoute route) { |
| return route != DUMMY_ROUTE && route != null; |
| } |
| |
| @VisibleForTesting |
| public void setIsScoManagedByAudio(boolean isScoManagedByAudio) { |
| mIsScoManagedByAudio = isScoManagedByAudio; |
| } |
| |
| @VisibleForTesting |
| public AudioModeSession getAudioModeSession() { |
| return mAudioModeSession; |
| } |
| |
| private void handleAvailableRoutesChanged(List<AudioModeSession.AudioRoute> audioModeRoutes) { |
| Set<AudioRoute> newAvailableRoutes = new HashSet<>(); |
| for (AudioModeSession.AudioRoute amRoute : audioModeRoutes) { |
| AudioRoute telecomRoute = mapAudioModeRouteToTelecomRoute(amRoute); |
| if (telecomRoute != null) { |
| newAvailableRoutes.add(telecomRoute); |
| } |
| } |
| |
| // Special handling for speaker and earpiece to ensure they are created if they appear. |
| for (AudioModeSession.AudioRoute amRoute : audioModeRoutes) { |
| AudioDeviceAttributes primary = amRoute.getPrimaryDevice(); |
| if (primary == null) continue; |
| int type = |
| DEVICE_INFO_TYPE_TO_AUDIO_ROUTE_TYPE.getOrDefault( |
| primary.getType(), TYPE_INVALID); |
| if (type == AudioRoute.TYPE_SPEAKER && mSpeakerDockRoute == null) { |
| createSpeakerRoute(); |
| } else if (type == AudioRoute.TYPE_EARPIECE |
| && mTypeRoutes.get(AudioRoute.TYPE_EARPIECE) == null) { |
| createEarpieceRoute(); |
| } |
| } |
| |
| if (!newAvailableRoutes.equals(mAvailableRoutes)) { |
| Log.i(this, "handleAvailableRoutesChanged: available routes changed from %s to %s", |
| mAvailableRoutes, newAvailableRoutes); |
| mWiredHeadsetManager.refreshHeadsetStatus(); |
| mAvailableRoutes = newAvailableRoutes; |
| mAvailableRoutesUpdated = true; |
| onAvailableRoutesChanged(); |
| } |
| } |
| } |