Merge "Resolve CallAudioRouteController issues." into main
diff --git a/src/com/android/server/telecom/AudioRoute.java b/src/com/android/server/telecom/AudioRoute.java
index 7b593d7..8a5e858 100644
--- a/src/com/android/server/telecom/AudioRoute.java
+++ b/src/com/android/server/telecom/AudioRoute.java
@@ -28,6 +28,7 @@
import android.media.AudioDeviceInfo;
import android.media.AudioManager;
import android.telecom.Log;
+import android.util.Pair;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.telecom.bluetooth.BluetoothRouteManager;
@@ -226,7 +227,7 @@
AUDIO_ROUTE_TYPE_TO_DEVICE_INFO_TYPE.put(TYPE_BLUETOOTH_LE, bluetoothLeDeviceInfoTypes);
}
- int getType() {
+ public int getType() {
return mAudioRouteType;
}
@@ -237,7 +238,7 @@
// Invoked when entered pending route whose dest route is this route
void onDestRouteAsPendingRoute(boolean active, PendingAudioRoute pendingAudioRoute,
BluetoothDevice device, AudioManager audioManager,
- BluetoothRouteManager bluetoothRouteManager) {
+ BluetoothRouteManager bluetoothRouteManager, boolean isScoAudioConnected) {
Log.i(this, "onDestRouteAsPendingRoute: active (%b), type (%d)", active, mAudioRouteType);
if (pendingAudioRoute.isActive() && !active) {
clearCommunicationDevice(pendingAudioRoute, bluetoothRouteManager, audioManager);
@@ -251,20 +252,19 @@
// Check if the communication device was set for the device, even if
// BluetoothHeadset#connectAudio reports that the SCO connection wasn't
// successfully established.
- boolean scoConnected = audioManager.getCommunicationDevice().equals(mInfo);
- if (connectedBtAudio || scoConnected) {
+ if (connectedBtAudio || isScoAudioConnected) {
pendingAudioRoute.setCommunicationDeviceType(mAudioRouteType);
- }
- if (connectedBtAudio) {
- pendingAudioRoute.addMessage(BT_AUDIO_CONNECTED);
- } else if (!scoConnected) {
- pendingAudioRoute.onMessageReceived(
- PENDING_ROUTE_FAILED, mBluetoothAddress);
+ if (!isScoAudioConnected) {
+ pendingAudioRoute.addMessage(BT_AUDIO_CONNECTED, mBluetoothAddress);
+ }
+ } else {
+ pendingAudioRoute.onMessageReceived(new Pair<>(PENDING_ROUTE_FAILED,
+ mBluetoothAddress), mBluetoothAddress);
}
return;
}
} else if (mAudioRouteType == TYPE_SPEAKER) {
- pendingAudioRoute.addMessage(SPEAKER_ON);
+ pendingAudioRoute.addMessage(SPEAKER_ON, null);
}
boolean result = false;
@@ -291,7 +291,7 @@
// before being able to successfully set the communication device. Refrain from sending
// pending route failed message for BT route until the second attempt fails.
if (!result && !BT_AUDIO_ROUTE_TYPES.contains(mAudioRouteType)) {
- pendingAudioRoute.onMessageReceived(PENDING_ROUTE_FAILED, null);
+ pendingAudioRoute.onMessageReceived(new Pair<>(PENDING_ROUTE_FAILED, null), null);
}
}
}
@@ -303,13 +303,13 @@
Log.i(this, "onOrigRouteAsPendingRoute: active (%b), type (%d)", active, mAudioRouteType);
if (active) {
if (mAudioRouteType == TYPE_SPEAKER) {
- pendingAudioRoute.addMessage(SPEAKER_OFF);
+ pendingAudioRoute.addMessage(SPEAKER_OFF, null);
}
int result = clearCommunicationDevice(pendingAudioRoute, bluetoothRouteManager,
audioManager);
// Only send BT_AUDIO_DISCONNECTED for SCO if disconnect was successful.
if (mAudioRouteType == TYPE_BLUETOOTH_SCO && result == BluetoothStatusCodes.SUCCESS) {
- pendingAudioRoute.addMessage(BT_AUDIO_DISCONNECTED);
+ pendingAudioRoute.addMessage(BT_AUDIO_DISCONNECTED, mBluetoothAddress);
}
}
}
@@ -370,7 +370,7 @@
return success;
}
- private int clearCommunicationDevice(PendingAudioRoute pendingAudioRoute,
+ int clearCommunicationDevice(PendingAudioRoute pendingAudioRoute,
BluetoothRouteManager bluetoothRouteManager, AudioManager audioManager) {
// Try to see if there's a previously set device for communication that should be cleared.
// This only serves to help in the SCO case to ensure that we disconnect the headset.
diff --git a/src/com/android/server/telecom/CallAudioRouteController.java b/src/com/android/server/telecom/CallAudioRouteController.java
index 7b29fc8..820219d 100644
--- a/src/com/android/server/telecom/CallAudioRouteController.java
+++ b/src/com/android/server/telecom/CallAudioRouteController.java
@@ -18,6 +18,7 @@
import static com.android.server.telecom.AudioRoute.BT_AUDIO_ROUTE_TYPES;
import static com.android.server.telecom.AudioRoute.TYPE_INVALID;
+import static com.android.server.telecom.AudioRoute.TYPE_SPEAKER;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
@@ -40,7 +41,9 @@
import android.telecom.CallAudioState;
import android.telecom.Log;
import android.telecom.Logging.Session;
+import android.telecom.VideoProfile;
import android.util.ArrayMap;
+import android.util.Pair;
import androidx.annotation.NonNull;
@@ -94,7 +97,9 @@
private StatusBarNotifier mStatusBarNotifier;
private FeatureFlags mFeatureFlags;
private int mFocusType;
+ private boolean mIsScoAudioConnected;
private final Object mLock = new Object();
+ private final TelecomSystem.SyncRoot mTelecomLock;
private final BroadcastReceiver mSpeakerPhoneChangeReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
@@ -105,7 +110,9 @@
AudioDeviceInfo info = mAudioManager.getCommunicationDevice();
if ((info != null) &&
(info.getType() == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER)) {
- sendMessageWithSessionInfo(SPEAKER_ON);
+ if (mCurrentRoute.getType() != AudioRoute.TYPE_SPEAKER) {
+ sendMessageWithSessionInfo(SPEAKER_ON);
+ }
} else {
sendMessageWithSessionInfo(SPEAKER_OFF);
}
@@ -171,6 +178,8 @@
mStatusBarNotifier = statusBarNotifier;
mFeatureFlags = featureFlags;
mFocusType = NO_FOCUS;
+ mIsScoAudioConnected = false;
+ mTelecomLock = callsManager.getLock();
HandlerThread handlerThread = new HandlerThread(this.getClass().getSimpleName());
handlerThread.start();
@@ -282,7 +291,7 @@
handleMuteChanged(false);
break;
case MUTE_EXTERNALLY_CHANGED:
- handleMuteChanged(mAudioManager.isMasterMute());
+ handleMuteChanged(mAudioManager.isMicrophoneMute());
break;
case SWITCH_FOCUS:
focus = msg.arg1;
@@ -455,11 +464,13 @@
Log.i(this, "Override current pending route destination from %s(active=%b) to "
+ "%s(active=%b)",
mPendingAudioRoute.getDestRoute(), mIsActive, destRoute, active);
+ // Ensure we don't keep waiting for SPEAKER_ON if dest route gets overridden.
+ if (active && mPendingAudioRoute.getDestRoute().getType() == TYPE_SPEAKER) {
+ mPendingAudioRoute.clearPendingMessage(new Pair<>(SPEAKER_ON, null));
+ }
// override pending route while keep waiting for still pending messages for the
// previous pending route
mPendingAudioRoute.setOrigRoute(mIsActive, mPendingAudioRoute.getDestRoute());
- mPendingAudioRoute.setDestRoute(active, destRoute, mBluetoothRoutes.get(destRoute));
- mIsActive = active;
} else {
if (mCurrentRoute.equals(destRoute) && (mIsActive == active)) {
return;
@@ -473,10 +484,11 @@
// Avoid waiting for pending messages for an unavailable route
mPendingAudioRoute.setOrigRoute(mIsActive, DUMMY_ROUTE);
}
- mPendingAudioRoute.setDestRoute(active, destRoute, mBluetoothRoutes.get(destRoute));
- mIsActive = active;
mIsPending = true;
}
+ mPendingAudioRoute.setDestRoute(active, destRoute, mBluetoothRoutes.get(destRoute),
+ mIsScoAudioConnected);
+ mIsActive = active;
mPendingAudioRoute.evaluatePendingState();
postTimeoutMessage();
}
@@ -599,7 +611,8 @@
Log.i(this, "handleBtAudioActive: is pending path");
if (Objects.equals(mPendingAudioRoute.getDestRoute().getBluetoothAddress(),
bluetoothDevice.getAddress())) {
- mPendingAudioRoute.onMessageReceived(BT_AUDIO_CONNECTED, null);
+ mPendingAudioRoute.onMessageReceived(new Pair<>(BT_AUDIO_CONNECTED,
+ bluetoothDevice.getAddress()), null);
}
} else {
// ignore, not triggered by telecom
@@ -620,7 +633,8 @@
Log.i(this, "handleBtAudioInactive: is pending path");
if (Objects.equals(mPendingAudioRoute.getOrigRoute().getBluetoothAddress(),
bluetoothDevice.getAddress())) {
- mPendingAudioRoute.onMessageReceived(BT_AUDIO_DISCONNECTED, null);
+ mPendingAudioRoute.onMessageReceived(new Pair<>(BT_AUDIO_DISCONNECTED,
+ bluetoothDevice.getAddress()), null);
}
} else {
// ignore, not triggered by telecom
@@ -719,7 +733,7 @@
private void handleMuteChanged(boolean mute) {
mIsMute = mute;
- if (mIsMute != mAudioManager.isMasterMute() && mIsActive) {
+ if (mIsMute != mAudioManager.isMicrophoneMute() && mIsActive) {
IAudioService audioService = mAudioServiceFactory.getAudioService();
Log.i(this, "changing microphone mute state to: %b [serviceIsNull=%b]", mute,
audioService == null);
@@ -752,10 +766,9 @@
}
}
case ACTIVE_FOCUS -> {
- // Route to active baseline route, otherwise ignore if route is already active.
- if (!mIsActive) {
- routeTo(true, getBaseRoute(true, null));
- }
+ // Route to active baseline route (we may need to change audio route in the case
+ // when a video call is put on hold).
+ routeTo(true, getBaseRoute(true, null));
}
case RINGING_FOCUS -> {
if (!mIsActive) {
@@ -836,7 +849,7 @@
private void handleSpeakerOn() {
if (isPending()) {
Log.i(this, "handleSpeakerOn: sending SPEAKER_ON to pending audio route");
- mPendingAudioRoute.onMessageReceived(SPEAKER_ON, null);
+ mPendingAudioRoute.onMessageReceived(new Pair<>(SPEAKER_ON, null), null);
// Update status bar notification if we are in a call.
mStatusBarNotifier.notifySpeakerphone(mCallsManager.hasAnyCalls());
} else {
@@ -854,7 +867,7 @@
private void handleSpeakerOff() {
if (isPending()) {
Log.i(this, "handleSpeakerOff - sending SPEAKER_OFF to pending audio route");
- mPendingAudioRoute.onMessageReceived(SPEAKER_OFF, null);
+ mPendingAudioRoute.onMessageReceived(new Pair<>(SPEAKER_OFF, null), null);
// Update status bar notification
mStatusBarNotifier.notifySpeakerphone(false);
} else if (mCurrentRoute.getType() == AudioRoute.TYPE_SPEAKER) {
@@ -878,6 +891,7 @@
Log.addEvent(mCallsManager.getForegroundCall(), LogUtils.Events.AUDIO_ROUTE,
"Entering audio route: " + mCurrentRoute + " (active=" + mIsActive + ")");
mIsPending = false;
+ mPendingAudioRoute.clearPendingMessages();
onCurrentRouteChanged();
}
}
@@ -909,7 +923,8 @@
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 (route.getType() == AudioRoute.TYPE_BLUETOOTH_LE) {
+ if (route.getType() == AudioRoute.TYPE_BLUETOOTH_LE
+ && getLeAudioService() != null) {
int groupId = getLeAudioService().getGroupId(deviceToAdd);
if (groupId != BluetoothLeAudio.GROUP_ID_INVALID) {
deviceToAdd = getLeAudioService().getConnectedGroupLeadDevice(groupId);
@@ -988,6 +1003,12 @@
private AudioRoute getPreferredAudioRouteFromDefault(boolean includeBluetooth,
String btAddressToExclude) {
+ boolean skipEarpiece;
+ Call foregroundCall = mCallAudioManager.getForegroundCall();
+ synchronized (mTelecomLock) {
+ skipEarpiece = foregroundCall != null
+ && VideoProfile.isVideo(foregroundCall.getVideoState());
+ }
// Route to earpiece, wired, or speaker route if there are not bluetooth routes or if there
// are only wearables available.
AudioRoute activeWatchOrNonWatchDeviceRoute =
@@ -996,7 +1017,17 @@
|| activeWatchOrNonWatchDeviceRoute == null) {
Log.i(this, "getPreferredAudioRouteFromDefault: Audio routing defaulting to "
+ "available non-BT route.");
- return mEarpieceWiredRoute != null ? mEarpieceWiredRoute : mSpeakerDockRoute;
+ AudioRoute defaultRoute = mEarpieceWiredRoute != null
+ ? mEarpieceWiredRoute
+ : mSpeakerDockRoute;
+ // Ensure that we default to speaker route if we're in a video call, but disregard it if
+ // a wired headset is plugged in.
+ if (skipEarpiece && 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).
@@ -1042,8 +1073,8 @@
return mCurrentRoute;
}
- private AudioRoute getBluetoothRoute(@AudioRoute.AudioRouteType int audioRouteType,
- String address) {
+ public AudioRoute getBluetoothRoute(@AudioRoute.AudioRouteType int audioRouteType,
+ String address) {
for (AudioRoute route : mBluetoothRoutes.keySet()) {
if (route.getType() == audioRouteType && route.getBluetoothAddress().equals(address)) {
return route;
@@ -1129,7 +1160,7 @@
BluetoothDevice device = mBluetoothRoutes.get(route);
// Skip excluded BT address and LE audio if it's not the lead device.
if (route.getBluetoothAddress().equals(btAddressToExclude)
- || isLeAudioNonLeadDevice(route.getType(), device)) {
+ || isLeAudioNonLeadDeviceOrServiceUnavailable(route.getType(), device)) {
continue;
}
// Check if the most recently active device is a watch device.
@@ -1158,7 +1189,8 @@
for (int i = bluetoothRoutes.size() - 1; i >= 0; i--) {
AudioRoute route = bluetoothRoutes.get(i);
// Skip LE route if it's not the lead device.
- if (isLeAudioNonLeadDevice(route.getType(), mBluetoothRoutes.get(route))) {
+ if (isLeAudioNonLeadDeviceOrServiceUnavailable(
+ route.getType(), mBluetoothRoutes.get(route))) {
continue;
}
if (!route.getBluetoothAddress().equals(btAddressToExclude)) {
@@ -1168,15 +1200,19 @@
return null;
}
- private boolean isLeAudioNonLeadDevice(@AudioRoute.AudioRouteType int type,
+ private boolean isLeAudioNonLeadDeviceOrServiceUnavailable(@AudioRoute.AudioRouteType int type,
BluetoothDevice device) {
if (type != AudioRoute.TYPE_BLUETOOTH_LE) {
return false;
+ } else if (getLeAudioService() == null) {
+ return true;
}
+
int groupId = getLeAudioService().getGroupId(device);
if (groupId != BluetoothLeAudio.GROUP_ID_INVALID) {
- return !device.getAddress().equals(
- getLeAudioService().getConnectedGroupLeadDevice(groupId).getAddress());
+ BluetoothDevice leadDevice = getLeAudioService().getConnectedGroupLeadDevice(groupId);
+ Log.i(this, "Lead device for device (%s) is %s.", device, leadDevice);
+ return leadDevice == null || !device.getAddress().equals(leadDevice.getAddress());
}
return false;
}
@@ -1195,6 +1231,18 @@
mAudioRouteFactory = audioRouteFactory;
}
+ public Map<AudioRoute, BluetoothDevice> getBluetoothRoutes() {
+ return mBluetoothRoutes;
+ }
+
+ public void overrideIsPending(boolean isPending) {
+ mIsPending = isPending;
+ }
+
+ public void setIsScoAudioConnected(boolean value) {
+ mIsScoAudioConnected = value;
+ }
+
@VisibleForTesting
public void setActive(boolean active) {
if (active) {
diff --git a/src/com/android/server/telecom/CallsManager.java b/src/com/android/server/telecom/CallsManager.java
index defc9bb..0744502 100644
--- a/src/com/android/server/telecom/CallsManager.java
+++ b/src/com/android/server/telecom/CallsManager.java
@@ -659,6 +659,7 @@
}
callAudioRouteAdapter.initialize();
bluetoothStateReceiver.setCallAudioRouteAdapter(callAudioRouteAdapter);
+ bluetoothDeviceManager.setCallAudioRouteAdapter(callAudioRouteAdapter);
CallAudioRoutePeripheralAdapter callAudioRoutePeripheralAdapter =
new CallAudioRoutePeripheralAdapter(
diff --git a/src/com/android/server/telecom/PendingAudioRoute.java b/src/com/android/server/telecom/PendingAudioRoute.java
index 6ba09a5..f9cdc35 100644
--- a/src/com/android/server/telecom/PendingAudioRoute.java
+++ b/src/com/android/server/telecom/PendingAudioRoute.java
@@ -22,10 +22,12 @@
import android.bluetooth.BluetoothDevice;
import android.media.AudioManager;
import android.telecom.Log;
+import android.util.ArraySet;
+import android.util.Pair;
import com.android.server.telecom.bluetooth.BluetoothRouteManager;
-import java.util.ArrayList;
+import java.util.Set;
/**
* Used to represent the intermediate state during audio route switching.
@@ -47,7 +49,7 @@
* by new switching request during the ongoing switching
*/
private AudioRoute mDestRoute;
- private ArrayList<Integer> mPendingMessages;
+ private Set<Pair<Integer, String>> mPendingMessages;
private boolean mActive;
/**
* The device that has been set for communication by Telecom
@@ -59,7 +61,7 @@
mCallAudioRouteController = controller;
mAudioManager = audioManager;
mBluetoothRouteManager = bluetoothRouteManager;
- mPendingMessages = new ArrayList<>();
+ mPendingMessages = new ArraySet<>();
mActive = false;
mCommunicationDeviceType = AudioRoute.TYPE_INVALID;
}
@@ -73,24 +75,25 @@
return mOrigRoute;
}
- void setDestRoute(boolean active, AudioRoute destRoute, BluetoothDevice device) {
+ void setDestRoute(boolean active, AudioRoute destRoute, BluetoothDevice device,
+ boolean isScoAudioConnected) {
destRoute.onDestRouteAsPendingRoute(active, this, device,
- mAudioManager, mBluetoothRouteManager);
+ mAudioManager, mBluetoothRouteManager, isScoAudioConnected);
mActive = active;
mDestRoute = destRoute;
}
- AudioRoute getDestRoute() {
+ public AudioRoute getDestRoute() {
return mDestRoute;
}
- public void addMessage(int message) {
- mPendingMessages.add(message);
+ public void addMessage(int message, String bluetoothDevice) {
+ mPendingMessages.add(new Pair<>(message, bluetoothDevice));
}
- public void onMessageReceived(int message, String btAddressToExclude) {
+ public void onMessageReceived(Pair<Integer, String> message, String btAddressToExclude) {
Log.i(this, "onMessageReceived: message - %s", message);
- if (message == PENDING_ROUTE_FAILED) {
+ if (message.first == PENDING_ROUTE_FAILED) {
// Fallback to base route
mCallAudioRouteController.sendMessageWithSessionInfo(
SWITCH_BASELINE_ROUTE, 0, btAddressToExclude);
@@ -98,7 +101,7 @@
}
// Removes the first occurrence of the specified message from this list, if it is present.
- mPendingMessages.remove((Object) message);
+ mPendingMessages.remove(message);
evaluatePendingState();
}
@@ -107,9 +110,7 @@
mCallAudioRouteController.sendMessageWithSessionInfo(
CallAudioRouteAdapter.EXIT_PENDING_ROUTE);
} else {
- for(Integer i: mPendingMessages) {
- Log.d(this, "evaluatePendingState: pending Messages - %d", i);
- }
+ Log.i(this, "evaluatePendingState: mPendingMessages - %s", mPendingMessages);
}
}
@@ -117,6 +118,10 @@
mPendingMessages.clear();
}
+ public void clearPendingMessage(Pair<Integer, String> message) {
+ mPendingMessages.remove(message);
+ }
+
public boolean isActive() {
return mActive;
}
@@ -129,4 +134,8 @@
@AudioRoute.AudioRouteType int communicationDeviceType) {
mCommunicationDeviceType = communicationDeviceType;
}
+
+ public void overrideDestRoute(AudioRoute route) {
+ mDestRoute = route;
+ }
}
diff --git a/src/com/android/server/telecom/bluetooth/BluetoothDeviceManager.java b/src/com/android/server/telecom/bluetooth/BluetoothDeviceManager.java
index f220648..0a8ce5a 100644
--- a/src/com/android/server/telecom/bluetooth/BluetoothDeviceManager.java
+++ b/src/com/android/server/telecom/bluetooth/BluetoothDeviceManager.java
@@ -18,6 +18,8 @@
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.CallAudioRouteAdapter.BT_DEVICE_REMOVED;
+import static com.android.server.telecom.CallAudioRouteAdapter.SWITCH_BASELINE_ROUTE;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
@@ -34,20 +36,25 @@
import android.telecom.Log;
import android.util.ArraySet;
import android.util.LocalLog;
+import android.util.Pair;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.IndentingPrintWriter;
import com.android.server.telecom.AudioRoute;
import com.android.server.telecom.CallAudioCommunicationDeviceTracker;
+import com.android.server.telecom.CallAudioRouteAdapter;
+import com.android.server.telecom.CallAudioRouteController;
import com.android.server.telecom.flags.FeatureFlags;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
+import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
+import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
@@ -62,6 +69,16 @@
public static final int DEVICE_TYPE_HEARING_AID = 1;
public static final int DEVICE_TYPE_LE_AUDIO = 2;
+ private static final Map<Integer, Integer> PROFILE_TO_AUDIO_ROUTE_MAP = new HashMap<>();
+ static {
+ PROFILE_TO_AUDIO_ROUTE_MAP.put(BluetoothProfile.HEADSET,
+ AudioRoute.TYPE_BLUETOOTH_SCO);
+ PROFILE_TO_AUDIO_ROUTE_MAP.put(BluetoothProfile.LE_AUDIO,
+ AudioRoute.TYPE_BLUETOOTH_LE);
+ PROFILE_TO_AUDIO_ROUTE_MAP.put(BluetoothProfile.HEARING_AID,
+ TYPE_BLUETOOTH_HA);
+ }
+
private BluetoothLeAudio.Callback mLeAudioCallbacks =
new BluetoothLeAudio.Callback() {
@Override
@@ -198,11 +215,15 @@
Log.i(BluetoothDeviceManager.this, logString);
mLocalLog.log(logString);
- List<BluetoothDevice> devicesToRemove = new LinkedList<>(
- lostServiceDevices.values());
- lostServiceDevices.clear();
- for (BluetoothDevice device : devicesToRemove) {
- mBluetoothRouteManager.onDeviceLost(device.getAddress());
+ if (mFeatureFlags.useRefactoredAudioRouteSwitching()) {
+ handleAudioRefactoringServiceDisconnected(profile);
+ } else {
+ List<BluetoothDevice> devicesToRemove = new LinkedList<>(
+ lostServiceDevices.values());
+ lostServiceDevices.clear();
+ for (BluetoothDevice device : devicesToRemove) {
+ mBluetoothRouteManager.onDeviceLost(device.getAddress());
+ }
}
}
} finally {
@@ -211,6 +232,34 @@
}
};
+ private void handleAudioRefactoringServiceDisconnected(int profile) {
+ CallAudioRouteController controller = (CallAudioRouteController)
+ mCallAudioRouteAdapter;
+ Map<AudioRoute, BluetoothDevice> btRoutes = controller
+ .getBluetoothRoutes();
+ List<Pair<AudioRoute, BluetoothDevice>> btRoutesToRemove =
+ new ArrayList<>();
+ for (AudioRoute route: btRoutes.keySet()) {
+ if (route.getType() != PROFILE_TO_AUDIO_ROUTE_MAP.get(profile)) {
+ continue;
+ }
+ BluetoothDevice device = btRoutes.get(route);
+ // Prevent concurrent modification exception by just iterating through keys instead of
+ // simultaneously removing them.
+ btRoutesToRemove.add(new Pair<>(route, device));
+ }
+
+ for (Pair<AudioRoute, BluetoothDevice> routeToRemove:
+ btRoutesToRemove) {
+ AudioRoute route = routeToRemove.first;
+ BluetoothDevice device = routeToRemove.second;
+ mCallAudioRouteAdapter.sendMessageWithSessionInfo(
+ BT_DEVICE_REMOVED, route.getType(), device);
+ }
+ mCallAudioRouteAdapter.sendMessageWithSessionInfo(
+ SWITCH_BASELINE_ROUTE, 0, (String) null);
+ }
+
private final LinkedHashMap<String, BluetoothDevice> mHfpDevicesByAddress =
new LinkedHashMap<>();
private final LinkedHashMap<String, BluetoothDevice> mHearingAidDevicesByAddress =
@@ -249,6 +298,7 @@
private AudioManager mAudioManager;
private Executor mExecutor;
private CallAudioCommunicationDeviceTracker mCommunicationDeviceTracker;
+ private CallAudioRouteAdapter mCallAudioRouteAdapter;
private FeatureFlags mFeatureFlags;
public BluetoothDeviceManager(Context context, BluetoothAdapter bluetoothAdapter,
@@ -889,6 +939,10 @@
}
}
+ public void setCallAudioRouteAdapter(CallAudioRouteAdapter adapter) {
+ mCallAudioRouteAdapter = adapter;
+ }
+
public void dump(IndentingPrintWriter pw) {
mLocalLog.dump(pw);
}
diff --git a/src/com/android/server/telecom/bluetooth/BluetoothStateReceiver.java b/src/com/android/server/telecom/bluetooth/BluetoothStateReceiver.java
index af9e07b..9168388 100644
--- a/src/com/android/server/telecom/bluetooth/BluetoothStateReceiver.java
+++ b/src/com/android/server/telecom/bluetooth/BluetoothStateReceiver.java
@@ -23,7 +23,6 @@
import static com.android.server.telecom.CallAudioRouteAdapter.BT_DEVICE_ADDED;
import static com.android.server.telecom.CallAudioRouteAdapter.BT_DEVICE_REMOVED;
import static com.android.server.telecom.CallAudioRouteAdapter.PENDING_ROUTE_FAILED;
-import static com.android.server.telecom.CallAudioRouteAdapter.SWITCH_BLUETOOTH;
import static com.android.server.telecom.bluetooth.BluetoothRouteManager.BT_AUDIO_IS_ON;
import static com.android.server.telecom.bluetooth.BluetoothRouteManager.BT_AUDIO_LOST;
@@ -41,6 +40,7 @@
import android.os.Bundle;
import android.telecom.Log;
import android.telecom.Logging.Session;
+import android.util.Pair;
import com.android.internal.os.SomeArgs;
import com.android.server.telecom.AudioRoute;
@@ -50,7 +50,6 @@
import com.android.server.telecom.flags.FeatureFlags;
import com.android.server.telecom.flags.Flags;
-
public class BluetoothStateReceiver extends BroadcastReceiver {
private static final String LOG_TAG = BluetoothStateReceiver.class.getSimpleName();
public static final IntentFilter INTENT_FILTER;
@@ -119,9 +118,28 @@
args.arg2 = device.getAddress();
switch (bluetoothHeadsetAudioState) {
case BluetoothHeadset.STATE_AUDIO_CONNECTED:
- if (Flags.useRefactoredAudioRouteSwitching()) {
- mCallAudioRouteAdapter.sendMessageWithSessionInfo(BT_AUDIO_CONNECTED, 0,
- device);
+ if (mFeatureFlags.useRefactoredAudioRouteSwitching()) {
+ CallAudioRouteController audioRouteController =
+ (CallAudioRouteController) mCallAudioRouteAdapter;
+ audioRouteController.setIsScoAudioConnected(true);
+ if (audioRouteController.isPending()) {
+ mCallAudioRouteAdapter.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.
+ AudioRoute btRoute = audioRouteController.getBluetoothRoute(
+ AudioRoute.TYPE_BLUETOOTH_SCO, device.getAddress());
+ if (btRoute != null) {
+ audioRouteController.getPendingAudioRoute().overrideDestRoute(btRoute);
+ audioRouteController.overrideIsPending(true);
+ audioRouteController.getPendingAudioRoute()
+ .setCommunicationDeviceType(AudioRoute.TYPE_BLUETOOTH_SCO);
+ mCallAudioRouteAdapter.sendMessageWithSessionInfo(
+ CallAudioRouteAdapter.EXIT_PENDING_ROUTE);
+ }
+ }
} else {
if (!mIsInCall) {
Log.i(LOG_TAG, "Ignoring BT audio on since we're not in a call");
@@ -132,6 +150,9 @@
break;
case BluetoothHeadset.STATE_AUDIO_DISCONNECTED:
if (Flags.useRefactoredAudioRouteSwitching()) {
+ CallAudioRouteController audioRouteController =
+ (CallAudioRouteController) mCallAudioRouteAdapter;
+ audioRouteController.setIsScoAudioConnected(false);
mCallAudioRouteAdapter.sendMessageWithSessionInfo(BT_AUDIO_DISCONNECTED, 0,
device);
} else {
@@ -229,7 +250,8 @@
+ "communication device for %s. Sending PENDING_ROUTE_FAILED to "
+ "pending audio route.", device);
mCallAudioRouteAdapter.getPendingAudioRoute()
- .onMessageReceived(PENDING_ROUTE_FAILED, device.getAddress());
+ .onMessageReceived(new Pair<>(PENDING_ROUTE_FAILED,
+ device.getAddress()), device.getAddress());
} else {
// Track the currently set communication device.
int routeType = deviceType == BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO
diff --git a/tests/src/com/android/server/telecom/tests/CallAudioRouteControllerTest.java b/tests/src/com/android/server/telecom/tests/CallAudioRouteControllerTest.java
index f770b6a..59473bd 100644
--- a/tests/src/com/android/server/telecom/tests/CallAudioRouteControllerTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallAudioRouteControllerTest.java
@@ -34,7 +34,7 @@
import static com.android.server.telecom.CallAudioRouteAdapter.SPEAKER_ON;
import static com.android.server.telecom.CallAudioRouteAdapter.STREAMING_FORCE_DISABLED;
import static com.android.server.telecom.CallAudioRouteAdapter.STREAMING_FORCE_ENABLED;
-import static com.android.server.telecom.CallAudioRouteAdapter.SWITCH_BLUETOOTH;
+import static com.android.server.telecom.CallAudioRouteAdapter.SWITCH_BASELINE_ROUTE;
import static com.android.server.telecom.CallAudioRouteAdapter.SWITCH_FOCUS;
import static com.android.server.telecom.CallAudioRouteAdapter.USER_SWITCH_BLUETOOTH;
import static com.android.server.telecom.CallAudioRouteAdapter.USER_SWITCH_EARPIECE;
@@ -64,14 +64,18 @@
import android.media.audiopolicy.AudioProductStrategy;
import android.os.UserHandle;
import android.telecom.CallAudioState;
+import android.telecom.VideoProfile;
import androidx.test.filters.SmallTest;
import com.android.server.telecom.AudioRoute;
+import com.android.server.telecom.Call;
import com.android.server.telecom.CallAudioManager;
import com.android.server.telecom.CallAudioRouteController;
import com.android.server.telecom.CallsManager;
+import com.android.server.telecom.PendingAudioRoute;
import com.android.server.telecom.StatusBarNotifier;
+import com.android.server.telecom.TelecomSystem;
import com.android.server.telecom.WiredHeadsetManager;
import com.android.server.telecom.bluetooth.BluetoothDeviceManager;
import com.android.server.telecom.bluetooth.BluetoothRouteManager;
@@ -102,6 +106,9 @@
@Mock StatusBarNotifier mockStatusBarNotifier;
@Mock AudioDeviceInfo mAudioDeviceInfo;
@Mock BluetoothLeAudio mBluetoothLeAudio;
+ @Mock CallAudioManager mCallAudioManager;
+ @Mock Call mCall;
+ @Mock private TelecomSystem.SyncRoot mLock;
private AudioRoute mEarpieceRoute;
private AudioRoute mSpeakerRoute;
private static final String BT_ADDRESS_1 = "00:00:00:00:00:01";
@@ -143,6 +150,7 @@
any(CallAudioState.class));
when(mCallsManager.getCurrentUserHandle()).thenReturn(
new UserHandle(UserHandle.USER_SYSTEM));
+ when(mCallsManager.getLock()).thenReturn(mLock);
when(mBluetoothRouteManager.getDeviceManager()).thenReturn(mBluetoothDeviceManager);
when(mBluetoothDeviceManager.connectAudio(any(BluetoothDevice.class), anyInt()))
.thenReturn(true);
@@ -160,6 +168,9 @@
mController.setAudioManager(mAudioManager);
mEarpieceRoute = new AudioRoute(AudioRoute.TYPE_EARPIECE, null, null);
mSpeakerRoute = new AudioRoute(AudioRoute.TYPE_SPEAKER, null, null);
+ mController.setCallAudioManager(mCallAudioManager);
+ when(mCallAudioManager.getForegroundCall()).thenReturn(mCall);
+ when(mCall.getVideoState()).thenReturn(VideoProfile.STATE_AUDIO_ONLY);
when(mFeatureFlags.ignoreAutoRouteToWatchDevice()).thenReturn(false);
}
@@ -191,6 +202,78 @@
@SmallTest
@Test
+ public void testInitializeWithWiredHeadset() {
+ AudioRoute wiredHeadsetRoute = new AudioRoute(AudioRoute.TYPE_WIRED, null, null);
+ when(mWiredHeadsetManager.isPluggedIn()).thenReturn(true);
+ mController.initialize();
+ assertEquals(wiredHeadsetRoute, mController.getCurrentRoute());
+ assertEquals(2, mController.getAvailableRoutes().size());
+ assertTrue(mController.getAvailableRoutes().contains(mSpeakerRoute));
+ }
+
+ @SmallTest
+ @Test
+ public void testNormalCallRouteToEarpiece() {
+ mController.initialize();
+ mController.sendMessageWithSessionInfo(SWITCH_FOCUS, ACTIVE_FOCUS);
+ // Verify that pending audio destination route is set to speaker. This will trigger pending
+ // message to wait for SPEAKER_ON message once communication device is set before routing.
+ waitForHandlerAction(mController.getAdapterHandler(), TEST_TIMEOUT);
+ PendingAudioRoute pendingRoute = mController.getPendingAudioRoute();
+ assertEquals(AudioRoute.TYPE_EARPIECE, pendingRoute.getDestRoute().getType());
+
+ CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE,
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER, null,
+ new HashSet<>());
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+ }
+
+ @SmallTest
+ @Test
+ public void testVideoCallHoldRouteToEarpiece() {
+ mController.initialize();
+ mController.sendMessageWithSessionInfo(SWITCH_FOCUS, ACTIVE_FOCUS);
+ // Verify that pending audio destination route is not defaulted to speaker when a video call
+ // is not the foreground call.
+ waitForHandlerAction(mController.getAdapterHandler(), TEST_TIMEOUT);
+ PendingAudioRoute pendingRoute = mController.getPendingAudioRoute();
+ assertEquals(AudioRoute.TYPE_EARPIECE, pendingRoute.getDestRoute().getType());
+ }
+
+ @SmallTest
+ @Test
+ public void testVideoCallRouteToSpeaker() {
+ when(mCall.getVideoState()).thenReturn(VideoProfile.STATE_BIDIRECTIONAL);
+ mController.initialize();
+ mController.sendMessageWithSessionInfo(SWITCH_FOCUS, ACTIVE_FOCUS);
+ // Verify that pending audio destination route is set to speaker. This will trigger pending
+ // message to wait for SPEAKER_ON message once communication device is set before routing.
+ waitForHandlerAction(mController.getAdapterHandler(), TEST_TIMEOUT);
+ PendingAudioRoute pendingRoute = mController.getPendingAudioRoute();
+ assertEquals(AudioRoute.TYPE_SPEAKER, pendingRoute.getDestRoute().getType());
+
+ // Mock SPEAKER_ON message received by controller.
+ mController.sendMessageWithSessionInfo(SPEAKER_ON);
+ CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_SPEAKER,
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER, null,
+ new HashSet<>());
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+
+ // Verify that audio is routed to wired headset if it's present.
+ expectedState = new CallAudioState(false, CallAudioState.ROUTE_WIRED_HEADSET,
+ CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_SPEAKER, null,
+ new HashSet<>());
+ mController.sendMessageWithSessionInfo(CONNECT_WIRED_HEADSET);
+ waitForHandlerAction(mController.getAdapterHandler(), TEST_TIMEOUT);
+ mController.sendMessageWithSessionInfo(SPEAKER_OFF);
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+ }
+
+ @SmallTest
+ @Test
public void testActiveDeactivateBluetoothDevice() {
mController.initialize();
mController.sendMessageWithSessionInfo(BT_DEVICE_ADDED, AudioRoute.TYPE_BLUETOOTH_SCO,
@@ -435,8 +518,7 @@
@SmallTest
@Test
public void testToggleMute() throws Exception {
- when(mAudioManager.isMasterMute()).thenReturn(false);
-
+ when(mAudioManager.isMicrophoneMute()).thenReturn(false);
mController.initialize();
mController.setActive(true);
@@ -449,7 +531,7 @@
verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
any(CallAudioState.class), eq(expectedState));
- when(mAudioManager.isMasterMute()).thenReturn(true);
+ when(mAudioManager.isMicrophoneMute()).thenReturn(true);
mController.sendMessageWithSessionInfo(MUTE_OFF);
expectedState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE,
CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER, null,
@@ -462,6 +544,34 @@
@SmallTest
@Test
+ public void testMuteOffAfterCallEnds() throws Exception {
+ when(mAudioManager.isMicrophoneMute()).thenReturn(false);
+ mController.initialize();
+ mController.setActive(true);
+
+ mController.sendMessageWithSessionInfo(MUTE_ON);
+ CallAudioState expectedState = new CallAudioState(true, CallAudioState.ROUTE_EARPIECE,
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER, null,
+ new HashSet<>());
+ verify(mAudioService, timeout(TEST_TIMEOUT)).setMicrophoneMute(eq(true), anyString(),
+ anyInt(), anyString());
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+
+ // Switch to NO_FOCUS to indicate call termination and verify mute is reset.
+ when(mAudioManager.isMicrophoneMute()).thenReturn(true);
+ mController.sendMessageWithSessionInfo(SWITCH_FOCUS, NO_FOCUS);
+ expectedState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE,
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER, null,
+ new HashSet<>());
+ verify(mAudioService, timeout(TEST_TIMEOUT)).setMicrophoneMute(eq(false), anyString(),
+ anyInt(), anyString());
+ verify(mCallsManager, timeout(TEST_TIMEOUT).atLeastOnce()).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+ }
+
+ @SmallTest
+ @Test
public void testIgnoreAutoRouteToWatch() {
when(mFeatureFlags.ignoreAutoRouteToWatchDevice()).thenReturn(true);
when(mBluetoothRouteManager.isWatch(any(BluetoothDevice.class))).thenReturn(true);
@@ -591,6 +701,23 @@
BLUETOOTH_DEVICES.remove(scoDevice);
}
+ @SmallTest
+ @Test
+ public void testIgnoreLeRouteWhenServiceUnavailable() {
+ when(mBluetoothLeAudio.getConnectedGroupLeadDevice(anyInt()))
+ .thenReturn(BLUETOOTH_DEVICE_1);
+ verifyConnectBluetoothDevice(AudioRoute.TYPE_BLUETOOTH_LE);
+
+ when(mBluetoothDeviceManager.getLeAudioService()).thenReturn(null);
+ // Switch baseline to verify that we don't route back to LE audio this time.
+ mController.sendMessageWithSessionInfo(SWITCH_BASELINE_ROUTE, 0, (String) null);
+ CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE,
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH
+ | CallAudioState.ROUTE_SPEAKER, null, BLUETOOTH_DEVICES);
+ verify(mCallsManager, timeout(TEST_TIMEOUT).atLeastOnce()).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+ }
+
private void verifyConnectBluetoothDevice(int audioType) {
mController.initialize();
mController.setActive(true);