Merge "Show a consent dialog for Private Space calls before redirecting to the main user" into main
diff --git a/flags/telecom_callaudioroutestatemachine_flags.aconfig b/flags/telecom_callaudioroutestatemachine_flags.aconfig
index 1608869..6cca8f9 100644
--- a/flags/telecom_callaudioroutestatemachine_flags.aconfig
+++ b/flags/telecom_callaudioroutestatemachine_flags.aconfig
@@ -80,3 +80,11 @@
   description: "Clear the requested communication device after the audio operations are completed."
   bug: "315865533"
 }
+
+# OWNER=pmadapurmath TARGET=24Q3
+flag {
+  name: "resolve_switching_bt_devices_computation"
+  namespace: "telecom"
+  description: "Update switching bt devices based on arbitrary device chosen if no device is specified."
+  bug: "333751408"
+}
diff --git a/res/values-vi/strings.xml b/res/values-vi/strings.xml
index 5ae2e79..70f9bfc 100644
--- a/res/values-vi/strings.xml
+++ b/res/values-vi/strings.xml
@@ -133,5 +133,5 @@
     <string name="callendpoint_name_unknown" msgid="2199074708477193852">"Không xác định"</string>
     <string name="call_streaming_notification_body" msgid="502216105683378263">"Đang truyền trực tuyến âm thanh tới thiết bị khác"</string>
     <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Kết thúc"</string>
-    <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Chuyển đổi tại đây"</string>
+    <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Chuyển qua thiết bị này"</string>
 </resources>
diff --git a/src/com/android/server/telecom/CachedVideoStateChange.java b/src/com/android/server/telecom/CachedVideoStateChange.java
new file mode 100644
index 0000000..0892c33
--- /dev/null
+++ b/src/com/android/server/telecom/CachedVideoStateChange.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.telecom;
+
+import static com.android.server.telecom.voip.VideoStateTranslation.TransactionalVideoStateToString;
+
+import android.telecom.Log;
+
+public class CachedVideoStateChange implements CachedCallback {
+    public static final String ID = CachedVideoStateChange.class.getSimpleName();
+    int mCurrentVideoState;
+
+    public int getCurrentCallEndpoint() {
+        return mCurrentVideoState;
+    }
+
+    public CachedVideoStateChange(int videoState) {
+        mCurrentVideoState = videoState;
+    }
+
+    @Override
+    public void executeCallback(CallSourceService service, Call call) {
+        service.onVideoStateChanged(call, mCurrentVideoState);
+        Log.addEvent(call, LogUtils.Events.VIDEO_STATE_CHANGED,
+                TransactionalVideoStateToString(mCurrentVideoState));
+    }
+
+    @Override
+    public String getCallbackId() {
+        return ID;
+    }
+
+    @Override
+    public int hashCode() {
+        return mCurrentVideoState;
+    }
+
+    @Override
+    public boolean equals(Object obj){
+        if (obj == null) {
+            return false;
+        }
+        if (!(obj instanceof CachedVideoStateChange other)) {
+            return false;
+        }
+        return mCurrentVideoState == other.mCurrentVideoState;
+    }
+}
+
diff --git a/src/com/android/server/telecom/Call.java b/src/com/android/server/telecom/Call.java
index 85b184f..606178d 100644
--- a/src/com/android/server/telecom/Call.java
+++ b/src/com/android/server/telecom/Call.java
@@ -19,6 +19,7 @@
 import static android.provider.CallLog.Calls.MISSED_REASON_NOT_MISSED;
 import static android.telephony.TelephonyManager.EVENT_DISPLAY_EMERGENCY_MESSAGE;
 
+import static com.android.server.telecom.voip.VideoStateTranslation.TransactionalVideoStateToString;
 import static com.android.server.telecom.voip.VideoStateTranslation.VideoProfileStateToTransactionalVideoState;
 
 import android.annotation.NonNull;
@@ -2539,6 +2540,17 @@
         }
     }
 
+    boolean completedProcessingAllAttempts() {
+        if (mCreateConnectionProcessor != null) {
+            return (!mCreateConnectionProcessor.isCallTimedOut() &&
+                    mCreateConnectionProcessor.isProcessingComplete());
+        } else {
+            // If mCreateConnectionProcessor is null then there are no attempts to process:
+            return true;
+        }
+
+    }
+
     /**
      * Starts the create connection sequence. Upon completion, there should exist an active
      * connection through a connection service (or the call will have failed).
@@ -4114,14 +4126,8 @@
             videoState = VideoProfile.STATE_AUDIO_ONLY;
         }
 
-        // Transactional calls have the ability to change video calling capabilities on a per-call
-        // basis as opposed to ConnectionService calls which are only based on the PhoneAccount.
-        if (mFlags.transactionalVideoState()
-                && mIsTransactionalCall && !mTransactionalCallSupportsVideoCalling) {
-            Log.i(this, "setVideoState: The transactional does NOT support video calling."
-                    + " defaulted to audio (video not supported)");
-            videoState = VideoProfile.STATE_AUDIO_ONLY;
-        }
+        // TODO:: b/338280297. If a transactional call does not have the
+        //   CallAttributes.SUPPORTS_VIDEO_CALLING capability, the videoState should be set to audio
 
         // Track Video State history during the duration of the call.
         // Only update the history when the call is active or disconnected. This ensures we do
@@ -4138,17 +4144,24 @@
         int previousVideoState = mVideoState;
         mVideoState = videoState;
         if (mVideoState != previousVideoState) {
-            Log.addEvent(this, LogUtils.Events.VIDEO_STATE_CHANGED,
-                    VideoProfile.videoStateToString(videoState));
+            if (!mIsTransactionalCall) {
+                Log.addEvent(this, LogUtils.Events.VIDEO_STATE_CHANGED,
+                        VideoProfile.videoStateToString(videoState));
+            }
             for (Listener l : mListeners) {
                 l.onVideoStateChanged(this, previousVideoState, mVideoState);
             }
         }
 
-        if (mFlags.transactionalVideoState()
-                && mIsTransactionalCall && mTransactionalService != null) {
+        if (mFlags.transactionalVideoState() && mIsTransactionalCall) {
             int transactionalVS = VideoProfileStateToTransactionalVideoState(mVideoState);
-            mTransactionalService.onVideoStateChanged(this, transactionalVS);
+            if (mTransactionalService != null) {
+                Log.addEvent(this, LogUtils.Events.VIDEO_STATE_CHANGED,
+                        TransactionalVideoStateToString(transactionalVS));
+                mTransactionalService.onVideoStateChanged(this, transactionalVS);
+            } else {
+                cacheServiceCallback(new CachedVideoStateChange(transactionalVS));
+            }
         }
 
         if (VideoProfile.isVideo(videoState)) {
diff --git a/src/com/android/server/telecom/CallAudioCommunicationDeviceTracker.java b/src/com/android/server/telecom/CallAudioCommunicationDeviceTracker.java
index 3a05eb5..8130685 100644
--- a/src/com/android/server/telecom/CallAudioCommunicationDeviceTracker.java
+++ b/src/com/android/server/telecom/CallAudioCommunicationDeviceTracker.java
@@ -31,6 +31,8 @@
 import java.util.Arrays;
 import java.util.List;
 import java.util.concurrent.Semaphore;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
 
 /**
  * Helper class used to keep track of the requested communication device within Telecom for audio
@@ -47,7 +49,7 @@
     private int mAudioDeviceType = sAUDIO_DEVICE_TYPE_INVALID;
     // Keep track of the locally requested BT audio device if set
     private String mBtAudioDevice = null;
-    private final Semaphore mLock =  new Semaphore(1);
+    private final Lock mLock = new ReentrantLock();
 
     public CallAudioCommunicationDeviceTracker(Context context) {
         mAudioManager = context.getSystemService(AudioManager.class);
@@ -58,11 +60,29 @@
     }
 
     public boolean isAudioDeviceSetForType(int audioDeviceType) {
-        return mAudioDeviceType == audioDeviceType;
+        if (Flags.communicationDeviceProtectedByLock()) {
+            mLock.lock();
+        }
+        try {
+            return mAudioDeviceType == audioDeviceType;
+        } finally {
+            if (Flags.communicationDeviceProtectedByLock()) {
+                mLock.unlock();
+            }
+        }
     }
 
     public int getCurrentLocallyRequestedCommunicationDevice() {
-       return mAudioDeviceType;
+        if (Flags.communicationDeviceProtectedByLock()) {
+            mLock.lock();
+        }
+        try {
+            return mAudioDeviceType;
+        } finally {
+            if (Flags.communicationDeviceProtectedByLock()) {
+                mLock.unlock();
+            }
+        }
     }
 
     @VisibleForTesting
@@ -71,13 +91,22 @@
     }
 
     public void clearBtCommunicationDevice() {
-        if (mBtAudioDevice == null) {
-            Log.i(this, "No bluetooth device was set for communication that can be cleared.");
-            return;
+        if (Flags.communicationDeviceProtectedByLock()) {
+            mLock.lock();
         }
-        // If mBtAudioDevice is set, we know a BT audio device was set for communication so
-        // mAudioDeviceType corresponds to a BT device type (e.g. hearing aid, SCO, LE).
-        clearCommunicationDevice(mAudioDeviceType);
+        try {
+            if (mBtAudioDevice == null) {
+                Log.i(this, "No bluetooth device was set for communication that can be cleared.");
+            } else {
+                // If mBtAudioDevice is set, we know a BT audio device was set for communication so
+                // mAudioDeviceType corresponds to a BT device type (e.g. hearing aid, SCO, LE).
+                processClearCommunicationDevice(mAudioDeviceType);
+            }
+        } finally {
+            if (Flags.communicationDeviceProtectedByLock()) {
+                mLock.unlock();
+            }
+        }
     }
 
     /*
@@ -93,8 +122,19 @@
     public boolean setCommunicationDevice(int audioDeviceType,
             BluetoothDevice btDevice) {
         if (Flags.communicationDeviceProtectedByLock()) {
-            mLock.tryAcquire();
+            mLock.lock();
         }
+        try {
+            return processSetCommunicationDevice(audioDeviceType, btDevice);
+        } finally {
+            if (Flags.communicationDeviceProtectedByLock()) {
+                mLock.unlock();
+            }
+        }
+    }
+
+    private boolean processSetCommunicationDevice(int audioDeviceType,
+            BluetoothDevice btDevice) {
         // There is only one audio device type associated with each type of BT device.
         boolean isBtDevice = BT_AUDIO_DEVICE_INFO_TYPES.contains(audioDeviceType);
         Log.i(this, "setCommunicationDevice: type = %s, isBtDevice = %s, btDevice = %s",
@@ -132,14 +172,14 @@
             Log.i(this, "No active device of type(s) %s available",
                     audioDeviceType == AudioDeviceInfo.TYPE_WIRED_HEADSET
                             ? Arrays.asList(AudioDeviceInfo.TYPE_WIRED_HEADSET,
-                                    AudioDeviceInfo.TYPE_USB_HEADSET)
+                            AudioDeviceInfo.TYPE_USB_HEADSET)
                             : audioDeviceType);
             return false;
         }
 
         // Force clear previous communication device, if one was set, before setting the new device.
         if (mAudioDeviceType != sAUDIO_DEVICE_TYPE_INVALID) {
-            clearCommunicationDevice(mAudioDeviceType);
+            processClearCommunicationDevice(mAudioDeviceType);
         }
 
         // Turn activeDevice ON.
@@ -161,12 +201,8 @@
                 mBtAudioDevice = null;
             }
         }
-        if (Flags.communicationDeviceProtectedByLock()) {
-            mLock.release();
-        }
         return result;
     }
-
     /*
      * Clears the communication device for the passed in audio device types, given that the device
      * has previously been set for communication.
@@ -174,8 +210,23 @@
      */
     public void clearCommunicationDevice(int audioDeviceType) {
         if (Flags.communicationDeviceProtectedByLock()) {
-            mLock.tryAcquire();
+            mLock.lock();
         }
+        try {
+            processClearCommunicationDevice(audioDeviceType);
+        } finally {
+            if (Flags.communicationDeviceProtectedByLock()) {
+                mLock.unlock();
+            }
+        }
+    }
+
+    public void processClearCommunicationDevice(int audioDeviceType) {
+        if (audioDeviceType == sAUDIO_DEVICE_TYPE_INVALID) {
+            Log.i(this, "clearCommunicationDevice: Skip clearing communication device"
+                    + "for invalid audio type (-1).");
+        }
+
         // There is only one audio device type associated with each type of BT device.
         boolean isBtDevice = BT_AUDIO_DEVICE_INFO_TYPES.contains(audioDeviceType);
         Log.i(this, "clearCommunicationDevice: type = %s, isBtDevice = %s",
@@ -184,10 +235,10 @@
         if (audioDeviceType != mAudioDeviceType
                 && !isUsbHeadsetType(audioDeviceType, mAudioDeviceType)) {
             Log.i(this, "Unable to clear communication device of type(s), %s. "
-                    + "Device does not correspond to the locally requested device type.",
+                            + "Device does not correspond to the locally requested device type.",
                     audioDeviceType == AudioDeviceInfo.TYPE_WIRED_HEADSET
                             ? Arrays.asList(AudioDeviceInfo.TYPE_WIRED_HEADSET,
-                                    AudioDeviceInfo.TYPE_USB_HEADSET)
+                            AudioDeviceInfo.TYPE_USB_HEADSET)
                             : audioDeviceType
             );
             return;
@@ -207,13 +258,10 @@
             mBluetoothRouteManager.onAudioLost(mBtAudioDevice);
             mBtAudioDevice = null;
         }
-        if (Flags.communicationDeviceProtectedByLock()) {
-            mLock.release();
-        }
     }
 
     private boolean isUsbHeadsetType(int audioDeviceType, int sourceType) {
-        return audioDeviceType != AudioDeviceInfo.TYPE_WIRED_HEADSET
-                ? false : sourceType == AudioDeviceInfo.TYPE_USB_HEADSET;
+        return audioDeviceType == AudioDeviceInfo.TYPE_WIRED_HEADSET
+                && sourceType == AudioDeviceInfo.TYPE_USB_HEADSET;
     }
 }
diff --git a/src/com/android/server/telecom/CallAudioRouteStateMachine.java b/src/com/android/server/telecom/CallAudioRouteStateMachine.java
index 621ba36..04dae5f 100644
--- a/src/com/android/server/telecom/CallAudioRouteStateMachine.java
+++ b/src/com/android/server/telecom/CallAudioRouteStateMachine.java
@@ -847,6 +847,14 @@
                     if (msg.arg1 == NO_FOCUS) {
                         // Only disconnect audio here instead of routing away from BT entirely.
                         if (mFeatureFlags.transitRouteBeforeAudioDisconnectBt()) {
+                            // Note: We have to turn off mute here rather than when entering the
+                            // QuiescentBluetooth route because setMuteOn will only work when there the
+                            // current state is active.
+                            // We don't need to do this in the unflagged path since reinitialize
+                            // will turn off mute.
+                            if (mFeatureFlags.resetMuteWhenEnteringQuiescentBtRoute()) {
+                                setMuteOn(false);
+                            }
                             transitionTo(mQuiescentBluetoothRoute);
                             mBluetoothRouteManager.disconnectAudio();
                         } else {
@@ -977,9 +985,6 @@
         public void enter() {
             super.enter();
             mHasUserExplicitlyLeftBluetooth = false;
-            if (mFeatureFlags.resetMuteWhenEnteringQuiescentBtRoute()) {
-                setMuteOn(false);
-            }
             updateInternalCallAudioState();
         }
 
diff --git a/src/com/android/server/telecom/CallSourceService.java b/src/com/android/server/telecom/CallSourceService.java
index 132118b..d579542 100644
--- a/src/com/android/server/telecom/CallSourceService.java
+++ b/src/com/android/server/telecom/CallSourceService.java
@@ -35,4 +35,6 @@
     void onCallEndpointChanged(Call activeCall, CallEndpoint callEndpoint);
 
     void onAvailableCallEndpointsChanged(Call activeCall, Set<CallEndpoint> availableCallEndpoints);
+
+    void onVideoStateChanged(Call activeCall, int videoState);
 }
diff --git a/src/com/android/server/telecom/ConnectionServiceWrapper.java b/src/com/android/server/telecom/ConnectionServiceWrapper.java
index f6f4889..df94f33 100644
--- a/src/com/android/server/telecom/ConnectionServiceWrapper.java
+++ b/src/com/android/server/telecom/ConnectionServiceWrapper.java
@@ -385,7 +385,15 @@
                             mCallsManager.markCallAsDisconnected(
                                     call, new DisconnectCause(DisconnectCause.REMOTE));
                         }
-                        mCallsManager.markCallAsRemoved(call);
+                        if (!mFlags.updatedRcsCallCountTracking()){
+                            mCallsManager.markCallAsRemoved(call);
+                        } else if (call.completedProcessingAllAttempts()
+                                || !call.isEmergencyCall()) {
+                            mCallsManager.markCallAsRemoved(call);
+                        } else {
+                            Log.i(this, "removeCall: emergency call has not "
+                                    + "completed processing all attempts so skipping removal");
+                        }
                     }
                 }
             } catch (Throwable t) {
@@ -1349,6 +1357,7 @@
     private final CallsManager mCallsManager;
     private final AppOpsManager mAppOpsManager;
     private final Context mContext;
+    private final FeatureFlags mFlags;
 
     private ConnectionServiceFocusManager.ConnectionServiceFocusListener mConnSvrFocusListener;
 
@@ -1383,6 +1392,7 @@
         mCallsManager = callsManager;
         mAppOpsManager = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
         mContext = context;
+        mFlags = featureFlags;
     }
 
     /** See {@link IConnectionService#addConnectionServiceAdapter}. */
@@ -1987,6 +1997,11 @@
         }
     }
 
+    @Override
+    public void onVideoStateChanged(Call call, int videoState){
+        // pass through. ConnectionService does not implement this method.
+    }
+
     /** @see IConnectionService#onMuteStateChanged(String, boolean, Session.Info) */
     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
     @Override
diff --git a/src/com/android/server/telecom/TelecomServiceImpl.java b/src/com/android/server/telecom/TelecomServiceImpl.java
index a57360b..230bb09 100644
--- a/src/com/android/server/telecom/TelecomServiceImpl.java
+++ b/src/com/android/server/telecom/TelecomServiceImpl.java
@@ -215,11 +215,11 @@
                 switch (callAttributes.getDirection()) {
                     case DIRECTION_OUTGOING:
                         transaction = new OutgoingCallTransaction(callId, mContext, callAttributes,
-                                mCallsManager, extras);
+                                mCallsManager, extras, mFeatureFlags);
                         break;
                     case DIRECTION_INCOMING:
                         transaction = new IncomingCallTransaction(callId, callAttributes,
-                                mCallsManager, extras);
+                                mCallsManager, extras, mFeatureFlags);
                         break;
                     default:
                         throw new IllegalArgumentException(String.format("Invalid Call Direction. "
@@ -1854,11 +1854,12 @@
                     throw new SecurityException("Package " + callingPackage + " is not allowed"
                             + " to start conference call");
                 }
-
+                // Binder is clearing the identity, so we need to keep the store the handle
+                UserHandle currentUserHandle = Binder.getCallingUserHandle();
                 long token = Binder.clearCallingIdentity();
                 try {
                     mCallsManager.startConference(participants, extras, callingPackage,
-                            Binder.getCallingUserHandle());
+                            currentUserHandle);
                 } finally {
                     Binder.restoreCallingIdentity(token);
                 }
diff --git a/src/com/android/server/telecom/TransactionalServiceWrapper.java b/src/com/android/server/telecom/TransactionalServiceWrapper.java
index 32cb896..fe4b1b7 100644
--- a/src/com/android/server/telecom/TransactionalServiceWrapper.java
+++ b/src/com/android/server/telecom/TransactionalServiceWrapper.java
@@ -602,6 +602,7 @@
         }
     }
 
+    @Override
     public void onVideoStateChanged(Call call, int videoState) {
         if (call != null) {
             try {
diff --git a/src/com/android/server/telecom/bluetooth/BluetoothRouteManager.java b/src/com/android/server/telecom/bluetooth/BluetoothRouteManager.java
index d2686e7..5a44041 100644
--- a/src/com/android/server/telecom/bluetooth/BluetoothRouteManager.java
+++ b/src/com/android/server/telecom/bluetooth/BluetoothRouteManager.java
@@ -28,6 +28,7 @@
 import android.os.Message;
 import android.telecom.Log;
 import android.telecom.Logging.Session;
+import android.util.Pair;
 import android.util.SparseArray;
 
 import com.android.internal.annotations.VisibleForTesting;
@@ -165,10 +166,23 @@
                         removeDevice((String) args.arg2);
                         break;
                     case CONNECT_BT:
-                        String actualAddress = connectBtAudio((String) args.arg2,
-                            false /* switchingBtDevices*/);
+                        String actualAddress;
+                        boolean connected;
+                        if (mFeatureFlags.resolveSwitchingBtDevicesComputation()) {
+                            Pair<String, Boolean> addressInfo = computeAddressToConnectTo(
+                                    (String) args.arg2, false, null);
+                            // See if we need to transition route if the device is already
+                            // connected. If connected, another connection will not occur.
+                            addressInfo = handleDeviceAlreadyConnected(addressInfo);
+                            actualAddress = addressInfo.first;
+                            connected = connectBtAudio(actualAddress, 0,
+                                    false /* switchingBtDevices*/);
+                        } else {
+                            actualAddress = connectBtAudioLegacy((String) args.arg2, false);
+                            connected = actualAddress != null;
+                        }
 
-                        if (actualAddress != null) {
+                        if (connected) {
                             transitionTo(getConnectingStateForAddress(actualAddress,
                                     "AudioOff/CONNECT_BT"));
                         } else {
@@ -181,10 +195,24 @@
                         break;
                     case RETRY_BT_CONNECTION:
                         Log.i(LOG_TAG, "Retrying BT connection to %s", (String) args.arg2);
-                        String retryAddress = connectBtAudio((String) args.arg2, args.argi1,
-                            false /* switchingBtDevices*/);
+                        String retryAddress;
+                        boolean retrySuccessful;
+                        if (mFeatureFlags.resolveSwitchingBtDevicesComputation()) {
+                            Pair<String, Boolean> retryAddressInfo = computeAddressToConnectTo(
+                                    (String) args.arg2, false, null);
+                            // See if we need to transition route if the device is already
+                            // connected. If connected, another connection will not occur.
+                            retryAddressInfo = handleDeviceAlreadyConnected(retryAddressInfo);
+                            retryAddress = retryAddressInfo.first;
+                            retrySuccessful = connectBtAudio(retryAddress, args.argi1,
+                                    false /* switchingBtDevices*/);
+                        } else {
+                            retryAddress = connectBtAudioLegacy((String) args.arg2, args.argi1,
+                                    false /* switchingBtDevices*/);
+                            retrySuccessful = retryAddress != null;
+                        }
 
-                        if (retryAddress != null) {
+                        if (retrySuccessful) {
                             transitionTo(getConnectingStateForAddress(retryAddress,
                                     "AudioOff/RETRY_BT_CONNECTION"));
                         } else {
@@ -255,7 +283,7 @@
             String address = (String) args.arg2;
             boolean switchingBtDevices = !Objects.equals(mDeviceAddress, address);
 
-            if (switchingBtDevices == true) { // check if it is an hearing aid pair
+            if (switchingBtDevices) { // check if it is an hearing aid pair
                 BluetoothAdapter bluetoothAdapter = mDeviceManager.getBluetoothAdapter();
                 if (bluetoothAdapter != null) {
                     List<BluetoothDevice> activeHearingAids =
@@ -272,7 +300,9 @@
                             }
                         }
                     }
-
+                }
+                if (mFeatureFlags.resolveSwitchingBtDevicesComputation()) {
+                    switchingBtDevices &= (mDeviceAddress != null);
                 }
             }
             try {
@@ -288,14 +318,30 @@
                         }
                         break;
                     case CONNECT_BT:
+                        String actualAddress = null;
+                        if (mFeatureFlags.resolveSwitchingBtDevicesComputation()) {
+                            Pair<String, Boolean> addressInfo = computeAddressToConnectTo(address,
+                                    switchingBtDevices, mDeviceAddress);
+                            // See if we need to transition route if the device is already
+                            // connected. If connected, another connection will not occur.
+                            addressInfo = handleDeviceAlreadyConnected(addressInfo);
+                            actualAddress = addressInfo.first;
+                            switchingBtDevices = addressInfo.second;
+                        }
+
                         if (!switchingBtDevices) {
                             // Ignore repeated connection attempts to the same device
                             break;
                         }
 
-                        String actualAddress = connectBtAudio(address,
-                            true /* switchingBtDevices*/);
-                        if (actualAddress != null) {
+                        if (!mFeatureFlags.resolveSwitchingBtDevicesComputation()) {
+                            actualAddress = connectBtAudioLegacy(address,
+                                    true /* switchingBtDevices*/);
+                        }
+                        boolean connected = mFeatureFlags.resolveSwitchingBtDevicesComputation()
+                                ? connectBtAudio(actualAddress, 0, true /* switchingBtDevices*/)
+                                : actualAddress != null;
+                        if (connected) {
                             transitionTo(getConnectingStateForAddress(actualAddress,
                                     "AudioConnecting/CONNECT_BT"));
                         } else {
@@ -307,14 +353,32 @@
                         mDeviceManager.disconnectAudio();
                         break;
                     case RETRY_BT_CONNECTION:
+                        String retryAddress = null;
+                        if (mFeatureFlags.resolveSwitchingBtDevicesComputation()) {
+                            Pair<String, Boolean> retryAddressInfo = computeAddressToConnectTo(
+                                    address, switchingBtDevices, mDeviceAddress);
+                            // See if we need to transition route if the device is already
+                            // connected. If connected, another connection will not occur.
+                            retryAddressInfo = handleDeviceAlreadyConnected(retryAddressInfo);
+                            retryAddress = retryAddressInfo.first;
+                            switchingBtDevices = retryAddressInfo.second;
+                        }
+
                         if (!switchingBtDevices) {
                             Log.d(LOG_TAG, "Retry message came through while connecting.");
                             break;
                         }
 
-                        String retryAddress = connectBtAudio(address, args.argi1,
-                            true /* switchingBtDevices*/);
-                        if (retryAddress != null) {
+                        if (!mFeatureFlags.resolveSwitchingBtDevicesComputation()) {
+                            retryAddress = connectBtAudioLegacy(address, args.argi1,
+                                    true /* switchingBtDevices*/);
+                        }
+                        boolean retrySuccessful = mFeatureFlags
+                                .resolveSwitchingBtDevicesComputation()
+                                ? connectBtAudio(retryAddress, args.argi1,
+                                        true /* switchingBtDevices*/)
+                                : retryAddress != null;
+                        if (retrySuccessful) {
                             transitionTo(getConnectingStateForAddress(retryAddress,
                                     "AudioConnecting/RETRY_BT_CONNECTION"));
                         } else {
@@ -393,6 +457,10 @@
             SomeArgs args = (SomeArgs) msg.obj;
             String address = (String) args.arg2;
             boolean switchingBtDevices = !Objects.equals(mDeviceAddress, address);
+            if (mFeatureFlags.resolveSwitchingBtDevicesComputation()) {
+                switchingBtDevices &= (mDeviceAddress != null);
+            }
+
             try {
                 switch (msg.what) {
                     case NEW_DEVICE_CONNECTED:
@@ -405,6 +473,17 @@
                         }
                         break;
                     case CONNECT_BT:
+                        String actualAddress = null;
+                        if (mFeatureFlags.resolveSwitchingBtDevicesComputation()) {
+                            Pair<String, Boolean> addressInfo = computeAddressToConnectTo(address,
+                                    switchingBtDevices, mDeviceAddress);
+                            // See if we need to transition route if the device is already
+                            // connected. If connected, another connection will not occur.
+                            addressInfo = handleDeviceAlreadyConnected(addressInfo);
+                            actualAddress = addressInfo.first;
+                            switchingBtDevices = addressInfo.second;
+                        }
+
                         if (!switchingBtDevices) {
                             // Ignore connection to already connected device but still notify
                             // CallAudioRouteStateMachine since this might be a switch from other
@@ -413,9 +492,14 @@
                             break;
                         }
 
-                        String actualAddress = connectBtAudio(address,
-                            true /* switchingBtDevices*/);
-                        if (actualAddress != null) {
+                        if (!mFeatureFlags.resolveSwitchingBtDevicesComputation()) {
+                            actualAddress = connectBtAudioLegacy(address,
+                                    true /* switchingBtDevices*/);
+                        }
+                        boolean connected = mFeatureFlags.resolveSwitchingBtDevicesComputation()
+                                ? connectBtAudio(actualAddress, 0, true /* switchingBtDevices*/)
+                                : actualAddress != null;
+                        if (connected) {
                             if (mFeatureFlags.useActualAddressToEnterConnectingState()) {
                                 transitionTo(getConnectingStateForAddress(actualAddress,
                                         "AudioConnected/CONNECT_BT"));
@@ -432,14 +516,32 @@
                         mDeviceManager.disconnectAudio();
                         break;
                     case RETRY_BT_CONNECTION:
+                        String retryAddress = null;
+                        if (mFeatureFlags.resolveSwitchingBtDevicesComputation()) {
+                            Pair<String, Boolean> retryAddressInfo = computeAddressToConnectTo(
+                                    address, switchingBtDevices, mDeviceAddress);
+                            // See if we need to transition route if the device is already
+                            // connected. If connected, another connection will not occur.
+                            retryAddressInfo = handleDeviceAlreadyConnected(retryAddressInfo);
+                            retryAddress = retryAddressInfo.first;
+                            switchingBtDevices = retryAddressInfo.second;
+                        }
+
                         if (!switchingBtDevices) {
                             Log.d(LOG_TAG, "Retry message came through while connected.");
                             break;
                         }
 
-                        String retryAddress = connectBtAudio(address, args.argi1,
-                            true /* switchingBtDevices*/);
-                        if (retryAddress != null) {
+                        if (!mFeatureFlags.resolveSwitchingBtDevicesComputation()) {
+                            retryAddress = connectBtAudioLegacy(address, args.argi1,
+                                    true /* switchingBtDevices*/);
+                        }
+                        boolean retrySuccessful = mFeatureFlags
+                                .resolveSwitchingBtDevicesComputation()
+                                ? connectBtAudio(retryAddress, args.argi1,
+                                        true /* switchingBtDevices*/)
+                                : retryAddress != null;
+                        if (retrySuccessful) {
                             transitionTo(getConnectingStateForAddress(retryAddress,
                                     "AudioConnected/RETRY_BT_CONNECTION"));
                         } else {
@@ -740,8 +842,124 @@
         return false;
     }
 
-    private String connectBtAudio(String address, boolean switchingBtDevices) {
-        return connectBtAudio(address, 0, switchingBtDevices);
+    /**
+     * Determines the address that should be used for the connection attempt. In the case that the
+     * specified address to be used is null, Telecom will try to find an arbitrary address to
+     * connect instead.
+     *
+     * @param address The address that should be prioritized for the connection attempt
+     * @param switchingBtDevices Used when there is existing audio connection to other Bt device.
+     * @param stateAddress The address stored in the state that indicates the connecting/connected
+     *                     device.
+     * @return {@link Pair} containing the address to connect to and whether an existing BT audio
+     *                      connection for a different device exists.
+     */
+    private Pair<String, Boolean> computeAddressToConnectTo(
+            String address, boolean switchingBtDevices, String stateAddress) {
+        Collection<BluetoothDevice> deviceList = mDeviceManager.getConnectedDevices();
+        Optional<BluetoothDevice> matchingDevice = deviceList.stream()
+                .filter(d -> Objects.equals(d.getAddress(), address))
+                .findAny();
+
+        String actualAddress = matchingDevice.isPresent()
+                ? address : getActiveDeviceAddress();
+        if (actualAddress == null) {
+            Log.i(this, "No device specified and BT stack has no active device."
+                    + " Using arbitrary device - except watch");
+            if (deviceList.size() > 0) {
+                for (BluetoothDevice device : deviceList) {
+                    if (mFeatureFlags.ignoreAutoRouteToWatchDevice() && isWatch(device)) {
+                        Log.i(this, "Skipping a watch device: " + device);
+                        continue;
+                    }
+                    actualAddress = device.getAddress();
+                    break;
+                }
+            }
+
+            if (actualAddress == null) {
+                Log.i(this, "No devices available at all. Not connecting.");
+                return new Pair<>(null, false);
+            }
+            if (switchingBtDevices && actualAddress.equals(stateAddress)) {
+                switchingBtDevices = false;
+            }
+        }
+        if (!matchingDevice.isPresent()) {
+            Log.i(this, "No device with address %s available. Using %s instead.",
+                    address, actualAddress);
+        }
+        return new Pair<>(actualAddress, switchingBtDevices);
+    }
+
+    /**
+     * Handles route switching to the connected state for a device. This currently handles the case
+     * for hearing aids when the route manager reports AudioOff since Telecom doesn't treat HA as
+     * the active device outside of a call.
+     *
+     * @param addressInfo A {@link Pair} containing the BT address to connect to as well as if we're
+     *                    handling a switch of BT devices.
+     * @return {@link Pair} indicating the address to connect to as well as if we're handling a
+     *                      switch of BT devices. If the device is already connected, then the
+     *                      return value will be {null, false} to indicate that a connection attempt
+     *                      is not required.
+     */
+    private Pair<String, Boolean> handleDeviceAlreadyConnected(Pair<String, Boolean> addressInfo) {
+        String address = addressInfo.first;
+        BluetoothDevice alreadyConnectedDevice = getBluetoothAudioConnectedDevice();
+        if (alreadyConnectedDevice != null && alreadyConnectedDevice.getAddress().equals(
+                address)) {
+            Log.i(this, "trying to connect to already connected device -- skipping connection"
+                    + " and going into the actual connected state.");
+            transitionToActualState();
+            return new Pair<>(null, false);
+        }
+        return addressInfo;
+    }
+
+    /**
+     * Initiates a connection to the BT address specified.
+     * Note: This method is not synchronized on the Telecom lock, so don't try and call back into
+     * Telecom from within it.
+     * @param address The address that should be tried first. May be null.
+     * @param retryCount The number of times this connection attempt has been retried.
+     * @param switchingBtDevices Used when there is existing audio connection to other Bt device.
+     * @return {@code true} if the connection to the address was successful, otherwise {@code false}
+     *          if the connection fails.
+     *
+     * Note: This should only be used in par with the resolveSwitchingBtDevicesComputation flag.
+     */
+    private boolean connectBtAudio(String address, int retryCount, boolean switchingBtDevices) {
+        if (address == null) {
+            return false;
+        }
+
+        if (switchingBtDevices) {
+            /* When new Bluetooth connects audio, make sure previous one has disconnected audio. */
+            mDeviceManager.disconnectAudio();
+        }
+
+        if (!mDeviceManager.connectAudio(address, switchingBtDevices)) {
+            boolean shouldRetry = retryCount < MAX_CONNECTION_RETRIES;
+            Log.w(LOG_TAG, "Could not connect to %s. Will %s", address,
+                    shouldRetry ? "retry" : "not retry");
+            if (shouldRetry) {
+                SomeArgs args = SomeArgs.obtain();
+                args.arg1 = Log.createSubsession();
+                args.arg2 = address;
+                args.argi1 = retryCount + 1;
+                sendMessageDelayed(RETRY_BT_CONNECTION, args,
+                        mTimeoutsAdapter.getRetryBluetoothConnectAudioBackoffMillis(
+                                mContext.getContentResolver()));
+            }
+            return false;
+        }
+
+        return true;
+    }
+
+    private String connectBtAudioLegacy(String address, boolean switchingBtDevices) {
+        return connectBtAudioLegacy(address, 0, switchingBtDevices);
     }
 
     /**
@@ -754,7 +972,8 @@
      * @return The address of the device that's actually being connected to, or null if no
      * connection was successful.
      */
-    private String connectBtAudio(String address, int retryCount, boolean switchingBtDevices) {
+    private String connectBtAudioLegacy(String address, int retryCount,
+            boolean switchingBtDevices) {
         Collection<BluetoothDevice> deviceList = mDeviceManager.getConnectedDevices();
         Optional<BluetoothDevice> matchingDevice = deviceList.stream()
                 .filter(d -> Objects.equals(d.getAddress(), address))
diff --git a/src/com/android/server/telecom/voip/IncomingCallTransaction.java b/src/com/android/server/telecom/voip/IncomingCallTransaction.java
index d35030c..ed0c7d6 100644
--- a/src/com/android/server/telecom/voip/IncomingCallTransaction.java
+++ b/src/com/android/server/telecom/voip/IncomingCallTransaction.java
@@ -19,14 +19,18 @@
 import static android.telecom.CallAttributes.CALL_CAPABILITIES_KEY;
 import static android.telecom.CallAttributes.DISPLAY_NAME_KEY;
 
+import static com.android.server.telecom.voip.VideoStateTranslation.TransactionalVideoStateToVideoProfileState;
+
 import android.os.Bundle;
 import android.telecom.CallAttributes;
 import android.telecom.CallException;
 import android.telecom.TelecomManager;
 import android.util.Log;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.telecom.Call;
 import com.android.server.telecom.CallsManager;
+import com.android.server.telecom.flags.FeatureFlags;
 
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.CompletionStage;
@@ -38,19 +42,25 @@
     private final CallAttributes mCallAttributes;
     private final CallsManager mCallsManager;
     private final Bundle mExtras;
+    private FeatureFlags mFeatureFlags;
+
+    public void setFeatureFlags(FeatureFlags featureFlags) {
+        mFeatureFlags = featureFlags;
+    }
 
     public IncomingCallTransaction(String callId, CallAttributes callAttributes,
-            CallsManager callsManager, Bundle extras) {
+            CallsManager callsManager, Bundle extras, FeatureFlags featureFlags) {
         super(callsManager.getLock());
         mExtras = extras;
         mCallId = callId;
         mCallAttributes = callAttributes;
         mCallsManager = callsManager;
+        mFeatureFlags = featureFlags;
     }
 
     public IncomingCallTransaction(String callId, CallAttributes callAttributes,
-            CallsManager callsManager) {
-        this(callId, callAttributes, callsManager, new Bundle());
+            CallsManager callsManager, FeatureFlags featureFlags) {
+        this(callId, callAttributes, callsManager, new Bundle(), featureFlags);
     }
 
     @Override
@@ -77,10 +87,19 @@
         }
     }
 
-    private Bundle generateExtras(CallAttributes callAttributes) {
+    @VisibleForTesting
+    public Bundle generateExtras(CallAttributes callAttributes) {
         mExtras.putString(TelecomManager.TRANSACTION_CALL_ID_KEY, mCallId);
         mExtras.putInt(CALL_CAPABILITIES_KEY, callAttributes.getCallCapabilities());
-        mExtras.putInt(TelecomManager.EXTRA_INCOMING_VIDEO_STATE, callAttributes.getCallType());
+        if (mFeatureFlags.transactionalVideoState()) {
+            // Transactional calls need to remap the CallAttributes video state to the existing
+            // VideoProfile for consistency.
+            mExtras.putInt(TelecomManager.EXTRA_INCOMING_VIDEO_STATE,
+                    TransactionalVideoStateToVideoProfileState(callAttributes.getCallType()));
+        } else {
+            mExtras.putInt(TelecomManager.EXTRA_INCOMING_VIDEO_STATE,
+                    callAttributes.getCallType());
+        }
         mExtras.putCharSequence(DISPLAY_NAME_KEY, callAttributes.getDisplayName());
         mExtras.putParcelable(TelecomManager.EXTRA_INCOMING_CALL_ADDRESS,
                 callAttributes.getAddress());
diff --git a/src/com/android/server/telecom/voip/OutgoingCallTransaction.java b/src/com/android/server/telecom/voip/OutgoingCallTransaction.java
index b2625e6..8c970db 100644
--- a/src/com/android/server/telecom/voip/OutgoingCallTransaction.java
+++ b/src/com/android/server/telecom/voip/OutgoingCallTransaction.java
@@ -21,6 +21,8 @@
 import static android.telecom.CallAttributes.DISPLAY_NAME_KEY;
 import static android.telecom.CallException.CODE_CALL_NOT_PERMITTED_AT_PRESENT_TIME;
 
+import static com.android.server.telecom.voip.VideoStateTranslation.TransactionalVideoStateToVideoProfileState;
+
 import android.content.Context;
 import android.content.Intent;
 import android.content.pm.PackageManager;
@@ -29,9 +31,11 @@
 import android.telecom.TelecomManager;
 import android.util.Log;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.telecom.Call;
 import com.android.server.telecom.CallsManager;
 import com.android.server.telecom.LoggedHandlerExecutor;
+import com.android.server.telecom.flags.FeatureFlags;
 
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.CompletionStage;
@@ -45,9 +49,14 @@
     private final CallAttributes mCallAttributes;
     private final CallsManager mCallsManager;
     private final Bundle mExtras;
+    private FeatureFlags mFeatureFlags;
+
+    public void setFeatureFlags(FeatureFlags featureFlags) {
+        mFeatureFlags = featureFlags;
+    }
 
     public OutgoingCallTransaction(String callId, Context context, CallAttributes callAttributes,
-            CallsManager callsManager, Bundle extras) {
+            CallsManager callsManager, Bundle extras, FeatureFlags featureFlags) {
         super(callsManager.getLock());
         mCallId = callId;
         mContext = context;
@@ -55,11 +64,12 @@
         mCallsManager = callsManager;
         mExtras = extras;
         mCallingPackage = mContext.getOpPackageName();
+        mFeatureFlags = featureFlags;
     }
 
     public OutgoingCallTransaction(String callId, Context context, CallAttributes callAttributes,
-            CallsManager callsManager) {
-        this(callId, context, callAttributes, callsManager, new Bundle());
+            CallsManager callsManager, FeatureFlags featureFlags) {
+        this(callId, context, callAttributes, callsManager, new Bundle(), featureFlags);
     }
 
     @Override
@@ -121,12 +131,20 @@
         }
     }
 
-    private Bundle generateExtras(CallAttributes callAttributes) {
+    @VisibleForTesting
+    public Bundle generateExtras(CallAttributes callAttributes) {
         mExtras.setDefusable(true);
         mExtras.putString(TelecomManager.TRANSACTION_CALL_ID_KEY, mCallId);
         mExtras.putInt(CALL_CAPABILITIES_KEY, callAttributes.getCallCapabilities());
-        mExtras.putInt(TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE,
-                callAttributes.getCallType());
+        if (mFeatureFlags.transactionalVideoState()) {
+            // Transactional calls need to remap the CallAttributes video state to the existing
+            // VideoProfile for consistency.
+            mExtras.putInt(TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE,
+                    TransactionalVideoStateToVideoProfileState(callAttributes.getCallType()));
+        } else {
+            mExtras.putInt(TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE,
+                    callAttributes.getCallType());
+        }
         mExtras.putCharSequence(DISPLAY_NAME_KEY, callAttributes.getDisplayName());
         return mExtras;
     }
diff --git a/src/com/android/server/telecom/voip/RequestVideoStateTransaction.java b/src/com/android/server/telecom/voip/RequestVideoStateTransaction.java
index acd7d7a..c1bc343 100644
--- a/src/com/android/server/telecom/voip/RequestVideoStateTransaction.java
+++ b/src/com/android/server/telecom/voip/RequestVideoStateTransaction.java
@@ -51,11 +51,6 @@
             future.complete(new VoipCallTransactionResult(
                     CallException.CODE_ERROR_UNKNOWN /*TODO:: define error code. b/335703584 */,
                     "Video calling is not supported by the target account"));
-        } else if (isRequestingVideoTransmission(mVideoProfileState) &&
-                !mCall.isTransactionalCallSupportsVideoCalling()) {
-            future.complete(new VoipCallTransactionResult(
-                    CallException.CODE_ERROR_UNKNOWN /*TODO:: define error code. b/335703584 */,
-                    "Video calling is not supported according to the callAttributes"));
         } else {
             mCall.setVideoState(mVideoProfileState);
             future.complete(new VoipCallTransactionResult(
diff --git a/src/com/android/server/telecom/voip/VideoStateTranslation.java b/src/com/android/server/telecom/voip/VideoStateTranslation.java
index 615e4bc..3812d15 100644
--- a/src/com/android/server/telecom/voip/VideoStateTranslation.java
+++ b/src/com/android/server/telecom/voip/VideoStateTranslation.java
@@ -20,6 +20,11 @@
 import android.telecom.Log;
 import android.telecom.VideoProfile;
 
+import com.android.server.telecom.AnomalyReporterAdapter;
+import com.android.server.telecom.AnomalyReporterAdapterImpl;
+
+import java.util.UUID;
+
 /**
  * This remapping class is needed because {@link VideoProfile} has more fine grain levels of video
  * states as apposed to Transactional video states (defined in  {@link CallAttributes.CallType}.
@@ -41,15 +46,16 @@
      * This should be used when the client application is signaling they are changing the video
      * state.
      */
-    public static int TransactionalVideoStateToVideoProfileState(int transactionalVideo) {
-        if (transactionalVideo == CallAttributes.AUDIO_CALL) {
-            Log.i(TAG, "%s --> VideoProfile.STATE_AUDIO_ONLY",
-                    TransactionalVideoState_toString(transactionalVideo));
+    public static int TransactionalVideoStateToVideoProfileState(int callType) {
+        if (callType == CallAttributes.AUDIO_CALL) {
+            Log.i(TAG, "CallAttributes.AUDIO_CALL --> VideoProfile.STATE_AUDIO_ONLY");
             return VideoProfile.STATE_AUDIO_ONLY;
-        } else {
-            Log.i(TAG, "%s --> VideoProfile.STATE_BIDIRECTIONAL",
-                    TransactionalVideoState_toString(transactionalVideo));
+        } else if (callType == CallAttributes.VIDEO_CALL) {
+            Log.i(TAG, "CallAttributes.VIDEO_CALL--> VideoProfile.STATE_BIDIRECTIONAL");
             return VideoProfile.STATE_BIDIRECTIONAL;
+        } else {
+            Log.w(TAG, "CallType=[%d] does not have a VideoProfile mapping", callType);
+            return VideoProfile.STATE_AUDIO_ONLY;
         }
     }
 
@@ -58,26 +64,36 @@
      * This should be used when Telecom is informing the client of a video state change.
      */
     public static int VideoProfileStateToTransactionalVideoState(int videoProfileState) {
-        if (videoProfileState == VideoProfile.STATE_AUDIO_ONLY) {
-            Log.i(TAG, "%s --> CallAttributes.AUDIO_CALL",
-                    VideoProfileState_toString(videoProfileState));
-            return CallAttributes.AUDIO_CALL;
-        } else {
-            Log.i(TAG, "%s --> CallAttributes.VIDEO_CALL",
-                    VideoProfileState_toString(videoProfileState));
-            return CallAttributes.VIDEO_CALL;
+        switch (videoProfileState) {
+            case VideoProfile.STATE_AUDIO_ONLY -> {
+                Log.i(TAG, "%s --> CallAttributes.AUDIO_CALL",
+                        VideoProfileStateToString(videoProfileState));
+                return CallAttributes.AUDIO_CALL;
+            }
+            case VideoProfile.STATE_BIDIRECTIONAL, VideoProfile.STATE_TX_ENABLED,
+                    VideoProfile.STATE_RX_ENABLED -> {
+                Log.i(TAG, "%s --> CallAttributes.VIDEO_CALL",
+                        VideoProfileStateToString(videoProfileState));
+                return CallAttributes.VIDEO_CALL;
+            }
+            default -> {
+                Log.w(TAG, "VideoProfile=[%d] does not have a CallType mapping", videoProfileState);
+                return CallAttributes.AUDIO_CALL;
+            }
         }
     }
 
-    private static String TransactionalVideoState_toString(int transactionalVideoState) {
+    public static String TransactionalVideoStateToString(int transactionalVideoState) {
         if (transactionalVideoState == CallAttributes.AUDIO_CALL) {
             return "CallAttributes.AUDIO_CALL";
-        } else {
+        } else if (transactionalVideoState == CallAttributes.VIDEO_CALL) {
             return "CallAttributes.VIDEO_CALL";
+        } else {
+            return "CallAttributes.UNKNOWN";
         }
     }
 
-    private static String VideoProfileState_toString(int videoProfileState) {
+    private static String VideoProfileStateToString(int videoProfileState) {
         switch (videoProfileState) {
             case VideoProfile.STATE_BIDIRECTIONAL -> {
                 return "VideoProfile.STATE_BIDIRECTIONAL";
@@ -88,7 +104,12 @@
             case VideoProfile.STATE_TX_ENABLED -> {
                 return "VideoProfile.STATE_TX_ENABLED";
             }
+            case VideoProfile.STATE_AUDIO_ONLY -> {
+                return "VideoProfile.STATE_AUDIO_ONLY";
+            }
+            default -> {
+                return "VideoProfile.UNKNOWN";
+            }
         }
-        return "VideoProfile.STATE_AUDIO_ONLY";
     }
 }
diff --git a/tests/src/com/android/server/telecom/tests/BluetoothRouteManagerTest.java b/tests/src/com/android/server/telecom/tests/BluetoothRouteManagerTest.java
index 07dd350..1c885c1 100644
--- a/tests/src/com/android/server/telecom/tests/BluetoothRouteManagerTest.java
+++ b/tests/src/com/android/server/telecom/tests/BluetoothRouteManagerTest.java
@@ -21,6 +21,7 @@
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.ArgumentMatchers.nullable;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
@@ -249,7 +250,18 @@
 
     @SmallTest
     @Test
-    public void testConnectBtWithoutAddress() {
+    public void testConnectBtWithoutAddress_SwitchingBtDeviceFlag() {
+        when(mFeatureFlags.resolveSwitchingBtDevicesComputation()).thenReturn(true);
+        verifyConnectBtWithoutAddress();
+    }
+
+    @SmallTest
+    @Test
+    public void testConnectBtWithoutAddress_SwitchingBtDeviceFlagDisabled() {
+        verifyConnectBtWithoutAddress();
+    }
+
+    private void verifyConnectBtWithoutAddress() {
         when(mFeatureFlags.useActualAddressToEnterConnectingState()).thenReturn(true);
         BluetoothRouteManager sm = setupStateMachine(
                 BluetoothRouteManager.AUDIO_CONNECTED_STATE_NAME_PREFIX, DEVICE1);
@@ -266,7 +278,15 @@
         waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT);
         waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT);
         waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT);
-        verifyConnectionAttempt(DEVICE1, 1);
+        // We should not expect explicit connection attempt (BluetoothDeviceManager#connectAudio)
+        // as the device is already "connected" as per how the state machine was initialized.
+        if (mFeatureFlags.resolveSwitchingBtDevicesComputation()) {
+            verify(mDeviceManager, never()).disconnectAudio();
+        } else {
+            // Legacy behavior
+            verifyConnectionAttempt(DEVICE1, 1);
+            verify(mDeviceManager, times(1)).disconnectAudio();
+        }
         assertEquals(BluetoothRouteManager.AUDIO_CONNECTED_STATE_NAME_PREFIX
                         + ":" + DEVICE1.getAddress(),
                 sm.getCurrentState().getName());
diff --git a/tests/src/com/android/server/telecom/tests/CallAudioRouteStateMachineTest.java b/tests/src/com/android/server/telecom/tests/CallAudioRouteStateMachineTest.java
index d2da505..e97de2e 100644
--- a/tests/src/com/android/server/telecom/tests/CallAudioRouteStateMachineTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallAudioRouteStateMachineTest.java
@@ -20,6 +20,7 @@
 import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.ArgumentMatchers.nullable;
@@ -1230,8 +1231,9 @@
 
     @SmallTest
     @Test
-    public void testQuiescentBluetoothRouteResetMute() {
+    public void testQuiescentBluetoothRouteResetMute() throws Exception {
         when(mFeatureFlags.resetMuteWhenEnteringQuiescentBtRoute()).thenReturn(true);
+        when(mFeatureFlags.transitRouteBeforeAudioDisconnectBt()).thenReturn(true);
         CallAudioRouteStateMachine stateMachine = new CallAudioRouteStateMachine(
                 mContext,
                 mockCallsManager,
@@ -1264,6 +1266,7 @@
                 CallAudioState.ROUTE_BLUETOOTH, CallAudioState.ROUTE_SPEAKER
                 | CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH);
         assertEquals(expectedState, stateMachine.getCurrentCallAudioState());
+        when(mockAudioManager.isMicrophoneMute()).thenReturn(true);
 
         stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.SWITCH_FOCUS,
                 CallAudioRouteStateMachine.NO_FOCUS);
@@ -1272,9 +1275,8 @@
         expectedState = new CallAudioState(false,
                 CallAudioState.ROUTE_BLUETOOTH, CallAudioState.ROUTE_SPEAKER
                 | CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH);
-        // TODO: Re-enable this part of the test; this is now failing because we have to
-        // revert ag/23783145.
-        // assertEquals(expectedState, stateMachine.getCurrentCallAudioState());
+        assertEquals(expectedState, stateMachine.getCurrentCallAudioState());
+        verify(mockAudioService).setMicrophoneMute(eq(false), anyString(), anyInt(), eq(null));
     }
 
     @SmallTest
diff --git a/tests/src/com/android/server/telecom/tests/CallTest.java b/tests/src/com/android/server/telecom/tests/CallTest.java
index b79775b..a22d2ca 100644
--- a/tests/src/com/android/server/telecom/tests/CallTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallTest.java
@@ -152,6 +152,69 @@
         assertTrue(call.hasGoneActiveBefore());
     }
 
+    /**
+     * Verify Call#setVideoState will only upgrade to video if the PhoneAccount supports video
+     * state capabilities
+     */
+    @Test
+    @SmallTest
+    public void testSetVideoStateForTransactionalCalls() {
+        Call call = createCall("1", Call.CALL_DIRECTION_INCOMING);
+        TransactionalServiceWrapper tsw = Mockito.mock(TransactionalServiceWrapper.class);
+        call.setIsTransactionalCall(true);
+        call.setTransactionServiceWrapper(tsw);
+        assertTrue(call.isTransactionalCall());
+        assertNotNull(call.getTransactionServiceWrapper());
+        when(mFeatureFlags.transactionalVideoState()).thenReturn(true);
+
+        // VoIP apps using transactional APIs must register a PhoneAccount that supports
+        // video calling capabilities or the video state will be defaulted to audio
+        assertFalse(call.isVideoCallingSupportedByPhoneAccount());
+        call.setVideoState(VideoProfile.STATE_BIDIRECTIONAL);
+        assertEquals(VideoProfile.STATE_AUDIO_ONLY, call.getVideoState());
+
+        call.setVideoCallingSupportedByPhoneAccount(true);
+        assertTrue(call.isVideoCallingSupportedByPhoneAccount());
+
+        // After the PhoneAccount signals it supports video calling, video state changes can occur
+        call.setVideoState(VideoProfile.STATE_BIDIRECTIONAL);
+        assertEquals(VideoProfile.STATE_BIDIRECTIONAL, call.getVideoState());
+        verify(tsw, times(1)).onVideoStateChanged(call, CallAttributes.VIDEO_CALL);
+    }
+
+    /**
+     * Verify all video state changes are echoed out to the TransactionalServiceWrapper
+     */
+    @Test
+    @SmallTest
+    public void testToggleTransactionalVideoState() {
+        Call call = createCall("1", Call.CALL_DIRECTION_INCOMING);
+        TransactionalServiceWrapper tsw = Mockito.mock(TransactionalServiceWrapper.class);
+        call.setIsTransactionalCall(true);
+        call.setTransactionServiceWrapper(tsw);
+        call.setVideoCallingSupportedByPhoneAccount(true);
+        assertTrue(call.isTransactionalCall());
+        assertNotNull(call.getTransactionServiceWrapper());
+        assertTrue(call.isVideoCallingSupportedByPhoneAccount());
+        when(mFeatureFlags.transactionalVideoState()).thenReturn(true);
+
+        call.setVideoState(VideoProfile.STATE_BIDIRECTIONAL);
+        assertEquals(VideoProfile.STATE_BIDIRECTIONAL, call.getVideoState());
+        verify(tsw, times(1)).onVideoStateChanged(call, CallAttributes.VIDEO_CALL);
+
+        call.setVideoState(VideoProfile.STATE_BIDIRECTIONAL);
+        assertEquals(VideoProfile.STATE_BIDIRECTIONAL, call.getVideoState());
+        verify(tsw, times(2)).onVideoStateChanged(call, CallAttributes.VIDEO_CALL);
+
+        call.setVideoState(VideoProfile.STATE_AUDIO_ONLY);
+        assertEquals(VideoProfile.STATE_AUDIO_ONLY, call.getVideoState());
+        verify(tsw, times(1)).onVideoStateChanged(call, CallAttributes.AUDIO_CALL);
+
+        call.setVideoState(VideoProfile.STATE_BIDIRECTIONAL);
+        assertEquals(VideoProfile.STATE_BIDIRECTIONAL, call.getVideoState());
+        verify(tsw, times(3)).onVideoStateChanged(call, CallAttributes.VIDEO_CALL);
+    }
+
     @Test
     public void testMultipleCachedMuteStateChanges() {
         when(mFeatureFlags.cacheCallAudioCallbacks()).thenReturn(true);
diff --git a/tests/src/com/android/server/telecom/tests/TransactionTests.java b/tests/src/com/android/server/telecom/tests/TransactionTests.java
index b495b45..707ed9f 100644
--- a/tests/src/com/android/server/telecom/tests/TransactionTests.java
+++ b/tests/src/com/android/server/telecom/tests/TransactionTests.java
@@ -16,6 +16,9 @@
 
 package com.android.server.telecom.tests;
 
+import static com.android.server.telecom.voip.VideoStateTranslation.TransactionalVideoStateToVideoProfileState;
+import static com.android.server.telecom.voip.VideoStateTranslation.VideoProfileStateToTransactionalVideoState;
+
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
@@ -45,6 +48,8 @@
 import android.telecom.CallAttributes;
 import android.telecom.DisconnectCause;
 import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
+import android.telecom.VideoProfile;
 
 import androidx.test.filters.SmallTest;
 
@@ -66,6 +71,7 @@
 import com.android.server.telecom.voip.RequestNewActiveCallTransaction;
 import com.android.server.telecom.voip.TransactionManager;
 import com.android.server.telecom.voip.VerifyCallStateChangeTransaction;
+import com.android.server.telecom.voip.VideoStateTranslation;
 import com.android.server.telecom.voip.VoipCallTransactionResult;
 
 import org.junit.After;
@@ -229,7 +235,8 @@
                 CallAttributes.DIRECTION_OUTGOING, TEST_NAME, TEST_URI).build();
 
         OutgoingCallTransaction transaction =
-                new OutgoingCallTransaction(CALL_ID_1, mMockContext, callAttributes, mCallsManager);
+                new OutgoingCallTransaction(CALL_ID_1, mMockContext, callAttributes, mCallsManager,
+                        mFeatureFlags);
 
         // WHEN
         when(mMockContext.getOpPackageName()).thenReturn("testPackage");
@@ -256,7 +263,8 @@
                 CallAttributes.DIRECTION_INCOMING, TEST_NAME, TEST_URI).build();
 
         IncomingCallTransaction transaction =
-                new IncomingCallTransaction(CALL_ID_1, callAttributes, mCallsManager);
+                new IncomingCallTransaction(CALL_ID_1, callAttributes, mCallsManager,
+                        mFeatureFlags);
 
         // WHEN
         when(mCallsManager.isIncomingCallPermitted(callAttributes.getPhoneAccountHandle()))
@@ -271,6 +279,100 @@
     }
 
     /**
+     * Verify that transactional OUTGOING calls are re-mapping the CallAttributes video state to
+     * VideoProfile states when starting the call via CallsManager#startOugoingCall.
+     */
+    @Test
+    public void testOutgoingCallTransactionRemapsVideoState() {
+        // GIVEN
+        CallAttributes audioOnlyAttributes = new CallAttributes.Builder(mHandle,
+                CallAttributes.DIRECTION_OUTGOING, TEST_NAME, TEST_URI)
+                .setCallType(CallAttributes.AUDIO_CALL)
+                .build();
+
+        CallAttributes videoAttributes = new CallAttributes.Builder(mHandle,
+                CallAttributes.DIRECTION_OUTGOING, TEST_NAME, TEST_URI)
+                .setCallType(CallAttributes.VIDEO_CALL)
+                .build();
+
+        OutgoingCallTransaction t = new OutgoingCallTransaction(null,
+                mContext, null, mCallsManager, new Bundle(), mFeatureFlags);
+
+        // WHEN
+        when(mFeatureFlags.transactionalVideoState()).thenReturn(true);
+        t.setFeatureFlags(mFeatureFlags);
+
+        // THEN
+        assertEquals(VideoProfile.STATE_AUDIO_ONLY, t
+                .generateExtras(audioOnlyAttributes)
+                .getInt(TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE));
+
+        assertEquals(VideoProfile.STATE_BIDIRECTIONAL, t
+                .generateExtras(videoAttributes)
+                .getInt(TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE));
+    }
+
+    /**
+     * Verify that transactional INCOMING calls are re-mapping the CallAttributes video state to
+     * VideoProfile states when starting the call in CallsManager#processIncomingCallIntent.
+     */
+    @Test
+    public void testIncomingCallTransactionRemapsVideoState() {
+        // GIVEN
+        CallAttributes audioOnlyAttributes = new CallAttributes.Builder(mHandle,
+                CallAttributes.DIRECTION_INCOMING, TEST_NAME, TEST_URI)
+                .setCallType(CallAttributes.AUDIO_CALL)
+                .build();
+
+        CallAttributes videoAttributes = new CallAttributes.Builder(mHandle,
+                CallAttributes.DIRECTION_INCOMING, TEST_NAME, TEST_URI)
+                .setCallType(CallAttributes.VIDEO_CALL)
+                .build();
+
+        IncomingCallTransaction t = new IncomingCallTransaction(null, null,
+                mCallsManager, new Bundle(), mFeatureFlags);
+
+        // WHEN
+        when(mFeatureFlags.transactionalVideoState()).thenReturn(true);
+        t.setFeatureFlags(mFeatureFlags);
+
+        // THEN
+        assertEquals(VideoProfile.STATE_AUDIO_ONLY, t
+                .generateExtras(audioOnlyAttributes)
+                .getInt(TelecomManager.EXTRA_INCOMING_VIDEO_STATE));
+
+        assertEquals(VideoProfile.STATE_BIDIRECTIONAL, t
+                .generateExtras(videoAttributes)
+                .getInt(TelecomManager.EXTRA_INCOMING_VIDEO_STATE));
+    }
+
+    @Test
+    public void testTransactionalVideoStateToVideoProfileState() {
+        assertEquals(VideoProfile.STATE_AUDIO_ONLY,
+                TransactionalVideoStateToVideoProfileState(CallAttributes.AUDIO_CALL));
+        assertEquals(VideoProfile.STATE_BIDIRECTIONAL,
+                TransactionalVideoStateToVideoProfileState(CallAttributes.VIDEO_CALL));
+        // ensure non-defined values default to audio
+        assertEquals(VideoProfile.STATE_AUDIO_ONLY,
+                TransactionalVideoStateToVideoProfileState(-1));
+    }
+
+    @Test
+    public void testVideoProfileStateToTransactionalVideoState() {
+        assertEquals(CallAttributes.AUDIO_CALL,
+                VideoProfileStateToTransactionalVideoState(VideoProfile.STATE_AUDIO_ONLY));
+        assertEquals(CallAttributes.VIDEO_CALL,
+                VideoProfileStateToTransactionalVideoState(VideoProfile.STATE_RX_ENABLED));
+        assertEquals(CallAttributes.VIDEO_CALL,
+                VideoProfileStateToTransactionalVideoState(VideoProfile.STATE_TX_ENABLED));
+        assertEquals(CallAttributes.VIDEO_CALL,
+                VideoProfileStateToTransactionalVideoState(VideoProfile.STATE_BIDIRECTIONAL));
+        // ensure non-defined values default to audio
+        assertEquals(CallAttributes.AUDIO_CALL,
+                VideoProfileStateToTransactionalVideoState(-1));
+    }
+
+    /**
      * This test verifies if the ConnectionService call is NOT transitioned to the desired call
      * state (within timeout period), Telecom will disconnect the call.
      */