Loading flags/telecom_callaudioroutestatemachine_flags.aconfig +11 −0 Original line number Diff line number Diff line Loading @@ -184,3 +184,14 @@ flag { purpose: PURPOSE_BUGFIX } } # OWNER=pmadapurmath TARGET=25Q3 flag { name: "update_preferred_audio_device_logic" namespace: "telecom" description: "Change the use of preferred device for strategy to only use it at the start of the call and include relevant syncing with AudioManager#getCommunicationDevice" bug: "377345692" metadata { purpose: PURPOSE_BUGFIX } } src/com/android/server/telecom/CallAudioRouteController.java +60 −5 Original line number Diff line number Diff line Loading @@ -119,6 +119,8 @@ public class CallAudioRouteController implements CallAudioRouteAdapter { private int mCallSupportedRouteMask = -1; private boolean mIsScoAudioConnected; private boolean mAvailableRoutesUpdated; private boolean mUsePreferredDeviceStrategy; private AudioDeviceInfo mCurrentCommunicationDevice; private final Object mLock = new Object(); private final TelecomSystem.SyncRoot mTelecomLock; private CountDownLatch mAudioOperationsCompleteLatch; Loading @@ -130,7 +132,9 @@ public class CallAudioRouteController implements CallAudioRouteAdapter { try { if (AudioManager.ACTION_SPEAKERPHONE_STATE_CHANGED.equals(intent.getAction())) { if (mAudioManager != null) { AudioDeviceInfo info = mAudioManager.getCommunicationDevice(); AudioDeviceInfo info = mFeatureFlags.updatePreferredAudioDeviceLogic() ? getCurrentCommunicationDevice() : mAudioManager.getCommunicationDevice(); if ((info != null) && (info.getType() == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER)) { if (mCurrentRoute.getType() != AudioRoute.TYPE_SPEAKER) { Loading Loading @@ -204,6 +208,9 @@ public class CallAudioRouteController implements CallAudioRouteAdapter { mMetricsController = metricsController; mFocusType = NO_FOCUS; mIsScoAudioConnected = false; mUsePreferredDeviceStrategy = true; setCurrentCommunicationDevice(null); mTelecomLock = callsManager.getLock(); HandlerThread handlerThread = new HandlerThread(this.getClass().getSimpleName()); handlerThread.start(); Loading Loading @@ -232,10 +239,8 @@ public class CallAudioRouteController implements CallAudioRouteAdapter { mCommunicationDeviceListener = new AudioManager.OnCommunicationDeviceChangedListener() { @Override public void onCommunicationDeviceChanged(AudioDeviceInfo device) { @AudioRoute.AudioRouteType int audioType = device != null ? DEVICE_INFO_TYPE_TO_AUDIO_ROUTE_TYPE.getOrDefault( device.getType(), TYPE_INVALID) : TYPE_INVALID; @AudioRoute.AudioRouteType int audioType = getAudioType(device); setCurrentCommunicationDevice(device); Log.i(this, "onCommunicationDeviceChanged: device (%s), audioType (%d)", device, audioType); if (audioType == TYPE_SPEAKER) { Loading Loading @@ -931,6 +936,7 @@ public class CallAudioRouteController implements CallAudioRouteAdapter { // Clear pending messages mPendingAudioRoute.clearPendingMessages(); clearRingingBluetoothAddress(); mUsePreferredDeviceStrategy = true; } case ACTIVE_FOCUS -> { // Route to active baseline route (we may need to change audio route in the case Loading @@ -948,6 +954,9 @@ public class CallAudioRouteController implements CallAudioRouteAdapter { mCurrentRoute.getBluetoothAddress()) ? mCurrentRoute : getBaseRoute(true, null); // Once we have processed active focus once during the call, we can ignore using // the preferred device strategy. mUsePreferredDeviceStrategy = false; routeTo(true, audioRoute); clearRingingBluetoothAddress(); } Loading Loading @@ -1287,6 +1296,22 @@ public class CallAudioRouteController implements CallAudioRouteAdapter { // Get corresponding audio route @AudioRoute.AudioRouteType int type = DEVICE_INFO_TYPE_TO_AUDIO_ROUTE_TYPE.get( deviceAttr.getType()); AudioDeviceInfo currentCommunicationDevice = null; if (mFeatureFlags.updatePreferredAudioDeviceLogic()) { currentCommunicationDevice = getCurrentCommunicationDevice(); } // We will default to TYPE_INVALID if the currentCommunicationDevice is null or the type // cannot be resolved from the given audio device info. int communicationDeviceAudioType = getAudioType(currentCommunicationDevice); // Sync the preferred device strategy with the current communication device if there's a // valid audio device output set as the preferred device strategy. This will address timing // issues between updates made to the preferred device strategy. From the audio fwk // standpoint, updates to the communication device take precedent to changes in the // preferred device strategy so the former should be used as the source of truth. if (type != TYPE_INVALID && communicationDeviceAudioType != TYPE_INVALID && communicationDeviceAudioType != type) { type = communicationDeviceAudioType; } if (BT_AUDIO_ROUTE_TYPES.contains(type)) { return getBluetoothRoute(type, deviceAttr.getAddress()); } else { Loading Loading @@ -1420,6 +1445,11 @@ public class CallAudioRouteController implements CallAudioRouteAdapter { } public AudioRoute getBaseRoute(boolean includeBluetooth, String btAddressToExclude) { // Catch-all case for all invocations to this method where we shouldn't be using // getPreferredAudioRouteFromStrategy if (mFeatureFlags.updatePreferredAudioDeviceLogic() && !mUsePreferredDeviceStrategy) { return calculateBaselineRoute(false, includeBluetooth, btAddressToExclude); } AudioRoute destRoute = getPreferredAudioRouteFromStrategy(); Log.i(this, "getBaseRoute: preferred audio route is %s", destRoute); if (destRoute == null || (destRoute.getBluetoothAddress() != null && (!includeBluetooth Loading Loading @@ -1694,4 +1724,29 @@ public class CallAudioRouteController implements CallAudioRouteAdapter { public CountDownLatch getAudioActiveCompleteLatch() { return mAudioActiveCompleteLatch; } private @AudioRoute.AudioRouteType int getAudioType(AudioDeviceInfo device) { return device != null ? DEVICE_INFO_TYPE_TO_AUDIO_ROUTE_TYPE.getOrDefault( device.getType(), TYPE_INVALID) : TYPE_INVALID; } @VisibleForTesting public boolean getUsePreferredDeviceStrategy() { return mUsePreferredDeviceStrategy; } @VisibleForTesting public void setCurrentCommunicationDevice(AudioDeviceInfo device) { synchronized (mLock) { mCurrentCommunicationDevice = device; } } public AudioDeviceInfo getCurrentCommunicationDevice() { synchronized (mLock) { return mCurrentCommunicationDevice; } } } tests/src/com/android/server/telecom/tests/CallAudioRouteControllerTest.java +71 −0 Original line number Diff line number Diff line Loading @@ -68,6 +68,8 @@ import android.bluetooth.BluetoothLeAudio; import android.content.BroadcastReceiver; import android.content.Intent; import android.content.IntentFilter; import android.media.AudioAttributes; import android.media.AudioDeviceAttributes; import android.media.AudioDeviceInfo; import android.media.AudioManager; import android.media.IAudioService; Loading Loading @@ -101,6 +103,7 @@ import org.junit.runners.JUnit4; import org.mockito.ArgumentCaptor; import org.mockito.Mock; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; Loading Loading @@ -276,6 +279,65 @@ public class CallAudioRouteControllerTest extends TelecomTestCase { assertTrue(mController.getAvailableRoutes().contains(mEarpieceRoute)); } @SmallTest @Test public void testAudioRouteForPreferredDeviceStrategy() { when(mFeatureFlags.updatePreferredAudioDeviceLogic()).thenReturn(true); mController.initialize(); mController.sendMessageWithSessionInfo(SWITCH_FOCUS, RINGING_FOCUS, 0); waitForRouteActiveStateAndVerify(true); // Verify preferred device strategy still needs to be used since audio routing hasn't gone // active assertTrue(mController.getUsePreferredDeviceStrategy()); mController.sendMessageWithSessionInfo(SWITCH_FOCUS, ACTIVE_FOCUS, 0); waitForHandlerAction(mController.getAdapterHandler(), TEST_TIMEOUT); assertTrue(mController.isActive()); // Verify that we should no longer are using the preferred device strategy once we process // active focus switch. assertFalse(mController.getUsePreferredDeviceStrategy()); } @SmallTest @Test public void testAudioRouteCommunicationDeviceSyncWithPreferredDeviceStrategy() { when(mFeatureFlags.updatePreferredAudioDeviceLogic()).thenReturn(true); mController.initialize(); // Set up tests so that the current communication device is different from the preferred // device for strategy. AudioDeviceInfo infoCommunicationDevice = mock(AudioDeviceInfo.class); when(infoCommunicationDevice.getType()).thenReturn(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER); mController.setCurrentCommunicationDevice(infoCommunicationDevice); // Setup mocks to test the preferred device strategy. setUpPreferredDeviceMocks(); mController.sendMessageWithSessionInfo(SWITCH_FOCUS, RINGING_FOCUS, 0); waitForRouteActiveStateAndVerify(true); mController.sendMessageWithSessionInfo(SPEAKER_ON); CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_SPEAKER, CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER, null, new HashSet<>()); verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged( any(CallAudioState.class), eq(expectedState)); // Verify that routing remains unchanged once active focus is processed (we still check // for preferred device strategy). Do note that we still end up using the reported // communication device instead as it's not synced with the preferred device). mController.sendMessageWithSessionInfo(SWITCH_FOCUS, ACTIVE_FOCUS, 0); verify(mCallsManager, timeout(TEST_TIMEOUT).atLeastOnce()).onCallAudioStateChanged( any(CallAudioState.class), eq(expectedState)); // For sanity, verify that routing falls back on earpiece if focus is switched to active // again (we don't try to use the preferred device strategy). mController.sendMessageWithSessionInfo(SWITCH_FOCUS, ACTIVE_FOCUS, 0); mController.sendMessageWithSessionInfo(SPEAKER_OFF); expectedState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE, CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER, null, new HashSet<>()); verify(mCallsManager, timeout(TEST_TIMEOUT).atLeastOnce()).onCallAudioStateChanged( any(CallAudioState.class), eq(expectedState)); } @SmallTest @Test public void testNormalCallRouteToEarpiece() { Loading Loading @@ -1398,4 +1460,13 @@ public class CallAudioRouteControllerTest extends TelecomTestCase { assertEquals(mController.isActive(), expectActive); } } private void setUpPreferredDeviceMocks() { AudioProductStrategy s = mock(AudioProductStrategy.class); when(s.supportsAudioAttributes(any(AudioAttributes.class))).thenReturn(true); AudioDeviceAttributes deviceAttr = mock(AudioDeviceAttributes.class); when(mAudioManager.getPreferredDeviceForStrategy(any(AudioProductStrategy.class))) .thenReturn(deviceAttr); when(deviceAttr.getType()).thenReturn(AudioDeviceInfo.TYPE_BUILTIN_EARPIECE); } } Loading
flags/telecom_callaudioroutestatemachine_flags.aconfig +11 −0 Original line number Diff line number Diff line Loading @@ -184,3 +184,14 @@ flag { purpose: PURPOSE_BUGFIX } } # OWNER=pmadapurmath TARGET=25Q3 flag { name: "update_preferred_audio_device_logic" namespace: "telecom" description: "Change the use of preferred device for strategy to only use it at the start of the call and include relevant syncing with AudioManager#getCommunicationDevice" bug: "377345692" metadata { purpose: PURPOSE_BUGFIX } }
src/com/android/server/telecom/CallAudioRouteController.java +60 −5 Original line number Diff line number Diff line Loading @@ -119,6 +119,8 @@ public class CallAudioRouteController implements CallAudioRouteAdapter { private int mCallSupportedRouteMask = -1; private boolean mIsScoAudioConnected; private boolean mAvailableRoutesUpdated; private boolean mUsePreferredDeviceStrategy; private AudioDeviceInfo mCurrentCommunicationDevice; private final Object mLock = new Object(); private final TelecomSystem.SyncRoot mTelecomLock; private CountDownLatch mAudioOperationsCompleteLatch; Loading @@ -130,7 +132,9 @@ public class CallAudioRouteController implements CallAudioRouteAdapter { try { if (AudioManager.ACTION_SPEAKERPHONE_STATE_CHANGED.equals(intent.getAction())) { if (mAudioManager != null) { AudioDeviceInfo info = mAudioManager.getCommunicationDevice(); AudioDeviceInfo info = mFeatureFlags.updatePreferredAudioDeviceLogic() ? getCurrentCommunicationDevice() : mAudioManager.getCommunicationDevice(); if ((info != null) && (info.getType() == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER)) { if (mCurrentRoute.getType() != AudioRoute.TYPE_SPEAKER) { Loading Loading @@ -204,6 +208,9 @@ public class CallAudioRouteController implements CallAudioRouteAdapter { mMetricsController = metricsController; mFocusType = NO_FOCUS; mIsScoAudioConnected = false; mUsePreferredDeviceStrategy = true; setCurrentCommunicationDevice(null); mTelecomLock = callsManager.getLock(); HandlerThread handlerThread = new HandlerThread(this.getClass().getSimpleName()); handlerThread.start(); Loading Loading @@ -232,10 +239,8 @@ public class CallAudioRouteController implements CallAudioRouteAdapter { mCommunicationDeviceListener = new AudioManager.OnCommunicationDeviceChangedListener() { @Override public void onCommunicationDeviceChanged(AudioDeviceInfo device) { @AudioRoute.AudioRouteType int audioType = device != null ? DEVICE_INFO_TYPE_TO_AUDIO_ROUTE_TYPE.getOrDefault( device.getType(), TYPE_INVALID) : TYPE_INVALID; @AudioRoute.AudioRouteType int audioType = getAudioType(device); setCurrentCommunicationDevice(device); Log.i(this, "onCommunicationDeviceChanged: device (%s), audioType (%d)", device, audioType); if (audioType == TYPE_SPEAKER) { Loading Loading @@ -931,6 +936,7 @@ public class CallAudioRouteController implements CallAudioRouteAdapter { // Clear pending messages mPendingAudioRoute.clearPendingMessages(); clearRingingBluetoothAddress(); mUsePreferredDeviceStrategy = true; } case ACTIVE_FOCUS -> { // Route to active baseline route (we may need to change audio route in the case Loading @@ -948,6 +954,9 @@ public class CallAudioRouteController implements CallAudioRouteAdapter { mCurrentRoute.getBluetoothAddress()) ? mCurrentRoute : getBaseRoute(true, null); // Once we have processed active focus once during the call, we can ignore using // the preferred device strategy. mUsePreferredDeviceStrategy = false; routeTo(true, audioRoute); clearRingingBluetoothAddress(); } Loading Loading @@ -1287,6 +1296,22 @@ public class CallAudioRouteController implements CallAudioRouteAdapter { // Get corresponding audio route @AudioRoute.AudioRouteType int type = DEVICE_INFO_TYPE_TO_AUDIO_ROUTE_TYPE.get( deviceAttr.getType()); AudioDeviceInfo currentCommunicationDevice = null; if (mFeatureFlags.updatePreferredAudioDeviceLogic()) { currentCommunicationDevice = getCurrentCommunicationDevice(); } // We will default to TYPE_INVALID if the currentCommunicationDevice is null or the type // cannot be resolved from the given audio device info. int communicationDeviceAudioType = getAudioType(currentCommunicationDevice); // Sync the preferred device strategy with the current communication device if there's a // valid audio device output set as the preferred device strategy. This will address timing // issues between updates made to the preferred device strategy. From the audio fwk // standpoint, updates to the communication device take precedent to changes in the // preferred device strategy so the former should be used as the source of truth. if (type != TYPE_INVALID && communicationDeviceAudioType != TYPE_INVALID && communicationDeviceAudioType != type) { type = communicationDeviceAudioType; } if (BT_AUDIO_ROUTE_TYPES.contains(type)) { return getBluetoothRoute(type, deviceAttr.getAddress()); } else { Loading Loading @@ -1420,6 +1445,11 @@ public class CallAudioRouteController implements CallAudioRouteAdapter { } public AudioRoute getBaseRoute(boolean includeBluetooth, String btAddressToExclude) { // Catch-all case for all invocations to this method where we shouldn't be using // getPreferredAudioRouteFromStrategy if (mFeatureFlags.updatePreferredAudioDeviceLogic() && !mUsePreferredDeviceStrategy) { return calculateBaselineRoute(false, includeBluetooth, btAddressToExclude); } AudioRoute destRoute = getPreferredAudioRouteFromStrategy(); Log.i(this, "getBaseRoute: preferred audio route is %s", destRoute); if (destRoute == null || (destRoute.getBluetoothAddress() != null && (!includeBluetooth Loading Loading @@ -1694,4 +1724,29 @@ public class CallAudioRouteController implements CallAudioRouteAdapter { public CountDownLatch getAudioActiveCompleteLatch() { return mAudioActiveCompleteLatch; } private @AudioRoute.AudioRouteType int getAudioType(AudioDeviceInfo device) { return device != null ? DEVICE_INFO_TYPE_TO_AUDIO_ROUTE_TYPE.getOrDefault( device.getType(), TYPE_INVALID) : TYPE_INVALID; } @VisibleForTesting public boolean getUsePreferredDeviceStrategy() { return mUsePreferredDeviceStrategy; } @VisibleForTesting public void setCurrentCommunicationDevice(AudioDeviceInfo device) { synchronized (mLock) { mCurrentCommunicationDevice = device; } } public AudioDeviceInfo getCurrentCommunicationDevice() { synchronized (mLock) { return mCurrentCommunicationDevice; } } }
tests/src/com/android/server/telecom/tests/CallAudioRouteControllerTest.java +71 −0 Original line number Diff line number Diff line Loading @@ -68,6 +68,8 @@ import android.bluetooth.BluetoothLeAudio; import android.content.BroadcastReceiver; import android.content.Intent; import android.content.IntentFilter; import android.media.AudioAttributes; import android.media.AudioDeviceAttributes; import android.media.AudioDeviceInfo; import android.media.AudioManager; import android.media.IAudioService; Loading Loading @@ -101,6 +103,7 @@ import org.junit.runners.JUnit4; import org.mockito.ArgumentCaptor; import org.mockito.Mock; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; Loading Loading @@ -276,6 +279,65 @@ public class CallAudioRouteControllerTest extends TelecomTestCase { assertTrue(mController.getAvailableRoutes().contains(mEarpieceRoute)); } @SmallTest @Test public void testAudioRouteForPreferredDeviceStrategy() { when(mFeatureFlags.updatePreferredAudioDeviceLogic()).thenReturn(true); mController.initialize(); mController.sendMessageWithSessionInfo(SWITCH_FOCUS, RINGING_FOCUS, 0); waitForRouteActiveStateAndVerify(true); // Verify preferred device strategy still needs to be used since audio routing hasn't gone // active assertTrue(mController.getUsePreferredDeviceStrategy()); mController.sendMessageWithSessionInfo(SWITCH_FOCUS, ACTIVE_FOCUS, 0); waitForHandlerAction(mController.getAdapterHandler(), TEST_TIMEOUT); assertTrue(mController.isActive()); // Verify that we should no longer are using the preferred device strategy once we process // active focus switch. assertFalse(mController.getUsePreferredDeviceStrategy()); } @SmallTest @Test public void testAudioRouteCommunicationDeviceSyncWithPreferredDeviceStrategy() { when(mFeatureFlags.updatePreferredAudioDeviceLogic()).thenReturn(true); mController.initialize(); // Set up tests so that the current communication device is different from the preferred // device for strategy. AudioDeviceInfo infoCommunicationDevice = mock(AudioDeviceInfo.class); when(infoCommunicationDevice.getType()).thenReturn(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER); mController.setCurrentCommunicationDevice(infoCommunicationDevice); // Setup mocks to test the preferred device strategy. setUpPreferredDeviceMocks(); mController.sendMessageWithSessionInfo(SWITCH_FOCUS, RINGING_FOCUS, 0); waitForRouteActiveStateAndVerify(true); mController.sendMessageWithSessionInfo(SPEAKER_ON); CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_SPEAKER, CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER, null, new HashSet<>()); verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged( any(CallAudioState.class), eq(expectedState)); // Verify that routing remains unchanged once active focus is processed (we still check // for preferred device strategy). Do note that we still end up using the reported // communication device instead as it's not synced with the preferred device). mController.sendMessageWithSessionInfo(SWITCH_FOCUS, ACTIVE_FOCUS, 0); verify(mCallsManager, timeout(TEST_TIMEOUT).atLeastOnce()).onCallAudioStateChanged( any(CallAudioState.class), eq(expectedState)); // For sanity, verify that routing falls back on earpiece if focus is switched to active // again (we don't try to use the preferred device strategy). mController.sendMessageWithSessionInfo(SWITCH_FOCUS, ACTIVE_FOCUS, 0); mController.sendMessageWithSessionInfo(SPEAKER_OFF); expectedState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE, CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER, null, new HashSet<>()); verify(mCallsManager, timeout(TEST_TIMEOUT).atLeastOnce()).onCallAudioStateChanged( any(CallAudioState.class), eq(expectedState)); } @SmallTest @Test public void testNormalCallRouteToEarpiece() { Loading Loading @@ -1398,4 +1460,13 @@ public class CallAudioRouteControllerTest extends TelecomTestCase { assertEquals(mController.isActive(), expectActive); } } private void setUpPreferredDeviceMocks() { AudioProductStrategy s = mock(AudioProductStrategy.class); when(s.supportsAudioAttributes(any(AudioAttributes.class))).thenReturn(true); AudioDeviceAttributes deviceAttr = mock(AudioDeviceAttributes.class); when(mAudioManager.getPreferredDeviceForStrategy(any(AudioProductStrategy.class))) .thenReturn(deviceAttr); when(deviceAttr.getType()).thenReturn(AudioDeviceInfo.TYPE_BUILTIN_EARPIECE); } }