Merge "clearCallingIdentity for every CallControl API in TSW" into main
diff --git a/res/values-night/styles.xml b/res/values-night/styles.xml
index 5b81fac..b94b9e4 100644
--- a/res/values-night/styles.xml
+++ b/res/values-night/styles.xml
@@ -23,6 +23,11 @@
<item name="android:actionOverflowButtonStyle">@style/TelecomDialerSettingsActionOverflowButtonStyle</item>
<item name="android:windowLightNavigationBar">true</item>
<item name="android:windowContentOverlay">@null</item>
+
+ <!--
+ TODO(b/309578419): Make activities handle insets properly and then remove this.
+ -->
+ <item name="android:windowOptOutEdgeToEdgeEnforcement">true</item>
</style>
<style name="Theme.Telecom.BlockedNumbers" parent="@android:style/Theme.DeviceDefault.Light">
@@ -31,6 +36,11 @@
<item name="android:windowLightNavigationBar">true</item>
<item name="android:windowContentOverlay">@null</item>
<item name="android:listDivider">@null</item>
+
+ <!--
+ TODO(b/309578419): Make activities handle insets properly and then remove this.
+ -->
+ <item name="android:windowOptOutEdgeToEdgeEnforcement">true</item>
</style>
</resources>
\ No newline at end of file
diff --git a/res/values/styles.xml b/res/values/styles.xml
index cd608f5..0624082 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -32,6 +32,11 @@
<item name="android:navigationBarColor">@android:color/transparent</item>
<item name="android:windowLightStatusBar">true</item>
<item name="android:windowLightNavigationBar">true</item>
+
+ <!--
+ TODO(b/309578419): Make activities handle insets properly and then remove this.
+ -->
+ <item name="android:windowOptOutEdgeToEdgeEnforcement">true</item>
</style>
<style name="Theme.Telecom.EnableAccount" parent="Theme.Telecom.DialerSettings">
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 44f6430..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) {
@@ -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 fbba7de..c3eb3b8 100644
--- a/src/com/android/server/telecom/CallsManager.java
+++ b/src/com/android/server/telecom/CallsManager.java
@@ -654,7 +654,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));
+ }
}