blob: c26baf39bbe8b27bd17f0dcfbc9f8caca63f4893 [file]
/*
* 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();
}
}
}