Resolve call audio refactoring issues.
As part of the new call audio refactor changes that were introduced,
this CL resolves the issues that were found during testing as well as
other known issues that were originally found in the legacy code.
In the new code path, BT SCO was leveraging setCommunicationDevice but
this currently isn't available so explicit connection has to be
established as is done in the legacy path. There were also problems with
switching between LE/HFP devices which is being addressed in this CL. It
also takes care of ignoring auto routing to wearable devices as well as
ensuring that audio routing to HA devices is working as intended.
Bug: 328287261
Test: Manual testing with single case scenario of
connecting/disconnecting HFP/LE/HA devices as well as testing switching
between LE/HFP devices. Testing was also done to ensure auto-routing to
wearable devices is ignored unless explicitly requested by the user.
Test: atest CallAudioRouteControllerTest
Change-Id: I989b2d018512e73b11a597a1d688faf88a8433c2
diff --git a/src/com/android/server/telecom/AudioRoute.java b/src/com/android/server/telecom/AudioRoute.java
index cdf44a8..7b593d7 100644
--- a/src/com/android/server/telecom/AudioRoute.java
+++ b/src/com/android/server/telecom/AudioRoute.java
@@ -23,11 +23,14 @@
import static com.android.server.telecom.CallAudioRouteAdapter.SPEAKER_ON;
import android.annotation.IntDef;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothStatusCodes;
import android.media.AudioDeviceInfo;
import android.media.AudioManager;
import android.telecom.Log;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.telecom.bluetooth.BluetoothRouteManager;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -38,6 +41,7 @@
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
+import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
@@ -46,9 +50,10 @@
public static class Factory {
private final ScheduledExecutorService mScheduledExecutorService =
new ScheduledThreadPoolExecutor(1);
- private final CompletableFuture<AudioRoute> mAudioRouteFuture = new CompletableFuture<>();
+ private CompletableFuture<AudioRoute> mAudioRouteFuture;
public AudioRoute create(@AudioRouteType int type, String bluetoothAddress,
AudioManager audioManager) throws RuntimeException {
+ mAudioRouteFuture = new CompletableFuture();
createRetry(type, bluetoothAddress, audioManager, MAX_CONNECTION_RETRIES);
try {
return mAudioRouteFuture.get();
@@ -58,8 +63,10 @@
}
private void createRetry(@AudioRouteType int type, String bluetoothAddress,
AudioManager audioManager, int retryCount) {
+ // Early exit if exceeded max number of retries (and complete the future).
if (retryCount == 0) {
mAudioRouteFuture.complete(null);
+ return;
}
Log.i(this, "creating AudioRoute with type %s and address %s, retry count %d",
@@ -81,10 +88,17 @@
}
}
}
- if (routeInfo == null) {
- mScheduledExecutorService.schedule(
- () -> createRetry(type, bluetoothAddress, audioManager, retryCount - 1),
- RETRY_TIME_DELAY, TimeUnit.MILLISECONDS);
+ // Try connecting BT device anyway (to handle wearables not showing as available
+ // communication device or LE device not showing up since it may not be the lead
+ // device).
+ if (routeInfo == null && bluetoothAddress == null) {
+ try {
+ mScheduledExecutorService.schedule(
+ () -> createRetry(type, bluetoothAddress, audioManager, retryCount - 1),
+ RETRY_TIME_DELAY, TimeUnit.MILLISECONDS);
+ } catch (RejectedExecutionException e) {
+ Log.e(this, e, "Could not schedule retry for audio routing.");
+ }
} else {
mAudioRouteFuture.complete(new AudioRoute(type, bluetoothAddress, routeInfo));
}
@@ -222,30 +236,81 @@
// Invoked when entered pending route whose dest route is this route
void onDestRouteAsPendingRoute(boolean active, PendingAudioRoute pendingAudioRoute,
- AudioManager audioManager) {
+ BluetoothDevice device, AudioManager audioManager,
+ BluetoothRouteManager bluetoothRouteManager) {
+ Log.i(this, "onDestRouteAsPendingRoute: active (%b), type (%d)", active, mAudioRouteType);
if (pendingAudioRoute.isActive() && !active) {
- Log.i(this, "clearCommunicationDevice");
- audioManager.clearCommunicationDevice();
+ clearCommunicationDevice(pendingAudioRoute, bluetoothRouteManager, audioManager);
} else if (active) {
- if (mAudioRouteType == TYPE_BLUETOOTH_SCO) {
- pendingAudioRoute.addMessage(BT_AUDIO_CONNECTED);
+ // Handle BT routing case.
+ if (BT_AUDIO_ROUTE_TYPES.contains(mAudioRouteType)) {
+ boolean connectedBtAudio = connectBtAudio(pendingAudioRoute, device,
+ audioManager, bluetoothRouteManager);
+ // Special handling for SCO case.
+ if (mAudioRouteType == TYPE_BLUETOOTH_SCO) {
+ // 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) {
+ pendingAudioRoute.setCommunicationDeviceType(mAudioRouteType);
+ }
+ if (connectedBtAudio) {
+ pendingAudioRoute.addMessage(BT_AUDIO_CONNECTED);
+ } else if (!scoConnected) {
+ pendingAudioRoute.onMessageReceived(
+ PENDING_ROUTE_FAILED, mBluetoothAddress);
+ }
+ return;
+ }
} else if (mAudioRouteType == TYPE_SPEAKER) {
pendingAudioRoute.addMessage(SPEAKER_ON);
}
- if (!audioManager.setCommunicationDevice(mInfo)) {
- pendingAudioRoute.onMessageReceived(PENDING_ROUTE_FAILED);
+
+ boolean result = false;
+ List<AudioDeviceInfo> devices = audioManager.getAvailableCommunicationDevices();
+ for (AudioDeviceInfo deviceInfo : devices) {
+ // It's possible for the AudioDeviceInfo to be updated for the BT device so adjust
+ // mInfo accordingly.
+ if (BT_AUDIO_ROUTE_TYPES.contains(mAudioRouteType) && mBluetoothAddress
+ .equals(deviceInfo.getAddress())) {
+ mInfo = deviceInfo;
+ }
+ if (deviceInfo.equals(mInfo)) {
+ result = audioManager.setCommunicationDevice(mInfo);
+ if (result) {
+ pendingAudioRoute.setCommunicationDeviceType(mAudioRouteType);
+ }
+ Log.i(this, "Result of setting communication device for audio "
+ + "route (%s) - %b", this, result);
+ break;
+ }
+ }
+
+ // It's possible that BluetoothStateReceiver needs to report that the device is active
+ // before being able to successfully set the communication device. Refrain from sending
+ // pending route failed message for BT route until the second attempt fails.
+ if (!result && !BT_AUDIO_ROUTE_TYPES.contains(mAudioRouteType)) {
+ pendingAudioRoute.onMessageReceived(PENDING_ROUTE_FAILED, null);
}
}
}
+ // Takes care of cleaning up original audio route (i.e. clearCommunicationDevice,
+ // sending SPEAKER_OFF, or disconnecting SCO).
void onOrigRouteAsPendingRoute(boolean active, PendingAudioRoute pendingAudioRoute,
- AudioManager audioManager) {
+ AudioManager audioManager, BluetoothRouteManager bluetoothRouteManager) {
+ Log.i(this, "onOrigRouteAsPendingRoute: active (%b), type (%d)", active, mAudioRouteType);
if (active) {
- if (mAudioRouteType == TYPE_BLUETOOTH_SCO) {
- pendingAudioRoute.addMessage(BT_AUDIO_DISCONNECTED);
- } else if (mAudioRouteType == TYPE_SPEAKER) {
+ if (mAudioRouteType == TYPE_SPEAKER) {
pendingAudioRoute.addMessage(SPEAKER_OFF);
}
+ 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);
+ }
}
}
@@ -282,4 +347,50 @@
+ ", Address=" + ((mBluetoothAddress != null) ? mBluetoothAddress : "invalid")
+ "]";
}
+
+ private boolean connectBtAudio(PendingAudioRoute pendingAudioRoute, BluetoothDevice device,
+ AudioManager audioManager, BluetoothRouteManager bluetoothRouteManager) {
+ // Ensure that if another BT device was set, it is disconnected before connecting
+ // the new one.
+ AudioRoute currentRoute = pendingAudioRoute.getOrigRoute();
+ if (currentRoute.getBluetoothAddress() != null &&
+ !currentRoute.getBluetoothAddress().equals(device.getAddress())) {
+ clearCommunicationDevice(pendingAudioRoute, bluetoothRouteManager, audioManager);
+ }
+
+ // Connect to the device (explicit handling for HFP devices).
+ boolean success = false;
+ if (device != null) {
+ success = bluetoothRouteManager.getDeviceManager()
+ .connectAudio(device, mAudioRouteType);
+ }
+
+ Log.i(this, "connectBtAudio: routeToConnectTo = %s, successful = %b",
+ this, success);
+ return success;
+ }
+
+ private 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.
+ if (pendingAudioRoute.getCommunicationDeviceType() == AudioRoute.TYPE_INVALID) {
+ return -1;
+ }
+
+ int result = BluetoothStatusCodes.SUCCESS;
+ if (pendingAudioRoute.getCommunicationDeviceType() == TYPE_BLUETOOTH_SCO) {
+ Log.i(this, "Disconnecting SCO device.");
+ result = bluetoothRouteManager.getDeviceManager().disconnectSco();
+ } else {
+ Log.i(this, "Clearing communication device for audio type %d.",
+ pendingAudioRoute.getCommunicationDeviceType());
+ audioManager.clearCommunicationDevice();
+ }
+
+ if (result == BluetoothStatusCodes.SUCCESS) {
+ pendingAudioRoute.setCommunicationDeviceType(AudioRoute.TYPE_INVALID);
+ }
+ return result;
+ }
}
diff --git a/src/com/android/server/telecom/CallAudioRouteAdapter.java b/src/com/android/server/telecom/CallAudioRouteAdapter.java
index 5585d09..9927c22 100644
--- a/src/com/android/server/telecom/CallAudioRouteAdapter.java
+++ b/src/com/android/server/telecom/CallAudioRouteAdapter.java
@@ -134,5 +134,6 @@
CallAudioState getCurrentCallAudioState();
boolean isHfpDeviceAvailable();
Handler getAdapterHandler();
+ PendingAudioRoute getPendingAudioRoute();
void dump(IndentingPrintWriter pw);
}
diff --git a/src/com/android/server/telecom/CallAudioRouteController.java b/src/com/android/server/telecom/CallAudioRouteController.java
index d38577d..7b29fc8 100644
--- a/src/com/android/server/telecom/CallAudioRouteController.java
+++ b/src/com/android/server/telecom/CallAudioRouteController.java
@@ -19,25 +19,24 @@
import static com.android.server.telecom.AudioRoute.BT_AUDIO_ROUTE_TYPES;
import static com.android.server.telecom.AudioRoute.TYPE_INVALID;
-import android.app.ActivityManager;
+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.UserInfo;
import android.media.AudioAttributes;
import android.media.AudioDeviceAttributes;
import android.media.AudioDeviceInfo;
import android.media.AudioManager;
import android.media.IAudioService;
import android.media.audiopolicy.AudioProductStrategy;
-import android.os.Binder;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Message;
import android.os.RemoteException;
-import android.os.UserHandle;
import android.telecom.CallAudioState;
import android.telecom.Log;
import android.telecom.Logging.Session;
@@ -49,8 +48,10 @@
import com.android.internal.os.SomeArgs;
import com.android.internal.util.IndentingPrintWriter;
import com.android.server.telecom.bluetooth.BluetoothRouteManager;
+import com.android.server.telecom.flags.FeatureFlags;
import java.util.HashSet;
+import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -90,6 +91,8 @@
private Map<Integer, AudioRoute> mTypeRoutes;
private PendingAudioRoute mPendingAudioRoute;
private AudioRoute.Factory mAudioRouteFactory;
+ private StatusBarNotifier mStatusBarNotifier;
+ private FeatureFlags mFeatureFlags;
private int mFocusType;
private final Object mLock = new Object();
private final BroadcastReceiver mSpeakerPhoneChangeReceiver = new BroadcastReceiver() {
@@ -152,12 +155,11 @@
private boolean mIsActive;
public CallAudioRouteController(
- Context context,
- CallsManager callsManager,
+ Context context, CallsManager callsManager,
CallAudioManager.AudioServiceFactory audioServiceFactory,
- AudioRoute.Factory audioRouteFactory,
- WiredHeadsetManager wiredHeadsetManager,
- BluetoothRouteManager bluetoothRouteManager) {
+ AudioRoute.Factory audioRouteFactory, WiredHeadsetManager wiredHeadsetManager,
+ BluetoothRouteManager bluetoothRouteManager, StatusBarNotifier statusBarNotifier,
+ FeatureFlags featureFlags) {
mContext = context;
mCallsManager = callsManager;
mAudioManager = context.getSystemService(AudioManager.class);
@@ -166,6 +168,8 @@
mWiredHeadsetManager = wiredHeadsetManager;
mIsMute = false;
mBluetoothRouteManager = bluetoothRouteManager;
+ mStatusBarNotifier = statusBarNotifier;
+ mFeatureFlags = featureFlags;
mFocusType = NO_FOCUS;
HandlerThread handlerThread = new HandlerThread(this.getClass().getSimpleName());
handlerThread.start();
@@ -246,8 +250,10 @@
case USER_SWITCH_SPEAKER:
handleSwitchSpeaker();
break;
+ case SWITCH_BASELINE_ROUTE:
case USER_SWITCH_BASELINE_ROUTE:
- handleSwitchBaselineRoute();
+ address = (String) ((SomeArgs) msg.obj).arg2;
+ handleSwitchBaselineRoute(address);
break;
case SPEAKER_ON:
handleSpeakerOn();
@@ -296,16 +302,16 @@
@Override
public void initialize() {
mAvailableRoutes = new HashSet<>();
- mBluetoothRoutes = new ArrayMap<>();
+ mBluetoothRoutes = new LinkedHashMap<>();
mTypeRoutes = new ArrayMap<>();
mStreamingRoutes = new HashSet<>();
- mPendingAudioRoute = new PendingAudioRoute(this, mAudioManager);
+ mPendingAudioRoute = new PendingAudioRoute(this, mAudioManager, mBluetoothRouteManager);
mStreamingRoute = new AudioRoute(AudioRoute.TYPE_STREAMING, null, null);
mStreamingRoutes.add(mStreamingRoute);
- int supportMask = calculateSupportedRouteMask();
+ int supportMask = calculateSupportedRouteMaskInit();
if ((supportMask & CallAudioState.ROUTE_SPEAKER) != 0) {
- // Create spekaer routes
+ // Create speaker routes
mSpeakerDockRoute = mAudioRouteFactory.create(AudioRoute.TYPE_SPEAKER, null,
mAudioManager);
if (mSpeakerDockRoute == null) {
@@ -391,7 +397,7 @@
@Override
public CallAudioState getCurrentCallAudioState() {
- return null;
+ return mCallAudioState;
}
@Override
@@ -405,6 +411,11 @@
}
@Override
+ public PendingAudioRoute getPendingAudioRoute() {
+ return mPendingAudioRoute;
+ }
+
+ @Override
public void dump(IndentingPrintWriter pw) {
}
@@ -434,6 +445,7 @@
private void routeTo(boolean active, AudioRoute destRoute) {
if (!destRoute.equals(mStreamingRoute) && !getAvailableRoutes().contains(destRoute)) {
+ Log.i(this, "Ignore routing to unavailable route: %s", destRoute);
return;
}
if (mIsPending) {
@@ -445,9 +457,9 @@
mPendingAudioRoute.getDestRoute(), mIsActive, destRoute, active);
// override pending route while keep waiting for still pending messages for the
// previous pending route
- mIsActive = active;
mPendingAudioRoute.setOrigRoute(mIsActive, mPendingAudioRoute.getDestRoute());
- mPendingAudioRoute.setDestRoute(active, destRoute);
+ mPendingAudioRoute.setDestRoute(active, destRoute, mBluetoothRoutes.get(destRoute));
+ mIsActive = active;
} else {
if (mCurrentRoute.equals(destRoute) && (mIsActive == active)) {
return;
@@ -461,7 +473,7 @@
// Avoid waiting for pending messages for an unavailable route
mPendingAudioRoute.setOrigRoute(mIsActive, DUMMY_ROUTE);
}
- mPendingAudioRoute.setDestRoute(active, destRoute);
+ mPendingAudioRoute.setDestRoute(active, destRoute, mBluetoothRoutes.get(destRoute));
mIsActive = active;
mIsPending = true;
}
@@ -512,7 +524,7 @@
// Route to expected state
if (mCurrentRoute.equals(wiredHeadsetRoute)) {
- routeTo(mIsActive, getBaseRoute(true));
+ routeTo(mIsActive, getBaseRoute(true, null));
}
}
@@ -551,7 +563,7 @@
// Route to expected state
if (mCurrentRoute.equals(dockRoute)) {
- routeTo(mIsActive, getBaseRoute(true));
+ routeTo(mIsActive, getBaseRoute(true, null));
}
}
@@ -567,38 +579,69 @@
if (mCurrentRoute.equals(mStreamingRoute)) {
mCurrentRoute = DUMMY_ROUTE;
onAvailableRoutesChanged();
- routeTo(mIsActive, getBaseRoute(true));
+ routeTo(mIsActive, getBaseRoute(true, null));
} 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.
+ *
+ * Message being handled: BT_AUDIO_CONNECTED
+ */
private void handleBtAudioActive(BluetoothDevice bluetoothDevice) {
if (mIsPending) {
+ Log.i(this, "handleBtAudioActive: is pending path");
if (Objects.equals(mPendingAudioRoute.getDestRoute().getBluetoothAddress(),
bluetoothDevice.getAddress())) {
- mPendingAudioRoute.onMessageReceived(BT_AUDIO_CONNECTED);
+ mPendingAudioRoute.onMessageReceived(BT_AUDIO_CONNECTED, null);
}
} else {
// ignore, not triggered by telecom
+ Log.i(this, "handleBtAudioActive: ignoring handling bt audio active.");
}
}
+ /**
+ * 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.
+ *
+ * Message being handled: BT_AUDIO_DISCONNECTED
+ */
private void handleBtAudioInactive(BluetoothDevice bluetoothDevice) {
if (mIsPending) {
+ Log.i(this, "handleBtAudioInactive: is pending path");
if (Objects.equals(mPendingAudioRoute.getOrigRoute().getBluetoothAddress(),
bluetoothDevice.getAddress())) {
- mPendingAudioRoute.onMessageReceived(BT_AUDIO_DISCONNECTED);
+ mPendingAudioRoute.onMessageReceived(BT_AUDIO_DISCONNECTED, null);
}
} else {
// ignore, not triggered by telecom
+ Log.i(this, "handleBtAudioInactive: ignoring handling bt audio inactive.");
}
}
+ /**
+ * 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.
+ *
+ * Message being handled: BT_DEVICE_ADDED
+ */
private void handleBtConnected(@AudioRoute.AudioRouteType int type,
BluetoothDevice bluetoothDevice) {
- AudioRoute bluetoothRoute = null;
- bluetoothRoute = mAudioRouteFactory.create(type, bluetoothDevice.getAddress(),
+ if (containsHearingAidPair(type, bluetoothDevice)) {
+ return;
+ }
+
+ AudioRoute bluetoothRoute = mAudioRouteFactory.create(type, bluetoothDevice.getAddress(),
mAudioManager);
if (bluetoothRoute == null) {
Log.w(this, "Can't find available audio device info for route type:"
@@ -611,6 +654,14 @@
}
}
+ /**
+ * 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.
+ *
+ * Message being handled: BT_DEVICE_REMOVED
+ */
private void handleBtDisconnected(@AudioRoute.AudioRouteType int type,
BluetoothDevice bluetoothDevice) {
// Clean up unavailable routes
@@ -624,25 +675,45 @@
// Fallback to an available route
if (Objects.equals(mCurrentRoute, bluetoothRoute)) {
- routeTo(mIsActive, getBaseRoute(false));
+ routeTo(mIsActive, getBaseRoute(true, null));
}
}
+ /**
+ * 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.
+ *
+ * Message being handled: BT_ACTIVE_DEVICE_PRESENT
+ */
private void handleBtActiveDevicePresent(@AudioRoute.AudioRouteType int type,
String deviceAddress) {
AudioRoute bluetoothRoute = getBluetoothRoute(type, deviceAddress);
if (bluetoothRoute != null) {
- Log.i(this, "request to route to bluetooth route: %s(active=%b)", bluetoothRoute,
+ Log.i(this, "request to route to bluetooth route: %s (active=%b)", bluetoothRoute,
mIsActive);
routeTo(mIsActive, bluetoothRoute);
+ } else {
+ Log.i(this, "request to route to unavailable bluetooth route - 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.
+ *
+ * Message being handled: BT_ACTIVE_DEVICE_GONE
+ */
private void handleBtActiveDeviceGone(@AudioRoute.AudioRouteType int type) {
if ((mIsPending && mPendingAudioRoute.getDestRoute().getType() == type)
|| (!mIsPending && mCurrentRoute.getType() == type)) {
// Fallback to an available route
- routeTo(mIsActive, getBaseRoute(true));
+ routeTo(mIsActive, getBaseRoute(true, null));
}
}
@@ -667,30 +738,38 @@
}
private void handleSwitchFocus(int focus) {
+ Log.i(this, "handleSwitchFocus: focus (%s)", focus);
mFocusType = focus;
switch (focus) {
case NO_FOCUS -> {
if (mIsActive) {
+ // Reset mute state after call ends.
handleMuteChanged(false);
+ // Route back to inactive route.
routeTo(false, mCurrentRoute);
+ // Clear pending messages
+ mPendingAudioRoute.clearPendingMessages();
}
}
case ACTIVE_FOCUS -> {
+ // Route to active baseline route, otherwise ignore if route is already active.
if (!mIsActive) {
- routeTo(true, getBaseRoute(true));
+ routeTo(true, getBaseRoute(true, null));
}
}
case RINGING_FOCUS -> {
if (!mIsActive) {
- AudioRoute route = getBaseRoute(true);
+ AudioRoute route = getBaseRoute(true, null);
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(device)) {
routeTo(false, route);
} else {
routeTo(true, route);
}
} else {
- // active
+ // Route is already active.
BluetoothDevice device = mBluetoothRoutes.get(mCurrentRoute);
if (device != null && !mBluetoothRouteManager.isInbandRingEnabled(device)) {
routeTo(false, mCurrentRoute);
@@ -729,7 +808,7 @@
routeTo(mIsActive, bluetoothRoute);
}
} else {
- Log.i(this, "ignore switch bluetooth request");
+ Log.i(this, "ignore switch bluetooth request to unavailable address");
}
}
@@ -738,7 +817,7 @@
if (headsetRoute != null && getAvailableRoutes().contains(headsetRoute)) {
routeTo(mIsActive, headsetRoute);
} else {
- Log.i(this, "ignore switch speaker request");
+ Log.i(this, "ignore switch headset request");
}
}
@@ -750,13 +829,16 @@
}
}
- private void handleSwitchBaselineRoute() {
- routeTo(mIsActive, getBaseRoute(true));
+ private void handleSwitchBaselineRoute(String btAddressToExclude) {
+ routeTo(mIsActive, getBaseRoute(true, btAddressToExclude));
}
private void handleSpeakerOn() {
if (isPending()) {
- mPendingAudioRoute.onMessageReceived(SPEAKER_ON);
+ Log.i(this, "handleSpeakerOn: sending SPEAKER_ON to pending audio route");
+ mPendingAudioRoute.onMessageReceived(SPEAKER_ON, null);
+ // Update status bar notification if we are in a call.
+ mStatusBarNotifier.notifySpeakerphone(mCallsManager.hasAnyCalls());
} else {
if (mSpeakerDockRoute != null && getAvailableRoutes().contains(mSpeakerDockRoute)) {
routeTo(mIsActive, mSpeakerDockRoute);
@@ -771,9 +853,12 @@
private void handleSpeakerOff() {
if (isPending()) {
- mPendingAudioRoute.onMessageReceived(SPEAKER_OFF);
+ Log.i(this, "handleSpeakerOff - sending SPEAKER_OFF to pending audio route");
+ mPendingAudioRoute.onMessageReceived(SPEAKER_OFF, null);
+ // Update status bar notification
+ mStatusBarNotifier.notifySpeakerphone(false);
} else if (mCurrentRoute.getType() == AudioRoute.TYPE_SPEAKER) {
- routeTo(mIsActive, getBaseRoute(true));
+ routeTo(mIsActive, getBaseRoute(true, null));
// 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) {
@@ -783,11 +868,15 @@
}
}
+ /**
+ * 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 (mIsPending) {
- Log.i(this, "Exit pending route and enter %s(active=%b)",
- mPendingAudioRoute.getDestRoute(), mIsActive);
mCurrentRoute = mPendingAudioRoute.getDestRoute();
+ Log.addEvent(mCallsManager.getForegroundCall(), LogUtils.Events.AUDIO_ROUTE,
+ "Entering audio route: " + mCurrentRoute + " (active=" + mIsActive + ")");
mIsPending = false;
onCurrentRouteChanged();
}
@@ -817,7 +906,20 @@
for (AudioRoute route : getAvailableRoutes()) {
routeMask |= ROUTE_MAP.get(route.getType());
if (BT_AUDIO_ROUTE_TYPES.contains(route.getType())) {
- availableBluetoothDevices.add(mBluetoothRoutes.get(route));
+ 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) {
+ 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);
+ }
}
}
updateCallAudioState(new CallAudioState(mIsMute, mCallAudioState.getRoute(), routeMask,
@@ -831,10 +933,12 @@
mCallAudioState.getSupportedBluetoothDevices()));
}
- private void updateCallAudioState(CallAudioState callAudioState) {
- Log.i(this, "updateCallAudioState: " + callAudioState);
+ private void updateCallAudioState(CallAudioState newCallAudioState) {
+ Log.i(this, "updateCallAudioState: updating call audio state to %s", newCallAudioState);
CallAudioState oldState = mCallAudioState;
- mCallAudioState = callAudioState;
+ mCallAudioState = newCallAudioState;
+ // Update status bar notification
+ mStatusBarNotifier.notifyMute(newCallAudioState.isMuted());
mCallsManager.onCallAudioStateChanged(oldState, mCallAudioState);
updateAudioStateForTrackedCalls(mCallAudioState);
}
@@ -866,6 +970,7 @@
// Get preferred device
AudioDeviceAttributes deviceAttr = mAudioManager.getPreferredDeviceForStrategy(strategy);
+ Log.i(this, "getPreferredAudioRouteFromStrategy: preferred device is %s", deviceAttr);
if (deviceAttr == null) {
return null;
}
@@ -881,16 +986,32 @@
}
}
- private AudioRoute getPreferredAudioRouteFromDefault(boolean includeBluetooth) {
- if (mBluetoothRoutes.isEmpty() || !includeBluetooth) {
+ private AudioRoute getPreferredAudioRouteFromDefault(boolean includeBluetooth,
+ String btAddressToExclude) {
+ // 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 (mBluetoothRoutes.isEmpty() || !includeBluetooth
+ || activeWatchOrNonWatchDeviceRoute == null) {
+ Log.i(this, "getPreferredAudioRouteFromDefault: Audio routing defaulting to "
+ + "available non-BT route.");
return mEarpieceWiredRoute != null ? mEarpieceWiredRoute : mSpeakerDockRoute;
} else {
- // Most recent active route will always be the last in the array
- return mBluetoothRoutes.keySet().stream().toList().get(mBluetoothRoutes.size() - 1);
+ // 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 = mFeatureFlags.ignoreAutoRouteToWatchDevice()
+ ? " (except watch)"
+ : "";
+ Log.i(this, "getPreferredAudioRouteFromDefault: Audio routing defaulting to "
+ + "most recently active BT route" + autoRoutingToWatchExcerpt + ".");
+ return activeWatchOrNonWatchDeviceRoute;
}
}
- private int calculateSupportedRouteMask() {
+ private int calculateSupportedRouteMaskInit() {
+ Log.i(this, "calculateSupportedRouteMaskInit: is wired headset plugged in - %s",
+ mWiredHeadsetManager.isPluggedIn());
int routeMask = CallAudioState.ROUTE_SPEAKER;
if (mWiredHeadsetManager.isPluggedIn()) {
@@ -931,17 +1052,139 @@
return null;
}
- public AudioRoute getBaseRoute(boolean includeBluetooth) {
+ public AudioRoute getBaseRoute(boolean includeBluetooth, String btAddressToExclude) {
AudioRoute destRoute = getPreferredAudioRouteFromStrategy();
if (destRoute == null) {
- destRoute = getPreferredAudioRouteFromDefault(includeBluetooth);
+ destRoute = getPreferredAudioRouteFromDefault(includeBluetooth, btAddressToExclude);
}
if (destRoute != null && !getAvailableRoutes().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;
+ 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) {
+ 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");
+ return true;
+ }
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * 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) {
+ if (!mFeatureFlags.ignoreAutoRouteToWatchDevice()) {
+ Log.i(this, "getActiveWatchOrNonWatchDeviceRoute: ignore_auto_route_to_watch_device "
+ + "flag is disabled. Routing to most recently reported active device.");
+ return getMostRecentlyActiveBtRoute(btAddressToExclude);
+ }
+
+ List<AudioRoute> bluetoothRoutes = mBluetoothRoutes.keySet().stream().toList();
+ // 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)
+ || isLeAudioNonLeadDevice(route.getType(), device)) {
+ continue;
+ }
+ // Check if the most recently active device is a watch device.
+ if (i == (bluetoothRoutes.size() - 1) && device.equals(mCallAudioState
+ .getActiveBluetoothDevice()) && mBluetoothRouteManager.isWatch(device)) {
+ Log.i(this, "getActiveWatchOrNonWatchDeviceRoute: Routing to active watch - %s",
+ bluetoothRoutes.get(0));
+ return bluetoothRoutes.get(0);
+ }
+ // Record the first occurrence of a non-watch device route if found.
+ if (!mBluetoothRouteManager.isWatch(device) && nonWatchDeviceRoute == null) {
+ nonWatchDeviceRoute = route;
+ break;
+ }
+ }
+
+ Log.i(this, "Routing to a non-watch device - %s", nonWatchDeviceRoute);
+ return nonWatchDeviceRoute;
+ }
+
+ /**
+ * Returns the most actively reported bluetooth route excluding the passed in route.
+ */
+ private AudioRoute getMostRecentlyActiveBtRoute(String btAddressToExclude) {
+ List<AudioRoute> bluetoothRoutes = mBluetoothRoutes.keySet().stream().toList();
+ 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))) {
+ continue;
+ }
+ if (!route.getBluetoothAddress().equals(btAddressToExclude)) {
+ return route;
+ }
+ }
+ return null;
+ }
+
+ private boolean isLeAudioNonLeadDevice(@AudioRoute.AudioRouteType int type,
+ BluetoothDevice device) {
+ if (type != AudioRoute.TYPE_BLUETOOTH_LE) {
+ return false;
+ }
+ int groupId = getLeAudioService().getGroupId(device);
+ if (groupId != BluetoothLeAudio.GROUP_ID_INVALID) {
+ return !device.getAddress().equals(
+ getLeAudioService().getConnectedGroupLeadDevice(groupId).getAddress());
+ }
+ return false;
+ }
+
+ private BluetoothLeAudio getLeAudioService() {
+ return mBluetoothRouteManager.getDeviceManager().getLeAudioService();
+ }
+
@VisibleForTesting
public void setAudioManager(AudioManager audioManager) {
mAudioManager = audioManager;
diff --git a/src/com/android/server/telecom/CallAudioRouteStateMachine.java b/src/com/android/server/telecom/CallAudioRouteStateMachine.java
index 26c25e8..621ba36 100644
--- a/src/com/android/server/telecom/CallAudioRouteStateMachine.java
+++ b/src/com/android/server/telecom/CallAudioRouteStateMachine.java
@@ -2095,7 +2095,14 @@
return base;
}
+ @Override
public Handler getAdapterHandler() {
return getHandler();
}
+
+ @Override
+ public PendingAudioRoute getPendingAudioRoute() {
+ // Only used by CallAudioRouteController.
+ return null;
+ }
}
diff --git a/src/com/android/server/telecom/CallsManager.java b/src/com/android/server/telecom/CallsManager.java
index 5d98bb8..7cd7bc6 100644
--- a/src/com/android/server/telecom/CallsManager.java
+++ b/src/com/android/server/telecom/CallsManager.java
@@ -652,7 +652,8 @@
);
} else {
callAudioRouteAdapter = new CallAudioRouteController(context, this, audioServiceFactory,
- new AudioRoute.Factory(), wiredHeadsetManager, mBluetoothRouteManager);
+ new AudioRoute.Factory(), wiredHeadsetManager, mBluetoothRouteManager,
+ statusBarNotifier, featureFlags);
}
callAudioRouteAdapter.initialize();
bluetoothStateReceiver.setCallAudioRouteAdapter(callAudioRouteAdapter);
diff --git a/src/com/android/server/telecom/PendingAudioRoute.java b/src/com/android/server/telecom/PendingAudioRoute.java
index 8de62ed..6ba09a5 100644
--- a/src/com/android/server/telecom/PendingAudioRoute.java
+++ b/src/com/android/server/telecom/PendingAudioRoute.java
@@ -17,8 +17,13 @@
package com.android.server.telecom;
import static com.android.server.telecom.CallAudioRouteAdapter.PENDING_ROUTE_FAILED;
+import static com.android.server.telecom.CallAudioRouteAdapter.SWITCH_BASELINE_ROUTE;
+import android.bluetooth.BluetoothDevice;
import android.media.AudioManager;
+import android.telecom.Log;
+
+import com.android.server.telecom.bluetooth.BluetoothRouteManager;
import java.util.ArrayList;
@@ -32,6 +37,7 @@
public class PendingAudioRoute {
private CallAudioRouteController mCallAudioRouteController;
private AudioManager mAudioManager;
+ private BluetoothRouteManager mBluetoothRouteManager;
/**
* The {@link AudioRoute} that this pending audio switching started with
*/
@@ -43,15 +49,23 @@
private AudioRoute mDestRoute;
private ArrayList<Integer> mPendingMessages;
private boolean mActive;
- PendingAudioRoute(CallAudioRouteController controller, AudioManager audioManager) {
+ /**
+ * The device that has been set for communication by Telecom
+ */
+ private @AudioRoute.AudioRouteType int mCommunicationDeviceType = AudioRoute.TYPE_INVALID;
+
+ PendingAudioRoute(CallAudioRouteController controller, AudioManager audioManager,
+ BluetoothRouteManager bluetoothRouteManager) {
mCallAudioRouteController = controller;
mAudioManager = audioManager;
+ mBluetoothRouteManager = bluetoothRouteManager;
mPendingMessages = new ArrayList<>();
mActive = false;
+ mCommunicationDeviceType = AudioRoute.TYPE_INVALID;
}
void setOrigRoute(boolean active, AudioRoute origRoute) {
- origRoute.onOrigRouteAsPendingRoute(active, this, mAudioManager);
+ origRoute.onOrigRouteAsPendingRoute(active, this, mAudioManager, mBluetoothRouteManager);
mOrigRoute = origRoute;
}
@@ -59,8 +73,9 @@
return mOrigRoute;
}
- void setDestRoute(boolean active, AudioRoute destRoute) {
- destRoute.onDestRouteAsPendingRoute(active, this, mAudioManager);
+ void setDestRoute(boolean active, AudioRoute destRoute, BluetoothDevice device) {
+ destRoute.onDestRouteAsPendingRoute(active, this, device,
+ mAudioManager, mBluetoothRouteManager);
mActive = active;
mDestRoute = destRoute;
}
@@ -73,12 +88,13 @@
mPendingMessages.add(message);
}
- public void onMessageReceived(int message) {
+ public void onMessageReceived(int message, String btAddressToExclude) {
+ Log.i(this, "onMessageReceived: message - %s", message);
if (message == PENDING_ROUTE_FAILED) {
// Fallback to base route
- mDestRoute = mCallAudioRouteController.getBaseRoute(true);
mCallAudioRouteController.sendMessageWithSessionInfo(
- CallAudioRouteAdapter.EXIT_PENDING_ROUTE);
+ SWITCH_BASELINE_ROUTE, 0, btAddressToExclude);
+ return;
}
// Removes the first occurrence of the specified message from this list, if it is present.
@@ -90,10 +106,27 @@
if (mPendingMessages.isEmpty()) {
mCallAudioRouteController.sendMessageWithSessionInfo(
CallAudioRouteAdapter.EXIT_PENDING_ROUTE);
+ } else {
+ for(Integer i: mPendingMessages) {
+ Log.d(this, "evaluatePendingState: pending Messages - %d", i);
+ }
}
}
+ public void clearPendingMessages() {
+ mPendingMessages.clear();
+ }
+
public boolean isActive() {
return mActive;
}
+
+ public @AudioRoute.AudioRouteType int getCommunicationDeviceType() {
+ return mCommunicationDeviceType;
+ }
+
+ public void setCommunicationDeviceType(
+ @AudioRoute.AudioRouteType int communicationDeviceType) {
+ mCommunicationDeviceType = communicationDeviceType;
+ }
}
diff --git a/src/com/android/server/telecom/bluetooth/BluetoothDeviceManager.java b/src/com/android/server/telecom/bluetooth/BluetoothDeviceManager.java
index a0ffe63..50476fe 100644
--- a/src/com/android/server/telecom/bluetooth/BluetoothDeviceManager.java
+++ b/src/com/android/server/telecom/bluetooth/BluetoothDeviceManager.java
@@ -16,6 +16,9 @@
package com.android.server.telecom.bluetooth;
+import static com.android.server.telecom.AudioRoute.TYPE_BLUETOOTH_HA;
+import static com.android.server.telecom.AudioRoute.TYPE_BLUETOOTH_SCO;
+
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothHeadset;
@@ -34,6 +37,7 @@
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.flags.FeatureFlags;
@@ -489,12 +493,14 @@
}
}
- public void disconnectSco() {
+ public int disconnectSco() {
+ int result = BluetoothStatusCodes.ERROR_UNKNOWN;
if (getBluetoothHeadset() == null) {
Log.w(this, "Trying to disconnect audio but no headset service exists.");
} else {
- mBluetoothHeadset.disconnectAudio();
+ result = mBluetoothHeadset.disconnectAudio();
}
+ return result;
}
public boolean isLeAudioCommunicationDevice() {
@@ -651,6 +657,28 @@
return result;
}
+ public boolean setCommunicationDeviceForAddress(String address) {
+ AudioDeviceInfo deviceInfo = null;
+ List<AudioDeviceInfo> devices = mAudioManager.getAvailableCommunicationDevices();
+ if (devices.size() == 0) {
+ Log.w(this, " No communication devices available.");
+ return false;
+ }
+
+ for (AudioDeviceInfo device : devices) {
+ Log.i(this, " Available device type: " + device.getType());
+ if (device.getAddress().equals(address)) {
+ deviceInfo = device;
+ break;
+ }
+ }
+
+ if (!mAudioManager.getCommunicationDevice().equals(deviceInfo)) {
+ return mAudioManager.setCommunicationDevice(deviceInfo);
+ }
+ return true;
+ }
+
// Connect audio to the bluetooth device at address, checking to see whether it's
// le audio, hearing aid or a HFP device, and using the proper BT API.
public boolean connectAudio(String address, boolean switchingBtDevices) {
@@ -747,6 +775,54 @@
}
}
+ /**
+ * Used by CallAudioRouteController in order to connect the BT device.
+ * @param device {@link BluetoothDevice} to connect to.
+ * @param type {@link AudioRoute.AudioRouteType} associated with the device.
+ * @return {@code true} if device was successfully connected, {@code false} otherwise.
+ */
+ public boolean connectAudio(BluetoothDevice device, @AudioRoute.AudioRouteType int type) {
+ String address = device.getAddress();
+ int callProfile = BluetoothProfile.LE_AUDIO;
+ if (type == TYPE_BLUETOOTH_SCO) {
+ callProfile = BluetoothProfile.HEADSET;
+ } else if (type == TYPE_BLUETOOTH_HA) {
+ callProfile = BluetoothProfile.HEARING_AID;
+ }
+
+ Bundle preferredAudioProfiles = mBluetoothAdapter.getPreferredAudioProfiles(device);
+ if (preferredAudioProfiles != null && !preferredAudioProfiles.isEmpty()
+ && preferredAudioProfiles.getInt(BluetoothAdapter.AUDIO_MODE_DUPLEX) != 0) {
+ Log.i(this, "Preferred duplex profile for device=" + address + " is "
+ + preferredAudioProfiles.getInt(BluetoothAdapter.AUDIO_MODE_DUPLEX));
+ callProfile = preferredAudioProfiles.getInt(BluetoothAdapter.AUDIO_MODE_DUPLEX);
+ }
+
+ if (callProfile == BluetoothProfile.LE_AUDIO
+ || callProfile == BluetoothProfile.HEARING_AID) {
+ return mBluetoothAdapter.setActiveDevice(device, BluetoothAdapter.ACTIVE_DEVICE_ALL);
+ } else if (callProfile == BluetoothProfile.HEADSET) {
+ boolean success = mBluetoothAdapter.setActiveDevice(device,
+ BluetoothAdapter.ACTIVE_DEVICE_PHONE_CALL);
+ if (!success) {
+ Log.w(this, "Couldn't set active device to %s", address);
+ return false;
+ }
+ if (getBluetoothHeadset() != null) {
+ int scoConnectionRequest = mBluetoothHeadset.connectAudio();
+ return scoConnectionRequest == BluetoothStatusCodes.SUCCESS ||
+ scoConnectionRequest
+ == BluetoothStatusCodes.ERROR_AUDIO_DEVICE_ALREADY_CONNECTED;
+ } else {
+ Log.w(this, "Couldn't find bluetooth headset service");
+ return false;
+ }
+ } else {
+ Log.w(this, "Attempting to turn on audio for a disconnected device");
+ return false;
+ }
+ }
+
public void cacheHearingAidDevice() {
if (mBluetoothAdapter != null) {
for (BluetoothDevice device : mBluetoothAdapter.getActiveDevices(
diff --git a/src/com/android/server/telecom/bluetooth/BluetoothRouteManager.java b/src/com/android/server/telecom/bluetooth/BluetoothRouteManager.java
index 7da5339..d2686e7 100644
--- a/src/com/android/server/telecom/bluetooth/BluetoothRouteManager.java
+++ b/src/com/android/server/telecom/bluetooth/BluetoothRouteManager.java
@@ -40,14 +40,12 @@
import com.android.server.telecom.Timeouts;
import com.android.server.telecom.flags.FeatureFlags;
-import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
-import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
@@ -1048,4 +1046,8 @@
mHfpActiveDeviceCache = device;
}
}
+
+ public BluetoothDeviceManager getDeviceManager() {
+ return mDeviceManager;
+ }
}
diff --git a/src/com/android/server/telecom/bluetooth/BluetoothStateReceiver.java b/src/com/android/server/telecom/bluetooth/BluetoothStateReceiver.java
index 6d80cd5..af9e07b 100644
--- a/src/com/android/server/telecom/bluetooth/BluetoothStateReceiver.java
+++ b/src/com/android/server/telecom/bluetooth/BluetoothStateReceiver.java
@@ -22,6 +22,8 @@
import static com.android.server.telecom.CallAudioRouteAdapter.BT_AUDIO_DISCONNECTED;
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;
@@ -44,6 +46,7 @@
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 com.android.server.telecom.flags.Flags;
@@ -218,6 +221,24 @@
} else {
mCallAudioRouteAdapter.sendMessageWithSessionInfo(BT_ACTIVE_DEVICE_PRESENT,
audioRouteType, device.getAddress());
+ if (deviceType == BluetoothDeviceManager.DEVICE_TYPE_HEARING_AID
+ || deviceType == BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO) {
+ if (!mBluetoothDeviceManager.setCommunicationDeviceForAddress(
+ device.getAddress())) {
+ Log.i(this, "handleActiveDeviceChanged: Failed to set "
+ + "communication device for %s. Sending PENDING_ROUTE_FAILED to "
+ + "pending audio route.", device);
+ mCallAudioRouteAdapter.getPendingAudioRoute()
+ .onMessageReceived(PENDING_ROUTE_FAILED, device.getAddress());
+ } else {
+ // Track the currently set communication device.
+ int routeType = deviceType == BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO
+ ? AudioRoute.TYPE_BLUETOOTH_LE
+ : AudioRoute.TYPE_BLUETOOTH_HA;
+ mCallAudioRouteAdapter.getPendingAudioRoute()
+ .setCommunicationDeviceType(routeType);
+ }
+ }
}
} else {
mBluetoothRouteManager.onActiveDeviceChanged(device, deviceType);
diff --git a/tests/src/com/android/server/telecom/tests/CallAudioRouteControllerTest.java b/tests/src/com/android/server/telecom/tests/CallAudioRouteControllerTest.java
index 0a53eb0..f770b6a 100644
--- a/tests/src/com/android/server/telecom/tests/CallAudioRouteControllerTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallAudioRouteControllerTest.java
@@ -20,7 +20,6 @@
import static com.android.server.telecom.CallAudioRouteAdapter.BT_ACTIVE_DEVICE_GONE;
import static com.android.server.telecom.CallAudioRouteAdapter.BT_ACTIVE_DEVICE_PRESENT;
import static com.android.server.telecom.CallAudioRouteAdapter.BT_AUDIO_CONNECTED;
-import static com.android.server.telecom.CallAudioRouteAdapter.BT_AUDIO_DISCONNECTED;
import static com.android.server.telecom.CallAudioRouteAdapter.BT_DEVICE_ADDED;
import static com.android.server.telecom.CallAudioRouteAdapter.BT_DEVICE_REMOVED;
import static com.android.server.telecom.CallAudioRouteAdapter.CONNECT_DOCK;
@@ -35,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_EARPIECE;
+import static com.android.server.telecom.CallAudioRouteAdapter.SWITCH_BLUETOOTH;
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;
@@ -48,16 +47,17 @@
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.nullable;
import static org.mockito.Mockito.any;
-import static org.mockito.Mockito.atLeast;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.eq;
-import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothLeAudio;
import android.media.AudioDeviceInfo;
import android.media.AudioManager;
import android.media.IAudioService;
@@ -71,7 +71,9 @@
import com.android.server.telecom.CallAudioManager;
import com.android.server.telecom.CallAudioRouteController;
import com.android.server.telecom.CallsManager;
+import com.android.server.telecom.StatusBarNotifier;
import com.android.server.telecom.WiredHeadsetManager;
+import com.android.server.telecom.bluetooth.BluetoothDeviceManager;
import com.android.server.telecom.bluetooth.BluetoothRouteManager;
import org.junit.After;
@@ -82,6 +84,7 @@
import org.mockito.Mock;
import java.util.HashSet;
+import java.util.List;
import java.util.Set;
@RunWith(JUnit4.class)
@@ -94,6 +97,11 @@
@Mock CallAudioManager.AudioServiceFactory mAudioServiceFactory;
@Mock IAudioService mAudioService;
@Mock BluetoothRouteManager mBluetoothRouteManager;
+ @Mock BluetoothDeviceManager mBluetoothDeviceManager;
+ @Mock BluetoothAdapter mBluetoothAdapter;
+ @Mock StatusBarNotifier mockStatusBarNotifier;
+ @Mock AudioDeviceInfo mAudioDeviceInfo;
+ @Mock BluetoothLeAudio mBluetoothLeAudio;
private AudioRoute mEarpieceRoute;
private AudioRoute mSpeakerRoute;
private static final String BT_ADDRESS_1 = "00:00:00:00:00:01";
@@ -109,7 +117,7 @@
@Override
public AudioRoute create(@AudioRoute.AudioRouteType int type, String bluetoothAddress,
AudioManager audioManager) {
- return new AudioRoute(type, bluetoothAddress, null);
+ return new AudioRoute(type, bluetoothAddress, mAudioDeviceInfo);
}
};
@@ -124,18 +132,35 @@
});
when(mAudioManager.getPreferredDeviceForStrategy(nullable(AudioProductStrategy.class)))
.thenReturn(null);
+ when(mAudioManager.getAvailableCommunicationDevices())
+ .thenReturn(List.of(mAudioDeviceInfo));
+ when(mAudioManager.getCommunicationDevice()).thenReturn(mAudioDeviceInfo);
+ when(mAudioManager.setCommunicationDevice(any(AudioDeviceInfo.class)))
+ .thenReturn(true);
when(mAudioServiceFactory.getAudioService()).thenReturn(mAudioService);
when(mContext.getAttributionTag()).thenReturn("");
doNothing().when(mCallsManager).onCallAudioStateChanged(any(CallAudioState.class),
any(CallAudioState.class));
when(mCallsManager.getCurrentUserHandle()).thenReturn(
new UserHandle(UserHandle.USER_SYSTEM));
+ when(mBluetoothRouteManager.getDeviceManager()).thenReturn(mBluetoothDeviceManager);
+ when(mBluetoothDeviceManager.connectAudio(any(BluetoothDevice.class), anyInt()))
+ .thenReturn(true);
+ when(mBluetoothDeviceManager.getBluetoothAdapter()).thenReturn(mBluetoothAdapter);
+ when(mBluetoothAdapter.getActiveDevices(anyInt())).thenReturn(List.of(BLUETOOTH_DEVICE_1));
+ when(mBluetoothDeviceManager.getLeAudioService()).thenReturn(mBluetoothLeAudio);
+ when(mBluetoothLeAudio.getGroupId(any(BluetoothDevice.class))).thenReturn(1);
+ when(mBluetoothLeAudio.getConnectedGroupLeadDevice(anyInt()))
+ .thenReturn(BLUETOOTH_DEVICE_1);
+ when(mAudioDeviceInfo.getAddress()).thenReturn(BT_ADDRESS_1);
mController = new CallAudioRouteController(mContext, mCallsManager, mAudioServiceFactory,
- mAudioRouteFactory, mWiredHeadsetManager, mBluetoothRouteManager);
+ mAudioRouteFactory, mWiredHeadsetManager,
+ mBluetoothRouteManager, mockStatusBarNotifier, mFeatureFlags);
mController.setAudioRouteFactory(mAudioRouteFactory);
mController.setAudioManager(mAudioManager);
mEarpieceRoute = new AudioRoute(AudioRoute.TYPE_EARPIECE, null, null);
mSpeakerRoute = new AudioRoute(AudioRoute.TYPE_SPEAKER, null, null);
+ when(mFeatureFlags.ignoreAutoRouteToWatchDevice()).thenReturn(false);
}
@After
@@ -166,44 +191,6 @@
@SmallTest
@Test
- public void testActivateAndRemoveBluetoothDeviceDuringCall() {
- doAnswer(invocation -> {
- mController.sendMessageWithSessionInfo(BT_AUDIO_CONNECTED, 0, BLUETOOTH_DEVICE_1);
- return true;
- }).when(mAudioManager).setCommunicationDevice(nullable(AudioDeviceInfo.class));
-
- mController.initialize();
- mController.setActive(true);
- mController.sendMessageWithSessionInfo(BT_DEVICE_ADDED, AudioRoute.TYPE_BLUETOOTH_SCO,
- BLUETOOTH_DEVICE_1);
- 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)).onCallAudioStateChanged(
- any(CallAudioState.class), eq(expectedState));
-
- mController.sendMessageWithSessionInfo(BT_ACTIVE_DEVICE_PRESENT,
- AudioRoute.TYPE_BLUETOOTH_SCO, BT_ADDRESS_1);
- verify(mAudioManager, timeout(TEST_TIMEOUT)).setCommunicationDevice(
- nullable(AudioDeviceInfo.class));
-
- expectedState = new CallAudioState(false, CallAudioState.ROUTE_BLUETOOTH,
- CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH
- | CallAudioState.ROUTE_SPEAKER, BLUETOOTH_DEVICE_1, BLUETOOTH_DEVICES);
- verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
- any(CallAudioState.class), eq(expectedState));
-
- mController.sendMessageWithSessionInfo(BT_DEVICE_REMOVED, AudioRoute.TYPE_BLUETOOTH_SCO,
- BLUETOOTH_DEVICE_1);
- 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 testActiveDeactivateBluetoothDevice() {
mController.initialize();
mController.sendMessageWithSessionInfo(BT_DEVICE_ADDED, AudioRoute.TYPE_BLUETOOTH_SCO,
@@ -229,14 +216,6 @@
@SmallTest
@Test
public void testSwitchFocusForBluetoothDeviceSupportInbandRinging() {
- doAnswer(invocation -> {
- mController.sendMessageWithSessionInfo(BT_AUDIO_CONNECTED, 0, BLUETOOTH_DEVICE_1);
- return true;
- }).when(mAudioManager).setCommunicationDevice(nullable(AudioDeviceInfo.class));
- doAnswer(invocation -> {
- mController.sendMessageWithSessionInfo(BT_AUDIO_DISCONNECTED, 0, BLUETOOTH_DEVICE_1);
- return true;
- }).when(mAudioManager).clearCommunicationDevice();
when(mBluetoothRouteManager.isInbandRingEnabled(eq(BLUETOOTH_DEVICE_1))).thenReturn(true);
mController.initialize();
@@ -253,15 +232,15 @@
assertFalse(mController.isActive());
mController.sendMessageWithSessionInfo(SWITCH_FOCUS, RINGING_FOCUS);
- verify(mAudioManager, timeout(TEST_TIMEOUT)).setCommunicationDevice(
- nullable(AudioDeviceInfo.class));
+ verify(mBluetoothDeviceManager, timeout(TEST_TIMEOUT))
+ .connectAudio(BLUETOOTH_DEVICE_1, AudioRoute.TYPE_BLUETOOTH_SCO);
assertTrue(mController.isActive());
mController.sendMessageWithSessionInfo(SWITCH_FOCUS, ACTIVE_FOCUS);
assertTrue(mController.isActive());
mController.sendMessageWithSessionInfo(SWITCH_FOCUS, NO_FOCUS);
- verify(mAudioManager, timeout(TEST_TIMEOUT)).clearCommunicationDevice();
+ verify(mBluetoothDeviceManager, timeout(TEST_TIMEOUT).atLeastOnce()).disconnectSco();
assertFalse(mController.isActive());
}
@@ -480,4 +459,191 @@
verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
any(CallAudioState.class), eq(expectedState));
}
+
+ @SmallTest
+ @Test
+ public void testIgnoreAutoRouteToWatch() {
+ when(mFeatureFlags.ignoreAutoRouteToWatchDevice()).thenReturn(true);
+ when(mBluetoothRouteManager.isWatch(any(BluetoothDevice.class))).thenReturn(true);
+
+ mController.initialize();
+ mController.sendMessageWithSessionInfo(BT_DEVICE_ADDED, AudioRoute.TYPE_BLUETOOTH_SCO,
+ BLUETOOTH_DEVICE_1);
+ 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)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+
+ // Connect wired headset.
+ mController.sendMessageWithSessionInfo(CONNECT_WIRED_HEADSET);
+ expectedState = new CallAudioState(false, CallAudioState.ROUTE_WIRED_HEADSET,
+ CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_SPEAKER
+ | CallAudioState.ROUTE_BLUETOOTH, null, BLUETOOTH_DEVICES);
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+
+ // Disconnect wired headset and ensure Telecom routes to earpiece instead of the BT route.
+ mController.sendMessageWithSessionInfo(DISCONNECT_WIRED_HEADSET);
+ expectedState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE,
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER
+ | CallAudioState.ROUTE_BLUETOOTH, null , BLUETOOTH_DEVICES);
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+ }
+
+ @SmallTest
+ @Test
+ public void testConnectDisconnectScoDuringCall() {
+ verifyConnectBluetoothDevice(AudioRoute.TYPE_BLUETOOTH_SCO);
+ verifyDisconnectBluetoothDevice(AudioRoute.TYPE_BLUETOOTH_SCO);
+ }
+
+ @SmallTest
+ @Test
+ public void testConnectAndDisconnectLeDeviceDuringCall() {
+ when(mBluetoothLeAudio.getConnectedGroupLeadDevice(anyInt()))
+ .thenReturn(BLUETOOTH_DEVICE_1);
+ verifyConnectBluetoothDevice(AudioRoute.TYPE_BLUETOOTH_LE);
+ verifyDisconnectBluetoothDevice(AudioRoute.TYPE_BLUETOOTH_LE);
+ }
+
+ @SmallTest
+ @Test
+ public void testConnectAndDisconnectHearingAidDuringCall() {
+ verifyConnectBluetoothDevice(AudioRoute.TYPE_BLUETOOTH_HA);
+ verifyDisconnectBluetoothDevice(AudioRoute.TYPE_BLUETOOTH_HA);
+ }
+
+ @SmallTest
+ @Test
+ public void testSwitchBetweenLeAndScoDevices() {
+ when(mBluetoothLeAudio.getConnectedGroupLeadDevice(anyInt()))
+ .thenReturn(BLUETOOTH_DEVICE_1);
+ verifyConnectBluetoothDevice(AudioRoute.TYPE_BLUETOOTH_LE);
+ BluetoothDevice scoDevice =
+ BluetoothRouteManagerTest.makeBluetoothDevice("00:00:00:00:00:03");
+ BLUETOOTH_DEVICES.add(scoDevice);
+
+ // Add SCO device.
+ mController.sendMessageWithSessionInfo(BT_DEVICE_ADDED, AudioRoute.TYPE_BLUETOOTH_SCO,
+ scoDevice);
+ CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_BLUETOOTH,
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH
+ | CallAudioState.ROUTE_SPEAKER, BLUETOOTH_DEVICE_1, BLUETOOTH_DEVICES);
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+
+ // Switch to SCO and verify active device is updated.
+ mController.sendMessageWithSessionInfo(USER_SWITCH_BLUETOOTH, 0, scoDevice.getAddress());
+ mController.sendMessageWithSessionInfo(BT_AUDIO_CONNECTED, 0, scoDevice);
+ expectedState = new CallAudioState(false, CallAudioState.ROUTE_BLUETOOTH,
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH
+ | CallAudioState.ROUTE_SPEAKER, scoDevice, BLUETOOTH_DEVICES);
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+
+ // Disconnect SCO and verify audio routed back to LE audio.
+ BLUETOOTH_DEVICES.remove(scoDevice);
+ mController.sendMessageWithSessionInfo(BT_DEVICE_REMOVED, AudioRoute.TYPE_BLUETOOTH_SCO,
+ scoDevice);
+ expectedState = new CallAudioState(false, CallAudioState.ROUTE_BLUETOOTH,
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH
+ | CallAudioState.ROUTE_SPEAKER, BLUETOOTH_DEVICE_1, BLUETOOTH_DEVICES);
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+ }
+
+ @SmallTest
+ @Test
+ public void testFallbackWhenBluetoothConnectionFails() {
+ when(mBluetoothDeviceManager.connectAudio(any(BluetoothDevice.class), anyInt()))
+ .thenReturn(false);
+
+ AudioDeviceInfo mockAudioDeviceInfo = mock(AudioDeviceInfo.class);
+ when(mAudioManager.getCommunicationDevice()).thenReturn(mockAudioDeviceInfo);
+ verifyConnectBluetoothDevice(AudioRoute.TYPE_BLUETOOTH_LE);
+ BluetoothDevice scoDevice =
+ BluetoothRouteManagerTest.makeBluetoothDevice("00:00:00:00:00:03");
+ BLUETOOTH_DEVICES.add(scoDevice);
+
+ // Add SCO device.
+ mController.sendMessageWithSessionInfo(BT_DEVICE_ADDED, AudioRoute.TYPE_BLUETOOTH_SCO,
+ scoDevice);
+ CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_BLUETOOTH,
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH
+ | CallAudioState.ROUTE_SPEAKER, BLUETOOTH_DEVICE_1, BLUETOOTH_DEVICES);
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+
+ // Switch to SCO but reject connection and make sure audio is routed back to LE device.
+ mController.sendMessageWithSessionInfo(BT_ACTIVE_DEVICE_PRESENT,
+ AudioRoute.TYPE_BLUETOOTH_SCO, scoDevice.getAddress());
+ verify(mBluetoothDeviceManager, timeout(TEST_TIMEOUT))
+ .connectAudio(scoDevice, AudioRoute.TYPE_BLUETOOTH_SCO);
+ expectedState = new CallAudioState(false, CallAudioState.ROUTE_BLUETOOTH,
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH
+ | CallAudioState.ROUTE_SPEAKER, BLUETOOTH_DEVICE_1, BLUETOOTH_DEVICES);
+ verify(mCallsManager, timeout(TEST_TIMEOUT).atLeastOnce()).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+
+ // Cleanup supported devices for next test
+ BLUETOOTH_DEVICES.remove(scoDevice);
+ }
+
+ private void verifyConnectBluetoothDevice(int audioType) {
+ mController.initialize();
+ mController.setActive(true);
+
+ mController.sendMessageWithSessionInfo(BT_DEVICE_ADDED, audioType, BLUETOOTH_DEVICE_1);
+ 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)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+
+ mController.sendMessageWithSessionInfo(BT_ACTIVE_DEVICE_PRESENT, audioType, BT_ADDRESS_1);
+ if (audioType == AudioRoute.TYPE_BLUETOOTH_SCO) {
+ verify(mBluetoothDeviceManager, timeout(TEST_TIMEOUT))
+ .connectAudio(BLUETOOTH_DEVICE_1, AudioRoute.TYPE_BLUETOOTH_SCO);
+ mController.sendMessageWithSessionInfo(BT_AUDIO_CONNECTED,
+ 0, BLUETOOTH_DEVICE_1);
+ } else {
+ verify(mAudioManager, timeout(TEST_TIMEOUT))
+ .setCommunicationDevice(nullable(AudioDeviceInfo.class));
+ }
+
+ expectedState = new CallAudioState(false, CallAudioState.ROUTE_BLUETOOTH,
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH
+ | CallAudioState.ROUTE_SPEAKER, BLUETOOTH_DEVICE_1, BLUETOOTH_DEVICES);
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+
+ // Test hearing aid pair and ensure second device isn't added as a route
+ if (audioType == AudioRoute.TYPE_BLUETOOTH_HA) {
+ BluetoothDevice hearingAidDevice2 =
+ BluetoothRouteManagerTest.makeBluetoothDevice("00:00:00:00:00:02");
+ mController.sendMessageWithSessionInfo(BT_DEVICE_ADDED, audioType, hearingAidDevice2);
+ expectedState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE,
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH
+ | CallAudioState.ROUTE_SPEAKER, null, BLUETOOTH_DEVICES);
+ // Verify that supported BT devices only shows the first connected hearing aid device.
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+ }
+ }
+
+ private void verifyDisconnectBluetoothDevice(int audioType) {
+ mController.sendMessageWithSessionInfo(BT_DEVICE_REMOVED, audioType, BLUETOOTH_DEVICE_1);
+ CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE,
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER, null,
+ new HashSet<>());
+ if (audioType == AudioRoute.TYPE_BLUETOOTH_SCO) {
+ verify(mBluetoothDeviceManager, timeout(TEST_TIMEOUT)).disconnectSco();
+ } else {
+ verify(mAudioManager, timeout(TEST_TIMEOUT)).clearCommunicationDevice();
+ }
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+ }
}