Merge "Adds TransactionManager dumpsys info and cleanups" into main
diff --git a/flags/Android.bp b/flags/Android.bp
index 386831c..3497db8 100644
--- a/flags/Android.bp
+++ b/flags/Android.bp
@@ -21,23 +21,23 @@
 aconfig_declarations {
     name: "telecom_flags",
     package: "com.android.server.telecom.flags",
+    container: "system",
     srcs: [
-      "telecom_broadcast_flags.aconfig",
-      "telecom_ringer_flag_declarations.aconfig",
-      "telecom_api_flags.aconfig",
-      "telecom_call_filtering_flags.aconfig",
-      "telecom_incallservice_flags.aconfig",
-      "telecom_default_phone_account_flags.aconfig",
-      "telecom_callaudioroutestatemachine_flags.aconfig",
-      "telecom_call_flags.aconfig",
-      "telecom_calls_manager_flags.aconfig",
-      "telecom_anomaly_report_flags.aconfig",
-      "telecom_callaudiomodestatemachine_flags.aconfig",
-      "telecom_calllog_flags.aconfig",
-      "telecom_resolve_hidden_dependencies.aconfig",
-      "telecom_bluetoothroutemanager_flags.aconfig",
-      "telecom_work_profile_flags.aconfig",
-      "telecom_connection_service_wrapper_flags.aconfig",
+        "telecom_broadcast_flags.aconfig",
+        "telecom_ringer_flag_declarations.aconfig",
+        "telecom_api_flags.aconfig",
+        "telecom_call_filtering_flags.aconfig",
+        "telecom_incallservice_flags.aconfig",
+        "telecom_default_phone_account_flags.aconfig",
+        "telecom_callaudioroutestatemachine_flags.aconfig",
+        "telecom_call_flags.aconfig",
+        "telecom_calls_manager_flags.aconfig",
+        "telecom_anomaly_report_flags.aconfig",
+        "telecom_callaudiomodestatemachine_flags.aconfig",
+        "telecom_calllog_flags.aconfig",
+        "telecom_resolve_hidden_dependencies.aconfig",
+        "telecom_bluetoothroutemanager_flags.aconfig",
+        "telecom_work_profile_flags.aconfig",
+        "telecom_connection_service_wrapper_flags.aconfig",
     ],
 }
-
diff --git a/flags/telecom_anomaly_report_flags.aconfig b/flags/telecom_anomaly_report_flags.aconfig
index dbacc08..6879d86 100644
--- a/flags/telecom_anomaly_report_flags.aconfig
+++ b/flags/telecom_anomaly_report_flags.aconfig
@@ -1,4 +1,5 @@
 package: "com.android.server.telecom.flags"
+container: "system"
 
 flag {
   name: "gen_anom_report_on_focus_timeout"
diff --git a/flags/telecom_api_flags.aconfig b/flags/telecom_api_flags.aconfig
index 21b83b2..c44bea4 100644
--- a/flags/telecom_api_flags.aconfig
+++ b/flags/telecom_api_flags.aconfig
@@ -1,4 +1,5 @@
 package: "com.android.server.telecom.flags"
+container: "system"
 
 flag {
   name: "voip_app_actions_support"
@@ -21,10 +22,30 @@
   bug: "292597423"
 }
 
-
 flag{
   name: "set_mute_state"
   namespace: "telecom"
   description: "transactional calls need the ability to mute the call audio input"
   bug: "310669304"
 }
+
+flag{
+  name: "get_registered_phone_accounts"
+  namespace: "telecom"
+  description: "When set, self-managed clients can get their own phone accounts"
+  bug: "317132586"
+}
+
+flag{
+  name: "transactional_video_state"
+  namespace: "telecom"
+  description: "when set, clients using transactional implementations will be able to set & get the video state"
+  bug: "311265260"
+}
+
+flag{
+  name: "business_call_composer"
+  namespace: "telecom"
+  description: "Enables enriched calling features (e.g. Business name will show for a call)"
+  bug: "311688497"
+}
diff --git a/flags/telecom_bluetoothroutemanager_flags.aconfig b/flags/telecom_bluetoothroutemanager_flags.aconfig
index ddd8571..1df1e9b 100644
--- a/flags/telecom_bluetoothroutemanager_flags.aconfig
+++ b/flags/telecom_bluetoothroutemanager_flags.aconfig
@@ -1,4 +1,5 @@
 package: "com.android.server.telecom.flags"
+container: "system"
 
 flag {
   name: "use_actual_address_to_enter_connecting_state"
diff --git a/flags/telecom_broadcast_flags.aconfig b/flags/telecom_broadcast_flags.aconfig
index 348d574..de8dd27 100644
--- a/flags/telecom_broadcast_flags.aconfig
+++ b/flags/telecom_broadcast_flags.aconfig
@@ -1,4 +1,5 @@
 package: "com.android.server.telecom.flags"
+container: "system"
 
 flag {
   name: "is_new_outgoing_call_broadcast_unblocking"
diff --git a/flags/telecom_call_filtering_flags.aconfig b/flags/telecom_call_filtering_flags.aconfig
index 95e74ce..72f9db3 100644
--- a/flags/telecom_call_filtering_flags.aconfig
+++ b/flags/telecom_call_filtering_flags.aconfig
@@ -1,4 +1,5 @@
 package: "com.android.server.telecom.flags"
+container: "system"
 
 flag {
   name: "skip_filter_phone_account_perform_dnd_filter"
diff --git a/flags/telecom_call_flags.aconfig b/flags/telecom_call_flags.aconfig
index b5ea6a2..27a4b22 100644
--- a/flags/telecom_call_flags.aconfig
+++ b/flags/telecom_call_flags.aconfig
@@ -1,4 +1,5 @@
 package: "com.android.server.telecom.flags"
+container: "system"
 
 flag {
   name: "transactional_cs_verifier"
diff --git a/flags/telecom_callaudiomodestatemachine_flags.aconfig b/flags/telecom_callaudiomodestatemachine_flags.aconfig
index b263113..1d81535 100644
--- a/flags/telecom_callaudiomodestatemachine_flags.aconfig
+++ b/flags/telecom_callaudiomodestatemachine_flags.aconfig
@@ -1,4 +1,5 @@
 package: "com.android.server.telecom.flags"
+container: "system"
 
 flag {
   name: "set_audio_mode_before_abandon_focus"
diff --git a/flags/telecom_callaudioroutestatemachine_flags.aconfig b/flags/telecom_callaudioroutestatemachine_flags.aconfig
index fe21c92..f5da045 100644
--- a/flags/telecom_callaudioroutestatemachine_flags.aconfig
+++ b/flags/telecom_callaudioroutestatemachine_flags.aconfig
@@ -1,4 +1,5 @@
 package: "com.android.server.telecom.flags"
+container: "system"
 
 flag {
   name: "available_routes_never_updated_after_set_system_audio_state"
diff --git a/flags/telecom_calllog_flags.aconfig b/flags/telecom_calllog_flags.aconfig
index 3ce7b63..593b7e5 100644
--- a/flags/telecom_calllog_flags.aconfig
+++ b/flags/telecom_calllog_flags.aconfig
@@ -1,4 +1,5 @@
 package: "com.android.server.telecom.flags"
+container: "system"
 
 flag {
   name: "telecom_log_external_wearable_calls"
diff --git a/flags/telecom_calls_manager_flags.aconfig b/flags/telecom_calls_manager_flags.aconfig
index a68042e..de17eee 100644
--- a/flags/telecom_calls_manager_flags.aconfig
+++ b/flags/telecom_calls_manager_flags.aconfig
@@ -1,4 +1,5 @@
 package: "com.android.server.telecom.flags"
+container: "system"
 
 flag {
   name: "use_improved_listener_order"
diff --git a/flags/telecom_connection_service_wrapper_flags.aconfig b/flags/telecom_connection_service_wrapper_flags.aconfig
index 5f46c27..80a8dfe 100644
--- a/flags/telecom_connection_service_wrapper_flags.aconfig
+++ b/flags/telecom_connection_service_wrapper_flags.aconfig
@@ -1,4 +1,5 @@
 package: "com.android.server.telecom.flags"
+container: "system"
 
 flag {
   name: "updated_rcs_call_count_tracking"
diff --git a/flags/telecom_default_phone_account_flags.aconfig b/flags/telecom_default_phone_account_flags.aconfig
index 03f324c..e6badde 100644
--- a/flags/telecom_default_phone_account_flags.aconfig
+++ b/flags/telecom_default_phone_account_flags.aconfig
@@ -1,4 +1,5 @@
 package: "com.android.server.telecom.flags"
+container: "system"
 
 flag {
   name: "only_update_telephony_on_valid_sub_ids"
diff --git a/flags/telecom_incallservice_flags.aconfig b/flags/telecom_incallservice_flags.aconfig
index 1110ca4..08a82ba 100644
--- a/flags/telecom_incallservice_flags.aconfig
+++ b/flags/telecom_incallservice_flags.aconfig
@@ -1,4 +1,5 @@
 package: "com.android.server.telecom.flags"
+container: "system"
 
 flag {
   name: "early_binding_to_incall_service"
diff --git a/flags/telecom_resolve_hidden_dependencies.aconfig b/flags/telecom_resolve_hidden_dependencies.aconfig
index 6def938..674a968 100644
--- a/flags/telecom_resolve_hidden_dependencies.aconfig
+++ b/flags/telecom_resolve_hidden_dependencies.aconfig
@@ -1,8 +1,9 @@
 package: "com.android.server.telecom.flags"
+container: "system"
 
 flag {
     name: "telecom_resolve_hidden_dependencies"
     namespace: "telecom"
     description: "Mainland cleanup for hidden dependencies"
-    bug: "b/324090590"
+    bug: "323414215"
 }
diff --git a/flags/telecom_ringer_flag_declarations.aconfig b/flags/telecom_ringer_flag_declarations.aconfig
index 54748d0..13577bb 100644
--- a/flags/telecom_ringer_flag_declarations.aconfig
+++ b/flags/telecom_ringer_flag_declarations.aconfig
@@ -1,4 +1,5 @@
 package: "com.android.server.telecom.flags"
+container: "system"
 
 flag {
   name: "use_device_provided_serialized_ringer_vibration"
diff --git a/flags/telecom_work_profile_flags.aconfig b/flags/telecom_work_profile_flags.aconfig
index 180af59..854568b 100644
--- a/flags/telecom_work_profile_flags.aconfig
+++ b/flags/telecom_work_profile_flags.aconfig
@@ -1,4 +1,5 @@
 package: "com.android.server.telecom.flags"
+container: "system"
 
 flag {
   name: "associated_user_refactor_for_work_profile"
diff --git a/src/com/android/server/telecom/Analytics.java b/src/com/android/server/telecom/Analytics.java
index bbcf858..45e3340 100644
--- a/src/com/android/server/telecom/Analytics.java
+++ b/src/com/android/server/telecom/Analytics.java
@@ -721,7 +721,7 @@
 
     private static int getCarrierId(Context context) {
         SubscriptionManager subscriptionManager =
-                context.getSystemService(SubscriptionManager.class);
+                context.getSystemService(SubscriptionManager.class).createForAllUserProfiles();
         List<SubscriptionInfo> subInfos = subscriptionManager.getActiveSubscriptionInfoList();
         if (subInfos == null) {
             return -1;
diff --git a/src/com/android/server/telecom/AudioRoute.java b/src/com/android/server/telecom/AudioRoute.java
index 5037cf5..cdf44a8 100644
--- a/src/com/android/server/telecom/AudioRoute.java
+++ b/src/com/android/server/telecom/AudioRoute.java
@@ -82,13 +82,9 @@
                 }
             }
             if (routeInfo == null) {
-                CompletableFuture<Boolean> future = new CompletableFuture<>();
-                mScheduledExecutorService.schedule(new Runnable() {
-                    @Override
-                    public void run() {
-                        createRetry(type, bluetoothAddress, audioManager, retryCount - 1);
-                    }
-                }, RETRY_TIME_DELAY, TimeUnit.MILLISECONDS);
+                mScheduledExecutorService.schedule(
+                        () -> createRetry(type, bluetoothAddress, audioManager, retryCount - 1),
+                        RETRY_TIME_DELAY, TimeUnit.MILLISECONDS);
             } else {
                 mAudioRouteFuture.complete(new AudioRoute(type, bluetoothAddress, routeInfo));
             }
@@ -105,6 +101,7 @@
     public static final int TYPE_BLUETOOTH_SCO = 5;
     public static final int TYPE_BLUETOOTH_HA = 6;
     public static final int TYPE_BLUETOOTH_LE = 7;
+    public static final int TYPE_STREAMING = 8;
     @IntDef(prefix = "TYPE", value = {
             TYPE_INVALID,
             TYPE_EARPIECE,
@@ -113,7 +110,8 @@
             TYPE_DOCK,
             TYPE_BLUETOOTH_SCO,
             TYPE_BLUETOOTH_HA,
-            TYPE_BLUETOOTH_LE
+            TYPE_BLUETOOTH_LE,
+            TYPE_STREAMING
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface AudioRouteType {}
@@ -145,6 +143,7 @@
         DEVICE_TYPE_STRINGS.put(TYPE_BLUETOOTH_SCO, "TYPE_BLUETOOTH_SCO");
         DEVICE_TYPE_STRINGS.put(TYPE_BLUETOOTH_HA, "TYPE_BLUETOOTH_HA");
         DEVICE_TYPE_STRINGS.put(TYPE_BLUETOOTH_LE, "TYPE_BLUETOOTH_LE");
+        DEVICE_TYPE_STRINGS.put(TYPE_STREAMING, "TYPE_STREAMING");
     }
 
     public static final HashMap<Integer, Integer> DEVICE_INFO_TYPETO_AUDIO_ROUTE_TYPE;
@@ -225,6 +224,7 @@
     void onDestRouteAsPendingRoute(boolean active, PendingAudioRoute pendingAudioRoute,
                                    AudioManager audioManager) {
         if (pendingAudioRoute.isActive() && !active) {
+            Log.i(this, "clearCommunicationDevice");
             audioManager.clearCommunicationDevice();
         } else if (active) {
             if (mAudioRouteType == TYPE_BLUETOOTH_SCO) {
diff --git a/src/com/android/server/telecom/Call.java b/src/com/android/server/telecom/Call.java
index 624399b..f7ad93f 100644
--- a/src/com/android/server/telecom/Call.java
+++ b/src/com/android/server/telecom/Call.java
@@ -19,6 +19,8 @@
 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.VideoProfileStateToTransactionalVideoState;
+
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.content.Context;
@@ -652,6 +654,36 @@
     private boolean mIsVideoCallingSupportedByPhoneAccount = false;
 
     /**
+     * Indicates whether this individual calls video state can be changed as opposed to be gated
+     * by the {@link PhoneAccount}.
+     *
+     * {@code True} if the call is Transactional && has the CallAttributes.SUPPORTS_VIDEO_CALLING
+     * capability {@code false} otherwise.
+     */
+    private boolean mTransactionalCallSupportsVideoCalling = false;
+
+    public void setTransactionalCallSupportsVideoCalling(CallAttributes callAttributes) {
+        if (!mIsTransactionalCall) {
+            Log.i(this, "setTransactionalCallSupportsVideoCalling: call is not transactional");
+            return;
+        }
+        if (callAttributes == null) {
+            Log.i(this, "setTransactionalCallSupportsVideoCalling: callAttributes is null");
+            return;
+        }
+        if ((callAttributes.getCallCapabilities() & CallAttributes.SUPPORTS_VIDEO_CALLING)
+                == CallAttributes.SUPPORTS_VIDEO_CALLING) {
+            mTransactionalCallSupportsVideoCalling = true;
+        } else {
+            mTransactionalCallSupportsVideoCalling = false;
+        }
+    }
+
+    public boolean isTransactionalCallSupportsVideoCalling() {
+        return mTransactionalCallSupportsVideoCalling;
+    }
+
+    /**
      * Indicates whether or not this call can be pulled if it is an external call. If true, respect
      * the Connection Capability set by the ConnectionService. If false, override the capability
      * set and always remove the ability to pull this external call.
@@ -3176,8 +3208,7 @@
             } else if (mConnectionService != null) {
                 mConnectionService.onExtrasChanged(this, mExtras);
             } else {
-                Log.e(this, new NullPointerException(),
-                        "putExtras failed due to null CS callId=%s", getId());
+                Log.w(this, "putExtras failed due to null CS callId=%s", getId());
             }
         }
     }
@@ -4023,6 +4054,15 @@
             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;
+        }
+
         // 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
         // not include the video state history when:
@@ -4045,6 +4085,12 @@
             }
         }
 
+        if (mFlags.transactionalVideoState()
+                && mIsTransactionalCall && mTransactionalService != null) {
+            int transactionalVS = VideoProfileStateToTransactionalVideoState(mVideoState);
+            mTransactionalService.onVideoStateChanged(this, transactionalVS);
+        }
+
         if (VideoProfile.isVideo(videoState)) {
             mAnalytics.setCallIsVideo(true);
         }
diff --git a/src/com/android/server/telecom/CallAudioRouteAdapter.java b/src/com/android/server/telecom/CallAudioRouteAdapter.java
index f76d47d..5585d09 100644
--- a/src/com/android/server/telecom/CallAudioRouteAdapter.java
+++ b/src/com/android/server/telecom/CallAudioRouteAdapter.java
@@ -97,6 +97,9 @@
         put(SPEAKER_ON, "SPEAKER_ON");
         put(SPEAKER_OFF, "SPEAKER_OFF");
 
+        put(STREAMING_FORCE_ENABLED, "STREAMING_FORCE_ENABLED");
+        put(STREAMING_FORCE_DISABLED, "STREAMING_FORCE_DISABLED");
+
         put(USER_SWITCH_EARPIECE, "USER_SWITCH_EARPIECE");
         put(USER_SWITCH_BLUETOOTH, "USER_SWITCH_BLUETOOTH");
         put(USER_SWITCH_HEADSET, "USER_SWITCH_HEADSET");
diff --git a/src/com/android/server/telecom/CallAudioRouteController.java b/src/com/android/server/telecom/CallAudioRouteController.java
index c1d7d0c..091c8fc 100644
--- a/src/com/android/server/telecom/CallAudioRouteController.java
+++ b/src/com/android/server/telecom/CallAudioRouteController.java
@@ -19,24 +19,36 @@
 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.BluetoothDevice;
+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;
 import android.util.ArrayMap;
 
+import androidx.annotation.NonNull;
+
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.os.SomeArgs;
 import com.android.internal.util.IndentingPrintWriter;
+import com.android.server.telecom.bluetooth.BluetoothRouteManager;
 
 import java.util.HashSet;
 import java.util.List;
@@ -57,20 +69,83 @@
         ROUTE_MAP.put(AudioRoute.TYPE_BLUETOOTH_SCO, CallAudioState.ROUTE_BLUETOOTH);
         ROUTE_MAP.put(AudioRoute.TYPE_BLUETOOTH_HA, CallAudioState.ROUTE_BLUETOOTH);
         ROUTE_MAP.put(AudioRoute.TYPE_BLUETOOTH_LE, CallAudioState.ROUTE_BLUETOOTH);
+        ROUTE_MAP.put(AudioRoute.TYPE_STREAMING, CallAudioState.ROUTE_STREAMING);
     }
 
     private final CallsManager mCallsManager;
+    private final Context mContext;
     private AudioManager mAudioManager;
+    private CallAudioManager mCallAudioManager;
+    private final BluetoothRouteManager mBluetoothRouteManager;
+    private final CallAudioManager.AudioServiceFactory mAudioServiceFactory;
     private final Handler mHandler;
     private final WiredHeadsetManager mWiredHeadsetManager;
     private Set<AudioRoute> mAvailableRoutes;
     private AudioRoute mCurrentRoute;
     private AudioRoute mEarpieceWiredRoute;
     private AudioRoute mSpeakerDockRoute;
+    private AudioRoute mStreamingRoute;
+    private Set<AudioRoute> mStreamingRoutes;
     private Map<AudioRoute, BluetoothDevice> mBluetoothRoutes;
     private Map<Integer, AudioRoute> mTypeRoutes;
     private PendingAudioRoute mPendingAudioRoute;
     private AudioRoute.Factory mAudioRouteFactory;
+    private int mFocusType;
+    private final Object mLock = new Object();
+    private final BroadcastReceiver mSpeakerPhoneChangeReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            Log.startSession("CARC.mSPCR");
+            try {
+                if (AudioManager.ACTION_SPEAKERPHONE_STATE_CHANGED.equals(intent.getAction())) {
+                    if (mAudioManager != null) {
+                        AudioDeviceInfo info = mAudioManager.getCommunicationDevice();
+                        if ((info != null) &&
+                                (info.getType() == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER)) {
+                            sendMessageWithSessionInfo(SPEAKER_ON);
+                        } else {
+                            sendMessageWithSessionInfo(SPEAKER_OFF);
+                        }
+                    }
+                } else {
+                    Log.w(this, "Received non-speakerphone-change intent");
+                }
+            } finally {
+                Log.endSession();
+            }
+        }
+    };
+    private final BroadcastReceiver mMuteChangeReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            Log.startSession("CARC.mCR");
+            try {
+                if (AudioManager.ACTION_MICROPHONE_MUTE_CHANGED.equals(intent.getAction())) {
+                    if (mCallsManager.isInEmergencyCall()) {
+                        Log.i(this, "Mute was externally changed when there's an emergency call. "
+                                + "Forcing mute back off.");
+                        sendMessageWithSessionInfo(MUTE_OFF);
+                    } else {
+                        sendMessageWithSessionInfo(MUTE_EXTERNALLY_CHANGED);
+                    }
+                } else if (AudioManager.STREAM_MUTE_CHANGED_ACTION.equals(intent.getAction())) {
+                    int streamType = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, -1);
+                    boolean isStreamMuted = intent.getBooleanExtra(
+                            AudioManager.EXTRA_STREAM_VOLUME_MUTED, false);
+
+                    if (streamType == AudioManager.STREAM_RING && !isStreamMuted
+                            && mCallAudioManager != null) {
+                        Log.i(this, "Ring stream was un-muted.");
+                        mCallAudioManager.onRingerModeChange();
+                    }
+                } else {
+                    Log.w(this, "Received non-mute-change intent");
+                }
+            } finally {
+                Log.endSession();
+            }
+        }
+    };
     private CallAudioState mCallAudioState;
     private boolean mIsMute;
     private boolean mIsPending;
@@ -79,59 +154,142 @@
     public CallAudioRouteController(
             Context context,
             CallsManager callsManager,
+            CallAudioManager.AudioServiceFactory audioServiceFactory,
             AudioRoute.Factory audioRouteFactory,
-            WiredHeadsetManager wiredHeadsetManager) {
+            WiredHeadsetManager wiredHeadsetManager,
+            BluetoothRouteManager bluetoothRouteManager) {
+        mContext = context;
         mCallsManager = callsManager;
         mAudioManager = context.getSystemService(AudioManager.class);
+        mAudioServiceFactory = audioServiceFactory;
         mAudioRouteFactory = audioRouteFactory;
         mWiredHeadsetManager = wiredHeadsetManager;
         mIsMute = false;
+        mBluetoothRouteManager = bluetoothRouteManager;
+        mFocusType = NO_FOCUS;
         HandlerThread handlerThread = new HandlerThread(this.getClass().getSimpleName());
         handlerThread.start();
+
+        // Register broadcast receivers
+        IntentFilter speakerChangedFilter = new IntentFilter(
+                AudioManager.ACTION_SPEAKERPHONE_STATE_CHANGED);
+        speakerChangedFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
+        context.registerReceiver(mSpeakerPhoneChangeReceiver, speakerChangedFilter);
+
+        IntentFilter micMuteChangedFilter = new IntentFilter(
+                AudioManager.ACTION_MICROPHONE_MUTE_CHANGED);
+        micMuteChangedFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
+        context.registerReceiver(mMuteChangeReceiver, micMuteChangedFilter);
+
+        IntentFilter muteChangedFilter = new IntentFilter(AudioManager.STREAM_MUTE_CHANGED_ACTION);
+        muteChangedFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
+        context.registerReceiver(mMuteChangeReceiver, muteChangedFilter);
+
+        // Create handler
         mHandler = new Handler(handlerThread.getLooper()) {
             @Override
-            public void handleMessage(Message msg) {
-                preHandleMessage(msg);
-                String address;
-                BluetoothDevice bluetoothDevice;
-                @AudioRoute.AudioRouteType int type;
-                switch (msg.what) {
-                    case BT_AUDIO_CONNECTED:
-                        bluetoothDevice = (BluetoothDevice) ((SomeArgs) msg.obj).arg2;
-                        handleBtAudioActive(bluetoothDevice);
-                        break;
-                    case BT_AUDIO_DISCONNECTED:
-                        bluetoothDevice = (BluetoothDevice) ((SomeArgs) msg.obj).arg2;
-                        handleBtAudioInactive(bluetoothDevice);
-                        break;
-                    case BT_DEVICE_ADDED:
-                        type = msg.arg1;
-                        bluetoothDevice = (BluetoothDevice) ((SomeArgs) msg.obj).arg2;
-                        handleBtConnected(type, bluetoothDevice);
-                        break;
-                    case BT_DEVICE_REMOVED:
-                        type = msg.arg1;
-                        bluetoothDevice = (BluetoothDevice) ((SomeArgs) msg.obj).arg2;
-                        handleBtDisconnected(type, bluetoothDevice);
-                        break;
-                    case BLUETOOTH_DEVICE_LIST_CHANGED:
-                        break;
-                    case BT_ACTIVE_DEVICE_PRESENT:
-                        type = msg.arg1;
-                        address = (String) ((SomeArgs) msg.obj).arg2;
-                        handleBtActiveDevicePresent(type, address);
-                        break;
-                    case BT_ACTIVE_DEVICE_GONE:
-                        type = msg.arg1;
-                        handleBtActiveDeviceGone(type);
-                        break;
-                    case EXIT_PENDING_ROUTE:
-                        handleExitPendingRoute();
-                        break;
-                    default:
-                        break;
+            public void handleMessage(@NonNull Message msg) {
+                synchronized (this) {
+                    preHandleMessage(msg);
+                    String address;
+                    BluetoothDevice bluetoothDevice;
+                    int focus;
+                    @AudioRoute.AudioRouteType int type;
+                    switch (msg.what) {
+                        case CONNECT_WIRED_HEADSET:
+                            handleWiredHeadsetConnected();
+                            break;
+                        case DISCONNECT_WIRED_HEADSET:
+                            handleWiredHeadsetDisconnected();
+                            break;
+                        case CONNECT_DOCK:
+                            handleDockConnected();
+                            break;
+                        case DISCONNECT_DOCK:
+                            handleDockDisconnected();
+                            break;
+                        case BLUETOOTH_DEVICE_LIST_CHANGED:
+                            break;
+                        case BT_ACTIVE_DEVICE_PRESENT:
+                            type = msg.arg1;
+                            address = (String) ((SomeArgs) msg.obj).arg2;
+                            handleBtActiveDevicePresent(type, address);
+                            break;
+                        case BT_ACTIVE_DEVICE_GONE:
+                            type = msg.arg1;
+                            handleBtActiveDeviceGone(type);
+                            break;
+                        case BT_DEVICE_ADDED:
+                            type = msg.arg1;
+                            bluetoothDevice = (BluetoothDevice) ((SomeArgs) msg.obj).arg2;
+                            handleBtConnected(type, bluetoothDevice);
+                            break;
+                        case BT_DEVICE_REMOVED:
+                            type = msg.arg1;
+                            bluetoothDevice = (BluetoothDevice) ((SomeArgs) msg.obj).arg2;
+                            handleBtDisconnected(type, bluetoothDevice);
+                            break;
+                        case SWITCH_EARPIECE:
+                        case USER_SWITCH_EARPIECE:
+                            handleSwitchEarpiece();
+                            break;
+                        case SWITCH_BLUETOOTH:
+                        case USER_SWITCH_BLUETOOTH:
+                            address = (String) ((SomeArgs) msg.obj).arg2;
+                            handleSwitchBluetooth(address);
+                            break;
+                        case SWITCH_HEADSET:
+                        case USER_SWITCH_HEADSET:
+                            handleSwitchHeadset();
+                            break;
+                        case SWITCH_SPEAKER:
+                        case USER_SWITCH_SPEAKER:
+                            handleSwitchSpeaker();
+                            break;
+                        case USER_SWITCH_BASELINE_ROUTE:
+                            handleSwitchBaselineRoute();
+                            break;
+                        case SPEAKER_ON:
+                            handleSpeakerOn();
+                            break;
+                        case SPEAKER_OFF:
+                            handleSpeakerOff();
+                            break;
+                        case STREAMING_FORCE_ENABLED:
+                            handleStreamingEnabled();
+                            break;
+                        case STREAMING_FORCE_DISABLED:
+                            handleStreamingDisabled();
+                            break;
+                        case BT_AUDIO_CONNECTED:
+                            bluetoothDevice = (BluetoothDevice) ((SomeArgs) msg.obj).arg2;
+                            handleBtAudioActive(bluetoothDevice);
+                            break;
+                        case BT_AUDIO_DISCONNECTED:
+                            bluetoothDevice = (BluetoothDevice) ((SomeArgs) msg.obj).arg2;
+                            handleBtAudioInactive(bluetoothDevice);
+                            break;
+                        case MUTE_ON:
+                            handleMuteChanged(true);
+                            break;
+                        case MUTE_OFF:
+                            handleMuteChanged(false);
+                            break;
+                        case MUTE_EXTERNALLY_CHANGED:
+                            handleMuteChanged(mAudioManager.isMasterMute());
+                            break;
+                        case SWITCH_FOCUS:
+                            focus = msg.arg1;
+                            handleSwitchFocus(focus);
+                            break;
+                        case EXIT_PENDING_ROUTE:
+                            handleExitPendingRoute();
+                            break;
+                        default:
+                            break;
+                    }
+                    postHandleMessage(msg);
                 }
-                postHandleMessage(msg);
             }
         };
     }
@@ -140,7 +298,10 @@
         mAvailableRoutes = new HashSet<>();
         mBluetoothRoutes = new ArrayMap<>();
         mTypeRoutes = new ArrayMap<>();
+        mStreamingRoutes = new HashSet<>();
         mPendingAudioRoute = new PendingAudioRoute(this, mAudioManager);
+        mStreamingRoute = new AudioRoute(AudioRoute.TYPE_STREAMING, null, null);
+        mStreamingRoutes.add(mStreamingRoute);
 
         int supportMask = calculateSupportedRouteMask();
         if ((supportMask & CallAudioState.ROUTE_SPEAKER) != 0) {
@@ -225,6 +386,7 @@
 
     @Override
     public void setCallAudioManager(CallAudioManager callAudioManager) {
+        mCallAudioManager = callAudioManager;
     }
 
     @Override
@@ -271,6 +433,9 @@
     }
 
     private void routeTo(boolean active, AudioRoute destRoute) {
+        if (!destRoute.equals(mStreamingRoute) && !getAvailableRoutes().contains(destRoute)) {
+            return;
+        }
         if (mIsPending) {
             if (destRoute.equals(mPendingAudioRoute.getDestRoute()) && (mIsActive == active)) {
                 return;
@@ -280,22 +445,24 @@
                     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);
         } else {
-            if (mCurrentRoute.equals(destRoute) && (mIsActive = active)) {
+            if (mCurrentRoute.equals(destRoute) && (mIsActive == active)) {
                 return;
             }
             Log.i(this, "Enter pending route, orig%s(active=%b), dest%s(active=%b)", mCurrentRoute,
                     mIsActive, destRoute, active);
             // route to pending route
-            if (mAvailableRoutes.contains(mCurrentRoute)) {
+            if (getAvailableRoutes().contains(mCurrentRoute)) {
                 mPendingAudioRoute.setOrigRoute(mIsActive, mCurrentRoute);
             } else {
                 // Avoid waiting for pending messages for an unavailable route
                 mPendingAudioRoute.setOrigRoute(mIsActive, DUMMY_ROUTE);
             }
             mPendingAudioRoute.setDestRoute(active, destRoute);
+            mIsActive = active;
             mIsPending = true;
         }
         mPendingAudioRoute.evaluatePendingState();
@@ -309,6 +476,103 @@
                 Message.obtain(mHandler, PENDING_ROUTE_TIMEOUT)), TIMEOUT_LIMIT);
     }
 
+    private void handleWiredHeadsetConnected() {
+        AudioRoute wiredHeadsetRoute = null;
+        try {
+            wiredHeadsetRoute = mAudioRouteFactory.create(AudioRoute.TYPE_WIRED, null,
+                    mAudioManager);
+        } catch (IllegalArgumentException e) {
+            Log.e(this, e, "Can't find available audio device info for route type:"
+                    + AudioRoute.DEVICE_TYPE_STRINGS.get(AudioRoute.TYPE_WIRED));
+        }
+
+        if (wiredHeadsetRoute != null) {
+            mAvailableRoutes.add(wiredHeadsetRoute);
+            mAvailableRoutes.remove(mEarpieceWiredRoute);
+            mTypeRoutes.put(AudioRoute.TYPE_WIRED, wiredHeadsetRoute);
+            mEarpieceWiredRoute = wiredHeadsetRoute;
+            routeTo(mIsActive, wiredHeadsetRoute);
+            onAvailableRoutesChanged();
+        }
+    }
+
+    public void handleWiredHeadsetDisconnected() {
+        // Update audio route states
+        AudioRoute wiredHeadsetRoute = mTypeRoutes.remove(AudioRoute.TYPE_WIRED);
+        if (wiredHeadsetRoute != null) {
+            mAvailableRoutes.remove(wiredHeadsetRoute);
+            mEarpieceWiredRoute = null;
+        }
+        AudioRoute earpieceRoute = mTypeRoutes.get(AudioRoute.TYPE_EARPIECE);
+        if (earpieceRoute != null) {
+            mAvailableRoutes.add(earpieceRoute);
+            mEarpieceWiredRoute = earpieceRoute;
+        }
+        onAvailableRoutesChanged();
+
+        // Route to expected state
+        if (mCurrentRoute.equals(wiredHeadsetRoute)) {
+            routeTo(mIsActive, getBaseRoute(true));
+        }
+    }
+
+    private void handleDockConnected() {
+        AudioRoute dockRoute = null;
+        try {
+            dockRoute = mAudioRouteFactory.create(AudioRoute.TYPE_DOCK, null, mAudioManager);
+        } catch (IllegalArgumentException e) {
+            Log.e(this, e, "Can't find available audio device info for route type:"
+                    + AudioRoute.DEVICE_TYPE_STRINGS.get(AudioRoute.TYPE_WIRED));
+        }
+
+        if (dockRoute != null) {
+            mAvailableRoutes.add(dockRoute);
+            mAvailableRoutes.remove(mSpeakerDockRoute);
+            mTypeRoutes.put(AudioRoute.TYPE_DOCK, dockRoute);
+            mSpeakerDockRoute = dockRoute;
+            routeTo(mIsActive, dockRoute);
+            onAvailableRoutesChanged();
+        }
+    }
+
+    public void handleDockDisconnected() {
+        // Update audio route states
+        AudioRoute dockRoute = mTypeRoutes.get(AudioRoute.TYPE_DOCK);
+        if (dockRoute != null) {
+            mAvailableRoutes.remove(dockRoute);
+            mSpeakerDockRoute = null;
+        }
+        AudioRoute speakerRoute = mTypeRoutes.get(AudioRoute.TYPE_SPEAKER);
+        if (speakerRoute != null) {
+            mAvailableRoutes.add(speakerRoute);
+            mSpeakerDockRoute = speakerRoute;
+        }
+        onAvailableRoutesChanged();
+
+        // Route to expected state
+        if (mCurrentRoute.equals(dockRoute)) {
+            routeTo(mIsActive, getBaseRoute(true));
+        }
+    }
+
+    private void handleStreamingEnabled() {
+        if (!mCurrentRoute.equals(mStreamingRoute)) {
+            routeTo(mIsActive, mStreamingRoute);
+        } else {
+            Log.i(this, "ignore enable streaming, already in streaming");
+        }
+    }
+
+    private void handleStreamingDisabled() {
+        if (mCurrentRoute.equals(mStreamingRoute)) {
+            mCurrentRoute = DUMMY_ROUTE;
+            onAvailableRoutesChanged();
+            routeTo(mIsActive, getBaseRoute(true));
+        } else {
+            Log.i(this, "ignore disable streaming, not in streaming");
+        }
+    }
+
     private void handleBtAudioActive(BluetoothDevice bluetoothDevice) {
         if (mIsPending) {
             if (Objects.equals(mPendingAudioRoute.getDestRoute().getBluetoothAddress(),
@@ -360,13 +624,7 @@
 
         // Fallback to an available route
         if (Objects.equals(mCurrentRoute, bluetoothRoute)) {
-            // fallback policy
-            AudioRoute destRoute = getPreferredAudioRouteFromStrategy();
-            if (destRoute != null && mAvailableRoutes.contains(destRoute)) {
-                routeTo(mIsActive, destRoute);
-            } else {
-                routeTo(mIsActive, getPreferredAudioRouteFromDefault(true/* includeBluetooth */));
-            }
+            routeTo(mIsActive, getBaseRoute(false));
         }
     }
 
@@ -384,11 +642,142 @@
         if ((mIsPending && mPendingAudioRoute.getDestRoute().getType() == type)
                 || (!mIsPending && mCurrentRoute.getType() == type)) {
             // Fallback to an available route
-            AudioRoute destRoute = getPreferredAudioRouteFromStrategy();
-            if (destRoute != null && mAvailableRoutes.contains(destRoute)) {
-                routeTo(mIsActive, destRoute);
+            routeTo(mIsActive, getBaseRoute(true));
+        }
+    }
+
+    private void handleMuteChanged(boolean mute) {
+        mIsMute = mute;
+        if (mIsMute != mAudioManager.isMasterMute() && mIsActive) {
+            IAudioService audioService = mAudioServiceFactory.getAudioService();
+            Log.i(this, "changing microphone mute state to: %b [serviceIsNull=%b]", mute,
+                    audioService == null);
+            if (audioService != null) {
+                try {
+                    audioService.setMicrophoneMute(mute, mContext.getOpPackageName(),
+                            mCallsManager.getCurrentUserHandle().getIdentifier(),
+                            mContext.getAttributionTag());
+                } catch (RemoteException e) {
+                    Log.e(this, e, "Remote exception while toggling mute.");
+                    return;
+                }
+            }
+        }
+        onMuteStateChanged(mIsMute);
+    }
+
+    private void handleSwitchFocus(int focus) {
+        mFocusType = focus;
+        switch (focus) {
+            case NO_FOCUS -> {
+                if (mIsActive) {
+                    handleMuteChanged(false);
+                    routeTo(false, mCurrentRoute);
+                }
+            }
+            case ACTIVE_FOCUS -> {
+                if (!mIsActive) {
+                    routeTo(true, getBaseRoute(true));
+                }
+            }
+            case RINGING_FOCUS -> {
+                if (!mIsActive) {
+                    AudioRoute route = getBaseRoute(true);
+                    BluetoothDevice device = mBluetoothRoutes.get(route);
+                    if (device != null && !mBluetoothRouteManager.isInbandRingEnabled(device)) {
+                        routeTo(false, route);
+                    } else {
+                        routeTo(true, route);
+                    }
+                } else {
+                    // active
+                    BluetoothDevice device = mBluetoothRoutes.get(mCurrentRoute);
+                    if (device != null && !mBluetoothRouteManager.isInbandRingEnabled(device)) {
+                        routeTo(false, mCurrentRoute);
+                    }
+                }
+            }
+        }
+    }
+
+    public void handleSwitchEarpiece() {
+        AudioRoute earpieceRoute = mTypeRoutes.get(AudioRoute.TYPE_EARPIECE);
+        if (earpieceRoute != null && getAvailableRoutes().contains(earpieceRoute)) {
+            routeTo(mIsActive, earpieceRoute);
+        } else {
+            Log.i(this, "ignore switch earpiece request");
+        }
+    }
+
+    private void handleSwitchBluetooth(String address) {
+        Log.i(this, "handle switch to bluetooth with address %s", address);
+        AudioRoute bluetoothRoute = null;
+        BluetoothDevice bluetoothDevice = null;
+        for (AudioRoute route : getAvailableRoutes()) {
+            if (Objects.equals(address, route.getBluetoothAddress())) {
+                bluetoothRoute = route;
+                bluetoothDevice = mBluetoothRoutes.get(route);
+                break;
+            }
+        }
+
+        if (bluetoothRoute != null && bluetoothDevice != null) {
+            if (mFocusType == RINGING_FOCUS) {
+                routeTo(mBluetoothRouteManager.isInbandRingEnabled(bluetoothDevice) && mIsActive,
+                        bluetoothRoute);
             } else {
-                routeTo(mIsActive, getPreferredAudioRouteFromDefault(false/* includeBluetooth */));
+                routeTo(mIsActive, bluetoothRoute);
+            }
+        } else {
+            Log.i(this, "ignore switch bluetooth request");
+        }
+    }
+
+    private void handleSwitchHeadset() {
+        AudioRoute headsetRoute = mTypeRoutes.get(AudioRoute.TYPE_WIRED);
+        if (headsetRoute != null && getAvailableRoutes().contains(headsetRoute)) {
+            routeTo(mIsActive, headsetRoute);
+        } else {
+            Log.i(this, "ignore switch speaker request");
+        }
+    }
+
+    private void handleSwitchSpeaker() {
+        if (mSpeakerDockRoute != null && getAvailableRoutes().contains(mSpeakerDockRoute)) {
+            routeTo(mIsActive, mSpeakerDockRoute);
+        } else {
+            Log.i(this, "ignore switch speaker request");
+        }
+    }
+
+    private void handleSwitchBaselineRoute() {
+        routeTo(mIsActive, getBaseRoute(true));
+    }
+
+    private void handleSpeakerOn() {
+        if (isPending()) {
+            mPendingAudioRoute.onMessageReceived(SPEAKER_ON);
+        } else {
+            if (mSpeakerDockRoute != null && getAvailableRoutes().contains(mSpeakerDockRoute)) {
+                routeTo(mIsActive, mSpeakerDockRoute);
+                // 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) {
+                    sendMessageWithSessionInfo(SPEAKER_ON);
+                }
+            }
+        }
+    }
+
+    private void handleSpeakerOff() {
+        if (isPending()) {
+            mPendingAudioRoute.onMessageReceived(SPEAKER_OFF);
+        } else if (mCurrentRoute.getType() == AudioRoute.TYPE_SPEAKER) {
+            routeTo(mIsActive, getBaseRoute(true));
+            // 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) {
+                sendMessageWithSessionInfo(SPEAKER_OFF);
             }
             onAvailableRoutesChanged();
         }
@@ -397,39 +786,53 @@
     public void handleExitPendingRoute() {
         if (mIsPending) {
             Log.i(this, "Exit pending route and enter %s(active=%b)",
-                    mPendingAudioRoute.getDestRoute(), mPendingAudioRoute.isActive());
+                    mPendingAudioRoute.getDestRoute(), mIsActive);
             mCurrentRoute = mPendingAudioRoute.getDestRoute();
-            mIsActive = mPendingAudioRoute.isActive();
             mIsPending = false;
             onCurrentRouteChanged();
         }
     }
 
     private void onCurrentRouteChanged() {
-        BluetoothDevice activeBluetoothDevice = null;
-        int route = ROUTE_MAP.get(mCurrentRoute.getType());
-        if (route == CallAudioState.ROUTE_BLUETOOTH) {
-            activeBluetoothDevice = mBluetoothRoutes.get(mCurrentRoute);
+        synchronized (mLock) {
+            BluetoothDevice activeBluetoothDevice = null;
+            int route = ROUTE_MAP.get(mCurrentRoute.getType());
+            if (route == CallAudioState.ROUTE_STREAMING) {
+                updateCallAudioState(new CallAudioState(mIsMute, route, route));
+                return;
+            }
+            if (route == CallAudioState.ROUTE_BLUETOOTH) {
+                activeBluetoothDevice = mBluetoothRoutes.get(mCurrentRoute);
+            }
+            updateCallAudioState(new CallAudioState(mIsMute, route,
+                    mCallAudioState.getRawSupportedRouteMask(), activeBluetoothDevice,
+                    mCallAudioState.getSupportedBluetoothDevices()));
         }
-        updateCallAudioState(new CallAudioState(mIsMute, route,
-                mCallAudioState.getSupportedRouteMask(), activeBluetoothDevice,
-                mCallAudioState.getSupportedBluetoothDevices()));
     }
 
     private void onAvailableRoutesChanged() {
-        int routeMask = 0;
-        Set<BluetoothDevice> availableBluetoothDevices = new HashSet<>();
-        for (AudioRoute route : mAvailableRoutes) {
-            routeMask |= ROUTE_MAP.get(route.getType());
-            if (BT_AUDIO_ROUTE_TYPES.contains(route.getType())) {
-                availableBluetoothDevices.add(mBluetoothRoutes.get(route));
+        synchronized (mLock) {
+            int routeMask = 0;
+            Set<BluetoothDevice> availableBluetoothDevices = new HashSet<>();
+            for (AudioRoute route : getAvailableRoutes()) {
+                routeMask |= ROUTE_MAP.get(route.getType());
+                if (BT_AUDIO_ROUTE_TYPES.contains(route.getType())) {
+                    availableBluetoothDevices.add(mBluetoothRoutes.get(route));
+                }
             }
+            updateCallAudioState(new CallAudioState(mIsMute, mCallAudioState.getRoute(), routeMask,
+                    mCallAudioState.getActiveBluetoothDevice(), availableBluetoothDevices));
         }
-        updateCallAudioState(new CallAudioState(mIsMute, mCallAudioState.getRoute(), routeMask,
-                mCallAudioState.getActiveBluetoothDevice(), availableBluetoothDevices));
+    }
+
+    private void onMuteStateChanged(boolean mute) {
+        updateCallAudioState(new CallAudioState(mute, mCallAudioState.getRoute(),
+                mCallAudioState.getSupportedRouteMask(), mCallAudioState.getActiveBluetoothDevice(),
+                mCallAudioState.getSupportedBluetoothDevices()));
     }
 
     private void updateCallAudioState(CallAudioState callAudioState) {
+        Log.i(this, "updateCallAudioState: " + callAudioState);
         CallAudioState oldState = mCallAudioState;
         mCallAudioState = callAudioState;
         mCallsManager.onCallAudioStateChanged(oldState, mCallAudioState);
@@ -468,7 +871,7 @@
         }
     }
 
-    public AudioRoute getPreferredAudioRouteFromDefault(boolean includeBluetooth) {
+    private AudioRoute getPreferredAudioRouteFromDefault(boolean includeBluetooth) {
         if (mBluetoothRoutes.isEmpty() || !includeBluetooth) {
             return mEarpieceWiredRoute != null ? mEarpieceWiredRoute : mSpeakerDockRoute;
         } else {
@@ -495,8 +898,13 @@
         return routeMask;
     }
 
+    @VisibleForTesting
     public Set<AudioRoute> getAvailableRoutes() {
-        return mAvailableRoutes;
+        if (mCurrentRoute.equals(mStreamingRoute)) {
+            return mStreamingRoutes;
+        } else {
+            return mAvailableRoutes;
+        }
     }
 
     public AudioRoute getCurrentRoute() {
@@ -513,6 +921,17 @@
         return null;
     }
 
+    public AudioRoute getBaseRoute(boolean includeBluetooth) {
+        AudioRoute destRoute = getPreferredAudioRouteFromStrategy();
+        if (destRoute == null) {
+            destRoute = getPreferredAudioRouteFromDefault(includeBluetooth);
+        }
+        if (destRoute != null && !getAvailableRoutes().contains(destRoute)) {
+            destRoute = null;
+        }
+        return destRoute;
+    }
+
     @VisibleForTesting
     public void setAudioManager(AudioManager audioManager) {
         mAudioManager = audioManager;
@@ -525,6 +944,11 @@
 
     @VisibleForTesting
     public void setActive(boolean active) {
+        if (active) {
+            mFocusType = ACTIVE_FOCUS;
+        } else {
+            mFocusType = NO_FOCUS;
+        }
         mIsActive = active;
     }
 }
diff --git a/src/com/android/server/telecom/CallLogManager.java b/src/com/android/server/telecom/CallLogManager.java
index 6a9977d..f6360a3 100644
--- a/src/com/android/server/telecom/CallLogManager.java
+++ b/src/com/android/server/telecom/CallLogManager.java
@@ -54,6 +54,7 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.telecom.callfiltering.CallFilteringResult;
 import com.android.server.telecom.flags.FeatureFlags;
+import com.android.server.telecom.flags.Flags;
 
 import java.util.Arrays;
 import java.util.Locale;
@@ -418,7 +419,12 @@
         paramBuilder.setCallType(callLogType);
         paramBuilder.setIsRead(call.isSelfManaged());
         paramBuilder.setMissedReason(call.getMissedReason());
-
+        if (Flags.businessCallComposer() && call.getExtras() != null) {
+            paramBuilder.setIsBusinessCall(call.getExtras().getBoolean(
+                    android.telecom.Call.EXTRA_IS_BUSINESS_CALL, false));
+            paramBuilder.setBusinessName(call.getExtras().getString(
+                    android.telecom.Call.EXTRA_ASSERTED_DISPLAY_NAME, ""));
+        }
         sendAddCallBroadcast(callLogType, call.getAgeMillis());
 
         boolean okayToLog =
diff --git a/src/com/android/server/telecom/CallsManager.java b/src/com/android/server/telecom/CallsManager.java
index aa8eb57..ae096af 100644
--- a/src/com/android/server/telecom/CallsManager.java
+++ b/src/com/android/server/telecom/CallsManager.java
@@ -79,7 +79,7 @@
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.provider.BlockedNumberContract;
-import android.provider.BlockedNumberContract.SystemContract;
+import android.provider.BlockedNumberContract.BlockedNumbers;
 import android.provider.CallLog.Calls;
 import android.provider.Settings;
 import android.sysprop.TelephonyProperties;
@@ -118,6 +118,7 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.app.IntentForwarderActivity;
 import com.android.internal.util.IndentingPrintWriter;
+import com.android.server.telecom.bluetooth.BluetoothDeviceManager;
 import com.android.server.telecom.bluetooth.BluetoothRouteManager;
 import com.android.server.telecom.bluetooth.BluetoothStateReceiver;
 import com.android.server.telecom.callfiltering.BlockCheckerAdapter;
@@ -556,7 +557,7 @@
         public void onReceive(Context context, Intent intent) {
             String action = intent.getAction();
             if (CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED.equals(action)
-                    || SystemContract.ACTION_BLOCK_SUPPRESSION_STATE_CHANGED.equals(action)) {
+                    || BlockedNumbers.ACTION_BLOCK_SUPPRESSION_STATE_CHANGED.equals(action)) {
                 updateEmergencyCallNotificationAsync(context);
             }
         }
@@ -607,6 +608,7 @@
             EmergencyCallDiagnosticLogger emergencyCallDiagnosticLogger,
             CallAudioCommunicationDeviceTracker communicationDeviceTracker,
             CallStreamingNotification callStreamingNotification,
+            BluetoothDeviceManager bluetoothDeviceManager,
             FeatureFlags featureFlags,
             IncomingCallFilterGraphProvider incomingCallFilterGraphProvider) {
 
@@ -632,6 +634,9 @@
         mDtmfLocalTonePlayer =
                 new DtmfLocalTonePlayer(new DtmfLocalTonePlayer.ToneGeneratorProxy());
         CallAudioRouteAdapter callAudioRouteAdapter;
+        // TODO: add another flag check when
+        // bluetoothDeviceManager.getBluetoothHeadset().isScoManagedByAudio()
+        // available and return true
         if (!featureFlags.useRefactoredAudioRouteSwitching()) {
             callAudioRouteAdapter = callAudioRouteStateMachineFactory.create(
                     context,
@@ -646,8 +651,8 @@
                     featureFlags
             );
         } else {
-            callAudioRouteAdapter = new CallAudioRouteController(
-                    context, this, new AudioRoute.Factory(), wiredHeadsetManager);
+            callAudioRouteAdapter = new CallAudioRouteController(context, this, audioServiceFactory,
+                    new AudioRoute.Factory(), wiredHeadsetManager, mBluetoothRouteManager);
         }
         callAudioRouteAdapter.initialize();
         bluetoothStateReceiver.setCallAudioRouteAdapter(callAudioRouteAdapter);
@@ -752,7 +757,7 @@
         IntentFilter intentFilter = new IntentFilter(
                 CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED);
         intentFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
-        intentFilter.addAction(SystemContract.ACTION_BLOCK_SUPPRESSION_STATE_CHANGED);
+        intentFilter.addAction(BlockedNumbers.ACTION_BLOCK_SUPPRESSION_STATE_CHANGED);
         context.registerReceiver(mReceiver, intentFilter, Context.RECEIVER_EXPORTED);
         mGraphHandlerThreads = new LinkedList<>();
 
@@ -2927,7 +2932,7 @@
 
         if (call.isEmergencyCall()) {
             Executors.defaultThreadFactory().newThread(() ->
-                    BlockedNumberContract.SystemContract.notifyEmergencyContact(mContext))
+                    BlockedNumberContract.BlockedNumbers.notifyEmergencyContact(mContext))
                     .start();
         }
 
diff --git a/src/com/android/server/telecom/EmergencyCallDiagnosticLogger.java b/src/com/android/server/telecom/EmergencyCallDiagnosticLogger.java
index b8f5239..f1a4a97 100644
--- a/src/com/android/server/telecom/EmergencyCallDiagnosticLogger.java
+++ b/src/com/android/server/telecom/EmergencyCallDiagnosticLogger.java
@@ -404,7 +404,12 @@
             Log.i(this, "skipped dumping diagnostic data");
             return;
         }
-        dumpDiagnosticDataFromDropbox(pw);
+        try {
+            dumpDiagnosticDataFromDropbox(pw);
+        } catch (Exception e) {
+            pw.println("Exception was thrown while dumping diagnostic data from DropBox");
+            e.printStackTrace();
+        }
     }
 
     private static class CallEventTimestamps {
diff --git a/src/com/android/server/telecom/ParcelableCallUtils.java b/src/com/android/server/telecom/ParcelableCallUtils.java
index c77e605..3573de8 100644
--- a/src/com/android/server/telecom/ParcelableCallUtils.java
+++ b/src/com/android/server/telecom/ParcelableCallUtils.java
@@ -63,6 +63,7 @@
         RESTRICTED_CALL_SCREENING_EXTRA_KEYS = new ArrayList<>();
         RESTRICTED_CALL_SCREENING_EXTRA_KEYS.add(android.telecom.Connection.EXTRA_SIP_INVITE);
         RESTRICTED_CALL_SCREENING_EXTRA_KEYS.add(ImsCallProfile.EXTRA_IS_BUSINESS_CALL);
+        RESTRICTED_CALL_SCREENING_EXTRA_KEYS.add(ImsCallProfile.EXTRA_ASSERTED_DISPLAY_NAME);
     }
 
     public static class Converter {
diff --git a/src/com/android/server/telecom/PendingAudioRoute.java b/src/com/android/server/telecom/PendingAudioRoute.java
index 5fa3048..8de62ed 100644
--- a/src/com/android/server/telecom/PendingAudioRoute.java
+++ b/src/com/android/server/telecom/PendingAudioRoute.java
@@ -76,9 +76,7 @@
     public void onMessageReceived(int message) {
         if (message == PENDING_ROUTE_FAILED) {
             // Fallback to base route
-            //TODO: Replace getPreferredAudioRouteFromDefault by getBaseRoute when available and
-            // make the replaced one private
-            mDestRoute = mCallAudioRouteController.getPreferredAudioRouteFromDefault(true);
+            mDestRoute = mCallAudioRouteController.getBaseRoute(true);
             mCallAudioRouteController.sendMessageWithSessionInfo(
                     CallAudioRouteAdapter.EXIT_PENDING_ROUTE);
         }
diff --git a/src/com/android/server/telecom/TelecomServiceImpl.java b/src/com/android/server/telecom/TelecomServiceImpl.java
index 2932fb7..aa721d4 100644
--- a/src/com/android/server/telecom/TelecomServiceImpl.java
+++ b/src/com/android/server/telecom/TelecomServiceImpl.java
@@ -77,7 +77,6 @@
 import com.android.internal.telecom.ICallControl;
 import com.android.internal.telecom.ICallEventCallback;
 import com.android.internal.telecom.ITelecomService;
-import com.android.internal.telephony.flags.Flags;
 import com.android.internal.util.IndentingPrintWriter;
 import com.android.server.telecom.components.UserCallIntentProcessorFactory;
 import com.android.server.telecom.flags.FeatureFlags;
@@ -239,6 +238,9 @@
                                                 callEventCallback, mCallsManager, call);
 
                         call.setTransactionServiceWrapper(serviceWrapper);
+                        if (mFeatureFlags.transactionalVideoState()) {
+                            call.setTransactionalCallSupportsVideoCalling(callAttributes);
+                        }
                         ICallControl clientCallControl = serviceWrapper.getICallControl();
 
                         if (clientCallControl == null) {
@@ -600,6 +602,53 @@
         }
 
         @Override
+        public ParceledListSlice<PhoneAccount> getRegisteredPhoneAccounts(String callingPackage,
+                String callingFeatureId) {
+            try {
+                Log.startSession("TSI.gRPA", Log.getPackageAbbreviation(callingPackage));
+                try {
+                    enforceCallingPackage(callingPackage, "getRegisteredPhoneAccounts");
+                } catch (SecurityException se) {
+                    EventLog.writeEvent(0x534e4554, "307609763", Binder.getCallingUid(),
+                            "getRegisteredPhoneAccounts: invalid calling package");
+                    throw se;
+                }
+
+                boolean hasCrossUserAccess = false;
+                try {
+                    enforceInAppCrossUserPermission();
+                    hasCrossUserAccess = true;
+                } catch (SecurityException e) {
+                    // pass through
+                }
+
+                synchronized (mLock) {
+                    final UserHandle callingUserHandle = Binder.getCallingUserHandle();
+                    long token = Binder.clearCallingIdentity();
+                    try {
+                        return new ParceledListSlice<>(
+                                mPhoneAccountRegistrar.getPhoneAccounts(
+                                        0 /* capabilities */,
+                                        0 /* excludedCapabilities */,
+                                        null /* UriScheme */,
+                                        callingPackage,
+                                        true /* includeDisabledAccounts */,
+                                        callingUserHandle,
+                                        hasCrossUserAccess /* crossUserAccess */,
+                                        false /* includeAll */));
+                    } catch (Exception e) {
+                        Log.e(this, e, "getRegisteredPhoneAccounts");
+                        throw e;
+                    } finally {
+                        Binder.restoreCallingIdentity(token);
+                    }
+                }
+            } finally {
+                Log.endSession();
+            }
+        }
+
+        @Override
         public int getAllPhoneAccountsCount() {
             try {
                 Log.startSession("TSI.gAPAC");
@@ -787,6 +836,9 @@
                         Bundle extras = account.getExtras();
                         if (extras != null
                                 && extras.getBoolean(PhoneAccount.EXTRA_SKIP_CALL_FILTERING)) {
+                            // System apps should be granted the MODIFY_PHONE_STATE permission.
+                            enforceModifyPermission(
+                                    "registerPhoneAccount requires MODIFY_PHONE_STATE permission.");
                             enforceRegisterSkipCallFiltering();
                         }
                         final int callingUid = Binder.getCallingUid();
@@ -1951,7 +2003,7 @@
                 synchronized (mLock) {
                     long token = Binder.clearCallingIdentity();
                     try {
-                        BlockedNumberContract.SystemContract.endBlockSuppression(mContext);
+                        BlockedNumberContract.BlockedNumbers.endBlockSuppression(mContext);
                     } finally {
                         Binder.restoreCallingIdentity(token);
                     }
@@ -3119,7 +3171,6 @@
                 + " does not meet the requirements to access the phone number");
     }
 
-
     private boolean canReadPrivilegedPhoneState(String callingPackage, String message) {
         // The system/default dialer can always read phone state - so that emergency calls will
         // still work.
diff --git a/src/com/android/server/telecom/TelecomSystem.java b/src/com/android/server/telecom/TelecomSystem.java
index 9f6fcba..b4c3a4d 100644
--- a/src/com/android/server/telecom/TelecomSystem.java
+++ b/src/com/android/server/telecom/TelecomSystem.java
@@ -425,6 +425,7 @@
                     emergencyCallDiagnosticLogger,
                     communicationDeviceTracker,
                     callStreamingNotification,
+                    bluetoothDeviceManager,
                     featureFlags,
                     IncomingCallFilterGraph::new);
 
diff --git a/src/com/android/server/telecom/TransactionalServiceWrapper.java b/src/com/android/server/telecom/TransactionalServiceWrapper.java
index 98f2c9c..d497c6a 100644
--- a/src/com/android/server/telecom/TransactionalServiceWrapper.java
+++ b/src/com/android/server/telecom/TransactionalServiceWrapper.java
@@ -43,10 +43,10 @@
 import com.android.server.telecom.voip.HoldCallTransaction;
 import com.android.server.telecom.voip.EndCallTransaction;
 import com.android.server.telecom.voip.MaybeHoldCallForNewCallTransaction;
-import com.android.server.telecom.voip.ParallelTransaction;
 import com.android.server.telecom.voip.RequestNewActiveCallTransaction;
 import com.android.server.telecom.voip.SerialTransaction;
 import com.android.server.telecom.voip.SetMuteStateTransaction;
+import com.android.server.telecom.voip.RequestVideoStateTransaction;
 import com.android.server.telecom.voip.TransactionManager;
 import com.android.server.telecom.voip.VoipCallTransaction;
 import com.android.server.telecom.voip.VoipCallTransactionResult;
@@ -71,6 +71,7 @@
     public static final String ANSWER = "Answer";
     public static final String DISCONNECT = "Disconnect";
     public static final String START_STREAMING = "StartStreaming";
+    public static final String REQUEST_VIDEO_STATE = "RequestVideoState";
 
     // CallEventCallback : Telecom --> Client (ex. voip app)
     public static final String ON_SET_ACTIVE = "onSetActive";
@@ -249,6 +250,17 @@
             }
         }
 
+        @Override
+        public void requestVideoState(int videoState, String callId, ResultReceiver callback)
+                throws RemoteException {
+            try {
+                Log.startSession("TSW.rVS");
+                createTransactions(callId, callback, REQUEST_VIDEO_STATE, videoState);
+            } finally {
+                Log.endSession();
+            }
+        }
+
         private void createTransactions(String callId, ResultReceiver callback, String action,
                 Object... objects) {
             Log.d(TAG, "createTransactions: callId=" + callId);
@@ -275,6 +287,11 @@
                         addTransactionsToManager(mStreamingController.getStartStreamingTransaction(mCallsManager,
                                 TransactionalServiceWrapper.this, call, mLock), callback);
                         break;
+                    case REQUEST_VIDEO_STATE:
+                        addTransactionsToManager(
+                                new RequestVideoStateTransaction(mCallsManager, call,
+                                        (int) objects[0]), callback);
+                        break;
                 }
             } else {
                 Bundle exceptionBundle = new Bundle();
@@ -563,6 +580,15 @@
         }
     }
 
+    public void onVideoStateChanged(Call call, int videoState) {
+        if (call != null) {
+            try {
+                mICallEventCallback.onVideoStateChanged(call.getId(), videoState);
+            } catch (RemoteException e) {
+            }
+        }
+    }
+
     public void removeCallFromWrappers(Call call) {
         if (call != null) {
             try {
diff --git a/src/com/android/server/telecom/bluetooth/BluetoothDeviceManager.java b/src/com/android/server/telecom/bluetooth/BluetoothDeviceManager.java
index 27e5a7d..a0ffe63 100644
--- a/src/com/android/server/telecom/bluetooth/BluetoothDeviceManager.java
+++ b/src/com/android/server/telecom/bluetooth/BluetoothDeviceManager.java
@@ -25,9 +25,8 @@
 import android.bluetooth.BluetoothProfile;
 import android.bluetooth.BluetoothStatusCodes;
 import android.content.Context;
-import android.media.AudioManager;
 import android.media.AudioDeviceInfo;
-import android.media.audio.common.AudioDevice;
+import android.media.AudioManager;
 import android.os.Bundle;
 import android.telecom.Log;
 import android.util.ArraySet;
@@ -41,13 +40,17 @@
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.concurrent.Executor;
 import java.util.LinkedHashMap;
 import java.util.LinkedHashSet;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Objects;
 import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
 
 public class BluetoothDeviceManager {
 
@@ -98,6 +101,9 @@
                         synchronized (mLock) {
                             String logString;
                             if (profile == BluetoothProfile.HEADSET) {
+                                if (mFeatureFlags.useRefactoredAudioRouteSwitching()) {
+                                    mBluetoothHeadsetFuture.complete((BluetoothHeadset) proxy);
+                                }
                                 mBluetoothHeadset = (BluetoothHeadset) proxy;
                                 logString = "Got BluetoothHeadset: " + mBluetoothHeadset;
                             } else if (profile == BluetoothProfile.HEARING_AID) {
@@ -137,6 +143,9 @@
                             LinkedHashMap<String, BluetoothDevice> lostServiceDevices;
                             String logString;
                             if (profile == BluetoothProfile.HEADSET) {
+                                if (mFeatureFlags.useRefactoredAudioRouteSwitching()) {
+                                    mBluetoothHeadsetFuture.complete(null);
+                                }
                                 mBluetoothHeadset = null;
                                 lostServiceDevices = mHfpDevicesByAddress;
                                 mBluetoothRouteManager.onActiveDeviceChanged(null,
@@ -201,6 +210,7 @@
 
     private BluetoothRouteManager mBluetoothRouteManager;
     private BluetoothHeadset mBluetoothHeadset;
+    private CompletableFuture<BluetoothHeadset> mBluetoothHeadsetFuture;
     private BluetoothHearingAid mBluetoothHearingAid;
     private boolean mLeAudioCallbackRegistered = false;
     private BluetoothLeAudio mBluetoothLeAudioService;
@@ -218,8 +228,12 @@
     public BluetoothDeviceManager(Context context, BluetoothAdapter bluetoothAdapter,
             CallAudioCommunicationDeviceTracker communicationDeviceTracker,
             FeatureFlags featureFlags) {
+        mFeatureFlags = featureFlags;
         if (bluetoothAdapter != null) {
             mBluetoothAdapter = bluetoothAdapter;
+            if (mFeatureFlags.useRefactoredAudioRouteSwitching()) {
+                mBluetoothHeadsetFuture = new CompletableFuture<>();
+            }
             bluetoothAdapter.getProfileProxy(context, mBluetoothProfileServiceListener,
                     BluetoothProfile.HEADSET);
             bluetoothAdapter.getProfileProxy(context, mBluetoothProfileServiceListener,
@@ -229,7 +243,6 @@
             mAudioManager = context.getSystemService(AudioManager.class);
             mExecutor = context.getMainExecutor();
             mCommunicationDeviceTracker = communicationDeviceTracker;
-            mFeatureFlags = featureFlags;
         }
     }
 
@@ -333,7 +346,19 @@
     }
 
     public BluetoothHeadset getBluetoothHeadset() {
-        return mBluetoothHeadset;
+        if (mFeatureFlags.useRefactoredAudioRouteSwitching()) {
+            try {
+                mBluetoothHeadset = mBluetoothHeadsetFuture.get(500L,
+                        TimeUnit.MILLISECONDS);
+                return mBluetoothHeadset;
+            } catch (TimeoutException | InterruptedException | ExecutionException e) {
+                // ignore
+                Log.w(this, "Acquire BluetoothHeadset service failed due to: " + e);
+                return null;
+            }
+        } else {
+            return mBluetoothHeadset;
+        }
     }
 
     public BluetoothAdapter getBluetoothAdapter() {
@@ -402,7 +427,7 @@
                 mHearingAidDeviceSyncIds.put(device, hiSyncId);
                 targetDeviceMap = mHearingAidDevicesByAddress;
             } else if (deviceType == DEVICE_TYPE_HEADSET) {
-                if (mBluetoothHeadset == null) {
+                if (getBluetoothHeadset() == null) {
                     Log.w(this, "Headset service null when receiving device added broadcast");
                     return;
                 }
@@ -465,7 +490,7 @@
     }
 
     public void disconnectSco() {
-        if (mBluetoothHeadset == null) {
+        if (getBluetoothHeadset() == null) {
             Log.w(this, "Trying to disconnect audio but no headset service exists.");
         } else {
             mBluetoothHeadset.disconnectAudio();
@@ -650,7 +675,7 @@
             callProfile = BluetoothProfile.HEARING_AID;
         } else if (mHfpDevicesByAddress.containsKey(address)) {
             Log.i(this, "Telecomm found HFP device for address: " + address);
-            if (mBluetoothHeadset == null) {
+            if (getBluetoothHeadset() == null) {
                 Log.w(this, "Attempting to turn on audio when the headset service is null");
                 return false;
             }
@@ -707,9 +732,15 @@
                 Log.w(this, "Couldn't set active device to %s", address);
                 return false;
             }
-            int scoConnectionRequest = mBluetoothHeadset.connectAudio();
-            return scoConnectionRequest == BluetoothStatusCodes.SUCCESS ||
-                scoConnectionRequest == BluetoothStatusCodes.ERROR_AUDIO_DEVICE_ALREADY_CONNECTED;
+            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;
@@ -739,16 +770,20 @@
         // Get the inband ringing enabled status of expected BT device to route call audio instead
         // of using the address of currently connected device.
         BluetoothDevice activeDevice = mBluetoothRouteManager.getMostRecentlyReportedActiveDevice();
-        Log.i(this, "isInbandRingingEnabled: activeDevice: " + activeDevice);
-        if (mBluetoothRouteManager.isCachedLeAudioDevice(activeDevice)) {
+        return isInbandRingEnabled(activeDevice);
+    }
+
+    public boolean isInbandRingEnabled(BluetoothDevice bluetoothDevice) {
+        Log.i(this, "isInbandRingEnabled: device: " + bluetoothDevice);
+        if (mBluetoothRouteManager.isCachedLeAudioDevice(bluetoothDevice)) {
             if (mBluetoothLeAudioService == null) {
                 Log.i(this, "isInbandRingingEnabled: no leaudio service available.");
                 return false;
             }
-            int groupId = mBluetoothLeAudioService.getGroupId(activeDevice);
+            int groupId = mBluetoothLeAudioService.getGroupId(bluetoothDevice);
             return mBluetoothLeAudioService.isInbandRingtoneEnabled(groupId);
         } else {
-            if (mBluetoothHeadset == null) {
+            if (getBluetoothHeadset() == null) {
                 Log.i(this, "isInbandRingingEnabled: no headset service available.");
                 return false;
             }
diff --git a/src/com/android/server/telecom/bluetooth/BluetoothRouteManager.java b/src/com/android/server/telecom/bluetooth/BluetoothRouteManager.java
index 235ba56..7da5339 100644
--- a/src/com/android/server/telecom/bluetooth/BluetoothRouteManager.java
+++ b/src/com/android/server/telecom/bluetooth/BluetoothRouteManager.java
@@ -957,6 +957,11 @@
         return mDeviceManager.isInbandRingingEnabled();
     }
 
+    @VisibleForTesting
+    public boolean isInbandRingEnabled(BluetoothDevice bluetoothDevice) {
+        return mDeviceManager.isInbandRingEnabled(bluetoothDevice);
+    }
+
     private boolean addDevice(String address) {
         if (mAudioConnectingStates.containsKey(address)) {
             Log.i(this, "Attempting to add device %s twice.", address);
diff --git a/src/com/android/server/telecom/callfiltering/BlockCheckerAdapter.java b/src/com/android/server/telecom/callfiltering/BlockCheckerAdapter.java
index a83f314..1fda542 100644
--- a/src/com/android/server/telecom/callfiltering/BlockCheckerAdapter.java
+++ b/src/com/android/server/telecom/callfiltering/BlockCheckerAdapter.java
@@ -34,22 +34,24 @@
      *
      * @param context the context of the caller.
      * @param phoneNumber the number to check.
-     * @param extras the extra attribute of the number.
+     * @param numberPresentation the presentation code associated with the call.
+     * @param isNumberInContacts indicates if the provided number exists as a contact.
      * @return result code indicating if the number should be blocked, and if so why.
-     *         Valid values are: {@link BlockedNumberContract#STATUS_NOT_BLOCKED},
-     *         {@link BlockedNumberContract#STATUS_BLOCKED_IN_LIST},
-     *         {@link BlockedNumberContract#STATUS_BLOCKED_NOT_IN_CONTACTS},
-     *         {@link BlockedNumberContract#STATUS_BLOCKED_PAYPHONE},
-     *         {@link BlockedNumberContract#STATUS_BLOCKED_RESTRICTED},
-     *         {@link BlockedNumberContract#STATUS_BLOCKED_UNKNOWN_NUMBER}.
+     *         Valid values are: {@link BlockCheckerFilter#STATUS_NOT_BLOCKED},
+     *         {@link BlockCheckerFilter#STATUS_BLOCKED_IN_LIST},
+     *         {@link BlockCheckerFilter#STATUS_BLOCKED_NOT_IN_CONTACTS},
+     *         {@link BlockCheckerFilter#STATUS_BLOCKED_PAYPHONE},
+     *         {@link BlockCheckerFilter#STATUS_BLOCKED_RESTRICTED},
+     *         {@link BlockCheckerFilter#STATUS_BLOCKED_UNKNOWN_NUMBER}.
      */
-    public int getBlockStatus(Context context, String phoneNumber, Bundle extras) {
+    public int getBlockStatus(Context context, String phoneNumber,
+            int numberPresentation, boolean isNumberInContacts) {
         int blockStatus = BlockedNumberContract.STATUS_NOT_BLOCKED;
         long startTimeNano = System.nanoTime();
 
         try {
-            blockStatus = BlockedNumberContract.SystemContract.shouldSystemBlockNumber(
-                    context, phoneNumber, extras);
+            blockStatus = BlockedNumberContract.BlockedNumbers.shouldSystemBlockNumber(
+                    context, phoneNumber, numberPresentation, isNumberInContacts);
             if (blockStatus != BlockedNumberContract.STATUS_NOT_BLOCKED) {
                 Log.d(TAG, phoneNumber + " is blocked.");
             }
diff --git a/src/com/android/server/telecom/callfiltering/BlockCheckerFilter.java b/src/com/android/server/telecom/callfiltering/BlockCheckerFilter.java
index 64060c8..5beb5f0 100644
--- a/src/com/android/server/telecom/callfiltering/BlockCheckerFilter.java
+++ b/src/com/android/server/telecom/callfiltering/BlockCheckerFilter.java
@@ -48,6 +48,61 @@
 
     public static final long CALLER_INFO_QUERY_TIMEOUT = 5000;
 
+    /**
+     * Integer reason indicating whether a call was blocked, and if so why.
+     * @hide
+     */
+    public static final String RES_BLOCK_STATUS = "block_status";
+
+    /**
+     * Integer reason code used with {@link #RES_BLOCK_STATUS} to indicate that a call was not
+     * blocked.
+     * @hide
+     */
+    public static final int STATUS_NOT_BLOCKED = 0;
+
+    /**
+     * Integer reason code used with {@link #RES_BLOCK_STATUS} to indicate that a call was blocked
+     * because it is in the list of blocked numbers maintained by the provider.
+     * @hide
+     */
+    public static final int STATUS_BLOCKED_IN_LIST = 1;
+
+    /**
+     * Integer reason code used with {@link #RES_BLOCK_STATUS} to indicate that a call was blocked
+     * because it is from a restricted number.
+     * @hide
+     */
+    public static final int STATUS_BLOCKED_RESTRICTED = 2;
+
+    /**
+     * Integer reason code used with {@link #RES_BLOCK_STATUS} to indicate that a call was blocked
+     * because it is from an unknown number.
+     * @hide
+     */
+    public static final int STATUS_BLOCKED_UNKNOWN_NUMBER = 3;
+
+    /**
+     * Integer reason code used with {@link #RES_BLOCK_STATUS} to indicate that a call was blocked
+     * because it is from a pay phone.
+     * @hide
+     */
+    public static final int STATUS_BLOCKED_PAYPHONE = 4;
+
+    /**
+     * Integer reason code used with {@link #RES_BLOCK_STATUS} to indicate that a call was blocked
+     * because it is from a number not in the users contacts.
+     * @hide
+     */
+    public static final int STATUS_BLOCKED_NOT_IN_CONTACTS = 5;
+
+    /**
+     * Integer reason code used with {@link #RES_BLOCK_STATUS} to indicate that a call was blocked
+     * because it is from a number not available.
+     * @hide
+     */
+    public static final int STATUS_BLOCKED_UNAVAILABLE = 6;
+
     public BlockCheckerFilter(Context context, Call call,
             CallerInfoLookupHelper callerInfoLookupHelper,
             BlockCheckerAdapter blockCheckerAdapter) {
@@ -96,14 +151,21 @@
 
     private void getBlockStatus(
             CompletableFuture<CallFilteringResult> resultFuture) {
-        // Set extras
-        Bundle extras = new Bundle();
+        // Set presentation and if contact exists. Used in determining if the system should block
+        // the passed in number. Use default values as they would be returned if the keys didn't
+        // exist in the extras to maintain existing behavior.
+        int presentation;
+        boolean isNumberInContacts;
         if (BlockedNumbersUtil.isEnhancedCallBlockingEnabledByPlatform(mContext)) {
-            int presentation = mCall.getHandlePresentation();
-            extras.putInt(BlockedNumberContract.EXTRA_CALL_PRESENTATION, presentation);
-            if (presentation == TelecomManager.PRESENTATION_ALLOWED) {
-                extras.putBoolean(BlockedNumberContract.EXTRA_CONTACT_EXIST, mContactExists);
-            }
+            presentation = mCall.getHandlePresentation();
+        } else {
+            presentation = 0;
+        }
+
+        if (presentation == TelecomManager.PRESENTATION_ALLOWED) {
+            isNumberInContacts = mContactExists;
+        } else {
+            isNumberInContacts = false;
         }
 
         // Set number
@@ -111,7 +173,8 @@
                 mCall.getHandle().getSchemeSpecificPart();
 
         CompletableFuture.supplyAsync(
-                () -> mBlockCheckerAdapter.getBlockStatus(mContext, number, extras),
+                () -> mBlockCheckerAdapter.getBlockStatus(mContext, number,
+                        presentation, isNumberInContacts),
                 new LoggedHandlerExecutor(mHandler, "BCF.gBS", null))
                 .thenApplyAsync((x) -> completeResult(resultFuture, x),
                         new LoggedHandlerExecutor(mHandler, "BCF.gBS", null));
@@ -120,7 +183,7 @@
     private int completeResult(CompletableFuture<CallFilteringResult> resultFuture,
             int blockStatus) {
         CallFilteringResult result;
-        if (blockStatus != BlockedNumberContract.STATUS_NOT_BLOCKED) {
+        if (blockStatus != STATUS_NOT_BLOCKED) {
             result = new CallFilteringResult.Builder()
                     .setShouldAllowCall(false)
                     .setShouldReject(true)
@@ -143,8 +206,7 @@
                     .build();
         }
         Log.addEvent(mCall, LogUtils.Events.BLOCK_CHECK_FINISHED,
-                BlockedNumberContract.SystemContract.blockStatusToString(blockStatus) + " "
-                        + result);
+                blockStatusToString(blockStatus) + " " + result);
         resultFuture.complete(result);
         mHandlerThread.quitSafely();
         return blockStatus;
@@ -152,20 +214,20 @@
 
     private int getBlockReason(int blockStatus) {
         switch (blockStatus) {
-            case BlockedNumberContract.STATUS_BLOCKED_IN_LIST:
+            case STATUS_BLOCKED_IN_LIST:
                 return CallLog.Calls.BLOCK_REASON_BLOCKED_NUMBER;
 
-            case BlockedNumberContract.STATUS_BLOCKED_UNKNOWN_NUMBER:
-            case BlockedNumberContract.STATUS_BLOCKED_UNAVAILABLE:
+            case STATUS_BLOCKED_UNKNOWN_NUMBER:
+            case STATUS_BLOCKED_UNAVAILABLE:
                 return CallLog.Calls.BLOCK_REASON_UNKNOWN_NUMBER;
 
-            case BlockedNumberContract.STATUS_BLOCKED_RESTRICTED:
+            case STATUS_BLOCKED_RESTRICTED:
                 return CallLog.Calls.BLOCK_REASON_RESTRICTED_NUMBER;
 
-            case BlockedNumberContract.STATUS_BLOCKED_PAYPHONE:
+            case STATUS_BLOCKED_PAYPHONE:
                 return CallLog.Calls.BLOCK_REASON_PAY_PHONE;
 
-            case BlockedNumberContract.STATUS_BLOCKED_NOT_IN_CONTACTS:
+            case STATUS_BLOCKED_NOT_IN_CONTACTS:
                 return CallLog.Calls.BLOCK_REASON_NOT_IN_CONTACTS;
 
             default:
@@ -174,4 +236,27 @@
                 return CallLog.Calls.BLOCK_REASON_BLOCKED_NUMBER;
         }
     }
+
+    /**
+     * Converts a block status constant to a string equivalent for logging.
+     */
+    private String blockStatusToString(int blockStatus) {
+        switch (blockStatus) {
+            case STATUS_NOT_BLOCKED:
+                return "not blocked";
+            case STATUS_BLOCKED_IN_LIST:
+                return "blocked - in list";
+            case STATUS_BLOCKED_RESTRICTED:
+                return "blocked - restricted";
+            case STATUS_BLOCKED_UNKNOWN_NUMBER:
+                return "blocked - unknown";
+            case STATUS_BLOCKED_PAYPHONE:
+                return "blocked - payphone";
+            case STATUS_BLOCKED_NOT_IN_CONTACTS:
+                return "blocked - not in contacts";
+            case STATUS_BLOCKED_UNAVAILABLE:
+                return "blocked - unavailable";
+        }
+        return "unknown";
+    }
 }
diff --git a/src/com/android/server/telecom/callfiltering/BlockedNumbersAdapter.java b/src/com/android/server/telecom/callfiltering/BlockedNumbersAdapter.java
index b8658d8..f640826 100644
--- a/src/com/android/server/telecom/callfiltering/BlockedNumbersAdapter.java
+++ b/src/com/android/server/telecom/callfiltering/BlockedNumbersAdapter.java
@@ -20,7 +20,7 @@
 
 /**
  * Adapter interface that wraps methods from
- * {@link android.provider.BlockedNumberContract.SystemContract} and
+ * {@link android.provider.BlockedNumberContract.BlockedNumbers} and
  * {@link com.android.server.telecom.settings.BlockedNumbersUtil} to make things testable.
  */
 public interface BlockedNumbersAdapter {
diff --git a/src/com/android/server/telecom/components/TelecomService.java b/src/com/android/server/telecom/components/TelecomService.java
index 9287d33..24e5d57 100644
--- a/src/com/android/server/telecom/components/TelecomService.java
+++ b/src/com/android/server/telecom/components/TelecomService.java
@@ -223,7 +223,7 @@
                                 @Override
                                 public boolean shouldShowEmergencyCallNotification(Context
                                         context) {
-                                    return BlockedNumberContract.SystemContract
+                                    return BlockedNumberContract.BlockedNumbers
                                             .shouldShowEmergencyCallNotification(context);
                                 }
 
diff --git a/src/com/android/server/telecom/settings/BlockedNumbersActivity.java b/src/com/android/server/telecom/settings/BlockedNumbersActivity.java
index 5fa5f06..819b270 100644
--- a/src/com/android/server/telecom/settings/BlockedNumbersActivity.java
+++ b/src/com/android/server/telecom/settings/BlockedNumbersActivity.java
@@ -155,7 +155,7 @@
             }
         };
         IntentFilter blockStatusIntentFilter = new IntentFilter(
-                BlockedNumberContract.SystemContract.ACTION_BLOCK_SUPPRESSION_STATE_CHANGED);
+                BlockedNumberContract.BlockedNumbers.ACTION_BLOCK_SUPPRESSION_STATE_CHANGED);
         blockStatusIntentFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
         registerReceiver(mBlockingStatusReceiver, blockStatusIntentFilter,
                 Context.RECEIVER_EXPORTED);
@@ -183,7 +183,8 @@
     }
 
     private void updateButterBar() {
-        if (BlockedNumberContract.SystemContract.getBlockSuppressionStatus(this).isSuppressed) {
+        if (BlockedNumberContract.BlockedNumbers
+                .getBlockSuppressionStatus(this).getIsSuppressed()) {
             mButterBar.setVisibility(View.VISIBLE);
         } else {
             mButterBar.setVisibility(View.GONE);
@@ -238,7 +239,7 @@
         if (view == mAddButton) {
             showAddBlockedNumberDialog();
         } else if (view == mReEnableButton) {
-            BlockedNumberContract.SystemContract.endBlockSuppression(this);
+            BlockedNumberContract.BlockedNumbers.endBlockSuppression(this);
             mButterBar.setVisibility(View.GONE);
         }
     }
diff --git a/src/com/android/server/telecom/settings/BlockedNumbersUtil.java b/src/com/android/server/telecom/settings/BlockedNumbersUtil.java
index 4be75f8..e0fe81e 100644
--- a/src/com/android/server/telecom/settings/BlockedNumbersUtil.java
+++ b/src/com/android/server/telecom/settings/BlockedNumbersUtil.java
@@ -23,7 +23,7 @@
 import android.content.Intent;
 import android.os.PersistableBundle;
 import android.os.UserHandle;
-import android.provider.BlockedNumberContract.SystemContract;
+import android.provider.BlockedNumberContract.BlockedNumbers;
 import android.telephony.CarrierConfigManager;
 import android.telephony.PhoneNumberUtils;
 import android.text.BidiFormatter;
@@ -148,8 +148,8 @@
      * @return If {@code true} means the key enabled in the SharedPreferences,
      *            {@code false} otherwise.
      */
-    public static boolean getEnhancedBlockSetting(Context context, String key) {
-        return SystemContract.getEnhancedBlockSetting(context, key);
+    public static boolean getBlockedNumberSetting(Context context, String key) {
+        return BlockedNumbers.getBlockedNumberSetting(context, key);
     }
 
     /**
@@ -159,7 +159,7 @@
      * @param key preference key of SharedPreferences.
      * @param value the register value to the SharedPreferences.
      */
-    public static void setEnhancedBlockSetting(Context context, String key, boolean value) {
-        SystemContract.setEnhancedBlockSetting(context, key, value);
+    public static void setBlockedNumberSetting(Context context, String key, boolean value) {
+        BlockedNumbers.setBlockedNumberSetting(context, key, value);
     }
 }
diff --git a/src/com/android/server/telecom/settings/CallBlockDisabledActivity.java b/src/com/android/server/telecom/settings/CallBlockDisabledActivity.java
index 5f42b37..35b7f70 100644
--- a/src/com/android/server/telecom/settings/CallBlockDisabledActivity.java
+++ b/src/com/android/server/telecom/settings/CallBlockDisabledActivity.java
@@ -58,9 +58,9 @@
                 .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
                     @Override
                     public void onClick(DialogInterface dialog, int which) {
-                        BlockedNumbersUtil.setEnhancedBlockSetting(
+                        BlockedNumbersUtil.setBlockedNumberSetting(
                                 CallBlockDisabledActivity.this,
-                                BlockedNumberContract.SystemContract
+                                BlockedNumberContract.BlockedNumbers
                                         .ENHANCED_SETTING_KEY_SHOW_EMERGENCY_CALL_NOTIFICATION,
                                 false);
                         BlockedNumbersUtil.updateEmergencyCallNotification(
diff --git a/src/com/android/server/telecom/settings/EnhancedCallBlockingFragment.java b/src/com/android/server/telecom/settings/EnhancedCallBlockingFragment.java
index b1a1b0e..7ea8926 100644
--- a/src/com/android/server/telecom/settings/EnhancedCallBlockingFragment.java
+++ b/src/com/android/server/telecom/settings/EnhancedCallBlockingFragment.java
@@ -23,12 +23,11 @@
 import android.preference.PreferenceFragment;
 import android.preference.PreferenceScreen;
 import android.preference.SwitchPreference;
-import android.provider.BlockedNumberContract.SystemContract;
+import android.provider.BlockedNumberContract.BlockedNumbers;
 import android.telephony.CarrierConfigManager;
 import android.telephony.SubscriptionManager;
 import android.telecom.Log;
 import android.view.LayoutInflater;
-import android.view.MenuItem;
 import android.view.View;
 import android.view.ViewGroup;
 
@@ -54,13 +53,14 @@
 
         maybeConfigureCallBlockingOptions();
 
-        setOnPreferenceChangeListener(SystemContract.ENHANCED_SETTING_KEY_BLOCK_UNREGISTERED);
-        setOnPreferenceChangeListener(SystemContract.ENHANCED_SETTING_KEY_BLOCK_PRIVATE);
-        setOnPreferenceChangeListener(SystemContract.ENHANCED_SETTING_KEY_BLOCK_PAYPHONE);
-        setOnPreferenceChangeListener(SystemContract.ENHANCED_SETTING_KEY_BLOCK_UNKNOWN);
-        setOnPreferenceChangeListener(SystemContract.ENHANCED_SETTING_KEY_BLOCK_UNAVAILABLE);
+        setOnPreferenceChangeListener(BlockedNumbers.ENHANCED_SETTING_KEY_BLOCK_UNREGISTERED);
+        setOnPreferenceChangeListener(BlockedNumbers.ENHANCED_SETTING_KEY_BLOCK_PRIVATE);
+        setOnPreferenceChangeListener(BlockedNumbers.ENHANCED_SETTING_KEY_BLOCK_PAYPHONE);
+        setOnPreferenceChangeListener(BlockedNumbers.ENHANCED_SETTING_KEY_BLOCK_UNKNOWN);
+        setOnPreferenceChangeListener(BlockedNumbers.ENHANCED_SETTING_KEY_BLOCK_UNAVAILABLE);
         if (!showPayPhoneBlocking()) {
-            Preference payPhoneOption = getPreferenceScreen().findPreference(SystemContract.ENHANCED_SETTING_KEY_BLOCK_PAYPHONE);
+            Preference payPhoneOption = getPreferenceScreen()
+                    .findPreference(BlockedNumbers.ENHANCED_SETTING_KEY_BLOCK_PAYPHONE);
             getPreferenceScreen().removePreference(payPhoneOption);
         }
     }
@@ -122,13 +122,13 @@
     public void onResume() {
         super.onResume();
 
-        updateEnhancedBlockPref(SystemContract.ENHANCED_SETTING_KEY_BLOCK_UNREGISTERED);
-        updateEnhancedBlockPref(SystemContract.ENHANCED_SETTING_KEY_BLOCK_PRIVATE);
+        updateEnhancedBlockPref(BlockedNumbers.ENHANCED_SETTING_KEY_BLOCK_UNREGISTERED);
+        updateEnhancedBlockPref(BlockedNumbers.ENHANCED_SETTING_KEY_BLOCK_PRIVATE);
         if (showPayPhoneBlocking()) {
-            updateEnhancedBlockPref(SystemContract.ENHANCED_SETTING_KEY_BLOCK_PAYPHONE);
+            updateEnhancedBlockPref(BlockedNumbers.ENHANCED_SETTING_KEY_BLOCK_PAYPHONE);
         }
-        updateEnhancedBlockPref(SystemContract.ENHANCED_SETTING_KEY_BLOCK_UNKNOWN);
-        updateEnhancedBlockPref(SystemContract.ENHANCED_SETTING_KEY_BLOCK_UNAVAILABLE);
+        updateEnhancedBlockPref(BlockedNumbers.ENHANCED_SETTING_KEY_BLOCK_UNKNOWN);
+        updateEnhancedBlockPref(BlockedNumbers.ENHANCED_SETTING_KEY_BLOCK_UNAVAILABLE);
     }
 
     /**
@@ -137,7 +137,7 @@
     private void updateEnhancedBlockPref(String key) {
         SwitchPreference pref = (SwitchPreference) findPreference(key);
         if (pref != null) {
-            pref.setChecked(BlockedNumbersUtil.getEnhancedBlockSetting(getActivity(), key));
+            pref.setChecked(BlockedNumbersUtil.getBlockedNumberSetting(getActivity(), key));
         }
     }
 
@@ -147,18 +147,18 @@
             if (mIsCombiningRestrictedAndUnknownOption) {
                 Log.i(this, "onPreferenceChange: changing %s and %s to %b",
                         preference.getKey(), BLOCK_RESTRICTED_NUMBERS_KEY, (boolean) objValue);
-                BlockedNumbersUtil.setEnhancedBlockSetting(getActivity(),
+                BlockedNumbersUtil.setBlockedNumberSetting(getActivity(),
                         BLOCK_RESTRICTED_NUMBERS_KEY, (boolean) objValue);
             }
 
             if (mIsCombiningUnavailableAndUnknownOption) {
                 Log.i(this, "onPreferenceChange: changing %s and %s to %b",
                         preference.getKey(), BLOCK_UNAVAILABLE_NUMBERS_KEY, (boolean) objValue);
-                BlockedNumbersUtil.setEnhancedBlockSetting(getActivity(),
+                BlockedNumbersUtil.setBlockedNumberSetting(getActivity(),
                         BLOCK_UNAVAILABLE_NUMBERS_KEY, (boolean) objValue);
             }
         }
-        BlockedNumbersUtil.setEnhancedBlockSetting(getActivity(), preference.getKey(),
+        BlockedNumbersUtil.setBlockedNumberSetting(getActivity(), preference.getKey(),
                 (boolean) objValue);
         return true;
     }
diff --git a/src/com/android/server/telecom/voip/RequestVideoStateTransaction.java b/src/com/android/server/telecom/voip/RequestVideoStateTransaction.java
new file mode 100644
index 0000000..64596b1
--- /dev/null
+++ b/src/com/android/server/telecom/voip/RequestVideoStateTransaction.java
@@ -0,0 +1,70 @@
+/*
+ * 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.voip;
+
+import static com.android.server.telecom.voip.VideoStateTranslation.TransactionalVideoStateToVideoProfileState;
+
+import android.telecom.VideoProfile;
+import android.util.Log;
+
+import com.android.server.telecom.CallsManager;
+import com.android.server.telecom.Call;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionStage;
+
+public class RequestVideoStateTransaction extends VoipCallTransaction {
+
+    private static final String TAG = RequestVideoStateTransaction.class.getSimpleName();
+    private final Call mCall;
+    private final int mVideoProfileState;
+
+    public RequestVideoStateTransaction(CallsManager callsManager, Call call,
+            int transactionalVideoState) {
+        super(callsManager.getLock());
+        mCall = call;
+        mVideoProfileState = TransactionalVideoStateToVideoProfileState(transactionalVideoState);
+    }
+
+    @Override
+    public CompletionStage<VoipCallTransactionResult> processTransaction(Void v) {
+        Log.d(TAG, "processTransaction");
+        CompletableFuture<VoipCallTransactionResult> future = new CompletableFuture<>();
+
+        if (isRequestingVideoTransmission(mVideoProfileState) &&
+                !mCall.isVideoCallingSupportedByPhoneAccount()) {
+            future.complete(new VoipCallTransactionResult(
+                    VoipCallTransactionResult.RESULT_FAILED,
+                    "Video calling is not supported by the target account"));
+        } else if (isRequestingVideoTransmission(mVideoProfileState) &&
+                !mCall.isTransactionalCallSupportsVideoCalling()) {
+            future.complete(new VoipCallTransactionResult(
+                    VoipCallTransactionResult.RESULT_FAILED,
+                    "Video calling is not supported according to the callAttributes"));
+        } else {
+            mCall.setVideoState(mVideoProfileState);
+            future.complete(new VoipCallTransactionResult(
+                    VoipCallTransactionResult.RESULT_SUCCEED,
+                    "The Video State was changed successfully"));
+        }
+        return future;
+    }
+
+    private boolean isRequestingVideoTransmission(int targetVideoState) {
+        return targetVideoState != VideoProfile.STATE_AUDIO_ONLY;
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/server/telecom/voip/VideoStateTranslation.java b/src/com/android/server/telecom/voip/VideoStateTranslation.java
new file mode 100644
index 0000000..615e4bc
--- /dev/null
+++ b/src/com/android/server/telecom/voip/VideoStateTranslation.java
@@ -0,0 +1,94 @@
+/*
+ * 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.voip;
+
+import android.telecom.CallAttributes;
+import android.telecom.Log;
+import android.telecom.VideoProfile;
+
+/**
+ * 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}.
+ * To be more specific, there are 3 video states (rx, tx, and bi-directional).
+ * {@link CallAttributes.CallType} only has 2 states (audio and video).
+ *
+ * The reason why Transactional calls have fewer states is due to the fact that the framework is
+ * only used by VoIP apps and Telecom only cares to know if the call is audio or video.
+ *
+ * Calls that are backed by a {@link android.telecom.ConnectionService} have the ability to be
+ * managed calls (non-VoIP) and Dialer needs more fine grain video states to update the UI. Thus,
+ * {@link VideoProfile} is used for {@link android.telecom.ConnectionService} backed calls.
+ */
+public class VideoStateTranslation {
+    private static final String TAG = VideoStateTranslation.class.getSimpleName();
+
+    /**
+     * Client --> Telecom
+     * 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));
+            return VideoProfile.STATE_AUDIO_ONLY;
+        } else {
+            Log.i(TAG, "%s --> VideoProfile.STATE_BIDIRECTIONAL",
+                    TransactionalVideoState_toString(transactionalVideo));
+            return VideoProfile.STATE_BIDIRECTIONAL;
+        }
+    }
+
+    /**
+     * Telecom --> Client
+     * 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;
+        }
+    }
+
+    private static String TransactionalVideoState_toString(int transactionalVideoState) {
+        if (transactionalVideoState == CallAttributes.AUDIO_CALL) {
+            return "CallAttributes.AUDIO_CALL";
+        } else {
+            return "CallAttributes.VIDEO_CALL";
+        }
+    }
+
+    private static String VideoProfileState_toString(int videoProfileState) {
+        switch (videoProfileState) {
+            case VideoProfile.STATE_BIDIRECTIONAL -> {
+                return "VideoProfile.STATE_BIDIRECTIONAL";
+            }
+            case VideoProfile.STATE_RX_ENABLED -> {
+                return "VideoProfile.STATE_RX_ENABLED";
+            }
+            case VideoProfile.STATE_TX_ENABLED -> {
+                return "VideoProfile.STATE_TX_ENABLED";
+            }
+        }
+        return "VideoProfile.STATE_AUDIO_ONLY";
+    }
+}
diff --git a/testapps/src/com/android/server/telecom/testapps/TestInCallUI.java b/testapps/src/com/android/server/telecom/testapps/TestInCallUI.java
index bdd4c1a..d2aca78 100644
--- a/testapps/src/com/android/server/telecom/testapps/TestInCallUI.java
+++ b/testapps/src/com/android/server/telecom/testapps/TestInCallUI.java
@@ -40,6 +40,8 @@
 import android.widget.TextView;
 import android.widget.Toast;
 
+import com.android.server.telecom.flags.Flags;
+
 import java.util.Collection;
 import java.util.List;
 import java.util.Optional;
@@ -263,6 +265,8 @@
                         .getIntentExtras().getString(TelecomManager.EXTRA_CALL_SUBJECT);
                 boolean isBusiness = call.getDetails()
                         .getExtras().getBoolean(ImsCallProfile.EXTRA_IS_BUSINESS_CALL);
+                String businessName = call.getDetails()
+                            .getExtras().getString(ImsCallProfile.EXTRA_ASSERTED_DISPLAY_NAME);
 
                 StringBuilder display = new StringBuilder();
                 display.append("priority=");
@@ -286,6 +290,7 @@
 
                 display.append(" subject=" + subject);
                 display.append(" isBusiness=" + isBusiness);
+                display.append(" businessName=" + businessName);
                 TextView attachmentsTextView = findViewById(R.id.incoming_composer_attachments);
                 attachmentsTextView.setText(display.toString());
                 break;
diff --git a/tests/src/com/android/server/telecom/tests/AnalyticsTests.java b/tests/src/com/android/server/telecom/tests/AnalyticsTests.java
index 148db51..9caf0b5 100644
--- a/tests/src/com/android/server/telecom/tests/AnalyticsTests.java
+++ b/tests/src/com/android/server/telecom/tests/AnalyticsTests.java
@@ -87,6 +87,7 @@
         super.setUp();
         // this is a mock
         mSubscriptionManager = mContext.getSystemService(SubscriptionManager.class);
+        when(mSubscriptionManager.createForAllUserProfiles()).thenReturn(mSubscriptionManager);
         when(mSubscriptionManager.getActiveSubscriptionInfoList())
                 .thenReturn(Collections.emptyList());
     }
diff --git a/tests/src/com/android/server/telecom/tests/BasicCallTests.java b/tests/src/com/android/server/telecom/tests/BasicCallTests.java
index 9ca3de1..4bca30d 100644
--- a/tests/src/com/android/server/telecom/tests/BasicCallTests.java
+++ b/tests/src/com/android/server/telecom/tests/BasicCallTests.java
@@ -16,6 +16,8 @@
 
 package com.android.server.telecom.tests;
 
+import static com.android.server.telecom.callfiltering.BlockCheckerFilter.RES_BLOCK_STATUS;
+import static com.android.server.telecom.callfiltering.BlockCheckerFilter.STATUS_BLOCKED_IN_LIST;
 import static com.android.server.telecom.tests.ConnectionServiceFixture.STATUS_HINTS_EXTRA;
 
 import static org.junit.Assert.assertEquals;
@@ -887,8 +889,7 @@
             @Override
             public Bundle answer(InvocationOnMock invocation) throws Throwable {
                 Bundle bundle = new Bundle();
-                bundle.putInt(BlockedNumberContract.RES_BLOCK_STATUS,
-                        BlockedNumberContract.STATUS_BLOCKED_IN_LIST);
+                bundle.putInt(RES_BLOCK_STATUS, STATUS_BLOCKED_IN_LIST);
                 return bundle;
             }
         });
diff --git a/tests/src/com/android/server/telecom/tests/BlockCheckerFilterTest.java b/tests/src/com/android/server/telecom/tests/BlockCheckerFilterTest.java
index 2584b02..e76989c 100644
--- a/tests/src/com/android/server/telecom/tests/BlockCheckerFilterTest.java
+++ b/tests/src/com/android/server/telecom/tests/BlockCheckerFilterTest.java
@@ -16,12 +16,14 @@
 
 package com.android.server.telecom.tests;
 
-import static android.provider.BlockedNumberContract.STATUS_BLOCKED_IN_LIST;
-import static android.provider.BlockedNumberContract.STATUS_NOT_BLOCKED;
+import static com.android.server.telecom.callfiltering.BlockCheckerFilter.STATUS_BLOCKED_IN_LIST;
+import static com.android.server.telecom.callfiltering.BlockCheckerFilter.STATUS_NOT_BLOCKED;
 
 import static junit.framework.TestCase.assertEquals;
 
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.timeout;
 import static org.mockito.Mockito.verify;
@@ -93,7 +95,7 @@
     @Test
     public void testBlockNumber() throws Exception {
         when(mBlockCheckerAdapter.getBlockStatus(any(Context.class),
-                eq(TEST_HANDLE.getSchemeSpecificPart()), any(Bundle.class)))
+                eq(TEST_HANDLE.getSchemeSpecificPart()), anyInt(), anyBoolean()))
                 .thenReturn(STATUS_BLOCKED_IN_LIST);
 
         setEnhancedBlockingEnabled(false);
@@ -105,7 +107,7 @@
     @Test
     public void testBlockNumberWhenEnhancedBlockingEnabled() throws Exception {
         when(mBlockCheckerAdapter.getBlockStatus(any(Context.class),
-                eq(TEST_HANDLE.getSchemeSpecificPart()), any(Bundle.class)))
+                eq(TEST_HANDLE.getSchemeSpecificPart()), anyInt(), anyBoolean()))
                 .thenReturn(STATUS_BLOCKED_IN_LIST);
 
         setEnhancedBlockingEnabled(true);
@@ -119,7 +121,7 @@
     @Test
     public void testDontBlockNumber() throws Exception {
         when(mBlockCheckerAdapter.getBlockStatus(any(Context.class),
-                eq(TEST_HANDLE.getSchemeSpecificPart()), any(Bundle.class)))
+                eq(TEST_HANDLE.getSchemeSpecificPart()), anyInt(), anyBoolean()))
                 .thenReturn(STATUS_NOT_BLOCKED);
 
         setEnhancedBlockingEnabled(false);
@@ -131,7 +133,7 @@
     @Test
     public void testDontBlockNumberWhenEnhancedBlockingEnabled() throws Exception {
         when(mBlockCheckerAdapter.getBlockStatus(any(Context.class),
-                eq(TEST_HANDLE.getSchemeSpecificPart()), any(Bundle.class)))
+                eq(TEST_HANDLE.getSchemeSpecificPart()), anyInt(), anyBoolean()))
                 .thenReturn(STATUS_NOT_BLOCKED);
 
         setEnhancedBlockingEnabled(true);
diff --git a/tests/src/com/android/server/telecom/tests/BluetoothDeviceManagerTest.java b/tests/src/com/android/server/telecom/tests/BluetoothDeviceManagerTest.java
index 648a831..c516c8e 100644
--- a/tests/src/com/android/server/telecom/tests/BluetoothDeviceManagerTest.java
+++ b/tests/src/com/android/server/telecom/tests/BluetoothDeviceManagerTest.java
@@ -403,7 +403,8 @@
         when(mAdapter.setActiveDevice(nullable(BluetoothDevice.class),
                     eq(BluetoothAdapter.ACTIVE_DEVICE_ALL))).thenReturn(true);
         mBluetoothDeviceManager.connectAudio(device1.getAddress(), false);
-        verify(mAdapter).setActiveDevice(device1, BluetoothAdapter.ACTIVE_DEVICE_PHONE_CALL);
+        verify(mAdapter).setActiveDevice(eq(device1),
+                eq(BluetoothAdapter.ACTIVE_DEVICE_PHONE_CALL));
         verify(mAdapter, never()).setActiveDevice(nullable(BluetoothDevice.class),
                 eq(BluetoothAdapter.ACTIVE_DEVICE_ALL));
         mBluetoothDeviceManager.disconnectAudio();
diff --git a/tests/src/com/android/server/telecom/tests/CallAudioRouteControllerTest.java b/tests/src/com/android/server/telecom/tests/CallAudioRouteControllerTest.java
index 08576fc..0a53eb0 100644
--- a/tests/src/com/android/server/telecom/tests/CallAudioRouteControllerTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallAudioRouteControllerTest.java
@@ -16,18 +16,43 @@
 
 package com.android.server.telecom.tests;
 
+import static com.android.server.telecom.CallAudioRouteAdapter.ACTIVE_FOCUS;
 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;
+import static com.android.server.telecom.CallAudioRouteAdapter.CONNECT_WIRED_HEADSET;
+import static com.android.server.telecom.CallAudioRouteAdapter.DISCONNECT_DOCK;
+import static com.android.server.telecom.CallAudioRouteAdapter.DISCONNECT_WIRED_HEADSET;
+import static com.android.server.telecom.CallAudioRouteAdapter.MUTE_OFF;
+import static com.android.server.telecom.CallAudioRouteAdapter.MUTE_ON;
+import static com.android.server.telecom.CallAudioRouteAdapter.NO_FOCUS;
+import static com.android.server.telecom.CallAudioRouteAdapter.RINGING_FOCUS;
+import static com.android.server.telecom.CallAudioRouteAdapter.SPEAKER_OFF;
+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_FOCUS;
+import static com.android.server.telecom.CallAudioRouteAdapter.USER_SWITCH_BLUETOOTH;
+import static com.android.server.telecom.CallAudioRouteAdapter.USER_SWITCH_EARPIECE;
+import static com.android.server.telecom.CallAudioRouteAdapter.USER_SWITCH_HEADSET;
+import static com.android.server.telecom.CallAudioRouteAdapter.USER_SWITCH_SPEAKER;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.anyInt;
+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.timeout;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
@@ -35,15 +60,19 @@
 import android.bluetooth.BluetoothDevice;
 import android.media.AudioDeviceInfo;
 import android.media.AudioManager;
+import android.media.IAudioService;
 import android.media.audiopolicy.AudioProductStrategy;
+import android.os.UserHandle;
 import android.telecom.CallAudioState;
 
 import androidx.test.filters.SmallTest;
 
 import com.android.server.telecom.AudioRoute;
+import com.android.server.telecom.CallAudioManager;
 import com.android.server.telecom.CallAudioRouteController;
 import com.android.server.telecom.CallsManager;
 import com.android.server.telecom.WiredHeadsetManager;
+import com.android.server.telecom.bluetooth.BluetoothRouteManager;
 
 import org.junit.After;
 import org.junit.Before;
@@ -62,6 +91,9 @@
     @Mock AudioManager mAudioManager;
     @Mock AudioDeviceInfo mEarpieceDeviceInfo;
     @Mock CallsManager mCallsManager;
+    @Mock CallAudioManager.AudioServiceFactory mAudioServiceFactory;
+    @Mock IAudioService mAudioService;
+    @Mock BluetoothRouteManager mBluetoothRouteManager;
     private AudioRoute mEarpieceRoute;
     private AudioRoute mSpeakerRoute;
     private static final String BT_ADDRESS_1 = "00:00:00:00:00:01";
@@ -90,10 +122,16 @@
                 new AudioDeviceInfo[] {
                         mEarpieceDeviceInfo
                 });
+        when(mAudioManager.getPreferredDeviceForStrategy(nullable(AudioProductStrategy.class)))
+                .thenReturn(null);
+        when(mAudioServiceFactory.getAudioService()).thenReturn(mAudioService);
+        when(mContext.getAttributionTag()).thenReturn("");
         doNothing().when(mCallsManager).onCallAudioStateChanged(any(CallAudioState.class),
                 any(CallAudioState.class));
-        mController = new CallAudioRouteController(mContext, mCallsManager, mAudioRouteFactory,
-                mWiredHeadsetManager);
+        when(mCallsManager.getCurrentUserHandle()).thenReturn(
+                new UserHandle(UserHandle.USER_SYSTEM));
+        mController = new CallAudioRouteController(mContext, mCallsManager, mAudioServiceFactory,
+                mAudioRouteFactory, mWiredHeadsetManager, mBluetoothRouteManager);
         mController.setAudioRouteFactory(mAudioRouteFactory);
         mController.setAudioManager(mAudioManager);
         mEarpieceRoute = new AudioRoute(AudioRoute.TYPE_EARPIECE, null, null);
@@ -148,6 +186,7 @@
                 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);
@@ -166,9 +205,6 @@
     @SmallTest
     @Test
     public void testActiveDeactivateBluetoothDevice() {
-        when(mAudioManager.getPreferredDeviceForStrategy(nullable(AudioProductStrategy.class)))
-                .thenReturn(null);
-
         mController.initialize();
         mController.sendMessageWithSessionInfo(BT_DEVICE_ADDED, AudioRoute.TYPE_BLUETOOTH_SCO,
                 BLUETOOTH_DEVICE_1);
@@ -192,7 +228,256 @@
 
     @SmallTest
     @Test
-    public void testSwitchFocusInBluetoothRoute() {
+    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();
+        mController.sendMessageWithSessionInfo(BT_DEVICE_ADDED, AudioRoute.TYPE_BLUETOOTH_SCO,
+                BLUETOOTH_DEVICE_1);
+
+        CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_BLUETOOTH,
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH
+                        | CallAudioState.ROUTE_SPEAKER, BLUETOOTH_DEVICE_1, BLUETOOTH_DEVICES);
+        mController.sendMessageWithSessionInfo(BT_ACTIVE_DEVICE_PRESENT,
+                AudioRoute.TYPE_BLUETOOTH_SCO, BT_ADDRESS_1);
+        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+        assertFalse(mController.isActive());
+
+        mController.sendMessageWithSessionInfo(SWITCH_FOCUS, RINGING_FOCUS);
+        verify(mAudioManager, timeout(TEST_TIMEOUT)).setCommunicationDevice(
+                nullable(AudioDeviceInfo.class));
+        assertTrue(mController.isActive());
+
+        mController.sendMessageWithSessionInfo(SWITCH_FOCUS, ACTIVE_FOCUS);
+        assertTrue(mController.isActive());
+
+        mController.sendMessageWithSessionInfo(SWITCH_FOCUS, NO_FOCUS);
+        verify(mAudioManager, timeout(TEST_TIMEOUT)).clearCommunicationDevice();
+        assertFalse(mController.isActive());
+    }
+
+    @SmallTest
+    @Test
+    public void testConnectAndDisconnectWiredHeadset() {
+        mController.initialize();
+        mController.sendMessageWithSessionInfo(CONNECT_WIRED_HEADSET);
+        CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_WIRED_HEADSET,
+                CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_SPEAKER, null,
+                new HashSet<>());
+        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+
+        mController.sendMessageWithSessionInfo(DISCONNECT_WIRED_HEADSET);
+        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 testConnectAndDisconnectDock() {
+        mController.initialize();
+        mController.sendMessageWithSessionInfo(CONNECT_DOCK);
+        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));
+
+        mController.sendMessageWithSessionInfo(DISCONNECT_DOCK);
+        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 testSpeakerToggle() {
+        mController.initialize();
+        mController.setActive(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));
+
+        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)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+    }
+
+    @SmallTest
+    @Test
+    public void testSpeakerToggleWhenDockConnected() {
+        mController.initialize();
+        mController.setActive(true);
+        mController.sendMessageWithSessionInfo(CONNECT_DOCK);
+        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));
+
+        mController.sendMessageWithSessionInfo(SPEAKER_ON);
+        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));
+
+        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)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+    }
+
+    @SmallTest
+    @Test
+    public void testSwitchEarpiece() {
+        mController.initialize();
+        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));
+
+        mController.sendMessageWithSessionInfo(USER_SWITCH_EARPIECE);
+        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)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+    }
+
+    @SmallTest
+    @Test
+    public void testSwitchBluetooth() {
+        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(USER_SWITCH_BLUETOOTH, 0,
+                BLUETOOTH_DEVICE_1.getAddress());
+        mController.sendMessageWithSessionInfo(BT_AUDIO_CONNECTED, 0, BLUETOOTH_DEVICE_1);
+        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 tesetSwitchSpeakerAndHeadset() {
+        mController.initialize();
+        mController.sendMessageWithSessionInfo(CONNECT_WIRED_HEADSET);
+        CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_WIRED_HEADSET,
+                CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_SPEAKER, null,
+                new HashSet<>());
+        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+
+        mController.sendMessageWithSessionInfo(USER_SWITCH_SPEAKER);
+        mController.sendMessageWithSessionInfo(SPEAKER_ON);
+        expectedState = new CallAudioState(false, CallAudioState.ROUTE_SPEAKER,
+                CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_SPEAKER, null,
+                new HashSet<>());
+        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+
+        mController.sendMessageWithSessionInfo(USER_SWITCH_HEADSET);
+        mController.sendMessageWithSessionInfo(SPEAKER_OFF);
+        expectedState = new CallAudioState(false, CallAudioState.ROUTE_WIRED_HEADSET,
+                CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_SPEAKER, null,
+                new HashSet<>());
+        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+    }
+
+    @SmallTest
+    @Test
+    public void testEnableAndDisableStreaming() {
+        mController.initialize();
+        mController.sendMessageWithSessionInfo(STREAMING_FORCE_ENABLED);
+        CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_STREAMING,
+                CallAudioState.ROUTE_STREAMING, null, new HashSet<>());
+        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+
+        mController.sendMessageWithSessionInfo(SPEAKER_ON);
+        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+
+        mController.sendMessageWithSessionInfo(CONNECT_WIRED_HEADSET);
+        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+
+        mController.sendMessageWithSessionInfo(STREAMING_FORCE_DISABLED);
+        expectedState = new CallAudioState(false, CallAudioState.ROUTE_WIRED_HEADSET,
+                CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_SPEAKER, null,
+                new HashSet<>());
+        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+    }
+
+    @SmallTest
+    @Test
+    public void testToggleMute() throws Exception {
+        when(mAudioManager.isMasterMute()).thenReturn(false);
+
+        mController.initialize();
+        mController.setActive(true);
+
+        mController.sendMessageWithSessionInfo(MUTE_ON);
+        CallAudioState expectedState = new CallAudioState(true, CallAudioState.ROUTE_EARPIECE,
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER, null,
+                new HashSet<>());
+        verify(mAudioService, timeout(TEST_TIMEOUT)).setMicrophoneMute(eq(true), anyString(),
+                anyInt(), anyString());
+        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+
+        when(mAudioManager.isMasterMute()).thenReturn(true);
+        mController.sendMessageWithSessionInfo(MUTE_OFF);
+        expectedState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE,
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER, null,
+                new HashSet<>());
+        verify(mAudioService, timeout(TEST_TIMEOUT)).setMicrophoneMute(eq(false), anyString(),
+                anyInt(), anyString());
+        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
     }
 }
diff --git a/tests/src/com/android/server/telecom/tests/CallsManagerTest.java b/tests/src/com/android/server/telecom/tests/CallsManagerTest.java
index f814d3e..1529629 100644
--- a/tests/src/com/android/server/telecom/tests/CallsManagerTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallsManagerTest.java
@@ -127,6 +127,7 @@
 import com.android.server.telecom.TelecomSystem;
 import com.android.server.telecom.Timeouts;
 import com.android.server.telecom.WiredHeadsetManager;
+import com.android.server.telecom.bluetooth.BluetoothDeviceManager;
 import com.android.server.telecom.bluetooth.BluetoothRouteManager;
 import com.android.server.telecom.bluetooth.BluetoothStateReceiver;
 import com.android.server.telecom.callfiltering.BlockedNumbersAdapter;
@@ -290,6 +291,7 @@
     @Mock private PhoneCapability mPhoneCapability;
     @Mock private CallAudioCommunicationDeviceTracker mCommunicationDeviceTracker;
     @Mock private CallStreamingNotification mCallStreamingNotification;
+    @Mock private BluetoothDeviceManager mBluetoothDeviceManager;
     @Mock private FeatureFlags mFeatureFlags;
     @Mock private IncomingCallFilterGraph mIncomingCallFilterGraph;
     private CallsManager mCallsManager;
@@ -366,6 +368,7 @@
                 mEmergencyCallDiagnosticLogger,
                 mCommunicationDeviceTracker,
                 mCallStreamingNotification,
+                mBluetoothDeviceManager,
                 mFeatureFlags,
                 (call, listener, context, timeoutsAdapter, lock) -> mIncomingCallFilterGraph);
 
@@ -3194,7 +3197,7 @@
                 .thenReturn(true);
         mComponentContextFixture.getBroadcastReceivers().forEach(c -> c.onReceive(mContext,
                 new Intent(
-                        BlockedNumberContract.SystemContract
+                        BlockedNumberContract.BlockedNumbers
                                 .ACTION_BLOCK_SUPPRESSION_STATE_CHANGED)));
         verify(mBlockedNumbersAdapter).updateEmergencyCallNotification(any(Context.class),
                 eq(true));
@@ -3203,7 +3206,7 @@
                 .thenReturn(false);
         mComponentContextFixture.getBroadcastReceivers().forEach(c -> c.onReceive(mContext,
                 new Intent(
-                        BlockedNumberContract.SystemContract
+                        BlockedNumberContract.BlockedNumbers
                                 .ACTION_BLOCK_SUPPRESSION_STATE_CHANGED)));
         verify(mBlockedNumbersAdapter).updateEmergencyCallNotification(any(Context.class),
                 eq(false));