[automerger skipped] Import translations. DO NOT MERGE ANYWHERE am: 625cbebf32 -s ours

am skip reason: subject contains skip directive

Original change: https://googleplex-android-review.googlesource.com/c/platform/packages/services/Telecomm/+/25127401

Change-Id: I53eebdb57992735b2a11465c1c2f45e62dfe67ce
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
diff --git a/Android.bp b/Android.bp
index 501b438..f46c206 100644
--- a/Android.bp
+++ b/Android.bp
@@ -28,6 +28,7 @@
     static_libs: [
         "androidx.annotation_annotation",
         "androidx.core_core",
+        "telecom_flags_core_java_lib",
     ],
     libs: [
         "services",
@@ -50,6 +51,7 @@
     name: "TelecomUnitTests",
     static_libs: [
         "android-ex-camera2",
+        "flag-junit",
         "guava",
         "mockito-target-extended",
         "androidx.test.rules",
@@ -60,6 +62,7 @@
         "androidx.fragment_fragment",
         "androidx.test.ext.junit",
         "platform-compat-test-rules",
+        "telecom_flags_core_java_lib",
     ],
     srcs: [
         "tests/src/**/*.java",
@@ -98,8 +101,8 @@
     platform_apis: true,
     certificate: "platform",
     jacoco: {
-        include_filter: ["com.android.server.telecom.*"],
-        exclude_filter: ["com.android.server.telecom.tests.*"],
+        include_filter: ["com.android.server.telecom.**"],
+        exclude_filter: ["com.android.server.telecom.tests.**"],
     },
     test_suites: ["device-tests"],
     defaults: ["SettingsLibDefaults"],
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index ab067d9..28c85e1 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -28,7 +28,6 @@
     <!-- Prevents the activity manager from delaying any activity-start
          requests by this package, including requests immediately after
          the user presses "home". -->
-    <uses-permission android:name="android.permission.BIND_CONNECTION_SERVICE"/>
     <uses-permission android:name="android.permission.BIND_INCALL_SERVICE"/>
     <uses-permission android:name="android.permission.BLUETOOTH"/>
     <uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
diff --git a/OWNERS b/OWNERS
index 97cc81f..7e68aea 100644
--- a/OWNERS
+++ b/OWNERS
@@ -1,7 +1,6 @@
 breadley@google.com
 tgunn@google.com
 xiaotonj@google.com
-chinmayd@google.com
 tjstuart@google.com
 rgreenwalt@google.com
 pmadapurmath@google.com
diff --git a/flags/Android.bp b/flags/Android.bp
new file mode 100644
index 0000000..99e2dc5
--- /dev/null
+++ b/flags/Android.bp
@@ -0,0 +1,36 @@
+//
+// Copyright (C) 2023 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+aconfig_declarations {
+    name: "telecom_flags",
+    package: "com.android.server.telecom.flags",
+    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_calllog_flags.aconfig",
+      "telecom_resolve_hidden_dependencies.aconfig"
+    ],
+}
+
diff --git a/flags/telecom_api_flags.aconfig b/flags/telecom_api_flags.aconfig
new file mode 100644
index 0000000..4dbc03b
--- /dev/null
+++ b/flags/telecom_api_flags.aconfig
@@ -0,0 +1,15 @@
+package: "com.android.server.telecom.flags"
+
+flag {
+  name: "voip_app_actions_support"
+  namespace: "telecom"
+  description: "When set, Telecom support for additional VOIP application actions is active."
+  bug: "296934278"
+}
+
+flag {
+  name: "call_details_id_changes"
+  namespace: "telecom"
+  description: "When set, call details/extras id updates to Telecom APIs for Android V are active."
+  bug: "301713560"
+}
\ No newline at end of file
diff --git a/flags/telecom_broadcast_flags.aconfig b/flags/telecom_broadcast_flags.aconfig
new file mode 100644
index 0000000..348d574
--- /dev/null
+++ b/flags/telecom_broadcast_flags.aconfig
@@ -0,0 +1,8 @@
+package: "com.android.server.telecom.flags"
+
+flag {
+  name: "is_new_outgoing_call_broadcast_unblocking"
+  namespace: "telecom"
+  description: "When set, the ACTION_NEW_OUTGOING_CALL broadcast is unblocking."
+  bug: "224550864"
+}
\ No newline at end of file
diff --git a/flags/telecom_call_filtering_flags.aconfig b/flags/telecom_call_filtering_flags.aconfig
new file mode 100644
index 0000000..95e74ce
--- /dev/null
+++ b/flags/telecom_call_filtering_flags.aconfig
@@ -0,0 +1,8 @@
+package: "com.android.server.telecom.flags"
+
+flag {
+  name: "skip_filter_phone_account_perform_dnd_filter"
+  namespace: "telecom"
+  description: "Gates whether to still perform Dnd filter when phone account has skip_filter call extra."
+  bug: "222333869"
+}
\ No newline at end of file
diff --git a/flags/telecom_callaudioroutestatemachine_flags.aconfig b/flags/telecom_callaudioroutestatemachine_flags.aconfig
new file mode 100644
index 0000000..c231551
--- /dev/null
+++ b/flags/telecom_callaudioroutestatemachine_flags.aconfig
@@ -0,0 +1,8 @@
+package: "com.android.server.telecom.flags"
+
+flag {
+  name: "available_routes_never_updated_after_set_system_audio_state"
+  namespace: "telecom"
+  description: "Fix supported routes wrongly include bluetooth issue."
+  bug: "292599751"
+}
\ No newline at end of file
diff --git a/flags/telecom_calllog_flags.aconfig b/flags/telecom_calllog_flags.aconfig
new file mode 100644
index 0000000..075e1f3
--- /dev/null
+++ b/flags/telecom_calllog_flags.aconfig
@@ -0,0 +1,8 @@
+package: "com.android.server.telecom.flags"
+
+flag {
+  name: "telecom_log_external_wearable_calls"
+  namespace: "telecom"
+  description: "log external call if current device is a wearable one"
+  bug: "292600751"
+}
\ No newline at end of file
diff --git a/flags/telecom_default_phone_account_flags.aconfig b/flags/telecom_default_phone_account_flags.aconfig
new file mode 100644
index 0000000..03f324c
--- /dev/null
+++ b/flags/telecom_default_phone_account_flags.aconfig
@@ -0,0 +1,15 @@
+package: "com.android.server.telecom.flags"
+
+flag {
+  name: "only_update_telephony_on_valid_sub_ids"
+  namespace: "telecom"
+  description: "For testing purposes, only update Telephony when the default calling subId is non-zero"
+  bug: "234846282"
+}
+
+flag {
+  name: "telephony_has_default_but_telecom_does_not"
+  namespace: "telecom"
+  description: "Telecom is requesting the user to select a sim account to place the outgoing call on but the user has a default account in the settings"
+  bug: "302397094"
+}
\ No newline at end of file
diff --git a/flags/telecom_incallservice_flags.aconfig b/flags/telecom_incallservice_flags.aconfig
new file mode 100644
index 0000000..d70b9cc
--- /dev/null
+++ b/flags/telecom_incallservice_flags.aconfig
@@ -0,0 +1,8 @@
+package: "com.android.server.telecom.flags"
+
+flag {
+  name: "early_binding_to_incall_service"
+  namespace: "telecom"
+  description: "Binds to InCallServices when call requires no call filtering on watch"
+  bug: "282113261"
+}
\ No newline at end of file
diff --git a/flags/telecom_resolve_hidden_dependencies.aconfig b/flags/telecom_resolve_hidden_dependencies.aconfig
new file mode 100644
index 0000000..ecc0123
--- /dev/null
+++ b/flags/telecom_resolve_hidden_dependencies.aconfig
@@ -0,0 +1,8 @@
+package: "com.android.server.telecom.flags"
+
+flag {
+    name: "telecom_resolve_hidden_dependencies"
+    namespace: "android_platform_telecom"
+    description: "Mainland cleanup for hidden dependencies"
+    bug: "b/303440370"
+}
diff --git a/flags/telecom_ringer_flag_declarations.aconfig b/flags/telecom_ringer_flag_declarations.aconfig
new file mode 100644
index 0000000..54748d0
--- /dev/null
+++ b/flags/telecom_ringer_flag_declarations.aconfig
@@ -0,0 +1,8 @@
+package: "com.android.server.telecom.flags"
+
+flag {
+  name: "use_device_provided_serialized_ringer_vibration"
+  namespace: "telecom"
+  description: "Gates whether to use a serialized, device-specific ring vibration."
+  bug: "282113261"
+}
\ No newline at end of file
diff --git a/proguard.flags b/proguard.flags
index 635eba6..7c71a15 100644
--- a/proguard.flags
+++ b/proguard.flags
@@ -9,17 +9,3 @@
 -keep class android.telecom.Log {
   *;
 }
-
-# Keep classes, annotations and members used by Lifecycle. Remove this once aapt2 is enabled
--keepattributes *Annotation*
-
--keep class * implements android.arch.lifecycle.LifecycleObserver {
-}
-
--keep class * implements android.arch.lifecycle.GeneratedAdapter {
-    <init>(...);
-}
-
--keepclassmembers class ** {
-    @android.arch.lifecycle.OnLifecycleEvent *;
-}
diff --git a/res/values/config.xml b/res/values/config.xml
index 15f765b..c38a6ec 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -49,8 +49,10 @@
     <bool name="grant_location_permission_enabled">false</bool>
 
     <!-- When true, a simple full intensity on/off vibration pattern will be used when calls ring.
-         When false, a fancy vibration pattern which ramps up and down will be used.
-         Devices should overlay this value based on the type of vibration hardware they employ. -->
+
+         When false, the vibration effect serialized in the raw `default_ringtone_vibration_effect`
+         resource (under `frameworks/base/core/res/res/raw/`) is used. Devices should overlay this
+         value based on the type of vibration hardware they employ. -->
     <bool name="use_simple_vibration_pattern">false</bool>
 
     <!-- Threshold for the X+Y component of gravity needed for the device orientation to be
diff --git a/res/xml/activity_blocked_numbers.xml b/res/xml/activity_blocked_numbers.xml
index e77184d..b6298e9 100644
--- a/res/xml/activity_blocked_numbers.xml
+++ b/res/xml/activity_blocked_numbers.xml
@@ -41,8 +41,8 @@
                     android:layout_height="wrap_content"
                     android:text="@string/non_primary_user"
                     android:paddingTop="@dimen/blocked_numbers_large_padding"
-                    android:paddingLeft="@dimen/blocked_numbers_large_padding"
-                    android:paddingRight="@dimen/blocked_numbers_large_padding"
+                    android:paddingStart="@dimen/blocked_numbers_large_padding"
+                    android:paddingEnd="@dimen/blocked_numbers_large_padding"
                     style="@style/BlockedNumbersTextPrimary2"
                     android:visibility="gone" />
 
@@ -62,8 +62,8 @@
                         android:layout_width="match_parent"
                         android:layout_height="wrap_content"
                         android:paddingTop="@dimen/blocked_numbers_large_padding"
-                        android:paddingLeft="@dimen/blocked_numbers_large_padding"
-                        android:paddingRight="@dimen/blocked_numbers_large_padding">
+                        android:paddingStart="@dimen/blocked_numbers_large_padding"
+                        android:paddingEnd="@dimen/blocked_numbers_large_padding">
 
                     <TextView
                             android:layout_width="wrap_content"
diff --git a/res/xml/blocking_suppressed_butterbar.xml b/res/xml/blocking_suppressed_butterbar.xml
index 8b941b9..2947340 100644
--- a/res/xml/blocking_suppressed_butterbar.xml
+++ b/res/xml/blocking_suppressed_butterbar.xml
@@ -25,19 +25,19 @@
             android:id="@+id/icon"
             android:layout_height="wrap_content"
             android:layout_width="wrap_content"
-            android:layout_alignParentLeft="true"
+            android:layout_alignParentStart="true"
             android:paddingTop="@dimen/blocked_numbers_large_padding"
-            android:paddingRight="@dimen/blocked_numbers_large_padding"
-            android:paddingLeft="@dimen/blocked_numbers_large_padding"
+            android:paddingEnd="@dimen/blocked_numbers_large_padding"
+            android:paddingStart="@dimen/blocked_numbers_large_padding"
             android:src="@drawable/ic_status_blocked_orange_40dp"/>
 
     <TextView
             android:id="@+id/title"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
-            android:layout_toRightOf="@id/icon"
+            android:layout_toEndOf="@id/icon"
             android:paddingTop="@dimen/blocked_numbers_large_padding"
-            android:paddingRight="@dimen/blocked_numbers_large_padding"
+            android:paddingEnd="@dimen/blocked_numbers_large_padding"
             android:text="@string/blocked_numbers_butter_bar_title"
             style="@style/BlockedNumbersTextPrimary2" />
 
@@ -45,11 +45,11 @@
             android:id="@+id/description"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
-            android:layout_toRightOf="@id/icon"
+            android:layout_toEndOf="@id/icon"
             android:layout_below="@id/title"
             android:paddingTop="@dimen/blocked_numbers_large_padding"
             android:paddingBottom="@dimen/blocked_numbers_large_padding"
-            android:paddingRight="@dimen/blocked_numbers_large_padding"
+            android:paddingEnd="@dimen/blocked_numbers_large_padding"
             android:text="@string/blocked_numbers_butter_bar_body"
             style="@style/BlockedNumbersTextSecondary" />
 
@@ -57,9 +57,9 @@
             android:id="@+id/reenable_button"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
-            android:layout_toRightOf="@id/icon"
+            android:layout_toEndOf="@id/icon"
             android:layout_below="@id/description"
-            android:paddingRight="@dimen/blocked_numbers_large_padding"
+            android:paddingEnd="@dimen/blocked_numbers_large_padding"
             android:text="@string/blocked_numbers_butter_bar_button"
             style="@style/BlockedNumbersButton"
             android:background="?android:attr/selectableItemBackgroundBorderless" />
diff --git a/src/com/android/server/telecom/AsyncRingtonePlayer.java b/src/com/android/server/telecom/AsyncRingtonePlayer.java
index 3fbac1f..912305b 100644
--- a/src/com/android/server/telecom/AsyncRingtonePlayer.java
+++ b/src/com/android/server/telecom/AsyncRingtonePlayer.java
@@ -30,6 +30,9 @@
 import com.android.internal.os.SomeArgs;
 import com.android.internal.util.Preconditions;
 
+import java.util.ArrayList;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
 import java.util.function.BiConsumer;
 import java.util.function.Supplier;
 
@@ -39,6 +42,9 @@
  */
 @VisibleForTesting
 public class AsyncRingtonePlayer {
+    // Maximum amount of time we will delay playing a ringtone while waiting for audio routing to
+    // be ready.
+    private static final int PLAY_DELAY_TIMEOUT_MS = 1000;
     // Message codes used with the ringtone thread.
     private static final int EVENT_PLAY = 1;
     private static final int EVENT_STOP = 2;
@@ -49,6 +55,23 @@
     /** The current ringtone. Only used by the ringtone thread. */
     private Ringtone mRingtone;
 
+    /**
+     * Set to true if we are setting up to play or are currently playing. False if we are stopping
+     * or have stopped playing.
+     */
+    private boolean mIsPlaying = false;
+
+    /**
+     * Set to true if BT HFP is active and audio is connected.
+     */
+    private boolean mIsBtActive = false;
+
+    /**
+     * A list of pending ringing ready latches, which are used to delay the ringing command until
+     * audio paths are set and ringing is ready.
+     */
+    private final ArrayList<CountDownLatch> mPendingRingingLatches = new ArrayList<>();
+
     public AsyncRingtonePlayer() {
         // Empty
     }
@@ -60,21 +83,81 @@
      *
      * @param ringtoneSupplier The {@link Ringtone} factory.
      * @param ringtoneConsumer The {@link Ringtone} post-creation callback (to start the vibration).
+     * @param isHfpDeviceConnected True if there is a HFP BT device connected, false otherwise.
      */
     public void play(@NonNull Supplier<Ringtone> ringtoneSupplier,
-            BiConsumer<Ringtone, Boolean> ringtoneConsumer) {
+            BiConsumer<Ringtone, Boolean> ringtoneConsumer,  boolean isHfpDeviceConnected) {
         Log.d(this, "Posting play.");
+        mIsPlaying = true;
         SomeArgs args = SomeArgs.obtain();
         args.arg1 = ringtoneSupplier;
         args.arg2 = ringtoneConsumer;
         args.arg3 = Log.createSubsession();
+        args.arg4 = prepareRingingReadyLatch(isHfpDeviceConnected);
         postMessage(EVENT_PLAY, true /* shouldCreateHandler */, args);
     }
 
     /** Stops playing the ringtone. */
     public void stop() {
         Log.d(this, "Posting stop.");
+        mIsPlaying = false;
         postMessage(EVENT_STOP, false /* shouldCreateHandler */, null);
+        // Clear any pending ringing latches so that we do not have to wait for its timeout to pass
+        // before calling stop.
+        clearPendingRingingLatches();
+    }
+
+    /**
+     * Called when the BT HFP profile active state changes.
+     * @param isBtActive A BT device is connected and audio is active.
+     */
+    public void updateBtActiveState(boolean isBtActive) {
+        Log.i(this, "updateBtActiveState: " + isBtActive);
+        synchronized (mPendingRingingLatches) {
+            mIsBtActive = isBtActive;
+            if (isBtActive) mPendingRingingLatches.forEach(CountDownLatch::countDown);
+        }
+    }
+
+    /**
+     * Prepares a new ringing ready latch and tracks it in a list. Once the ready latch has been
+     * used, {@link #removePendingRingingReadyLatch(CountDownLatch)} must be called on this latch.
+     * @param isHfpDeviceConnected true if there is a HFP device connected.
+     * @return the newly prepared CountDownLatch
+     */
+    private CountDownLatch prepareRingingReadyLatch(boolean isHfpDeviceConnected) {
+        CountDownLatch latch = new CountDownLatch(1);
+        synchronized (mPendingRingingLatches) {
+            // We only want to delay ringing if BT is connected but not active yet.
+            boolean isDelayRequired = isHfpDeviceConnected && !mIsBtActive;
+            Log.i(this, "prepareRingingReadyLatch:"
+                    + " connected=" + isHfpDeviceConnected
+                    + ", BT active=" + mIsBtActive
+                    + ", isDelayRequired=" + isDelayRequired);
+            if (!isDelayRequired) latch.countDown();
+            mPendingRingingLatches.add(latch);
+        }
+        return latch;
+    }
+
+    /**
+     * Remove a ringing ready latch that has been used and is no longer pending.
+     * @param l The latch to remove.
+     */
+    private void removePendingRingingReadyLatch(CountDownLatch l) {
+        synchronized (mPendingRingingLatches) {
+            mPendingRingingLatches.remove(l);
+        }
+    }
+
+    /**
+     * Count down all pending ringing ready latches and then clear the list.
+     */
+    private void clearPendingRingingLatches() {
+        synchronized (mPendingRingingLatches) {
+            mPendingRingingLatches.forEach(CountDownLatch::countDown);
+            mPendingRingingLatches.clear();
+        }
     }
 
     /**
@@ -129,6 +212,7 @@
         Supplier<Ringtone> ringtoneSupplier = (Supplier<Ringtone>) args.arg1;
         BiConsumer<Ringtone, Boolean> ringtoneConsumer = (BiConsumer<Ringtone, Boolean>) args.arg2;
         Session session = (Session) args.arg3;
+        CountDownLatch ringingReadyLatch = (CountDownLatch) args.arg4;
         args.recycle();
 
         Log.continueSession(session, "ARP.hP");
@@ -136,17 +220,29 @@
             // Don't bother with any of this if there is an EVENT_STOP waiting, but give the
             // consumer a chance to do anything no matter what.
             if (mHandler.hasMessages(EVENT_STOP)) {
+                Log.i(this, "handlePlay: skipping play early due to pending STOP");
+                removePendingRingingReadyLatch(ringingReadyLatch);
                 ringtoneConsumer.accept(null, /* stopped= */ true);
                 return;
             }
             Ringtone ringtone = null;
             boolean hasStopped = false;
             try {
+                try {
+                    Log.i(this, "handlePlay: delay ring for ready signal...");
+                    boolean reachedZero = ringingReadyLatch.await(PLAY_DELAY_TIMEOUT_MS,
+                            TimeUnit.MILLISECONDS);
+                    Log.i(this, "handlePlay: ringing ready, timeout=" + !reachedZero);
+                } catch (InterruptedException e) {
+                    Log.w(this, "handlePlay: latch exception: " + e);
+                }
                 ringtone = ringtoneSupplier.get();
-                // Ringtone supply can be slow. Re-check for stop event.
+                // Ringtone supply can be slow or stop command could have been issued while waiting
+                // for BT to move to CONNECTED state. Re-check for stop event.
                 if (mHandler.hasMessages(EVENT_STOP)) {
+                    Log.i(this, "handlePlay: skipping play due to pending STOP");
                     hasStopped = true;
-                    ringtone.stop();  // proactively release the ringtone.
+                    if (ringtone != null) ringtone.stop();  // proactively release the ringtone.
                     return;
                 }
                 // setRingtone even if null - it also stops any current ringtone to be consistent
@@ -168,6 +264,7 @@
                 mRingtone.play();
                 Log.i(this, "Play ringtone, looping.");
             } finally {
+                removePendingRingingReadyLatch(ringingReadyLatch);
                 ringtoneConsumer.accept(ringtone, hasStopped);
             }
         } finally {
@@ -196,11 +293,15 @@
         }
     }
 
+    /**
+     * @return true if we are currently preparing or playing a ringtone, false if we are not.
+     */
     public boolean isPlaying() {
-        return mRingtone != null;
+        return mIsPlaying;
     }
 
     private void setRingtone(@Nullable Ringtone ringtone) {
+        Log.i(this, "setRingtone: ringtone null="  + (ringtone == null));
         // Make sure that any previously created instance of Ringtone is stopped so the MediaPlayer
         // can be released, before replacing mRingtone with a new instance. This is always created
         // as a looping Ringtone, so if not stopped it will keep playing on the background.
diff --git a/src/com/android/server/telecom/Call.java b/src/com/android/server/telecom/Call.java
index dd8e7e8..d095522 100644
--- a/src/com/android/server/telecom/Call.java
+++ b/src/com/android/server/telecom/Call.java
@@ -17,7 +17,7 @@
 package com.android.server.telecom;
 
 import static android.provider.CallLog.Calls.MISSED_REASON_NOT_MISSED;
-import static android.telecom.Call.EVENT_DISPLAY_SOS_MESSAGE;
+import static android.telephony.TelephonyManager.EVENT_DISPLAY_SOS_MESSAGE;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -30,6 +30,7 @@
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.Looper;
+import android.os.OutcomeReceiver;
 import android.os.ParcelFileDescriptor;
 import android.os.Parcelable;
 import android.os.RemoteException;
@@ -43,6 +44,7 @@
 import android.telecom.CallAudioState;
 import android.telecom.CallDiagnosticService;
 import android.telecom.CallDiagnostics;
+import android.telecom.CallException;
 import android.telecom.CallerInfo;
 import android.telecom.Conference;
 import android.telecom.Connection;
@@ -73,6 +75,9 @@
 import com.android.server.telecom.stats.CallFailureCause;
 import com.android.server.telecom.stats.CallStateChangedAtomWriter;
 import com.android.server.telecom.ui.ToastFactory;
+import com.android.server.telecom.voip.TransactionManager;
+import com.android.server.telecom.voip.VerifyCallStateChangeTransaction;
+import com.android.server.telecom.voip.VoipCallTransactionResult;
 
 import java.io.IOException;
 import java.text.SimpleDateFormat;
@@ -118,6 +123,24 @@
 
     private static final char NO_DTMF_TONE = '\0';
 
+
+    /**
+     * Listener for CallState changes which can be leveraged by a Transaction.
+     */
+    public interface CallStateListener {
+        void onCallStateChanged(int newCallState);
+    }
+
+    public List<CallStateListener> mCallStateListeners = new ArrayList<>();
+
+    public void addCallStateListener(CallStateListener newListener) {
+        mCallStateListeners.add(newListener);
+    }
+
+    public boolean removeCallStateListener(CallStateListener newListener) {
+        return mCallStateListeners.remove(newListener);
+    }
+
     /**
      * Listener for events on the call.
      */
@@ -1328,6 +1351,10 @@
                 Log.addEvent(this, event, stringData);
             }
 
+            for (CallStateListener listener : mCallStateListeners) {
+                listener.onCallStateChanged(newState);
+            }
+
             mCallStateChangedAtomWriter
                     .setDisconnectCause(getDisconnectCause())
                     .setSelfManaged(isSelfManaged())
@@ -2898,11 +2925,16 @@
         hold(null /* reason */);
     }
 
+    /**
+     * This method requests the ConnectionService or TransactionalService hosting the call to put
+     * the call on hold
+     */
     public void hold(String reason) {
         if (mState == CallState.ACTIVE) {
             if (mTransactionalService != null) {
                 mTransactionalService.onSetInactive(this);
             } else if (mConnectionService != null) {
+                awaitCallStateChangeAndMaybeDisconnectCall(CallState.ON_HOLD, isSelfManaged(), "hold");
                 mConnectionService.hold(this);
             } else {
                 Log.e(this, new NullPointerException(),
@@ -2913,6 +2945,27 @@
     }
 
     /**
+     * helper that can be used for any callback that requests a call state change and wants to
+     * verify the change
+     */
+    public void awaitCallStateChangeAndMaybeDisconnectCall(int targetCallState,
+            boolean shouldDisconnectUponTimeout, String callingMethod) {
+        TransactionManager tm = TransactionManager.getInstance();
+        tm.addTransaction(new VerifyCallStateChangeTransaction(mCallsManager,
+                this, targetCallState, shouldDisconnectUponTimeout), new OutcomeReceiver<>() {
+            @Override
+            public void onResult(VoipCallTransactionResult result) {
+            }
+
+            @Override
+            public void onError(CallException e) {
+                Log.i(this, "awaitCallStateChangeAndMaybeDisconnectCall: %s: onError"
+                        + " due to CallException=[%s]", callingMethod, e);
+            }
+        });
+    }
+
+    /**
      * Releases the call from hold if it is currently active.
      */
     @VisibleForTesting
@@ -3663,7 +3716,8 @@
         }
 
         String newName = callerInfo.getName();
-        boolean contactNameChanged = mCallerInfo == null || !mCallerInfo.getName().equals(newName);
+        boolean contactNameChanged = mCallerInfo == null ||
+                !Objects.equals(mCallerInfo.getName(), newName);
 
         mCallerInfo = callerInfo;
         Log.i(this, "CallerInfo received for %s: %s", Log.piiHandle(mHandle), callerInfo);
@@ -4504,6 +4558,7 @@
     }
 
     public void setStartFailCause(CallFailureCause cause) {
+        Log.i(this, "setStartFailCause: cause = %s; callId = %s", cause, this.getId());
         mCallStateChangedAtomWriter.setStartFailCause(cause);
     }
 
diff --git a/src/com/android/server/telecom/CallAudioCommunicationDeviceTracker.java b/src/com/android/server/telecom/CallAudioCommunicationDeviceTracker.java
new file mode 100644
index 0000000..43624a3
--- /dev/null
+++ b/src/com/android/server/telecom/CallAudioCommunicationDeviceTracker.java
@@ -0,0 +1,200 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.telecom;
+
+import android.bluetooth.BluetoothDevice;
+import android.content.Context;
+import android.media.AudioDeviceInfo;
+import android.media.AudioManager;
+import android.telecom.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.telecom.bluetooth.BluetoothRouteManager;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Helper class used to keep track of the requested communication device within Telecom for audio
+ * use cases. Handles the set/clear communication use case logic for all audio routes (speaker, BT,
+ * headset, and earpiece). For BT devices, this handles switches between hearing aids, SCO, and LE
+ * audio (also takes into account switching between multiple LE audio devices).
+ */
+public class CallAudioCommunicationDeviceTracker {
+
+    // Use -1 indicates device is not set for any communication use case
+    private static final int sAUDIO_DEVICE_TYPE_INVALID = -1;
+    // Possible bluetooth audio device types
+    private static final Set<Integer> sBT_AUDIO_DEVICE_TYPES = Set.of(
+            AudioDeviceInfo.TYPE_BLE_HEADSET,
+            AudioDeviceInfo.TYPE_HEARING_AID,
+            AudioDeviceInfo.TYPE_BLUETOOTH_SCO
+    );
+    private AudioManager mAudioManager;
+    private BluetoothRouteManager mBluetoothRouteManager;
+    private int mAudioDeviceType = sAUDIO_DEVICE_TYPE_INVALID;
+    // Keep track of the locally requested BT audio device if set
+    private String mBtAudioDevice = null;
+
+    public CallAudioCommunicationDeviceTracker(Context context) {
+        mAudioManager = context.getSystemService(AudioManager.class);
+    }
+
+    public void setBluetoothRouteManager(BluetoothRouteManager bluetoothRouteManager) {
+        mBluetoothRouteManager = bluetoothRouteManager;
+    }
+
+    public boolean isAudioDeviceSetForType(int audioDeviceType) {
+        return mAudioDeviceType == audioDeviceType;
+    }
+
+    @VisibleForTesting
+    public void setTestCommunicationDevice(int audioDeviceType) {
+        mAudioDeviceType = audioDeviceType;
+    }
+
+    public void clearBtCommunicationDevice() {
+        if (mBtAudioDevice == null) {
+            Log.i(this, "No bluetooth device was set for communication that can be cleared.");
+            return;
+        }
+        // If mBtAudioDevice is set, we know a BT audio device was set for communication so
+        // mAudioDeviceType corresponds to a BT device type (e.g. hearing aid, SCO, LE).
+        clearCommunicationDevice(mAudioDeviceType);
+    }
+
+    /*
+     * Sets the communication device for the passed in audio device type, if it's available for
+     * communication use cases. Tries to clear any communication device which was previously
+     * requested for communication before setting the new device.
+     * @param audioDeviceTypes The supported audio device types for the device.
+     * @param btDevice The bluetooth device to connect to (only used for switching between multiple
+     *        LE audio devices).
+     * @return {@code true} if the device was set for communication, {@code false} if the device
+     * wasn't set.
+     */
+    public boolean setCommunicationDevice(int audioDeviceType,
+            BluetoothDevice btDevice) {
+        // There is only one audio device type associated with each type of BT device.
+        boolean isBtDevice = sBT_AUDIO_DEVICE_TYPES.contains(audioDeviceType);
+        Log.i(this, "setCommunicationDevice: type = %s, isBtDevice = %s, btDevice = %s",
+                audioDeviceType, isBtDevice, btDevice);
+
+        // Account for switching between multiple LE audio devices.
+        boolean handleLeAudioDeviceSwitch = btDevice != null
+                && !btDevice.getAddress().equals(mBtAudioDevice);
+        if ((audioDeviceType == mAudioDeviceType
+                || isUsbHeadsetType(audioDeviceType, mAudioDeviceType))
+                && !handleLeAudioDeviceSwitch) {
+            Log.i(this, "Communication device is already set for this audio type");
+            return false;
+        }
+
+        AudioDeviceInfo activeDevice = null;
+        List<AudioDeviceInfo> devices = mAudioManager.getAvailableCommunicationDevices();
+        if (devices.size() == 0) {
+            Log.w(this, "No communication devices available");
+            return false;
+        }
+
+        for (AudioDeviceInfo device : devices) {
+            Log.i(this, "Available device type: " + device.getType());
+            // Ensure that we do not select the same BT LE audio device for communication.
+            if ((audioDeviceType == device.getType()
+                    || isUsbHeadsetType(audioDeviceType, device.getType()))
+                    && !device.getAddress().equals(mBtAudioDevice)) {
+                activeDevice = device;
+                break;
+            }
+        }
+
+        if (activeDevice == null) {
+            Log.i(this, "No active device of type(s) %s available",
+                    audioDeviceType == AudioDeviceInfo.TYPE_WIRED_HEADSET
+                            ? Arrays.asList(AudioDeviceInfo.TYPE_WIRED_HEADSET,
+                                    AudioDeviceInfo.TYPE_USB_HEADSET)
+                            : audioDeviceType);
+            return false;
+        }
+
+        // Force clear previous communication device, if one was set, before setting the new device.
+        if (mAudioDeviceType != sAUDIO_DEVICE_TYPE_INVALID) {
+            clearCommunicationDevice(mAudioDeviceType);
+        }
+
+        // Turn activeDevice ON.
+        boolean result = mAudioManager.setCommunicationDevice(activeDevice);
+        if (!result) {
+            Log.w(this, "Could not set active device");
+        } else {
+            Log.i(this, "Active device set");
+            mAudioDeviceType = activeDevice.getType();
+            if (isBtDevice) {
+                mBtAudioDevice = activeDevice.getAddress();
+                if (audioDeviceType == AudioDeviceInfo.TYPE_BLE_HEADSET) {
+                    mBluetoothRouteManager.onAudioOn(mBtAudioDevice);
+                }
+            }
+        }
+        return result;
+    }
+
+    /*
+     * Clears the communication device for the passed in audio device types, given that the device
+     * has previously been set for communication.
+     * @param audioDeviceTypes The supported audio device types for the device.
+     */
+    public void clearCommunicationDevice(int audioDeviceType) {
+        // There is only one audio device type associated with each type of BT device.
+        boolean isBtDevice = sBT_AUDIO_DEVICE_TYPES.contains(audioDeviceType);
+        Log.i(this, "clearCommunicationDevice: type = %s, isBtDevice = %s",
+                audioDeviceType, isBtDevice);
+
+        if (audioDeviceType != mAudioDeviceType
+                && !isUsbHeadsetType(audioDeviceType, mAudioDeviceType)) {
+            Log.i(this, "Unable to clear communication device of type(s), %s. "
+                    + "Device does not correspond to the locally requested device type.",
+                    audioDeviceType == AudioDeviceInfo.TYPE_WIRED_HEADSET
+                            ? Arrays.asList(AudioDeviceInfo.TYPE_WIRED_HEADSET,
+                                    AudioDeviceInfo.TYPE_USB_HEADSET)
+                            : audioDeviceType
+            );
+            return;
+        }
+
+        if (isBtDevice && mBtAudioDevice != null) {
+            // Signal that BT audio was lost for device.
+            mBluetoothRouteManager.onAudioLost(mBtAudioDevice);
+            mBtAudioDevice = null;
+        }
+
+        if (mAudioManager == null) {
+            Log.i(this, "clearCommunicationDevice: mAudioManager is null");
+            return;
+        }
+
+        // Clear device and reset locally saved device type.
+        mAudioManager.clearCommunicationDevice();
+        mAudioDeviceType = sAUDIO_DEVICE_TYPE_INVALID;
+    }
+
+    private boolean isUsbHeadsetType(int audioDeviceType, int sourceType) {
+        return audioDeviceType != AudioDeviceInfo.TYPE_WIRED_HEADSET
+                ? false : sourceType == AudioDeviceInfo.TYPE_USB_HEADSET;
+    }
+}
diff --git a/src/com/android/server/telecom/CallAudioManager.java b/src/com/android/server/telecom/CallAudioManager.java
index ff76b9e..6557dc6 100644
--- a/src/com/android/server/telecom/CallAudioManager.java
+++ b/src/com/android/server/telecom/CallAudioManager.java
@@ -36,6 +36,8 @@
 import java.util.HashSet;
 import java.util.Set;
 import java.util.LinkedHashSet;
+import java.util.stream.Collectors;
+
 
 public class CallAudioManager extends CallsManagerListenerBase {
 
@@ -116,7 +118,7 @@
             // State did not change, so no need to do anything.
             return;
         }
-        Log.d(LOG_TAG, "Call state changed for TC@%s: %s -> %s", call.getId(),
+        Log.i(this, "onCallStateChanged: Call state changed for TC@%s: %s -> %s", call.getId(),
                 CallState.toString(oldState), CallState.toString(newState));
 
         removeCallFromAllBins(call);
@@ -761,6 +763,7 @@
 
     private void updateForegroundCall() {
         Call oldForegroundCall = mForegroundCall;
+
         if (mActiveDialingOrConnectingCalls.size() > 0) {
             // Give preference for connecting calls over active/dialing for foreground-ness.
             Call possibleConnectingCall = null;
@@ -769,8 +772,21 @@
                     possibleConnectingCall = call;
                 }
             }
-            mForegroundCall = possibleConnectingCall == null ?
-                    mActiveDialingOrConnectingCalls.iterator().next() : possibleConnectingCall;
+            // Prefer a connecting call
+            if (possibleConnectingCall != null) {
+                mForegroundCall = possibleConnectingCall;
+            } else {
+                // Next, prefer an active or dialing call which is not in the process of being
+                // disconnected.
+                mForegroundCall = mActiveDialingOrConnectingCalls
+                        .stream()
+                        .filter(c -> (c.getState() == CallState.ACTIVE
+                                || c.getState() == CallState.DIALING)
+                                && !c.isLocallyDisconnecting())
+                        .findFirst()
+                        // If we can't find one, then just fall back to the first one.
+                        .orElse(mActiveDialingOrConnectingCalls.iterator().next());
+            }
         } else if (mRingingCalls.size() > 0) {
             mForegroundCall = mRingingCalls.iterator().next();
         } else if (mHoldingCalls.size() > 0) {
@@ -778,10 +794,24 @@
         } else {
             mForegroundCall = null;
         }
-
+        Log.i(this, "updateForegroundCall; oldFg=%s, newFg=%s, aDC=%s, ring=%s, hold=%s",
+                (oldForegroundCall == null ? "none" : oldForegroundCall.getId()),
+                (mForegroundCall == null ? "none" : mForegroundCall.getId()),
+                mActiveDialingOrConnectingCalls.stream().map(c -> c.getId()).collect(
+                        Collectors.joining(",")),
+                mRingingCalls.stream().map(c -> c.getId()).collect(Collectors.joining(",")),
+                mHoldingCalls.stream().map(c -> c.getId()).collect(Collectors.joining(","))
+        );
         if (mForegroundCall != oldForegroundCall) {
             mCallAudioRouteStateMachine.sendMessageWithSessionInfo(
                     CallAudioRouteStateMachine.UPDATE_SYSTEM_AUDIO_ROUTE);
+
+            if (mForegroundCall != null) {
+                // Ensure the voip audio mode for the new foreground call is taken into account.
+                mCallAudioModeStateMachine.sendMessageWithArgs(
+                        CallAudioModeStateMachine.FOREGROUND_VOIP_MODE_CHANGE,
+                        makeArgsForModeStateMachine());
+            }
             mDtmfLocalTonePlayer.onForegroundCallChanged(oldForegroundCall, mForegroundCall);
             maybePlayHoldTone();
         }
diff --git a/src/com/android/server/telecom/CallAudioModeStateMachine.java b/src/com/android/server/telecom/CallAudioModeStateMachine.java
index 9ad9094..6831770 100644
--- a/src/com/android/server/telecom/CallAudioModeStateMachine.java
+++ b/src/com/android/server/telecom/CallAudioModeStateMachine.java
@@ -16,6 +16,8 @@
 
 package com.android.server.telecom;
 
+import android.media.AudioAttributes;
+import android.media.AudioFocusRequest;
 import android.media.AudioManager;
 import android.os.Looper;
 import android.os.Message;
@@ -29,6 +31,7 @@
 import com.android.internal.util.IndentingPrintWriter;
 import com.android.internal.util.State;
 import com.android.internal.util.StateMachine;
+import com.android.server.telecom.flags.FeatureFlags;
 
 public class CallAudioModeStateMachine extends StateMachine {
     /**
@@ -38,11 +41,27 @@
     private LocalLog mLocalLog = new LocalLog(20);
     public static class Factory {
         public CallAudioModeStateMachine create(SystemStateHelper systemStateHelper,
-                AudioManager am) {
-            return new CallAudioModeStateMachine(systemStateHelper, am);
+                                                AudioManager am, FeatureFlags featureFlags) {
+            return new CallAudioModeStateMachine(systemStateHelper, am, featureFlags);
         }
     }
 
+    private static final AudioAttributes RING_AUDIO_ATTRIBUTES = new AudioAttributes.Builder()
+            .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
+            .setLegacyStreamType(AudioManager.STREAM_RING)
+            .build();
+    public static final AudioFocusRequest RING_AUDIO_FOCUS_REQUEST = new AudioFocusRequest
+            .Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT)
+            .setAudioAttributes(RING_AUDIO_ATTRIBUTES).build();
+
+    private static final AudioAttributes CALL_AUDIO_ATTRIBUTES = new AudioAttributes.Builder()
+            .setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION)
+            .setLegacyStreamType(AudioManager.STREAM_VOICE_CALL)
+            .build();
+    public static final AudioFocusRequest CALL_AUDIO_FOCUS_REQUEST = new AudioFocusRequest
+            .Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT)
+            .setAudioAttributes(CALL_AUDIO_ATTRIBUTES).build();
+
     public static class MessageArgs {
         public boolean hasActiveOrDialingCalls;
         public boolean hasRingingCalls;
@@ -212,6 +231,8 @@
     public static final String STREAMING_STATE_NAME = StreamingFocusState.class.getSimpleName();
     public static final String COMMS_STATE_NAME = VoipCallFocusState.class.getSimpleName();
 
+    private AudioFocusRequest mCurrentAudioFocusRequest = null;
+
     private class BaseState extends State {
         @Override
         public boolean processMessage(Message msg) {
@@ -310,7 +331,14 @@
                     return HANDLED;
                 case AUDIO_OPERATIONS_COMPLETE:
                     Log.i(LOG_TAG, "Abandoning audio focus: now UNFOCUSED");
-                    mAudioManager.abandonAudioFocusForCall();
+                    if (mFeatureFlags.telecomResolveHiddenDependencies()) {
+                        if (mCurrentAudioFocusRequest != null) {
+                            mAudioManager.abandonAudioFocusRequest(mCurrentAudioFocusRequest);
+                            mCurrentAudioFocusRequest = null;
+                        }
+                    } else {
+                        mAudioManager.abandonAudioFocusForCall();
+                    }
                     return HANDLED;
                 default:
                     // The forced focus switch commands are handled by BaseState.
@@ -381,7 +409,14 @@
                     return HANDLED;
                 case AUDIO_OPERATIONS_COMPLETE:
                     Log.i(LOG_TAG, "Abandoning audio focus: now AUDIO_PROCESSING");
-                    mAudioManager.abandonAudioFocusForCall();
+                    if (mFeatureFlags.telecomResolveHiddenDependencies()) {
+                        if (mCurrentAudioFocusRequest != null) {
+                            mAudioManager.abandonAudioFocusRequest(mCurrentAudioFocusRequest);
+                            mCurrentAudioFocusRequest = null;
+                        }
+                    } else {
+                        mAudioManager.abandonAudioFocusForCall();
+                    }
                     return HANDLED;
                 default:
                     // The forced focus switch commands are handled by BaseState.
@@ -406,8 +441,13 @@
                 }
 
                 if (mCallAudioManager.startRinging()) {
-                    mAudioManager.requestAudioFocusForCall(
-                        AudioManager.STREAM_RING, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
+                    if (mFeatureFlags.telecomResolveHiddenDependencies()) {
+                        mCurrentAudioFocusRequest = RING_AUDIO_FOCUS_REQUEST;
+                        mAudioManager.requestAudioFocus(RING_AUDIO_FOCUS_REQUEST);
+                    } else {
+                        mAudioManager.requestAudioFocusForCall(
+                                AudioManager.STREAM_RING, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
+                    }
                     // Do not set MODE_RINGTONE if we were previously in the CALL_SCREENING mode --
                     // this trips up the audio system.
                     if (mAudioManager.getMode() != AudioManager.MODE_CALL_SCREENING) {
@@ -504,8 +544,13 @@
         public void enter() {
             Log.i(LOG_TAG, "Audio focus entering SIM CALL state");
             mLocalLog.log("Enter SIM_CALL");
-            mAudioManager.requestAudioFocusForCall(AudioManager.STREAM_VOICE_CALL,
-                    AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
+            if (mFeatureFlags.telecomResolveHiddenDependencies()) {
+                mCurrentAudioFocusRequest = CALL_AUDIO_FOCUS_REQUEST;
+                mAudioManager.requestAudioFocus(CALL_AUDIO_FOCUS_REQUEST);
+            } else {
+                mAudioManager.requestAudioFocusForCall(AudioManager.STREAM_VOICE_CALL,
+                        AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
+            }
             mAudioManager.setMode(AudioManager.MODE_IN_CALL);
             mLocalLog.log("Mode MODE_IN_CALL");
             mMostRecentMode = AudioManager.MODE_IN_CALL;
@@ -587,8 +632,13 @@
         public void enter() {
             Log.i(LOG_TAG, "Audio focus entering VOIP CALL state");
             mLocalLog.log("Enter VOIP_CALL");
-            mAudioManager.requestAudioFocusForCall(AudioManager.STREAM_VOICE_CALL,
-                    AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
+            if (mFeatureFlags.telecomResolveHiddenDependencies()) {
+                mCurrentAudioFocusRequest = CALL_AUDIO_FOCUS_REQUEST;
+                mAudioManager.requestAudioFocus(CALL_AUDIO_FOCUS_REQUEST);
+            } else {
+                mAudioManager.requestAudioFocusForCall(AudioManager.STREAM_VOICE_CALL,
+                        AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
+            }
             mAudioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
             mLocalLog.log("Mode MODE_IN_COMMUNICATION");
             mMostRecentMode = AudioManager.MODE_IN_COMMUNICATION;
@@ -742,8 +792,13 @@
         public void enter() {
             Log.i(LOG_TAG, "Audio focus entering TONE/HOLDING state");
             mLocalLog.log("Enter TONE/HOLDING");
-            mAudioManager.requestAudioFocusForCall(AudioManager.STREAM_VOICE_CALL,
-                    AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
+            if (mFeatureFlags.telecomResolveHiddenDependencies()) {
+                mCurrentAudioFocusRequest = CALL_AUDIO_FOCUS_REQUEST;
+                mAudioManager.requestAudioFocus(CALL_AUDIO_FOCUS_REQUEST);
+            } else {
+                mAudioManager.requestAudioFocusForCall(AudioManager.STREAM_VOICE_CALL,
+                        AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
+            }
             mAudioManager.setMode(mMostRecentMode);
             mLocalLog.log("Mode " + mMostRecentMode);
             mCallAudioManager.setCallAudioRouteFocusState(CallAudioRouteStateMachine.ACTIVE_FOCUS);
@@ -815,16 +870,18 @@
     private final AudioManager mAudioManager;
     private final SystemStateHelper mSystemStateHelper;
     private CallAudioManager mCallAudioManager;
+    private FeatureFlags mFeatureFlags;
 
     private int mMostRecentMode;
     private boolean mIsInitialized = false;
 
     public CallAudioModeStateMachine(SystemStateHelper systemStateHelper,
-            AudioManager audioManager) {
+            AudioManager audioManager, FeatureFlags featureFlags) {
         super(CallAudioModeStateMachine.class.getSimpleName());
         mAudioManager = audioManager;
         mSystemStateHelper = systemStateHelper;
         mMostRecentMode = AudioManager.MODE_NORMAL;
+        mFeatureFlags = featureFlags;
 
         createStates();
     }
@@ -833,11 +890,12 @@
      * Used for testing
      */
     public CallAudioModeStateMachine(SystemStateHelper systemStateHelper,
-            AudioManager audioManager, Looper looper) {
+            AudioManager audioManager, Looper looper, FeatureFlags featureFlags) {
         super(CallAudioModeStateMachine.class.getSimpleName(), looper);
         mAudioManager = audioManager;
         mSystemStateHelper = systemStateHelper;
         mMostRecentMode = AudioManager.MODE_NORMAL;
+        mFeatureFlags = featureFlags;
 
         createStates();
     }
diff --git a/src/com/android/server/telecom/CallAudioRoutePeripheralAdapter.java b/src/com/android/server/telecom/CallAudioRoutePeripheralAdapter.java
index d96f953..af0757c 100644
--- a/src/com/android/server/telecom/CallAudioRoutePeripheralAdapter.java
+++ b/src/com/android/server/telecom/CallAudioRoutePeripheralAdapter.java
@@ -27,14 +27,17 @@
 
     private final CallAudioRouteStateMachine mCallAudioRouteStateMachine;
     private final BluetoothRouteManager mBluetoothRouteManager;
+    private final AsyncRingtonePlayer mRingtonePlayer;
 
     public CallAudioRoutePeripheralAdapter(
             CallAudioRouteStateMachine callAudioRouteStateMachine,
             BluetoothRouteManager bluetoothManager,
             WiredHeadsetManager wiredHeadsetManager,
-            DockManager dockManager) {
+            DockManager dockManager,
+            AsyncRingtonePlayer ringtonePlayer) {
         mCallAudioRouteStateMachine = callAudioRouteStateMachine;
         mBluetoothRouteManager = bluetoothManager;
+        mRingtonePlayer = ringtonePlayer;
 
         mBluetoothRouteManager.setListener(this);
         wiredHeadsetManager.addListener(this);
@@ -75,12 +78,22 @@
 
     @Override
     public void onBluetoothAudioConnected() {
+        mRingtonePlayer.updateBtActiveState(true);
+        mCallAudioRouteStateMachine.sendMessageWithSessionInfo(
+                CallAudioRouteStateMachine.BT_AUDIO_CONNECTED);
+    }
+
+    @Override
+    public void onBluetoothAudioConnecting() {
+        mRingtonePlayer.updateBtActiveState(false);
+        // Pretend like audio is connected when communicating w/ CARSM.
         mCallAudioRouteStateMachine.sendMessageWithSessionInfo(
                 CallAudioRouteStateMachine.BT_AUDIO_CONNECTED);
     }
 
     @Override
     public void onBluetoothAudioDisconnected() {
+        mRingtonePlayer.updateBtActiveState(false);
         mCallAudioRouteStateMachine.sendMessageWithSessionInfo(
                 CallAudioRouteStateMachine.BT_AUDIO_DISCONNECTED);
     }
diff --git a/src/com/android/server/telecom/CallAudioRouteStateMachine.java b/src/com/android/server/telecom/CallAudioRouteStateMachine.java
index 4a03726..217b553 100644
--- a/src/com/android/server/telecom/CallAudioRouteStateMachine.java
+++ b/src/com/android/server/telecom/CallAudioRouteStateMachine.java
@@ -23,6 +23,7 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
+import android.content.pm.PackageManager;
 import android.content.pm.UserInfo;
 import android.media.AudioDeviceInfo;
 import android.media.AudioManager;
@@ -83,7 +84,8 @@
                 StatusBarNotifier statusBarNotifier,
                 CallAudioManager.AudioServiceFactory audioServiceFactory,
                 int earpieceControl,
-                Executor asyncTaskExecutor) {
+                Executor asyncTaskExecutor,
+                CallAudioCommunicationDeviceTracker communicationDeviceTracker) {
             return new CallAudioRouteStateMachine(context,
                     callsManager,
                     bluetoothManager,
@@ -91,7 +93,8 @@
                     statusBarNotifier,
                     audioServiceFactory,
                     earpieceControl,
-                    asyncTaskExecutor);
+                    asyncTaskExecutor,
+                    communicationDeviceTracker);
         }
     }
     /** Values for CallAudioRouteStateMachine constructor's earPieceRouting arg. */
@@ -371,6 +374,8 @@
         public void enter() {
             super.enter();
             setSpeakerphoneOn(false);
+            mCommunicationDeviceTracker.setCommunicationDevice(
+                    AudioDeviceInfo.TYPE_BUILTIN_EARPIECE, null);
             CallAudioState newState = new CallAudioState(mIsMuted, ROUTE_EARPIECE,
                     mAvailableRoutes, null,
                     mBluetoothRouteManager.getConnectedDevices());
@@ -401,6 +406,8 @@
                 case SWITCH_BLUETOOTH:
                 case USER_SWITCH_BLUETOOTH:
                     if ((mAvailableRoutes & ROUTE_BLUETOOTH) != 0) {
+                        mCommunicationDeviceTracker.clearCommunicationDevice(
+                                AudioDeviceInfo.TYPE_BUILTIN_EARPIECE);
                         if (mAudioFocusType == ACTIVE_FOCUS
                                 || mBluetoothRouteManager.isInbandRingingEnabled()) {
                             String address = (msg.obj instanceof SomeArgs) ?
@@ -417,6 +424,8 @@
                 case SWITCH_HEADSET:
                 case USER_SWITCH_HEADSET:
                     if ((mAvailableRoutes & ROUTE_WIRED_HEADSET) != 0) {
+                        mCommunicationDeviceTracker.clearCommunicationDevice(
+                                AudioDeviceInfo.TYPE_BUILTIN_EARPIECE);
                         transitionTo(mActiveHeadsetRoute);
                     } else {
                         Log.w(this, "Ignoring switch to headset command. Not available.");
@@ -426,6 +435,8 @@
                     // fall through; we want to switch to speaker mode when docked and in a call.
                 case SWITCH_SPEAKER:
                 case USER_SWITCH_SPEAKER:
+                    mCommunicationDeviceTracker.clearCommunicationDevice(
+                            AudioDeviceInfo.TYPE_BUILTIN_EARPIECE);
                     setSpeakerphoneOn(true);
                     // fall through
                 case SPEAKER_ON:
@@ -579,6 +590,8 @@
         public void enter() {
             super.enter();
             setSpeakerphoneOn(false);
+            mCommunicationDeviceTracker.setCommunicationDevice(
+                    AudioDeviceInfo.TYPE_WIRED_HEADSET, null);
             CallAudioState newState = new CallAudioState(mIsMuted, ROUTE_WIRED_HEADSET,
                     mAvailableRoutes, null, mBluetoothRouteManager.getConnectedDevices());
             setSystemAudioState(newState, true);
@@ -600,6 +613,8 @@
                 case SWITCH_EARPIECE:
                 case USER_SWITCH_EARPIECE:
                     if ((mAvailableRoutes & ROUTE_EARPIECE) != 0) {
+                        mCommunicationDeviceTracker.clearCommunicationDevice(
+                                AudioDeviceInfo.TYPE_WIRED_HEADSET);
                         transitionTo(mActiveEarpieceRoute);
                     } else {
                         Log.w(this, "Ignoring switch to earpiece command. Not available.");
@@ -615,6 +630,8 @@
                                 || mBluetoothRouteManager.isInbandRingingEnabled()) {
                             String address = (msg.obj instanceof SomeArgs) ?
                                     (String) ((SomeArgs) msg.obj).arg2 : null;
+                            mCommunicationDeviceTracker.clearCommunicationDevice(
+                                    AudioDeviceInfo.TYPE_WIRED_HEADSET);
                             // Omit transition to ActiveBluetoothRoute until actual connection.
                             setBluetoothOn(address);
                         } else {
@@ -631,6 +648,8 @@
                     return HANDLED;
                 case SWITCH_SPEAKER:
                 case USER_SWITCH_SPEAKER:
+                    mCommunicationDeviceTracker.clearCommunicationDevice(
+                            AudioDeviceInfo.TYPE_WIRED_HEADSET);
                     setSpeakerphoneOn(true);
                     // fall through
                 case SPEAKER_ON:
@@ -793,6 +812,12 @@
         public void enter() {
             super.enter();
             setSpeakerphoneOn(false);
+            // Try arbitrarily connecting to BT audio if we haven't already. This handles
+            // the edge case of when the audio route is in a quiescent route while in-call and
+            // the BT connection fails to be set. Previously, the logic was to setBluetoothOn in
+            // ACTIVE_FOCUS but the route would still remain in a quiescent route, so instead we
+            // should be transitioning directly into the active route.
+            setBluetoothOn(null);
             CallAudioState newState = new CallAudioState(mIsMuted, ROUTE_BLUETOOTH,
                     mAvailableRoutes, mBluetoothRouteManager.getBluetoothAudioConnectedDevice(),
                     mBluetoothRouteManager.getConnectedDevices());
@@ -894,7 +919,7 @@
                     if (msg.arg1 == NO_FOCUS) {
                         // Only disconnect audio here instead of routing away from BT entirely.
                         mBluetoothRouteManager.disconnectAudio();
-                        reinitialize();
+                        transitionTo(mQuiescentBluetoothRoute);
                         mCallAudioManager.notifyAudioOperationsComplete();
                     } else if (msg.arg1 == RINGING_FOCUS
                             && !mBluetoothRouteManager.isInbandRingingEnabled()) {
@@ -1065,7 +1090,9 @@
                     return HANDLED;
                 case SWITCH_FOCUS:
                     if (msg.arg1 == ACTIVE_FOCUS) {
-                        setBluetoothOn(null);
+                        // It is possible that the connection to BT will fail while in-call, in
+                        // which case, we want to transition into the active route.
+                        transitionTo(mActiveBluetoothRoute);
                     } else if (msg.arg1 == RINGING_FOCUS) {
                         if (mBluetoothRouteManager.isInbandRingingEnabled()) {
                             setBluetoothOn(null);
@@ -1215,7 +1242,13 @@
                     // Expected, since we just transitioned here
                     return HANDLED;
                 case SPEAKER_OFF:
-                    sendInternalMessage(SWITCH_BASELINE_ROUTE, INCLUDE_BLUETOOTH_IN_BASELINE);
+                    // Check if we already requested to connect to other devices and just waiting
+                    // for their response. In some cases, this SPEAKER_OFF message may come in
+                    // before the response, we can just ignore the message here to not re-evaluate
+                    // the baseline route incorrectly
+                    if (!mBluetoothRouteManager.isBluetoothAudioConnectedOrPending()) {
+                        sendInternalMessage(SWITCH_BASELINE_ROUTE, INCLUDE_BLUETOOTH_IN_BASELINE);
+                    }
                     return HANDLED;
                 case SWITCH_FOCUS:
                     if (msg.arg1 == NO_FOCUS) {
@@ -1514,6 +1547,7 @@
     private CallAudioState mLastKnownCallAudioState;
 
     private CallAudioManager mCallAudioManager;
+    private CallAudioCommunicationDeviceTracker mCommunicationDeviceTracker;
 
     public CallAudioRouteStateMachine(
             Context context,
@@ -1523,7 +1557,8 @@
             StatusBarNotifier statusBarNotifier,
             CallAudioManager.AudioServiceFactory audioServiceFactory,
             int earpieceControl,
-            Executor asyncTaskExecutor) {
+            Executor asyncTaskExecutor,
+            CallAudioCommunicationDeviceTracker communicationDeviceTracker) {
         super(NAME);
         mContext = context;
         mCallsManager = callsManager;
@@ -1534,6 +1569,7 @@
         mAudioServiceFactory = audioServiceFactory;
         mLock = callsManager.getLock();
         mAsyncTaskExecutor = asyncTaskExecutor;
+        mCommunicationDeviceTracker = communicationDeviceTracker;
         createStates(earpieceControl);
     }
 
@@ -1545,7 +1581,8 @@
             WiredHeadsetManager wiredHeadsetManager,
             StatusBarNotifier statusBarNotifier,
             CallAudioManager.AudioServiceFactory audioServiceFactory,
-            int earpieceControl, Looper looper, Executor asyncTaskExecutor) {
+            int earpieceControl, Looper looper, Executor asyncTaskExecutor,
+            CallAudioCommunicationDeviceTracker communicationDeviceTracker) {
         super(NAME, looper);
         mContext = context;
         mCallsManager = callsManager;
@@ -1556,6 +1593,7 @@
         mAudioServiceFactory = audioServiceFactory;
         mLock = callsManager.getLock();
         mAsyncTaskExecutor = asyncTaskExecutor;
+        mCommunicationDeviceTracker = communicationDeviceTracker;
 
         createStates(earpieceControl);
     }
@@ -1702,9 +1740,13 @@
                 }
                 return;
             case UPDATE_SYSTEM_AUDIO_ROUTE:
-                updateInternalCallAudioState();
+                // Ensure available routes is updated.
                 updateRouteForForegroundCall();
-                resendSystemAudioState();
+                // Ensure current audio state gets updated to take this into account.
+                updateInternalCallAudioState();
+                // Either resend the current audio state as it stands, or update to reflect any
+                // changes put into place based on mAvailableRoutes
+                setSystemAudioState(mCurrentCallAudioState, true);
                 return;
             case RUN_RUNNABLE:
                 java.lang.Runnable r = (java.lang.Runnable) msg.obj;
@@ -1741,31 +1783,15 @@
         final boolean hasAnyCalls = mCallsManager.hasAnyCalls();
         // These APIs are all via two-way binder calls so can potentially block Telecom.  Since none
         // of this has to happen in the Telecom lock we'll offload it to the async executor.
-
-        AudioDeviceInfo speakerDevice = null;
-        for (AudioDeviceInfo info : mAudioManager.getAvailableCommunicationDevices()) {
-            if (info.getType() == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER) {
-                speakerDevice = info;
-                break;
-            }
-        }
         boolean speakerOn = false;
-        if (speakerDevice != null && on) {
-            boolean result = mAudioManager.setCommunicationDevice(speakerDevice);
-            if (result) {
-                speakerOn = true;
-            }
+        if (on) {
+            speakerOn = mCommunicationDeviceTracker.setCommunicationDevice(
+                    AudioDeviceInfo.TYPE_BUILTIN_SPEAKER, null);
         } else {
-            AudioDeviceInfo curDevice = mAudioManager.getCommunicationDevice();
-            if (curDevice != null
-                    && curDevice.getType() == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER) {
-                mAudioManager.clearCommunicationDevice();
-            }
+            mCommunicationDeviceTracker.clearCommunicationDevice(
+                    AudioDeviceInfo.TYPE_BUILTIN_SPEAKER);
         }
-        final boolean isSpeakerOn = speakerOn;
-        mAsyncTaskExecutor.execute(() -> {
-            mStatusBarNotifier.notifySpeakerphone(hasAnyCalls && isSpeakerOn);
-        });
+        mStatusBarNotifier.notifySpeakerphone(hasAnyCalls && speakerOn);
     }
 
     private void setBluetoothOn(String address) {
@@ -1863,6 +1889,11 @@
         setSystemAudioState(mLastKnownCallAudioState, true);
     }
 
+    @VisibleForTesting
+    public CallAudioState getLastKnownCallAudioState() {
+        return mLastKnownCallAudioState;
+    }
+
     private void setSystemAudioState(CallAudioState newCallAudioState, boolean force) {
         synchronized (mLock) {
             Log.i(this, "setSystemAudioState: changing from %s to %s", mLastKnownCallAudioState,
diff --git a/src/com/android/server/telecom/CallEndpointController.java b/src/com/android/server/telecom/CallEndpointController.java
index 7e11b47..4738cd4 100644
--- a/src/com/android/server/telecom/CallEndpointController.java
+++ b/src/com/android/server/telecom/CallEndpointController.java
@@ -87,7 +87,7 @@
     }
 
     public void requestCallEndpointChange(CallEndpoint endpoint, ResultReceiver callback) {
-        Log.d(this, "requestCallEndpointChange %s", endpoint);
+        Log.i(this, "requestCallEndpointChange %s", endpoint);
         int route = mTypeToRouteMap.get(endpoint.getEndpointType());
         String bluetoothAddress = getBluetoothAddress(endpoint);
 
@@ -99,7 +99,6 @@
         }
 
         if (isCurrentEndpointRequestedEndpoint(route, bluetoothAddress)) {
-            Log.d(this, "requestCallEndpointChange: requested endpoint is already active");
             callback.send(CallEndpoint.ENDPOINT_OPERATION_SUCCESS, new Bundle());
             return;
         }
@@ -130,21 +129,27 @@
             return false;
         }
         CallAudioState currentAudioState = mCallsManager.getCallAudioManager().getCallAudioState();
-        // requested non-bt endpoint is already active
-        if (requestedRoute != CallAudioState.ROUTE_BLUETOOTH &&
-                requestedRoute == currentAudioState.getRoute()) {
-            return true;
-        }
-        // requested bt endpoint is already active
-        if (requestedRoute == CallAudioState.ROUTE_BLUETOOTH &&
-                currentAudioState.getActiveBluetoothDevice() != null &&
-                requestedAddress.equals(
-                        currentAudioState.getActiveBluetoothDevice().getAddress())) {
-            return true;
+        if (requestedRoute == currentAudioState.getRoute()) {
+            if (requestedRoute != CallAudioState.ROUTE_BLUETOOTH) {
+                // The audio route (earpiece, speaker, etc.) is already active
+                // and Telecom can ignore the spam request!
+                Log.i(this, "iCERE: user requested a non-BT route that is already active");
+                return true;
+            } else if (hasSameBluetoothAddress(currentAudioState, requestedAddress)) {
+                // if the requested (BT route, device) is active, ignore the request...
+                Log.i(this, "iCERE: user requested a BT endpoint that is already active");
+                return true;
+            }
         }
         return false;
     }
 
+    public boolean hasSameBluetoothAddress(CallAudioState audioState, String requestedAddress) {
+        boolean hasActiveBtDevice = audioState.getActiveBluetoothDevice() != null;
+        return hasActiveBtDevice && requestedAddress.equals(
+                audioState.getActiveBluetoothDevice().getAddress());
+    }
+
     private Bundle getErrorResult(int result) {
         String message;
         int resultCode;
diff --git a/src/com/android/server/telecom/CallIntentProcessor.java b/src/com/android/server/telecom/CallIntentProcessor.java
index 7953324..062c872 100644
--- a/src/com/android/server/telecom/CallIntentProcessor.java
+++ b/src/com/android/server/telecom/CallIntentProcessor.java
@@ -1,6 +1,7 @@
 package com.android.server.telecom;
 
 import com.android.server.telecom.components.ErrorDialogActivity;
+import com.android.server.telecom.flags.FeatureFlags;
 
 import android.content.Context;
 import android.content.Intent;
@@ -32,7 +33,7 @@
 public class CallIntentProcessor {
     public interface Adapter {
         void processOutgoingCallIntent(Context context, CallsManager callsManager,
-                Intent intent, String callingPackage);
+                Intent intent, String callingPackage, FeatureFlags featureFlags);
         void processIncomingCallIntent(CallsManager callsManager, Intent intent);
         void processUnknownCallIntent(CallsManager callsManager, Intent intent);
     }
@@ -45,9 +46,9 @@
 
         @Override
         public void processOutgoingCallIntent(Context context, CallsManager callsManager,
-                Intent intent, String callingPackage) {
+                Intent intent, String callingPackage, FeatureFlags featureFlags) {
             CallIntentProcessor.processOutgoingCallIntent(context, callsManager, intent,
-                    callingPackage, mDefaultDialerCache);
+                    callingPackage, mDefaultDialerCache, featureFlags);
         }
 
         @Override
@@ -73,12 +74,14 @@
     private final Context mContext;
     private final CallsManager mCallsManager;
     private final DefaultDialerCache mDefaultDialerCache;
+    private final FeatureFlags mFeatureFlags;
 
     public CallIntentProcessor(Context context, CallsManager callsManager,
-            DefaultDialerCache defaultDialerCache) {
+            DefaultDialerCache defaultDialerCache, FeatureFlags featureFlags) {
         this.mContext = context;
         this.mCallsManager = callsManager;
         this.mDefaultDialerCache = defaultDialerCache;
+        this.mFeatureFlags = featureFlags;
     }
 
     public void processIntent(Intent intent, String callingPackage) {
@@ -90,7 +93,7 @@
             processUnknownCallIntent(mCallsManager, intent);
         } else {
             processOutgoingCallIntent(mContext, mCallsManager, intent, callingPackage,
-                    mDefaultDialerCache);
+                    mDefaultDialerCache, mFeatureFlags);
         }
         Trace.endSection();
     }
@@ -107,7 +110,8 @@
             CallsManager callsManager,
             Intent intent,
             String callingPackage,
-            DefaultDialerCache defaultDialerCache) {
+            DefaultDialerCache defaultDialerCache,
+            FeatureFlags featureFlags) {
 
         Uri handle = intent.getData();
         String scheme = handle.getScheme();
@@ -182,10 +186,9 @@
         boolean isPrivilegedDialer = defaultDialerCache.isDefaultOrSystemDialer(callingPackage,
                 initiatingUser.getIdentifier());
 
-
         NewOutgoingCallIntentBroadcaster broadcaster = new NewOutgoingCallIntentBroadcaster(
                 context, callsManager, intent, callsManager.getPhoneNumberUtilsAdapter(),
-                isPrivilegedDialer, defaultDialerCache, new MmiUtils());
+                isPrivilegedDialer, defaultDialerCache, new MmiUtils(), featureFlags);
 
         // If the broadcaster comes back with an immediate error, disconnect and show a dialog.
         NewOutgoingCallIntentBroadcaster.CallDisposition disposition = broadcaster.evaluateCall();
diff --git a/src/com/android/server/telecom/CallLogManager.java b/src/com/android/server/telecom/CallLogManager.java
index 3005656..fcb7778 100644
--- a/src/com/android/server/telecom/CallLogManager.java
+++ b/src/com/android/server/telecom/CallLogManager.java
@@ -19,9 +19,13 @@
 import static android.provider.CallLog.Calls.BLOCK_REASON_NOT_BLOCKED;
 import static android.telephony.CarrierConfigManager.KEY_SUPPORT_IMS_CONFERENCE_EVENT_PACKAGE_BOOL;
 
+import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.content.ContentResolver;
 import android.content.Context;
 import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.database.Cursor;
 import android.location.Country;
 import android.location.CountryDetector;
 import android.location.Location;
@@ -30,6 +34,7 @@
 import android.os.Looper;
 import android.os.UserHandle;
 import android.os.PersistableBundle;
+import android.os.UserManager;
 import android.provider.CallLog;
 import android.provider.CallLog.Calls;
 import android.telecom.Connection;
@@ -42,13 +47,16 @@
 import android.telephony.CarrierConfigManager;
 import android.telephony.PhoneNumberUtils;
 import android.telephony.SubscriptionManager;
+import android.util.Pair;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.telecom.callfiltering.CallFilteringResult;
+import com.android.server.telecom.flags.FeatureFlags;
 
 import java.util.Arrays;
 import java.util.Locale;
 import java.util.Objects;
+import java.util.UUID;
 import java.util.stream.Stream;
 
 /**
@@ -68,16 +76,19 @@
      */
     private static class AddCallArgs {
         public AddCallArgs(Context context, CallLog.AddCallParams params,
-                @Nullable LogCallCompletedListener logCallCompletedListener) {
+                @Nullable LogCallCompletedListener logCallCompletedListener,
+                @NonNull Call call) {
             this.context = context;
             this.params = params;
             this.logCallCompletedListener = logCallCompletedListener;
+            this.call = call;
 
         }
         // Since the members are accessed directly, we don't use the
         // mXxxx notation.
         public final Context context;
         public final CallLog.AddCallParams params;
+        public final Call call;
         @Nullable
         public final LogCallCompletedListener logCallCompletedListener;
     }
@@ -88,29 +99,39 @@
     // TODO: come up with a better way to indicate in a android.telecom.DisconnectCause that
     // a conference was merged successfully
     private static final String REASON_IMS_MERGED_SUCCESSFULLY = "IMS_MERGED_SUCCESSFULLY";
+    private static final UUID LOG_CALL_FAILED_ANOMALY_ID =
+            UUID.fromString("d9b38771-ff36-417b-8723-2363a870c702");
+    private static final String LOG_CALL_FAILED_ANOMALY_DESC =
+            "Based on the current user, Telecom detected failure to record a call to the call log.";
 
     private final Context mContext;
     private final CarrierConfigManager mCarrierConfigManager;
     private final PhoneAccountRegistrar mPhoneAccountRegistrar;
     private final MissedCallNotifier mMissedCallNotifier;
+    private AnomalyReporterAdapter mAnomalyReporterAdapter;
     private static final String ACTION_CALLS_TABLE_ADD_ENTRY =
-                "com.android.server.telecom.intent.action.CALLS_ADD_ENTRY";
+            "com.android.server.telecom.intent.action.CALLS_ADD_ENTRY";
     private static final String PERMISSION_PROCESS_CALLLOG_INFO =
-                "android.permission.PROCESS_CALLLOG_INFO";
+            "android.permission.PROCESS_CALLLOG_INFO";
     private static final String CALL_TYPE = "callType";
     private static final String CALL_DURATION = "duration";
 
     private Object mLock;
     private String mCurrentCountryIso;
 
+    private final FeatureFlags mFeatureFlags;
+
     public CallLogManager(Context context, PhoneAccountRegistrar phoneAccountRegistrar,
-            MissedCallNotifier missedCallNotifier) {
+            MissedCallNotifier missedCallNotifier, AnomalyReporterAdapter anomalyReporterAdapter,
+            FeatureFlags featureFlags) {
         mContext = context;
         mCarrierConfigManager = (CarrierConfigManager) mContext
                 .getSystemService(Context.CARRIER_CONFIG_SERVICE);
         mPhoneAccountRegistrar = phoneAccountRegistrar;
         mMissedCallNotifier = missedCallNotifier;
+        mAnomalyReporterAdapter = anomalyReporterAdapter;
         mLock = new Object();
+        mFeatureFlags = featureFlags;
     }
 
     @Override
@@ -151,7 +172,7 @@
      * Call is NOT a child call from a conference which was remotely hosted.
      * Call is NOT simulating a single party conference.
      * Call was NOT explicitly canceled, except for disconnecting from a conference.
-     * Call is NOT an external call
+     * Call is NOT an external call or an external call on watch.
      * Call is NOT disconnected because of merging into a conference.
      * Call is NOT a self-managed call OR call is a self-managed call which has indicated it
      * should be logged in its PhoneAccount
@@ -200,8 +221,10 @@
                     & Connection.CAPABILITY_DISCONNECT_FROM_CONFERENCE)
                     == Connection.CAPABILITY_DISCONNECT_FROM_CONFERENCE;
         }
-        // An external call
-        if (call.isExternalCall()) {
+        // An external and non-watch call
+        if (call.isExternalCall() && (!mContext.getPackageManager().hasSystemFeature(
+                PackageManager.FEATURE_WATCH)
+                || !mFeatureFlags.telecomLogExternalWearableCalls())) {
             return false;
         }
 
@@ -263,7 +286,7 @@
      *     {@link android.provider.CallLog.Calls#BLOCKED_TYPE}.
      */
     void logCall(Call call, int callLogType,
-        @Nullable LogCallCompletedListener logCallCompletedListener, CallFilteringResult result) {
+            @Nullable LogCallCompletedListener logCallCompletedListener, CallFilteringResult result) {
 
         CallLog.AddCallParams.AddCallParametersBuilder paramBuilder =
                 new CallLog.AddCallParams.AddCallParametersBuilder();
@@ -385,7 +408,7 @@
                 okayToLogCall(accountHandle, logNumber, call.isEmergencyCall());
         if (okayToLog) {
             AddCallArgs args = new AddCallArgs(mContext, paramBuilder.build(),
-                    logCallCompletedListener);
+                    logCallCompletedListener, call);
             Log.addEvent(call, LogUtils.Events.LOG_CALL, "number=" + Log.piiHandle(logNumber)
                     + ",postDial=" + Log.piiHandle(call.getPostDialDigits()) + ",pres="
                     + call.getHandlePresentation());
@@ -516,8 +539,25 @@
                 AddCallArgs c = callList[i];
                 mListeners[i] = c.logCallCompletedListener;
                 try {
-                    // May block.
+                    Pair<Integer, Integer> startStats = getCallLogStats(c.call);
+                    Log.i(TAG, "LogCall; about to log callId=%s, "
+                                    + "startCount=%d, startMaxId=%d",
+                            c.call.getId(), startStats.first, startStats.second);
+
                     result[i] = Calls.addCall(c.context, c.params);
+                    Pair<Integer, Integer> endStats = getCallLogStats(c.call);
+                    Log.i(TAG, "LogCall; logged callId=%s, uri=%s, "
+                                    + "endCount=%d, endMaxId=%s",
+                            c.call.getId(), result, endStats.first, endStats.second);
+                    if ((endStats.second - startStats.second) <= 0) {
+                        // No call was added or even worse we lost a call in the log.  Trigger an
+                        // anomaly report.  Note: it technically possible that an app modified the
+                        // call log while we were writing to it here; that is pretty unlikely, and
+                        // the goal here is to try and identify potential anomalous conditions with
+                        // logging calls.
+                        mAnomalyReporterAdapter.reportAnomaly(LOG_CALL_FAILED_ANOMALY_ID,
+                                LOG_CALL_FAILED_ANOMALY_DESC);
+                    }
                 } catch (Exception e) {
                     // This is very rare but may happen in legitimate cases.
                     // E.g. If the phone is encrypted and thus write request fails, it may cause
@@ -526,8 +566,10 @@
                     //
                     // We don't want to crash the whole process just because of that, so just log
                     // it instead.
-                    Log.e(TAG, e, "Exception raised during adding CallLog entry.");
+                    Log.e(TAG, e, "LogCall: Exception raised adding callId=%s", c.call.getId());
                     result[i] = null;
+                    mAnomalyReporterAdapter.reportAnomaly(LOG_CALL_FAILED_ANOMALY_ID,
+                            LOG_CALL_FAILED_ANOMALY_DESC);
                 }
             }
             return result;
@@ -602,4 +644,56 @@
             return mCurrentCountryIso;
         }
     }
+
+
+    /**
+     * Returns a pair containing the number of rows in the call log, as well as the maximum call log
+     * ID.  There is a limit of 500 entries in the call log for a phone account, so once we hit 500
+     * we can reasonably expect that number to not change before and after logging a call.
+     * We determine the maximum ID in the call log since this is a way we can objectively check if
+     * the provider did record a call log entry or not.  Ideally there should be more call log
+     * entries after logging than before, and certainly not less.
+     * @return pair with number of rows in the call log and max id.
+     */
+    private Pair<Integer, Integer> getCallLogStats(@NonNull Call call) {
+        try {
+            // Ensure we query the call log based on the current user.
+            final Context currentUserContext = mContext.createContextAsUser(
+                    call.getAssociatedUser(), /* flags= */ 0);
+            final ContentResolver currentUserResolver = currentUserContext.getContentResolver();
+            final UserManager userManager = currentUserContext.getSystemService(UserManager.class);
+            final int currentUserId = userManager.getProcessUserId();
+
+            // Use shadow provider based on current user unlock state.
+            Uri providerUri;
+            if (userManager.isUserUnlocked(currentUserId)) {
+                providerUri = Calls.CONTENT_URI;
+            } else {
+                providerUri = Calls.SHADOW_CONTENT_URI;
+            }
+            int maxCallId = -1;
+            int numFound;
+            try (Cursor countCursor = currentUserResolver.query(providerUri,
+                    new String[]{Calls._ID},
+                    null,
+                    null,
+                    Calls._ID + " DESC")) {
+                numFound = countCursor.getCount();
+                if (numFound > 0) {
+                    countCursor.moveToFirst();
+                    maxCallId = countCursor.getInt(0);
+                }
+            }
+            return new Pair<>(numFound, maxCallId);
+        } catch (Exception e) {
+            // Oh jeepers, we crashed getting the call count.
+            Log.e(TAG, e, "getCountOfCallLogRows: failed");
+            return new Pair<>(-1, -1);
+        }
+    }
+
+    @VisibleForTesting
+    public void setAnomalyReporterAdapter(AnomalyReporterAdapter anomalyReporterAdapter){
+        mAnomalyReporterAdapter = anomalyReporterAdapter;
+    }
 }
diff --git a/src/com/android/server/telecom/CallsManager.java b/src/com/android/server/telecom/CallsManager.java
old mode 100644
new mode 100755
index 77570c3..0e78ca1
--- a/src/com/android/server/telecom/CallsManager.java
+++ b/src/com/android/server/telecom/CallsManager.java
@@ -129,9 +129,11 @@
 import com.android.server.telecom.callfiltering.DirectToVoicemailFilter;
 import com.android.server.telecom.callfiltering.DndCallFilter;
 import com.android.server.telecom.callfiltering.IncomingCallFilterGraph;
+import com.android.server.telecom.callfiltering.IncomingCallFilterGraphProvider;
 import com.android.server.telecom.callredirection.CallRedirectionProcessor;
 import com.android.server.telecom.components.ErrorDialogActivity;
 import com.android.server.telecom.components.TelecomBroadcastReceiver;
+import com.android.server.telecom.flags.FeatureFlags;
 import com.android.server.telecom.stats.CallFailureCause;
 import com.android.server.telecom.ui.AudioProcessingNotification;
 import com.android.server.telecom.ui.CallRedirectionTimeoutDialogActivity;
@@ -140,8 +142,8 @@
 import com.android.server.telecom.ui.DisconnectedCallNotifier;
 import com.android.server.telecom.ui.IncomingCallNotifier;
 import com.android.server.telecom.ui.ToastFactory;
-import com.android.server.telecom.voip.TransactionManager;
 import com.android.server.telecom.voip.VoipCallMonitor;
+import com.android.server.telecom.voip.TransactionManager;
 
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -286,15 +288,14 @@
     public static final String EXCEPTION_RETRIEVING_PHONE_ACCOUNTS_EMERGENCY_ERROR_MSG =
             "Exception thrown while retrieving list of potential phone accounts when placing an "
                     + "emergency call.";
-    public static final UUID EMERGENCY_CALL_DISCONNECTED_BEFORE_BEING_ADDED_ERROR_UUID =
-            UUID.fromString("f9a916c8-8d61-4550-9ad3-11c2e84f6364");
-    public static final String EMERGENCY_CALL_DISCONNECTED_BEFORE_BEING_ADDED_ERROR_MSG =
-            "An emergency call was disconnected after the connection was created but before the "
-                    + "call was successfully added to CallsManager.";
     public static final UUID EMERGENCY_CALL_ABORTED_NO_PHONE_ACCOUNTS_ERROR_UUID =
             UUID.fromString("2e994acb-1997-4345-8bf3-bad04303de26");
     public static final String EMERGENCY_CALL_ABORTED_NO_PHONE_ACCOUNTS_ERROR_MSG =
             "An emergency call was aborted since there were no available phone accounts.";
+    public static final UUID TELEPHONY_HAS_DEFAULT_BUT_TELECOM_DOES_NOT_UUID =
+            UUID.fromString("0a86157c-50ca-11ee-be56-0242ac120002");
+    public static final String TELEPHONY_HAS_DEFAULT_BUT_TELECOM_DOES_NOT_MSG =
+            "Telephony has a default MO acct but Telecom prompted user for MO";
 
     private static final int[] OUTGOING_CALL_STATES =
             {CallState.CONNECTING, CallState.SELECT_PHONE_ACCOUNT, CallState.DIALING,
@@ -463,6 +464,9 @@
     private final TransactionManager mTransactionManager;
     private final UserManager mUserManager;
     private final CallStreamingNotification mCallStreamingNotification;
+    private final FeatureFlags mFeatureFlags;
+
+    private final IncomingCallFilterGraphProvider mIncomingCallFilterGraphProvider;
 
     private final ConnectionServiceFocusManager.CallsManagerRequester mRequester =
             new ConnectionServiceFocusManager.CallsManagerRequester() {
@@ -572,10 +576,14 @@
             CallAnomalyWatchdog callAnomalyWatchdog,
             Ringer.AccessibilityManagerAdapter accessibilityManagerAdapter,
             Executor asyncTaskExecutor,
+            Executor asyncCallAudioTaskExecutor,
             BlockedNumbersAdapter blockedNumbersAdapter,
             TransactionManager transactionManager,
             EmergencyCallDiagnosticLogger emergencyCallDiagnosticLogger,
-            CallStreamingNotification callStreamingNotification) {
+            CallAudioCommunicationDeviceTracker communicationDeviceTracker,
+            CallStreamingNotification callStreamingNotification,
+            FeatureFlags featureFlags,
+            IncomingCallFilterGraphProvider incomingCallFilterGraphProvider) {
 
         mContext = context;
         mLock = lock;
@@ -594,6 +602,7 @@
         mEmergencyCallHelper = emergencyCallHelper;
         mCallerInfoLookupHelper = callerInfoLookupHelper;
         mEmergencyCallDiagnosticLogger = emergencyCallDiagnosticLogger;
+        mIncomingCallFilterGraphProvider = incomingCallFilterGraphProvider;
 
         mDtmfLocalTonePlayer =
                 new DtmfLocalTonePlayer(new DtmfLocalTonePlayer.ToneGeneratorProxy());
@@ -606,7 +615,8 @@
                         statusBarNotifier,
                         audioServiceFactory,
                         CallAudioRouteStateMachine.EARPIECE_AUTO_DETECT,
-                        asyncTaskExecutor
+                        asyncCallAudioTaskExecutor,
+                        communicationDeviceTracker
                 );
         callAudioRouteStateMachine.initialize();
 
@@ -615,7 +625,8 @@
                         callAudioRouteStateMachine,
                         bluetoothManager,
                         wiredHeadsetManager,
-                        mDockManager);
+                        mDockManager,
+                        asyncRingtonePlayer);
         AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
         InCallTonePlayer.MediaPlayerFactory mediaPlayerFactory =
                 (resourceId, attributes) ->
@@ -639,12 +650,12 @@
                 ringtoneFactory, systemVibrator,
                 new Ringer.VibrationEffectProxy(), mInCallController,
                 mContext.getSystemService(NotificationManager.class),
-                accessibilityManagerAdapter);
+                accessibilityManagerAdapter, featureFlags);
         mCallRecordingTonePlayer = new CallRecordingTonePlayer(mContext, audioManager,
                 mTimeoutsAdapter, mLock);
         mCallAudioManager = new CallAudioManager(callAudioRouteStateMachine,
                 this, callAudioModeStateMachineFactory.create(systemStateHelper,
-                (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE)),
+                (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE), featureFlags),
                 playerFactory, mRinger, new RingbackPlayer(playerFactory),
                 bluetoothStateReceiver, mDtmfLocalTonePlayer);
 
@@ -653,23 +664,25 @@
         mTtyManager = new TtyManager(context, mWiredHeadsetManager);
         mProximitySensorManager = proximitySensorManagerFactory.create(context, this);
         mPhoneStateBroadcaster = new PhoneStateBroadcaster(this);
-        mCallLogManager = new CallLogManager(context, phoneAccountRegistrar, mMissedCallNotifier);
+        mCallLogManager = new CallLogManager(context, phoneAccountRegistrar, mMissedCallNotifier,
+                mAnomalyReporter, featureFlags);
         mConnectionServiceRepository =
                 new ConnectionServiceRepository(mPhoneAccountRegistrar, mContext, mLock, this);
         mInCallWakeLockController = inCallWakeLockControllerFactory.create(context, this);
         mClockProxy = clockProxy;
         mToastFactory = toastFactory;
         mRoleManagerAdapter = roleManagerAdapter;
+        mVoipCallMonitor = new VoipCallMonitor(mContext, mLock);
         mTransactionManager = transactionManager;
         mBlockedNumbersAdapter = blockedNumbersAdapter;
         mCallStreamingController = new CallStreamingController(mContext, mLock);
-        mVoipCallMonitor = new VoipCallMonitor(mContext, mLock);
         mCallStreamingNotification = callStreamingNotification;
+        mFeatureFlags = featureFlags;
 
+        mListeners.add(mInCallController);
         mListeners.add(mInCallWakeLockController);
         mListeners.add(statusBarNotifier);
         mListeners.add(mCallLogManager);
-        mListeners.add(mInCallController);
         mListeners.add(mCallEndpointController);
         mListeners.add(mCallDiagnosticServiceController);
         mListeners.add(mCallAudioManager);
@@ -744,11 +757,10 @@
     @Override
     @VisibleForTesting
     public void onSuccessfulOutgoingCall(Call call, int callState) {
-        Log.v(this, "onSuccessfulOutgoingCall, %s", call);
+        Log.v(this, "onSuccessfulOutgoingCall, call=[%s], state=[%d]", call, callState);
         call.setPostCallPackageName(getRoleManagerAdapter().getDefaultCallScreeningApp(
                 call.getAssociatedUser()));
 
-        setCallState(call, callState, "successful outgoing call");
         if (!mCalls.contains(call)) {
             // Call was not added previously in startOutgoingCall due to it being a potential MMI
             // code, so add it now.
@@ -760,7 +772,13 @@
             listener.onConnectionServiceChanged(call, null, call.getConnectionService());
         }
 
-        markCallAsDialing(call);
+        // Allow the ConnectionService to start the call in the active state. This case is helpful
+        // for conference calls or meetings that can skip the dialing stage.
+        if (callState == CallState.ACTIVE) {
+            setCallState(call, callState, "skipping the dialing state and setting active");
+        } else {
+            markCallAsDialing(call);
+        }
     }
 
     @Override
@@ -779,11 +797,12 @@
                 ? new Bundle()
                 : phoneAccount.getExtras();
         TelephonyManager telephonyManager = getTelephonyManager();
+        boolean performDndFilter = mFeatureFlags.skipFilterPhoneAccountPerformDndFilter();
         if (incomingCall.hasProperty(Connection.PROPERTY_EMERGENCY_CALLBACK_MODE) ||
                 incomingCall.hasProperty(Connection.PROPERTY_NETWORK_IDENTIFIED_EMERGENCY_CALL) ||
                 telephonyManager.isInEmergencySmsMode() ||
                 incomingCall.isSelfManaged() ||
-                extras.getBoolean(PhoneAccount.EXTRA_SKIP_CALL_FILTERING)) {
+                (!performDndFilter && extras.getBoolean(PhoneAccount.EXTRA_SKIP_CALL_FILTERING))) {
             Log.i(this, "Skipping call filtering for %s (ecm=%b, "
                             + "networkIdentifiedEmergencyCall = %b, emergencySmsMode = %b, "
                             + "selfMgd=%b, skipExtra=%b)",
@@ -801,12 +820,27 @@
                     .build(), false);
             incomingCall.setIsUsingCallFiltering(false);
             return;
+        } else if (performDndFilter && extras.getBoolean(PhoneAccount.EXTRA_SKIP_CALL_FILTERING)) {
+            IncomingCallFilterGraph graph = setupDndFilterOnlyGraph(incomingCall);
+            graph.performFiltering();
+            return;
         }
 
         IncomingCallFilterGraph graph = setUpCallFilterGraph(incomingCall);
         graph.performFiltering();
     }
 
+    private IncomingCallFilterGraph setupDndFilterOnlyGraph(Call incomingHfpCall) {
+        incomingHfpCall.setIsUsingCallFiltering(true);
+        DndCallFilter dndCallFilter = new DndCallFilter(incomingHfpCall, mRinger);
+        IncomingCallFilterGraph graph = mIncomingCallFilterGraphProvider.createGraph(
+                incomingHfpCall,
+                this::onCallFilteringComplete, mContext, mTimeoutsAdapter, mLock);
+        graph.addFilter(dndCallFilter);
+        mGraphHandlerThreads.add(graph.getHandlerThread());
+        return graph;
+    }
+
     private IncomingCallFilterGraph setUpCallFilterGraph(Call incomingCall) {
         incomingCall.setIsUsingCallFiltering(true);
         String carrierPackageName = getCarrierPackageName();
@@ -819,7 +853,7 @@
                 mContext.getPackageManager(), packageName);
         ParcelableCallUtils.Converter converter = new ParcelableCallUtils.Converter();
 
-        IncomingCallFilterGraph graph = new IncomingCallFilterGraph(incomingCall,
+        IncomingCallFilterGraph graph = mIncomingCallFilterGraphProvider.createGraph(incomingCall,
                 this::onCallFilteringComplete, mContext, mTimeoutsAdapter, mLock);
         DirectToVoicemailFilter voicemailFilter = new DirectToVoicemailFilter(incomingCall,
                 mCallerInfoLookupHelper);
@@ -1299,7 +1333,7 @@
         return mCallAudioManager;
     }
 
-    InCallController getInCallController() {
+    public InCallController getInCallController() {
         return mInCallController;
     }
 
@@ -1379,8 +1413,11 @@
     }
 
     @VisibleForTesting
-    public void setAnomalyReporterAdapter(AnomalyReporterAdapter mAnomalyReporterAdapter){
-        mAnomalyReporter = mAnomalyReporterAdapter;
+    public void setAnomalyReporterAdapter(AnomalyReporterAdapter anomalyReporterAdapter){
+        mAnomalyReporter = anomalyReporterAdapter;
+        if (mCallLogManager != null) {
+            mCallLogManager.setAnomalyReporterAdapter(anomalyReporterAdapter);
+        }
     }
 
     void processIncomingConference(PhoneAccountHandle phoneAccountHandle, Bundle extras) {
@@ -2047,7 +2084,7 @@
                                         + " available accounts.");
                                 showErrorMessage(R.string.cant_call_due_to_no_supported_service);
                                 mListeners.forEach(l -> l.onCreateConnectionFailed(callToPlace));
-                                if (callToPlace.isEmergencyCall()){
+                                if (callToPlace.isEmergencyCall()) {
                                     mAnomalyReporter.reportAnomaly(
                                             EMERGENCY_CALL_ABORTED_NO_PHONE_ACCOUNTS_ERROR_UUID,
                                             EMERGENCY_CALL_ABORTED_NO_PHONE_ACCOUNTS_ERROR_MSG);
@@ -2060,6 +2097,21 @@
                                 return CompletableFuture.completedFuture(Pair.create(callToPlace,
                                         accountSuggestions.get(0).getPhoneAccountHandle()));
                             }
+
+                            // At this point Telecom is requesting the user to select a phone
+                            // account. However, Telephony is reporting that the user has a default
+                            // outgoing account (which is denoted by a non-negative subId number).
+                            // At some point, Telecom and Telephony are out of sync with the default
+                            // outgoing calling account.
+                            if(mFeatureFlags.telephonyHasDefaultButTelecomDoesNot()) {
+                                if (SubscriptionManager.getDefaultVoiceSubscriptionId() !=
+                                        SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
+                                    mAnomalyReporter.reportAnomaly(
+                                            TELEPHONY_HAS_DEFAULT_BUT_TELECOM_DOES_NOT_UUID,
+                                            TELEPHONY_HAS_DEFAULT_BUT_TELECOM_DOES_NOT_MSG);
+                                }
+                            }
+
                             // This is the state where the user is expected to select an account
                             callToPlace.setState(CallState.SELECT_PHONE_ACCOUNT,
                                     "needs account selection");
@@ -2303,6 +2355,15 @@
 
          PhoneAccountHandle phoneAccountHandle = clientExtras.getParcelable(
                  TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE);
+         PhoneAccount account =
+                mPhoneAccountRegistrar.getPhoneAccount(phoneAccountHandle, initiatingUser);
+         boolean isSelfManaged = account != null && account.isSelfManaged();
+         // Enforce outgoing call restriction for conference calls. This is handled via
+         // UserCallIntentProcessor for normal MO calls.
+         if (UserUtil.hasOutgoingCallsUserRestriction(mContext, initiatingUser,
+                 null, isSelfManaged, CallsManager.class.getCanonicalName())) {
+             return;
+         }
          CompletableFuture<Call> callFuture = startOutgoingCall(participants, phoneAccountHandle,
                  clientExtras, initiatingUser, null/* originalIntent */, callingPackage,
                  true/* isconference*/);
@@ -2880,8 +2941,10 @@
             call.answer(videoState);
         } else {
             // Hold or disconnect the active call and request call focus for the incoming call.
-            Call activeCall = (Call) mConnectionSvrFocusMgr.getCurrentFocusCall();
-            Log.d(this, "answerCall: Incoming call = %s Ongoing call %s", call, activeCall);
+            Bundle bundle = new Bundle();
+            bundle.putLong(TelecomManager.EXTRA_CALL_ANSWERED_TIME_MILLIS,
+                     mClockProxy.currentTimeMillis());
+            call.putConnectionServiceExtras(bundle);
             holdActiveCallForNewCall(call);
             mConnectionSvrFocusMgr.requestFocus(
                     call,
@@ -3714,11 +3777,6 @@
         // Notify listeners that the call was disconnected before being added to CallsManager.
         // Listeners will not receive onAdded or onRemoved callbacks.
         if (!mCalls.contains(call)) {
-            if (call.isEmergencyCall()) {
-                mAnomalyReporter.reportAnomaly(
-                        EMERGENCY_CALL_DISCONNECTED_BEFORE_BEING_ADDED_ERROR_UUID,
-                        EMERGENCY_CALL_DISCONNECTED_BEFORE_BEING_ADDED_ERROR_MSG);
-            }
             mListeners.forEach(l -> l.onCreateConnectionFailed(call));
         }
 
@@ -4737,15 +4795,21 @@
 
     /**
      * Determines the number of unholdable calls present in a connection service other than the one
-     * the passed phone account belonds to.
+     * the passed phone account belongs to. If a ConnectionService has not been associated with an
+     * outgoing call yet (for example, it is in the SELECT_PHONE_ACCOUNT state), then we do not
+     * count that call because it is not tracked as an active call yet.
      * @param phoneAccountHandle The handle of the PhoneAccount.
      * @return Number of unholdable calls owned by other connection service.
      */
     public int getNumUnholdableCallsForOtherConnectionService(
             PhoneAccountHandle phoneAccountHandle) {
         return (int) mCalls.stream().filter(call ->
-                !phoneAccountHandle.getComponentName().equals(
-                        call.getTargetPhoneAccount().getComponentName())
+                // If this convention needs to be changed, answerCall will need to be modified to
+                // change what an "active call" is so that the call in SELECT_PHONE_ACCOUNT state
+                // will be properly cancelled.
+                call.getTargetPhoneAccount() != null
+                        && !phoneAccountHandle.getComponentName().equals(
+                                call.getTargetPhoneAccount().getComponentName())
                         && call.getParentCall() == null
                         && !call.isExternalCall()
                         && !canHold(call)).count();
@@ -5469,9 +5533,10 @@
             // We are going to place the new outgoing call, so disconnect any ongoing self-managed
             // calls which are ongoing at this time.
             disconnectSelfManagedCalls("outgoing call " + callId);
-
-            mPendingCallConfirm.complete(mPendingCall);
-            mPendingCallConfirm = null;
+            if (mPendingCallConfirm != null) {
+                mPendingCallConfirm.complete(mPendingCall);
+                mPendingCallConfirm = null;
+            }
             mPendingCall = null;
         }
     }
@@ -5490,8 +5555,10 @@
             markCallAsDisconnected(mPendingCall, new DisconnectCause(DisconnectCause.CANCELED));
             markCallAsRemoved(mPendingCall);
             mPendingCall = null;
-            mPendingCallConfirm.complete(null);
-            mPendingCallConfirm = null;
+            if (mPendingCallConfirm != null) {
+                mPendingCallConfirm.complete(null);
+                mPendingCallConfirm = null;
+            }
         }
     }
 
diff --git a/src/com/android/server/telecom/ConnectionServiceFocusManager.java b/src/com/android/server/telecom/ConnectionServiceFocusManager.java
index 3694727..72cb7c4 100644
--- a/src/com/android/server/telecom/ConnectionServiceFocusManager.java
+++ b/src/com/android/server/telecom/ConnectionServiceFocusManager.java
@@ -26,6 +26,8 @@
 import android.telecom.Logging.Session;
 import android.text.TextUtils;
 import android.util.LocalLog;
+import android.util.LogPrinter;
+import android.util.Printer;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.IndentingPrintWriter;
@@ -35,6 +37,7 @@
 import java.util.Objects;
 import java.util.Optional;
 import java.util.Set;
+import java.util.UUID;
 import java.util.concurrent.BlockingQueue;
 import java.util.concurrent.LinkedBlockingQueue;
 import java.util.concurrent.TimeUnit;
@@ -44,6 +47,11 @@
     private static final String TAG = "ConnectionSvrFocusMgr";
     private static final int GET_CURRENT_FOCUS_TIMEOUT_MILLIS = 1000;
     private final LocalLog mLocalLog = new LocalLog(20);
+    private final AnomalyReporterAdapter mAnomalyReporter = new AnomalyReporterAdapterImpl();
+    public static final UUID WATCHDOG_GET_CALL_FOCUS_TIMEOUT_UUID =
+            UUID.fromString("edd7334a-ef87-432b-a1d0-a2f23959c73e");
+    public static final String WATCHDOG_GET_CALL_FOCUS_TIMEOUT_MSG =
+            "Telecom CallAnomalyWatchdog detected a timeout while getting the call focus.";
 
     /** Factory interface used to create the {@link ConnectionServiceFocusManager} instance. */
     public interface ConnectionServiceFocusManagerFactory {
@@ -333,7 +341,17 @@
                 return syncCallFocus.orElse(null);
             } else {
                 Log.w(TAG, "Timed out waiting for synchronous current focus. Returning possibly"
-                        + " inaccurate result");
+                        + " inaccurate result. returning currentFocusCall=[%s]", mCurrentFocusCall);
+
+                // dump the state of the handler to better understand the timeout
+                mEventHandler.dump(
+                        new LogPrinter(android.util.Log.INFO, TAG), "CsFocusMgr_timeout");
+
+                // report the timeout
+                mAnomalyReporter.reportAnomaly(
+                        WATCHDOG_GET_CALL_FOCUS_TIMEOUT_UUID,
+                        WATCHDOG_GET_CALL_FOCUS_TIMEOUT_MSG);
+
                 return mCurrentFocusCall;
             }
         } catch (InterruptedException e) {
diff --git a/src/com/android/server/telecom/ConnectionServiceWrapper.java b/src/com/android/server/telecom/ConnectionServiceWrapper.java
old mode 100755
new mode 100644
index c550ede..a43000a
--- a/src/com/android/server/telecom/ConnectionServiceWrapper.java
+++ b/src/com/android/server/telecom/ConnectionServiceWrapper.java
@@ -23,10 +23,10 @@
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.pm.PackageManager;
+import android.graphics.drawable.Icon;
 import android.location.Location;
 import android.location.LocationManager;
 import android.location.LocationRequest;
-import android.graphics.drawable.Icon;
 import android.net.Uri;
 import android.os.Binder;
 import android.os.Bundle;
@@ -67,6 +67,7 @@
 import com.android.internal.util.Preconditions;
 
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
@@ -516,10 +517,12 @@
             // Check status hints image for cross user access
             if (parcelableConference.getStatusHints() != null) {
                 Icon icon = parcelableConference.getStatusHints().getIcon();
-                parcelableConference.getStatusHints().setIcon(StatusHints.
-                        validateAccountIconUserBoundary(icon, callingUserHandle));
+                parcelableConference.getStatusHints().setIcon(StatusHints
+                        .validateAccountIconUserBoundary(icon, callingUserHandle));
             }
 
+            if (ConnectionServiceWrapper.this.mIsRemoteConnectionService) return;
+
             if (parcelableConference.getConnectElapsedTimeMillis() != 0
                     && mContext.checkCallingOrSelfPermission(MODIFY_PHONE_STATE)
                             != PackageManager.PERMISSION_GRANTED) {
@@ -934,6 +937,9 @@
         public void addExistingConnection(String callId, ParcelableConnection connection,
                 Session.Info sessionInfo) {
             Log.startSession(sessionInfo, "CSW.aEC", mPackageAbbreviation);
+
+            if (ConnectionServiceWrapper.this.mIsRemoteConnectionService) return;
+
             UserHandle userHandle = Binder.getCallingUserHandle();
             // Check that the Calling Package matches PhoneAccountHandle's Component Package
             PhoneAccountHandle callingPhoneAccountHandle = connection.getPhoneAccount();
@@ -992,6 +998,12 @@
                             connectIdToCheck = callId;
                         }
 
+                        // Check status hints image for cross user access
+                        if (connection.getStatusHints() != null) {
+                            Icon icon = connection.getStatusHints().getIcon();
+                            connection.getStatusHints().setIcon(StatusHints.
+                                    validateAccountIconUserBoundary(icon, userHandle));
+                        }
                         // Handle the case where an existing connection was added by Telephony via
                         // a connection manager.  The remote connection service API does not include
                         // the ability to specify a parent connection when adding an existing
@@ -1030,14 +1042,6 @@
                                     connection.getCallDirection(),
                                     connection.getCallerNumberVerificationStatus());
                         }
-
-                        // Check status hints image for cross user access
-                        if (connection.getStatusHints() != null) {
-                            Icon icon = connection.getStatusHints().getIcon();
-                            connection.getStatusHints().setIcon(StatusHints.
-                                    validateAccountIconUserBoundary(icon, userHandle));
-                        }
-
                         // Check to see if this Connection has already been added.
                         Call alreadyAddedConnection = mCallsManager
                                 .getAlreadyAddedConnection(connectIdToCheck);
@@ -1350,6 +1354,7 @@
     private final CallsManager mCallsManager;
     private final AppOpsManager mAppOpsManager;
     private final Context mContext;
+    public boolean mIsRemoteConnectionService = false;
 
     private ConnectionServiceFocusManager.ConnectionServiceFocusListener mConnSvrFocusListener;
 
@@ -2391,6 +2396,7 @@
         BindCallback callback = new BindCallback() {
             @Override
             public void onSuccess() {
+                if (!isServiceValid("connectionServiceFocusLost")) return;
                 try {
                     mServiceInterface.connectionServiceFocusLost(
                             Log.getExternalSession(TELECOM_ABBREVIATION));
@@ -2410,6 +2416,7 @@
         BindCallback callback = new BindCallback() {
             @Override
             public void onSuccess() {
+                if (!isServiceValid("connectionServiceFocusGained")) return;
                 try {
                     mServiceInterface.connectionServiceFocusGained(
                             Log.getExternalSession(TELECOM_ABBREVIATION));
@@ -2488,12 +2495,11 @@
      */
     private void handleConnectionServiceDeath() {
         if (!mPendingResponses.isEmpty()) {
-            CreateConnectionResponse[] responses = mPendingResponses.values().toArray(
-                    new CreateConnectionResponse[mPendingResponses.values().size()]);
+            Collection<CreateConnectionResponse> responses = mPendingResponses.values();
             mPendingResponses.clear();
-            for (int i = 0; i < responses.length; i++) {
-                responses[i].handleCreateConnectionFailure(
-                        new DisconnectCause(DisconnectCause.ERROR, "CS_DEATH"));
+            for (CreateConnectionResponse response : responses) {
+                response.handleCreateConnectionFailure(new DisconnectCause(DisconnectCause.ERROR,
+                        "CS_DEATH"));
             }
         }
         mCallIdMapper.clear();
@@ -2506,13 +2512,13 @@
     private void logIncoming(String msg, Object... params) {
         // Keep these as debug; the incoming logging is traced on a package level through the
         // session logging.
-        Log.d(this, "CS -> TC[" + Log.getPackageAbbreviation(mComponentName) + "]: "
-                + msg, params);
+        Log.d(this, "CS -> TC[" + Log.getPackageAbbreviation(mComponentName) + "]:"
+                + " isRCS = " + this.mIsRemoteConnectionService + ": " + msg, params);
     }
 
     private void logOutgoing(String msg, Object... params) {
-        Log.d(this, "TC -> CS[" + Log.getPackageAbbreviation(mComponentName) + "]: "
-                + msg, params);
+        Log.d(this, "TC -> CS[" + Log.getPackageAbbreviation(mComponentName) + "]:"
+                + " isRCS = " + this.mIsRemoteConnectionService + ": " + msg, params);
     }
 
     private void queryRemoteConnectionServices(final UserHandle userHandle,
@@ -2539,6 +2545,7 @@
             ConnectionServiceWrapper service = mConnectionServiceRepository.getService(
                     handle.getComponentName(), handle.getUserHandle());
             if (service != null && service != this) {
+                service.mIsRemoteConnectionService = true;
                 simServices.add(service);
             } else {
                 // This is unexpected, normally PhoneAccounts with CAPABILITY_CALL_PROVIDER are not
diff --git a/src/com/android/server/telecom/InCallController.java b/src/com/android/server/telecom/InCallController.java
index d5689ae..ca264c1 100644
--- a/src/com/android/server/telecom/InCallController.java
+++ b/src/com/android/server/telecom/InCallController.java
@@ -2121,7 +2121,7 @@
 
                 ComponentName foundComponentName =
                         new ComponentName(serviceInfo.packageName, serviceInfo.name);
-                if (requestedType == IN_CALL_SERVICE_TYPE_NON_UI) {
+                if (currentType == IN_CALL_SERVICE_TYPE_NON_UI) {
                     mKnownNonUiInCallServices.add(foundComponentName);
                 }
 
@@ -2430,7 +2430,9 @@
                 try {
                     inCallService.updateCall(
                             sanitizeParcelableCallForService(info, parcelableCall));
-                } catch (RemoteException ignored) {
+                } catch (RemoteException exception) {
+                    Log.w(this, "Call status update did not send to: "
+                                + componentName +" successfully with error " + exception);
                 }
             }
             Log.i(this, "Components updated: %s", componentsUpdated);
@@ -2885,4 +2887,25 @@
             return userFromCall;
         }
     }
+
+    /**
+     * Useful for debugging purposes and called on the command line via
+     * an "adb shell telecom command".
+     *
+     * @return true if a particular non-ui InCallService package is bound in a call.
+     */
+    public boolean isNonUiInCallServiceBound(String packageName) {
+        for (NonUIInCallServiceConnectionCollection ics : mNonUIInCallServiceConnections.values()) {
+            for (InCallServiceBindingConnection connection : ics.getSubConnections()) {
+                InCallServiceInfo serviceInfo = connection.mInCallServiceInfo;
+                Log.i(this, "isNonUiInCallServiceBound: found serviceInfo=[%s]", serviceInfo);
+                if (serviceInfo != null &&
+                        serviceInfo.mComponentName.getPackageName().contains(packageName)) {
+                    Log.i(this, "isNonUiInCallServiceBound: found target package");
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
 }
diff --git a/src/com/android/server/telecom/NewOutgoingCallIntentBroadcaster.java b/src/com/android/server/telecom/NewOutgoingCallIntentBroadcaster.java
index 3b402b1..6070baa 100644
--- a/src/com/android/server/telecom/NewOutgoingCallIntentBroadcaster.java
+++ b/src/com/android/server/telecom/NewOutgoingCallIntentBroadcaster.java
@@ -16,6 +16,7 @@
 
 package com.android.server.telecom;
 
+import android.Manifest;
 import android.app.Activity;
 import android.app.AppOpsManager;
 import android.app.BroadcastOptions;
@@ -37,6 +38,7 @@
 import android.text.TextUtils;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.telecom.flags.FeatureFlags;
 import com.android.server.telecom.callredirection.CallRedirectionProcessor;
 
 // TODO: Needed for move to system service: import com.android.internal.R;
@@ -77,6 +79,7 @@
     private final TelecomSystem.SyncRoot mLock;
     private final DefaultDialerCache mDefaultDialerCache;
     private final MmiUtils mMmiUtils;
+    private final FeatureFlags mFeatureFlags;
 
     /*
      * Whether or not the outgoing call intent originated from the default phone application. If
@@ -100,7 +103,8 @@
     @VisibleForTesting
     public NewOutgoingCallIntentBroadcaster(Context context, CallsManager callsManager,
             Intent intent, PhoneNumberUtilsAdapter phoneNumberUtilsAdapter,
-            boolean isDefaultPhoneApp, DefaultDialerCache defaultDialerCache, MmiUtils mmiUtils) {
+            boolean isDefaultPhoneApp, DefaultDialerCache defaultDialerCache, MmiUtils mmiUtils,
+            FeatureFlags featureFlags) {
         mContext = context;
         mCallsManager = callsManager;
         mIntent = intent;
@@ -109,6 +113,7 @@
         mLock = mCallsManager.getLock();
         mDefaultDialerCache = defaultDialerCache;
         mMmiUtils = mmiUtils;
+        mFeatureFlags = featureFlags;
     }
 
     /**
@@ -128,7 +133,8 @@
                     // Once the NEW_OUTGOING_CALL broadcast is finished, the resultData is
                     // used as the actual number to call. (If null, no call will be placed.)
                     String resultNumber = getResultData();
-                    Log.i(this, "Received new-outgoing-call-broadcast for %s with data %s", mCall,
+                    Log.i(NewOutgoingCallIntentBroadcaster.this,
+                            "Received new-outgoing-call-broadcast for %s with data %s", mCall,
                             Log.pii(resultNumber));
 
                     boolean endEarly = false;
@@ -320,6 +326,7 @@
         String scheme = mPhoneNumberUtilsAdapter.isUriNumber(number)
                 ? PhoneAccount.SCHEME_SIP : PhoneAccount.SCHEME_TEL;
         result.callingAddress = Uri.fromParts(scheme, number, null);
+
         return result;
     }
 
@@ -351,14 +358,57 @@
 
     public void processCall(Call call, CallDisposition disposition) {
         mCall = call;
+
+        // If the new outgoing call broadast doesn't block, trigger the legacy process call
+        // behavior and exit out here.
+        if (!mFeatureFlags.isNewOutgoingCallBroadcastUnblocking()) {
+            legacyProcessCall(disposition);
+            return;
+        }
+        boolean callRedirectionWithService = false;
+        // Only try to do redirection if it was requested and we're not calling immediately.
+        // We can expect callImmediately to be true for emergency calls and voip calls.
+        if (disposition.requestRedirection && !disposition.callImmediately) {
+            CallRedirectionProcessor callRedirectionProcessor = new CallRedirectionProcessor(
+                    mContext, mCallsManager, mCall, disposition.callingAddress,
+                    mCallsManager.getPhoneAccountRegistrar(),
+                    getGateWayInfoFromIntent(mIntent, mIntent.getData()),
+                    mIntent.getBooleanExtra(TelecomManager.EXTRA_START_CALL_WITH_SPEAKERPHONE,
+                            false),
+                    mIntent.getIntExtra(TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE,
+                            VideoProfile.STATE_AUDIO_ONLY));
+            /**
+             * If there is an available {@link android.telecom.CallRedirectionService}, use the
+             * {@link CallRedirectionProcessor} to perform call redirection instead of using
+             * broadcasting.
+             */
+            callRedirectionWithService = callRedirectionProcessor
+                    .canMakeCallRedirectionWithServiceAsUser(mCall.getAssociatedUser());
+            if (callRedirectionWithService) {
+                callRedirectionProcessor.performCallRedirection(mCall.getAssociatedUser());
+            }
+        }
+
+        // If no redirection was kicked off, place the call now.
+        if (!callRedirectionWithService) {
+            callImmediately(disposition);
+        }
+
+        // Finally, send the non-blocking broadcast if we're supposed to (ie for any non-voip call).
+        if (disposition.sendBroadcast) {
+            UserHandle targetUser = mCall.getAssociatedUser();
+            broadcastIntent(mIntent, disposition.number, false /* receiverRequired */, targetUser);
+        }
+    }
+
+    /**
+     * The legacy non-flagged version of processing a call.  Although there is some code duplication
+     * if makes the new flow cleaner to read.
+     * @param disposition
+     */
+    private void legacyProcessCall(CallDisposition disposition) {
         if (disposition.callImmediately) {
-            boolean speakerphoneOn = mIntent.getBooleanExtra(
-                    TelecomManager.EXTRA_START_CALL_WITH_SPEAKERPHONE, false);
-            int videoState = mIntent.getIntExtra(
-                    TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE,
-                    VideoProfile.STATE_AUDIO_ONLY);
-            placeOutgoingCallImmediately(mCall, disposition.callingAddress, null,
-                    speakerphoneOn, videoState);
+            callImmediately(disposition);
 
             // Don't return but instead continue and send the ACTION_NEW_OUTGOING_CALL broadcast
             // so that third parties can still inspect (but not intercept) the outgoing call. When
@@ -390,13 +440,26 @@
 
         if (disposition.sendBroadcast) {
             UserHandle targetUser = mCall.getAssociatedUser();
-            Log.i(this, "Sending NewOutgoingCallBroadcast for %s to %s", mCall, targetUser);
             broadcastIntent(mIntent, disposition.number,
                     !disposition.callImmediately && !callRedirectionWithService, targetUser);
         }
     }
 
     /**
+     * Place a call immediately.
+     * @param disposition The disposition; used for retrieving the address of the call.
+     */
+    private void callImmediately(CallDisposition disposition) {
+        boolean speakerphoneOn = mIntent.getBooleanExtra(
+                TelecomManager.EXTRA_START_CALL_WITH_SPEAKERPHONE, false);
+        int videoState = mIntent.getIntExtra(
+                TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE,
+                VideoProfile.STATE_AUDIO_ONLY);
+        placeOutgoingCallImmediately(mCall, disposition.callingAddress, null,
+                speakerphoneOn, videoState);
+    }
+
+    /**
      * Sends a new outgoing call ordered broadcast so that third party apps can cancel the
      * placement of the call or redirect it to a different number.
      *
@@ -415,28 +478,51 @@
         if (number != null) {
             broadcastIntent.putExtra(Intent.EXTRA_PHONE_NUMBER, number);
         }
-
-        // Force receivers of this broadcast intent to run at foreground priority because we
-        // want to finish processing the broadcast intent as soon as possible.
-        broadcastIntent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND
-                | Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND);
         Log.v(this, "Broadcasting intent: %s.", broadcastIntent);
 
         checkAndCopyProviderExtras(originalCallIntent, broadcastIntent);
 
-        final BroadcastOptions options = BroadcastOptions.makeBasic();
-        options.setBackgroundActivityStartsAllowed(true);
-        mContext.sendOrderedBroadcastAsUser(
-                broadcastIntent,
-                targetUser,
-                android.Manifest.permission.PROCESS_OUTGOING_CALLS,
-                AppOpsManager.OP_PROCESS_OUTGOING_CALLS,
-                options.toBundle(),
-                receiverRequired ? new NewOutgoingCallBroadcastIntentReceiver() : null,
-                null,  // scheduler
-                Activity.RESULT_OK,  // initialCode
-                number,  // initialData: initial value for the result data (number to be modified)
-                null);  // initialExtras
+        if (mFeatureFlags.isNewOutgoingCallBroadcastUnblocking()) {
+            // Where the new outgoing call broadcast is unblocking, do not give receiver FG priority
+            // and do not allow background activity starts.
+            broadcastIntent.addFlags(Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND);
+            Log.i(this, "broadcastIntent: Sending non-blocking for %s to %s", mCall.getId(),
+                    targetUser);
+            if (mFeatureFlags.telecomResolveHiddenDependencies()) {
+                mContext.sendBroadcastAsUser(
+                        broadcastIntent,
+                        targetUser,
+                        Manifest.permission.PROCESS_OUTGOING_CALLS);
+            } else {
+                mContext.sendBroadcastAsUser(
+                        broadcastIntent,
+                        targetUser,
+                        android.Manifest.permission.PROCESS_OUTGOING_CALLS,
+                        AppOpsManager.OP_PROCESS_OUTGOING_CALLS);  // initialExtras
+            }
+        } else {
+            Log.i(this, "broadcastIntent: Sending ordered for %s to %s, waitForResult=%b",
+                    mCall.getId(), targetUser, receiverRequired);
+            final BroadcastOptions options = BroadcastOptions.makeBasic();
+            options.setBackgroundActivityStartsAllowed(true);
+            // Force receivers of this broadcast intent to run at foreground priority because we
+            // want to finish processing the broadcast intent as soon as possible.
+            broadcastIntent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND
+                    | Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND);
+
+            mContext.sendOrderedBroadcastAsUser(
+                    broadcastIntent,
+                    targetUser,
+                    android.Manifest.permission.PROCESS_OUTGOING_CALLS,
+                    AppOpsManager.OP_PROCESS_OUTGOING_CALLS,
+                    options.toBundle(),
+                    receiverRequired ? new NewOutgoingCallBroadcastIntentReceiver() : null,
+                    null,  // scheduler
+                    Activity.RESULT_OK,  // initialCode
+                    number,  // initialData: initial value for the result data (number to be
+                             // modified)
+                    null);  // initialExtras
+        }
     }
 
     /**
diff --git a/src/com/android/server/telecom/ParcelableCallUtils.java b/src/com/android/server/telecom/ParcelableCallUtils.java
index 673b99a..c77e605 100644
--- a/src/com/android/server/telecom/ParcelableCallUtils.java
+++ b/src/com/android/server/telecom/ParcelableCallUtils.java
@@ -158,6 +158,10 @@
             properties |= android.telecom.Call.Details.PROPERTY_VOIP_AUDIO_MODE;
         }
 
+        if (call.isTransactionalCall()) {
+            properties |= android.telecom.Call.Details.PROPERTY_IS_TRANSACTIONAL;
+        }
+
         // If this is a single-SIM device, the "default SIM" will always be the only SIM.
         boolean isDefaultSmsAccount = phoneAccountRegistrar != null &&
                 phoneAccountRegistrar.isUserSelectedSmsPhoneAccount(call.getTargetPhoneAccount());
diff --git a/src/com/android/server/telecom/PhoneAccountRegistrar.java b/src/com/android/server/telecom/PhoneAccountRegistrar.java
index acf07e3..5f23e4d 100644
--- a/src/com/android/server/telecom/PhoneAccountRegistrar.java
+++ b/src/com/android/server/telecom/PhoneAccountRegistrar.java
@@ -61,6 +61,7 @@
 import com.android.internal.util.IndentingPrintWriter;
 import com.android.internal.util.XmlUtils;
 import com.android.modules.utils.ModifiedUtf8;
+import com.android.server.telecom.flags.Flags;
 
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
@@ -389,26 +390,62 @@
                             account.getGroupId()));
         }
 
-        // Potentially update the default voice subid in SubscriptionManager.
+        // Potentially update the default voice subid in SubscriptionManager so that Telephony and
+        // Telecom are in sync.
         int newSubId = accountHandle == null ? SubscriptionManager.INVALID_SUBSCRIPTION_ID :
                 getSubscriptionIdForPhoneAccount(accountHandle);
-        if (isSimAccount || accountHandle == null) {
-            int currentVoiceSubId = mSubscriptionManager.getDefaultVoiceSubscriptionId();
-            if (newSubId != currentVoiceSubId) {
-                Log.i(this, "setUserSelectedOutgoingPhoneAccount: update voice sub; "
-                        + "account=%s, subId=%d", accountHandle, newSubId);
-                mSubscriptionManager.setDefaultVoiceSubscriptionId(newSubId);
+        if (Flags.onlyUpdateTelephonyOnValidSubIds()) {
+            if (shouldUpdateTelephonyDefaultVoiceSubId(accountHandle, isSimAccount, newSubId)) {
+                updateDefaultVoiceSubId(newSubId, accountHandle);
             } else {
-                Log.i(this, "setUserSelectedOutgoingPhoneAccount: no change to voice sub");
+                Log.i(this, "setUserSelectedOutgoingPhoneAccount: %s is not a sub", accountHandle);
             }
         } else {
-            Log.i(this, "setUserSelectedOutgoingPhoneAccount: %s is not a sub", accountHandle);
+            if (isSimAccount || accountHandle == null) {
+                updateDefaultVoiceSubId(newSubId, accountHandle);
+            } else {
+                Log.i(this, "setUserSelectedOutgoingPhoneAccount: %s is not a sub", accountHandle);
+            }
         }
-
         write();
         fireDefaultOutgoingChanged();
     }
 
+    private void updateDefaultVoiceSubId(int newSubId, PhoneAccountHandle accountHandle){
+        int currentVoiceSubId = mSubscriptionManager.getDefaultVoiceSubscriptionId();
+        if (newSubId != currentVoiceSubId) {
+            Log.i(this, "setUserSelectedOutgoingPhoneAccount: update voice sub; "
+                    + "account=%s, subId=%d", accountHandle, newSubId);
+            mSubscriptionManager.setDefaultVoiceSubscriptionId(newSubId);
+        } else {
+            Log.i(this, "setUserSelectedOutgoingPhoneAccount: no change to voice sub");
+        }
+    }
+
+    // This helper is important for CTS testing.  [PhoneAccount]s created by Telecom in CTS are
+    // assigned a  subId value of INVALID_SUBSCRIPTION_ID (-1) by Telephony.  However, when
+    // Telephony has a default outgoing calling voice account of -1, that translates to no default
+    // account (user should be prompted to select an acct when making MOs).  In order to avoid
+    // Telephony clearing out the newly changed default [PhoneAccount] in Telecom, Telephony should
+    // not be updated. This situation will never occur in production since [PhoneAccount]s in
+    // production are assigned non-negative subId values.
+    private boolean shouldUpdateTelephonyDefaultVoiceSubId(PhoneAccountHandle phoneAccountHandle,
+            boolean isSimAccount, int newSubId) {
+        // user requests no call preference
+        if (phoneAccountHandle == null) {
+            return true;
+        }
+        // do not update Telephony if the newSubId is invalid
+        if (newSubId == SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
+            Log.w(this, "shouldUpdateTelephonyDefaultVoiceSubId: "
+                            + "invalid subId scenario, not updating Telephony. "
+                            + "phoneAccountHandle=[%s], isSimAccount=[%b], newSubId=[%s]",
+                    phoneAccountHandle, isSimAccount, newSubId);
+            return false;
+        }
+        return isSimAccount;
+    }
+
     boolean isUserSelectedSmsPhoneAccount(PhoneAccountHandle accountHandle) {
         return getSubscriptionIdForPhoneAccount(accountHandle) ==
                 SubscriptionManager.getDefaultSmsSubscriptionId();
@@ -897,13 +934,15 @@
      * @throws IllegalArgumentException if MAX_PHONE_ACCOUNT_REGISTRATIONS are reached
      */
     private void enforceMaxPhoneAccountLimit(@NonNull PhoneAccount account) {
-        final PhoneAccountHandle accountHandle = account.getAccountHandle();
-        final UserHandle user = accountHandle.getUserHandle();
-        final ComponentName componentName = accountHandle.getComponentName();
-
-        if (getPhoneAccountHandles(0, null, componentName.getPackageName(),
-                true /* includeDisabled */, user, false /* crossUserAccess */).size()
-                >= MAX_PHONE_ACCOUNT_REGISTRATIONS) {
+        List<PhoneAccount> unverifiedAccounts = getAccountsForPackage_BypassResolveComp(
+                account.getAccountHandle().getComponentName().getPackageName(),
+                account.getAccountHandle().getUserHandle());
+        // verify each phone account is backed by a valid ConnectionService. If the
+        // ConnectionService has been disabled or cannot be resolved, unregister the accounts.
+        List<PhoneAccount> verifiedAccounts =
+                cleanupUnresolvableConnectionServiceAccounts(unverifiedAccounts);
+        // enforce the max phone account limit for the application registering accounts
+        if (verifiedAccounts.size() >= MAX_PHONE_ACCOUNT_REGISTRATIONS) {
             EventLog.writeEvent(0x534e4554, "259064622", Binder.getCallingUid(),
                     "enforceMaxPhoneAccountLimit");
             throw new IllegalArgumentException(
@@ -1550,6 +1589,51 @@
     }
 
     /**
+     * This getter should be used when you want to bypass the {@link
+     * PhoneAccountRegistrar#resolveComponent(PhoneAccountHandle)} check when fetching accounts
+     */
+    @VisibleForTesting
+    public List<PhoneAccount> getAccountsForPackage_BypassResolveComp(String packageName,
+            UserHandle userHandle) {
+        List<PhoneAccount> accounts = new ArrayList<>(mState.accounts.size());
+        for (PhoneAccount m : mState.accounts) {
+            PhoneAccountHandle handle = m.getAccountHandle();
+
+            if (packageName != null && !packageName.equals(
+                    handle.getComponentName().getPackageName())) {
+                // Not the right package name; skip this one.
+                continue;
+            }
+
+            if (!isVisibleForUser(m, userHandle, false)) {
+                // Account is not visible for the current user; skip this one.
+                continue;
+            }
+            accounts.add(m);
+        }
+        return accounts;
+    }
+
+    @VisibleForTesting
+    public List<PhoneAccount> cleanupUnresolvableConnectionServiceAccounts(
+            List<PhoneAccount> accounts) {
+        ArrayList<PhoneAccount> verifiedAccounts = new ArrayList<>();
+        for (PhoneAccount account : accounts) {
+            PhoneAccountHandle handle = account.getAccountHandle();
+            // if the ConnectionService has been disabled or can longer be found, remove the handle
+            if (resolveComponent(handle).isEmpty()) {
+                Log.i(this,
+                        "Cannot resolve the ConnectionService for handle=[%s]; unregistering"
+                                + " account", handle);
+                unregisterPhoneAccount(handle);
+            } else {
+                verifiedAccounts.add(account);
+            }
+        }
+        return verifiedAccounts;
+    }
+
+    /**
      * Clean up the orphan {@code PhoneAccount}. An orphan {@code PhoneAccount} is a phone
      * account that does not have a {@code UserHandle} or belongs to a deleted package.
      *
@@ -1662,6 +1746,7 @@
             } else {
                 pw.println(defaultOutgoing);
             }
+            pw.println("defaultVoiceSubId: " + SubscriptionManager.getDefaultVoiceSubscriptionId());
             pw.println("simCallManager: " + getSimCallManager(mCurrentUserHandle));
             pw.println("phoneAccounts:");
             pw.increaseIndent();
diff --git a/src/com/android/server/telecom/RingbackPlayer.java b/src/com/android/server/telecom/RingbackPlayer.java
index a8af3ac..e0c6136 100644
--- a/src/com/android/server/telecom/RingbackPlayer.java
+++ b/src/com/android/server/telecom/RingbackPlayer.java
@@ -19,6 +19,7 @@
 import static com.android.server.telecom.LogUtils.Events.START_RINBACK;
 import static com.android.server.telecom.LogUtils.Events.STOP_RINGBACK;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.Preconditions;
 import android.telecom.Log;
 
@@ -42,8 +43,12 @@
      */
     private InCallTonePlayer mTonePlayer;
 
-    RingbackPlayer(InCallTonePlayer.Factory playerFactory) {
+    private final Object mLock;
+
+    @VisibleForTesting
+    public RingbackPlayer(InCallTonePlayer.Factory playerFactory) {
         mPlayerFactory = playerFactory;
+        mLock = new Object();
     }
 
     /**
@@ -52,25 +57,27 @@
      * @param call The call for which to ringback.
      */
     public void startRingbackForCall(Call call) {
-        Preconditions.checkState(call.getState() == CallState.DIALING);
+        synchronized (mLock) {
+            Preconditions.checkState(call.getState() == CallState.DIALING);
 
-        if (mCall == call) {
-            Log.w(this, "Ignoring duplicate requests to ring for %s.", call);
-            return;
-        }
+            if (mCall == call) {
+                Log.w(this, "Ignoring duplicate requests to ring for %s.", call);
+                return;
+            }
 
-        if (mCall != null) {
-            // We only get here for the foreground call so, there's no reason why there should
-            // exist a current dialing call.
-            Log.wtf(this, "Ringback player thinks there are two foreground-dialing calls.");
-        }
+            if (mCall != null) {
+                // We only get here for the foreground call so, there's no reason why there should
+                // exist a current dialing call.
+                Log.wtf(this, "Ringback player thinks there are two foreground-dialing calls.");
+            }
 
-        mCall = call;
-        if (mTonePlayer == null) {
-            Log.i(this, "Playing the ringback tone for %s.", call);
-            Log.addEvent(call, START_RINBACK);
-            mTonePlayer = mPlayerFactory.createPlayer(InCallTonePlayer.TONE_RING_BACK);
-            mTonePlayer.startTone();
+            mCall = call;
+            if (mTonePlayer == null) {
+                Log.i(this, "Playing the ringback tone for %s.", call);
+                Log.addEvent(call, START_RINBACK);
+                mTonePlayer = mPlayerFactory.createPlayer(InCallTonePlayer.TONE_RING_BACK);
+                mTonePlayer.startTone();
+            }
         }
     }
 
@@ -80,19 +87,27 @@
      * @param call The call for which to stop ringback.
      */
     public void stopRingbackForCall(Call call) {
-        if (mCall == call) {
-            // The foreground call is no longer dialing or is no longer the foreground call. In
-            // either case, stop the ringback tone.
-            mCall = null;
+        synchronized (mLock) {
+            if (mCall == call) {
+                // The foreground call is no longer dialing or is no longer the foreground call. In
+                // either case, stop the ringback tone.
+                mCall = null;
 
-            if (mTonePlayer == null) {
-                Log.w(this, "No player found to stop.");
-            } else {
-                Log.i(this, "Stopping the ringback tone for %s.", call);
-                Log.addEvent(call, STOP_RINGBACK);
-                mTonePlayer.stopTone();
-                mTonePlayer = null;
+                if (mTonePlayer == null) {
+                    Log.w(this, "No player found to stop.");
+                } else {
+                    Log.i(this, "Stopping the ringback tone for %s.", call);
+                    Log.addEvent(call, STOP_RINGBACK);
+                    mTonePlayer.stopTone();
+                    mTonePlayer = null;
+                }
             }
         }
     }
+
+    public boolean isRingbackPlaying() {
+        synchronized (mLock) {
+            return mTonePlayer != null;
+        }
+    }
 }
\ No newline at end of file
diff --git a/src/com/android/server/telecom/Ringer.java b/src/com/android/server/telecom/Ringer.java
index 1710604..5b86b82 100644
--- a/src/com/android/server/telecom/Ringer.java
+++ b/src/com/android/server/telecom/Ringer.java
@@ -22,10 +22,12 @@
 import static android.provider.Settings.Global.ZEN_MODE_OFF;
 
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.app.Notification;
 import android.app.NotificationManager;
 import android.app.Person;
 import android.content.Context;
+import android.content.res.Resources;
 import android.media.AudioManager;
 import android.media.Ringtone;
 import android.media.VolumeShaper;
@@ -38,13 +40,21 @@
 import android.os.VibrationAttributes;
 import android.os.VibrationEffect;
 import android.os.Vibrator;
+import android.os.vibrator.persistence.ParsedVibration;
+import android.os.vibrator.persistence.VibrationXmlParser;
 import android.telecom.Log;
 import android.telecom.TelecomManager;
+import android.text.TextUtils;
 import android.view.accessibility.AccessibilityManager;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.telecom.LogUtils.EventTimer;
+import com.android.server.telecom.flags.FeatureFlags;
 
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.CountDownLatch;
@@ -59,6 +69,8 @@
  */
 @VisibleForTesting
 public class Ringer {
+    private static final String TAG = "TelecomRinger";
+
     public interface AccessibilityManagerAdapter {
         boolean startFlashNotificationSequence(@NonNull Context context,
                 @AccessibilityManager.FlashNotificationReason int reason);
@@ -84,6 +96,16 @@
     // Used for test to notify the completion of RingerAttributes
     private CountDownLatch mAttributesLatch;
 
+    /**
+     * Delay to be used between consecutive vibrations when a non-repeating vibration effect is
+     * provided by the device.
+     *
+     * <p>If looking to customize the loop delay for a device's ring vibration, the desired repeat
+     * behavior should be encoded directly in the effect specification in the device configuration
+     * rather than changing the here (i.e. in `R.raw.default_ringtone_vibration_effect` resource).
+     */
+    private static int DEFAULT_RING_VIBRATION_LOOP_DELAY_MS = 1000;
+
     private static final long[] PULSE_PRIMING_PATTERN = {0,12,250,12,500}; // priming  + interval
 
     private static final int[] PULSE_PRIMING_AMPLITUDE = {0,255,0,255,0};  // priming  + interval
@@ -96,9 +118,11 @@
     private static final int[] PULSE_RAMPING_AMPLITUDE = {
         77,77,78,79,81,84,87,93,101,114,133,162,205,255,255,0};
 
-    private static final long[] PULSE_PATTERN;
+    @VisibleForTesting
+    public static final long[] PULSE_PATTERN;
 
-    private static final int[] PULSE_AMPLITUDE;
+    @VisibleForTesting
+    public static final int[] PULSE_AMPLITUDE;
 
     private static final int RAMPING_RINGER_VIBRATION_DURATION = 5000;
     private static final int RAMPING_RINGER_DURATION = 10000;
@@ -162,6 +186,7 @@
     private final InCallController mInCallController;
     private final VibrationEffectProxy mVibrationEffectProxy;
     private final boolean mIsHapticPlaybackSupportedByDevice;
+    private final FeatureFlags mFlags;
     /**
      * For unit testing purposes only; when set, {@link #startRinging(Call, boolean)} will complete
      * the future provided by the test using {@link #setBlockOnRingingFuture(CompletableFuture)}.
@@ -207,7 +232,8 @@
             VibrationEffectProxy vibrationEffectProxy,
             InCallController inCallController,
             NotificationManager notificationManager,
-            AccessibilityManagerAdapter accessibilityManagerAdapter) {
+            AccessibilityManagerAdapter accessibilityManagerAdapter,
+            FeatureFlags featureFlags) {
 
         mLock = new Object();
         mSystemSettingsUtil = systemSettingsUtil;
@@ -223,18 +249,15 @@
         mNotificationManager = notificationManager;
         mAccessibilityManagerAdapter = accessibilityManagerAdapter;
 
-        if (mContext.getResources().getBoolean(R.bool.use_simple_vibration_pattern)) {
-            mDefaultVibrationEffect = mVibrationEffectProxy.createWaveform(SIMPLE_VIBRATION_PATTERN,
-                    SIMPLE_VIBRATION_AMPLITUDE, REPEAT_SIMPLE_VIBRATION_AT);
-        } else {
-            mDefaultVibrationEffect = mVibrationEffectProxy.createWaveform(PULSE_PATTERN,
-                    PULSE_AMPLITUDE, REPEAT_VIBRATION_AT);
-        }
+        mDefaultVibrationEffect =
+                loadDefaultRingVibrationEffect(
+                        mContext, mVibrator, mVibrationEffectProxy, featureFlags);
 
         mIsHapticPlaybackSupportedByDevice =
                 mSystemSettingsUtil.isHapticPlaybackSupported(mContext);
 
         mAudioManager = mContext.getSystemService(AudioManager.class);
+        mFlags = featureFlags;
     }
 
     @VisibleForTesting
@@ -456,7 +479,7 @@
             };
             deferBlockOnRingingFuture = true;  // Run in vibrationLogic.
             if (ringtoneSupplier != null) {
-                mRingtonePlayer.play(ringtoneSupplier, afterRingtoneLogic);
+                mRingtonePlayer.play(ringtoneSupplier, afterRingtoneLogic, isHfpDeviceAttached);
             } else {
                 afterRingtoneLogic.accept(/* ringtone= */ null, /* stopped= */ false);
             }
@@ -629,14 +652,21 @@
             Log.i(this, "shouldRingForContact: returning computation from DndCallFilter.");
             return !call.isCallSuppressedByDoNotDisturb();
         }
-        final Uri contactUri = call.getHandle();
-        final Bundle peopleExtras = new Bundle();
-        if (contactUri != null) {
-            ArrayList<Person> personList = new ArrayList<>();
-            personList.add(new Person.Builder().setUri(contactUri.toString()).build());
-            peopleExtras.putParcelableArrayList(Notification.EXTRA_PEOPLE_LIST, personList);
+        Uri contactUri = call.getHandle();
+        if (mFlags.telecomResolveHiddenDependencies()) {
+            if (contactUri == null) {
+                contactUri = Uri.EMPTY;
+            }
+            return mNotificationManager.matchesCallFilter(contactUri);
+        } else {
+            final Bundle peopleExtras = new Bundle();
+            if (contactUri != null) {
+                ArrayList<Person> personList = new ArrayList<>();
+                personList.add(new Person.Builder().setUri(contactUri.toString()).build());
+                peopleExtras.putParcelableArrayList(Notification.EXTRA_PEOPLE_LIST, personList);
+            }
+            return mNotificationManager.matchesCallFilter(peopleExtras);
         }
-        return mNotificationManager.matchesCallFilter(peopleExtras);
     }
 
     private boolean hasExternalRinger(Call foregroundCall) {
@@ -757,4 +787,65 @@
             return false;
         }
     }
+
+    @Nullable
+    private static VibrationEffect loadSerializedDefaultRingVibration(
+            Resources resources, Vibrator vibrator) {
+        try {
+            InputStream vibrationInputStream =
+                    resources.openRawResource(
+                            com.android.internal.R.raw.default_ringtone_vibration_effect);
+            ParsedVibration parsedVibration = VibrationXmlParser
+                    .parseDocument(
+                            new InputStreamReader(vibrationInputStream, StandardCharsets.UTF_8));
+            if (parsedVibration == null) {
+                Log.w(TAG, "Got null parsed default ring vibration effect.");
+                return null;
+            }
+            return parsedVibration.resolve(vibrator);
+        } catch (IOException | Resources.NotFoundException e) {
+            Log.e(TAG, e, "Error parsing default ring vibration effect.");
+            return null;
+        }
+    }
+
+    private static VibrationEffect loadDefaultRingVibrationEffect(
+            Context context,
+            Vibrator vibrator,
+            VibrationEffectProxy vibrationEffectProxy,
+            FeatureFlags featureFlags) {
+        Resources resources = context.getResources();
+
+        if (resources.getBoolean(R.bool.use_simple_vibration_pattern)) {
+            Log.i(TAG, "Using simple default ring vibration.");
+            return createSimpleRingVibration(vibrationEffectProxy);
+        }
+
+        if (featureFlags.useDeviceProvidedSerializedRingerVibration()) {
+            VibrationEffect parsedEffect = loadSerializedDefaultRingVibration(resources, vibrator);
+            if (parsedEffect != null) {
+                Log.i(TAG, "Using parsed default ring vibration.");
+                // Make the parsed effect repeating to make it vibrate continuously during ring.
+                // If the effect is already repeating, this API call is a no-op.
+                // Otherwise, it  uses `DEFAULT_RING_VIBRATION_LOOP_DELAY_MS` when changing a
+                // non-repeating vibration to a repeating vibration.
+                // This is so that we ensure consecutive loops of the vibration play with some gap
+                // in between.
+                return parsedEffect.applyRepeatingIndefinitely(
+                        /* wantRepeating= */ true, DEFAULT_RING_VIBRATION_LOOP_DELAY_MS);
+            }
+            // Fallback to the simple vibration if the serialized effect cannot be loaded.
+            return createSimpleRingVibration(vibrationEffectProxy);
+        }
+
+        Log.i(TAG, "Using pulse default ring vibration.");
+        return vibrationEffectProxy.createWaveform(
+                PULSE_PATTERN, PULSE_AMPLITUDE, REPEAT_VIBRATION_AT);
+    }
+
+    private static VibrationEffect createSimpleRingVibration(
+            VibrationEffectProxy vibrationEffectProxy) {
+        return vibrationEffectProxy.createWaveform(SIMPLE_VIBRATION_PATTERN,
+                SIMPLE_VIBRATION_AMPLITUDE, REPEAT_SIMPLE_VIBRATION_AT);
+    }
 }
diff --git a/src/com/android/server/telecom/TelecomServiceImpl.java b/src/com/android/server/telecom/TelecomServiceImpl.java
index f33b185..1dd68c9 100644
--- a/src/com/android/server/telecom/TelecomServiceImpl.java
+++ b/src/com/android/server/telecom/TelecomServiceImpl.java
@@ -79,6 +79,7 @@
 import com.android.internal.telecom.ITelecomService;
 import com.android.internal.util.IndentingPrintWriter;
 import com.android.server.telecom.components.UserCallIntentProcessorFactory;
+import com.android.server.telecom.flags.FeatureFlags;
 import com.android.server.telecom.settings.BlockedNumbersActivity;
 import com.android.server.telecom.voip.IncomingCallTransaction;
 import com.android.server.telecom.voip.OutgoingCallTransaction;
@@ -88,6 +89,7 @@
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
+import java.lang.reflect.Method;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Objects;
@@ -1551,6 +1553,23 @@
                             }
                             mCallIntentProcessorAdapter.processIncomingCallIntent(
                                     mCallsManager, intent);
+                            if (mFeatureFlags.earlyBindingToIncallService()) {
+                                PhoneAccount account =
+                                        mPhoneAccountRegistrar.getPhoneAccountUnchecked(
+                                                phoneAccountHandle);
+                                Bundle accountExtra =
+                                        account == null ? new Bundle() : account.getExtras();
+                                PackageManager packageManager = mContext.getPackageManager();
+                                // Start binding to InCallServices for wearable calls that do not
+                                // require call filtering. This is to wake up default dialer earlier
+                                // to mitigate InCallService binding latency.
+                                if (packageManager.hasSystemFeature(PackageManager.FEATURE_WATCH)
+                                        && accountExtra != null && accountExtra.getBoolean(
+                                        PhoneAccount.EXTRA_SKIP_CALL_FILTERING,
+                                        false)) {
+                                    mCallsManager.getInCallController().bindToServices(null);
+                                }
+                            }
                         } finally {
                             Binder.restoreCallingIdentity(token);
                         }
@@ -1967,6 +1986,11 @@
                 pw.increaseIndent();
                 Analytics.dump(pw);
                 pw.decreaseIndent();
+
+                pw.println("Flag Configurations: ");
+                pw.increaseIndent();
+                reflectAndPrintFlagConfigs(pw);
+                pw.decreaseIndent();
             }
             if (isTimeLineView) {
                 Log.dumpEventsTimeline(pw);
@@ -1976,6 +2000,28 @@
         }
 
         /**
+         * Print all feature flag configurations that Telecom is using for debugging purposes.
+         */
+        private void reflectAndPrintFlagConfigs(IndentingPrintWriter pw) {
+
+            try {
+                // Look away, a forbidden technique (reflection) is being used to allow us to get
+                // all flag configs without having to add them manually to this method.
+                Method[] methods = FeatureFlags.class.getMethods();
+                if (methods.length == 0) {
+                    pw.println("NONE");
+                    return;
+                }
+                for (Method m : methods) {
+                    pw.println(m.getName() + "-> " + m.invoke(mFeatureFlags));
+                }
+            } catch (Exception e) {
+                pw.println("[ERROR]");
+            }
+
+        }
+
+        /**
          * @see android.telecom.TelecomManager#createManageBlockedNumbersIntent
          */
         @Override
@@ -2138,7 +2184,7 @@
                     try {
                         Log.i(this, "handleCallIntent: handling call intent");
                         mCallIntentProcessorAdapter.processOutgoingCallIntent(mContext,
-                                mCallsManager, intent, callingPackage);
+                                mCallsManager, intent, callingPackage, mFeatureFlags);
                     } finally {
                         Binder.restoreCallingIdentity(token);
                     }
@@ -2212,6 +2258,39 @@
         }
 
         /**
+         * A method intended for use in testing to query whether a particular non-ui inCallService
+         * is bound in a call.
+         * @param packageName of the service to query.
+         * @return whether it is bound or not.
+         */
+        @Override
+        public boolean isNonUiInCallServiceBound(String packageName) {
+            Log.startSession("TCI.iNUICSB");
+            try {
+                synchronized (mLock) {
+                    enforceShellOnly(Binder.getCallingUid(), "isNonUiInCallServiceBound");
+                    if (!(mContext.checkCallingOrSelfPermission(READ_PHONE_STATE)
+                            == PackageManager.PERMISSION_GRANTED) ||
+                            !(mContext.checkCallingOrSelfPermission(READ_PRIVILEGED_PHONE_STATE)
+                                    == PackageManager.PERMISSION_GRANTED)) {
+                        throw new SecurityException("isNonUiInCallServiceBound requires the"
+                                + " READ_PHONE_STATE or READ_PRIVILEGED_PHONE_STATE permission");
+                    }
+                    long token = Binder.clearCallingIdentity();
+                    try {
+                        return mCallsManager
+                                .getInCallController()
+                                .isNonUiInCallServiceBound(packageName);
+                    } finally {
+                        Binder.restoreCallingIdentity(token);
+                    }
+                }
+            } finally {
+                Log.endSession();
+            }
+        }
+
+        /**
          * A method intended for use in testing to reset car mode at all priorities.
          *
          * Runs during setup to avoid cascading failures from failing car mode CTS.
@@ -2478,6 +2557,7 @@
     private final TelecomSystem.SyncRoot mLock;
     private TransactionManager mTransactionManager;
     private final TransactionalServiceRepository mTransactionalServiceRepository;
+    private final FeatureFlags mFeatureFlags;
 
     public TelecomServiceImpl(
             Context context,
@@ -2488,6 +2568,7 @@
             DefaultDialerCache defaultDialerCache,
             SubscriptionManagerAdapter subscriptionManagerAdapter,
             SettingsSecureAdapter settingsSecureAdapter,
+            FeatureFlags featureFlags,
             TelecomSystem.SyncRoot lock) {
         mContext = context;
         mAppOpsManager = (AppOpsManager) mContext.getSystemService(Context.APP_OPS_SERVICE);
@@ -2495,6 +2576,7 @@
         mPackageManager = mContext.getPackageManager();
 
         mCallsManager = callsManager;
+        mFeatureFlags = featureFlags;
         mLock = lock;
         mPhoneAccountRegistrar = phoneAccountRegistrar;
         mUserCallIntentProcessorFactory = userCallIntentProcessorFactory;
@@ -2707,6 +2789,7 @@
         int packageUid = -1;
         int callingUid = Binder.getCallingUid();
         PackageManager pm;
+        long token = Binder.clearCallingIdentity();
         try{
             pm = mContext.createContextAsUser(
                     UserHandle.getUserHandleForUid(callingUid), 0).getPackageManager();
@@ -2715,6 +2798,8 @@
             Log.i(this, "callingUidMatchesPackageManagerRecords:"
                             + " createContextAsUser hit exception=[%s]", e.toString());
             return false;
+        } finally {
+            Binder.restoreCallingIdentity(token);
         }
         if (pm != null) {
             try {
diff --git a/src/com/android/server/telecom/TelecomSystem.java b/src/com/android/server/telecom/TelecomSystem.java
index 67bb81f..57d7139 100644
--- a/src/com/android/server/telecom/TelecomSystem.java
+++ b/src/com/android/server/telecom/TelecomSystem.java
@@ -45,8 +45,12 @@
 import com.android.server.telecom.bluetooth.BluetoothRouteManager;
 import com.android.server.telecom.bluetooth.BluetoothStateReceiver;
 import com.android.server.telecom.callfiltering.BlockedNumbersAdapter;
+import com.android.server.telecom.callfiltering.CallFilterResultCallback;
+import com.android.server.telecom.callfiltering.IncomingCallFilterGraph;
+import com.android.server.telecom.callfiltering.IncomingCallFilterGraphProvider;
 import com.android.server.telecom.components.UserCallIntentProcessor;
 import com.android.server.telecom.components.UserCallIntentProcessorFactory;
+import com.android.server.telecom.flags.FeatureFlags;
 import com.android.server.telecom.ui.AudioProcessingNotification;
 import com.android.server.telecom.ui.CallStreamingNotification;
 import com.android.server.telecom.ui.DisconnectedCallNotifier;
@@ -223,7 +227,9 @@
             DeviceIdleControllerAdapter deviceIdleControllerAdapter,
             Ringer.AccessibilityManagerAdapter accessibilityManagerAdapter,
             Executor asyncTaskExecutor,
-            BlockedNumbersAdapter blockedNumbersAdapter) {
+            Executor asyncCallAudioTaskExecutor,
+            BlockedNumbersAdapter blockedNumbersAdapter,
+            FeatureFlags featureFlags) {
         mContext = context.getApplicationContext();
         LogUtils.initLogging(mContext);
         android.telecom.Log.setLock(mLock);
@@ -249,13 +255,17 @@
                             return context.getContentResolver().openInputStream(uri);
                         }
                     });
+            CallAudioCommunicationDeviceTracker communicationDeviceTracker = new
+                    CallAudioCommunicationDeviceTracker(mContext);
             BluetoothDeviceManager bluetoothDeviceManager = new BluetoothDeviceManager(mContext,
-                    mContext.getSystemService(BluetoothManager.class).getAdapter());
+                    mContext.getSystemService(BluetoothManager.class).getAdapter(),
+                    communicationDeviceTracker);
             BluetoothRouteManager bluetoothRouteManager = new BluetoothRouteManager(mContext, mLock,
-                    bluetoothDeviceManager, new Timeouts.Adapter());
+                    bluetoothDeviceManager, new Timeouts.Adapter(), communicationDeviceTracker);
             BluetoothStateReceiver bluetoothStateReceiver = new BluetoothStateReceiver(
-                    bluetoothDeviceManager, bluetoothRouteManager);
+                    bluetoothDeviceManager, bluetoothRouteManager, communicationDeviceTracker);
             mContext.registerReceiver(bluetoothStateReceiver, BluetoothStateReceiver.INTENT_FILTER);
+            communicationDeviceTracker.setBluetoothRouteManager(bluetoothRouteManager);
 
             WiredHeadsetManager wiredHeadsetManager = new WiredHeadsetManager(mContext);
             SystemStateHelper systemStateHelper = new SystemStateHelper(mContext, mLock);
@@ -396,10 +406,14 @@
                     callAnomalyWatchdog,
                     accessibilityManagerAdapter,
                     asyncTaskExecutor,
+                    asyncCallAudioTaskExecutor,
                     blockedNumbersAdapter,
                     transactionManager,
                     emergencyCallDiagnosticLogger,
-                    callStreamingNotification);
+                    communicationDeviceTracker,
+                    callStreamingNotification,
+                    featureFlags,
+                    IncomingCallFilterGraph::new);
 
             mIncomingCallNotifier = incomingCallNotifier;
             incomingCallNotifier.setCallsManagerProxy(new IncomingCallNotifier.CallsManagerProxy() {
@@ -443,7 +457,7 @@
             }
 
             mCallIntentProcessor = new CallIntentProcessor(mContext, mCallsManager,
-                    defaultDialerCache);
+                    defaultDialerCache, featureFlags);
             mTelecomBroadcastIntentProcessor = new TelecomBroadcastIntentProcessor(
                     mContext, mCallsManager);
 
@@ -467,6 +481,7 @@
                     defaultDialerCache,
                     new TelecomServiceImpl.SubscriptionManagerAdapterImpl(),
                     new TelecomServiceImpl.SettingsSecureAdapterImpl(),
+                    featureFlags,
                     mLock);
         } finally {
             Log.endSession();
diff --git a/src/com/android/server/telecom/TransactionalServiceWrapper.java b/src/com/android/server/telecom/TransactionalServiceWrapper.java
index 25aaad7..02ccef7 100644
--- a/src/com/android/server/telecom/TransactionalServiceWrapper.java
+++ b/src/com/android/server/telecom/TransactionalServiceWrapper.java
@@ -451,7 +451,8 @@
 
                         @Override
                         public void onError(CallException exception) {
-                            Log.i(TAG, "onSetInactive: onError: with e=[%e]", exception);
+                            Log.w(TAG, "onSetInactive: onError: e.code=[%d], e.msg=[%s]",
+                                    exception.getCode(), exception.getMessage());
                         }
                     });
         } finally {
@@ -498,8 +499,9 @@
 
                         @Override
                         public void onError(CallException exception) {
-                            Log.i(TAG, "onCallStreamingStarted: onError: with e=[%e]",
-                                    exception);
+                            Log.w(TAG, "onCallStreamingStarted: onError: "
+                                            + "e.code=[%d], e.msg=[%s]",
+                                    exception.getCode(), exception.getMessage());
                             stopCallStreaming(call);
                         }
                     }
diff --git a/src/com/android/server/telecom/UserUtil.java b/src/com/android/server/telecom/UserUtil.java
index a304401..d0a561a 100644
--- a/src/com/android/server/telecom/UserUtil.java
+++ b/src/com/android/server/telecom/UserUtil.java
@@ -16,10 +16,16 @@
 
 package com.android.server.telecom;
 
+import android.app.admin.DevicePolicyManager;
 import android.content.Context;
+import android.content.Intent;
 import android.content.pm.UserInfo;
+import android.net.Uri;
 import android.os.UserHandle;
 import android.os.UserManager;
+import android.telecom.Log;
+
+import com.android.server.telecom.components.ErrorDialogActivity;
 
 public final class UserUtil {
 
@@ -40,4 +46,57 @@
         UserInfo userInfo = getUserInfoFromUserHandle(context, userHandle);
         return userInfo != null && userInfo.profileGroupId != userInfo.id;
     }
+
+    public static void showErrorDialogForRestrictedOutgoingCall(Context context,
+            int stringId, String tag, String reason) {
+        final Intent intent = new Intent(context, ErrorDialogActivity.class);
+        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        intent.putExtra(ErrorDialogActivity.ERROR_MESSAGE_ID_EXTRA, stringId);
+        context.startActivityAsUser(intent, UserHandle.CURRENT);
+        Log.w(tag, "Rejecting non-emergency phone call because "
+                + reason);
+    }
+
+    public static boolean hasOutgoingCallsUserRestriction(Context context,
+            UserHandle userHandle, Uri handle, boolean isSelfManaged, String tag) {
+        // Set handle for conference calls. Refer to {@link Connection#ADHOC_CONFERENCE_ADDRESS}.
+        if (handle == null) {
+            handle = Uri.parse("tel:conf-factory");
+        }
+
+        if(!isSelfManaged) {
+            // Check DISALLOW_OUTGOING_CALLS restriction. Note: We are skipping this
+            // check in a managed profile user because this check can always be bypassed
+            // by copying and pasting the phone number into the personal dialer.
+            if (!UserUtil.isManagedProfile(context, userHandle)) {
+                // Only emergency calls are allowed for users with the DISALLOW_OUTGOING_CALLS
+                // restriction.
+                if (!TelephonyUtil.shouldProcessAsEmergency(context, handle)) {
+                    final UserManager userManager =
+                            (UserManager) context.getSystemService(Context.USER_SERVICE);
+                    if (userManager.hasBaseUserRestriction(UserManager.DISALLOW_OUTGOING_CALLS,
+                            userHandle)) {
+                        String reason = "of DISALLOW_OUTGOING_CALLS restriction";
+                        showErrorDialogForRestrictedOutgoingCall(context,
+                                R.string.outgoing_call_not_allowed_user_restriction, tag, reason);
+                        return true;
+                    } else if (userManager.hasUserRestriction(UserManager.DISALLOW_OUTGOING_CALLS,
+                            userHandle)) {
+                        final DevicePolicyManager dpm =
+                                context.getSystemService(DevicePolicyManager.class);
+                        if (dpm == null) {
+                            return true;
+                        }
+                        final Intent adminSupportIntent = dpm.createAdminSupportIntent(
+                                UserManager.DISALLOW_OUTGOING_CALLS);
+                        if (adminSupportIntent != null) {
+                            context.startActivityAsUser(adminSupportIntent, userHandle);
+                        }
+                        return true;
+                    }
+                }
+            }
+        }
+        return false;
+    }
 }
diff --git a/src/com/android/server/telecom/bluetooth/BluetoothDeviceManager.java b/src/com/android/server/telecom/bluetooth/BluetoothDeviceManager.java
index 473e7b9..e32f72c 100644
--- a/src/com/android/server/telecom/bluetooth/BluetoothDeviceManager.java
+++ b/src/com/android/server/telecom/bluetooth/BluetoothDeviceManager.java
@@ -27,22 +27,25 @@
 import android.content.Context;
 import android.media.AudioManager;
 import android.media.AudioDeviceInfo;
-import android.media.audio.common.AudioDevice;
+import android.os.Bundle;
 import android.telecom.Log;
+import android.util.ArraySet;
 import android.util.LocalLog;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.IndentingPrintWriter;
+import com.android.server.telecom.CallAudioCommunicationDeviceTracker;
 
 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.Executor;
 
 public class BluetoothDeviceManager {
 
@@ -58,7 +61,8 @@
             public void onGroupStatusChanged(int groupId, int groupStatus) {}
             @Override
             public void onGroupNodeAdded(BluetoothDevice device, int groupId) {
-                Log.i(this, device.getAddress() + " group added " + groupId);
+                Log.i(this, (device == null ? "device is null" : device.getAddress())
+                        + " group added " + groupId);
                 if (device == null || groupId == BluetoothLeAudio.GROUP_ID_INVALID) {
                     Log.w(this, "invalid parameter");
                     return;
@@ -70,6 +74,8 @@
             }
             @Override
             public void onGroupNodeRemoved(BluetoothDevice device, int groupId) {
+                Log.i(this, (device == null ? "device is null" : device.getAddress())
+                        + " group removed " + groupId);
                 if (device == null || groupId == BluetoothLeAudio.GROUP_ID_INVALID) {
                     Log.w(this, "invalid parameter");
                     return;
@@ -85,7 +91,7 @@
             new BluetoothProfile.ServiceListener() {
                 @Override
                 public void onServiceConnected(int profile, BluetoothProfile proxy) {
-                    Log.startSession("BMSL.oSC");
+                    Log.startSession("BPSL.oSC");
                     try {
                         synchronized (mLock) {
                             String logString;
@@ -101,9 +107,13 @@
                                 logString = "Got BluetoothLeAudio: "
                                         + mBluetoothLeAudioService;
                                 if (!mLeAudioCallbackRegistered) {
-                                    mBluetoothLeAudioService.registerCallback(
-                                                mExecutor, mLeAudioCallbacks);
-                                    mLeAudioCallbackRegistered = true;
+                                    try {
+                                        mBluetoothLeAudioService.registerCallback(
+                                                    mExecutor, mLeAudioCallbacks);
+                                        mLeAudioCallbackRegistered = true;
+                                    } catch (IllegalStateException e) {
+                                        logString += ", but Bluetooth is down";
+                                    }
                                 }
                             } else {
                                 logString = "Connected to non-requested bluetooth service." +
@@ -119,7 +129,7 @@
 
                 @Override
                 public void onServiceDisconnected(int profile) {
-                    Log.startSession("BMSL.oSD");
+                    Log.startSession("BPSL.oSD");
                     try {
                         synchronized (mLock) {
                             LinkedHashMap<String, BluetoothDevice> lostServiceDevices;
@@ -174,6 +184,12 @@
             new LinkedHashMap<>();
     private final LinkedHashMap<BluetoothDevice, Integer> mGroupsByDevice =
             new LinkedHashMap<>();
+    private final ArrayList<LinkedHashMap<String, BluetoothDevice>>
+            mDevicesByAddressMaps = new ArrayList<LinkedHashMap<String, BluetoothDevice>>(); {
+        mDevicesByAddressMaps.add(mHfpDevicesByAddress);
+        mDevicesByAddressMaps.add(mHearingAidDevicesByAddress);
+        mDevicesByAddressMaps.add(mLeAudioDevicesByAddress);
+    }
     private int mGroupIdActive = BluetoothLeAudio.GROUP_ID_INVALID;
     private int mGroupIdPending = BluetoothLeAudio.GROUP_ID_INVALID;
     private final LocalLog mLocalLog = new LocalLog(20);
@@ -194,8 +210,10 @@
     private BluetoothAdapter mBluetoothAdapter;
     private AudioManager mAudioManager;
     private Executor mExecutor;
+    private CallAudioCommunicationDeviceTracker mCommunicationDeviceTracker;
 
-    public BluetoothDeviceManager(Context context, BluetoothAdapter bluetoothAdapter) {
+    public BluetoothDeviceManager(Context context, BluetoothAdapter bluetoothAdapter,
+            CallAudioCommunicationDeviceTracker communicationDeviceTracker) {
         if (bluetoothAdapter != null) {
             mBluetoothAdapter = bluetoothAdapter;
             bluetoothAdapter.getProfileProxy(context, mBluetoothProfileServiceListener,
@@ -206,6 +224,7 @@
                     BluetoothProfile.LE_AUDIO);
             mAudioManager = context.getSystemService(AudioManager.class);
             mExecutor = context.getMainExecutor();
+            mCommunicationDeviceTracker = communicationDeviceTracker;
         }
     }
 
@@ -234,18 +253,38 @@
     }
 
     public int getNumConnectedDevices() {
-        synchronized (mLock) {
-            return mHfpDevicesByAddress.size() +
-                    mHearingAidDevicesByAddress.size() +
-                    getLeAudioConnectedDevices().size();
-        }
+        return getConnectedDevices().size();
     }
 
     public Collection<BluetoothDevice> getConnectedDevices() {
         synchronized (mLock) {
-            ArrayList<BluetoothDevice> result = new ArrayList<>(mHfpDevicesByAddress.values());
+            ArraySet<BluetoothDevice> result = new ArraySet<>();
+
+            // Set storing the group ids of all dual mode audio devices to de-dupe them
+            Set<Integer> dualModeGroupIds = new ArraySet<>();
+            for (BluetoothDevice hfpDevice: mHfpDevicesByAddress.values()) {
+                result.add(hfpDevice);
+                if (mBluetoothLeAudioService == null) {
+                    continue;
+                }
+                int groupId = mBluetoothLeAudioService.getGroupId(hfpDevice);
+                if (groupId != BluetoothLeAudio.GROUP_ID_INVALID) {
+                    dualModeGroupIds.add(groupId);
+                }
+            }
+
             result.addAll(mHearingAidDevicesByAddress.values());
-            result.addAll(getLeAudioConnectedDevices());
+            if (mBluetoothLeAudioService == null) {
+                return Collections.unmodifiableCollection(result);
+            }
+            for (BluetoothDevice leAudioDevice: getLeAudioConnectedDevices()) {
+                // Exclude dual mode audio devices included from the HFP devices list
+                int groupId = mBluetoothLeAudioService.getGroupId(leAudioDevice);
+                if (groupId != BluetoothLeAudio.GROUP_ID_INVALID
+                        && !dualModeGroupIds.contains(groupId)) {
+                    result.add(leAudioDevice);
+                }
+            }
             return Collections.unmodifiableCollection(result);
         }
     }
@@ -253,9 +292,9 @@
     // Same as getConnectedDevices except it filters out the hearing aid devices that are linked
     // together by their hiSyncId.
     public Collection<BluetoothDevice> getUniqueConnectedDevices() {
-        ArrayList<BluetoothDevice> result;
+        ArraySet<BluetoothDevice> result;
         synchronized (mLock) {
-            result = new ArrayList<>(mHfpDevicesByAddress.values());
+            result = new ArraySet<>(mHfpDevicesByAddress.values());
         }
         Set<Long> seenHiSyncIds = new LinkedHashSet<>();
         // Add the left-most active device to the seen list so that we match up with the list
@@ -330,8 +369,10 @@
         }
     }
 
-    void onDeviceConnected(BluetoothDevice device, int deviceType) {
+    @VisibleForTesting
+    public void onDeviceConnected(BluetoothDevice device, int deviceType) {
         synchronized (mLock) {
+            clearDeviceFromDeviceMaps(device.getAddress());
             LinkedHashMap<String, BluetoothDevice> targetDeviceMap;
             if (deviceType == DEVICE_TYPE_LE_AUDIO) {
                 if (mBluetoothLeAudioService == null) {
@@ -367,12 +408,20 @@
                 return;
             }
             if (!targetDeviceMap.containsKey(device.getAddress())) {
+                Log.i(this, "Adding device with address: " + device + " and devicetype="
+                        + getDeviceTypeString(deviceType));
                 targetDeviceMap.put(device.getAddress(), device);
                 mBluetoothRouteManager.onDeviceAdded(device.getAddress());
             }
         }
     }
 
+    void clearDeviceFromDeviceMaps(String deviceAddress) {
+        for (LinkedHashMap<String, BluetoothDevice> deviceMap : mDevicesByAddressMaps) {
+            deviceMap.remove(deviceAddress);
+        }
+    }
+
     void onDeviceDisconnected(BluetoothDevice device, int deviceType) {
         mLocalLog.log("Device disconnected -- address: " + device.getAddress() + " deviceType: "
                 + deviceType);
@@ -391,6 +440,8 @@
                 return;
             }
             if (targetDeviceMap.containsKey(device.getAddress())) {
+                Log.i(this, "Removing device with address: " + device + " and devicetype="
+                        + getDeviceTypeString(deviceType));
                 targetDeviceMap.remove(device.getAddress());
                 mBluetoothRouteManager.onDeviceLost(device.getAddress());
             }
@@ -398,12 +449,7 @@
     }
 
     public void disconnectAudio() {
-        disconnectSco();
-        clearLeAudioCommunicationDevice();
-        clearHearingAidCommunicationDevice();
-    }
-
-    public void disconnectSco() {
+        mCommunicationDeviceTracker.clearBtCommunicationDevice();
         if (mBluetoothHeadset == null) {
             Log.w(this, "Trying to disconnect audio but no headset service exists.");
         } else {
@@ -419,13 +465,9 @@
         return mHearingAidSetAsCommunicationDevice;
     }
 
-    public void clearLeAudioCommunicationDevice() {
+    public void clearLeAudioOrSpeakerCommunicationDevice() {
         Log.i(this, "clearLeAudioCommunicationDevice: mLeAudioSetAsCommunicationDevice = " +
                 mLeAudioSetAsCommunicationDevice + " device = " + mLeAudioDevice);
-        if (!mLeAudioSetAsCommunicationDevice) {
-            return;
-        }
-        mLeAudioSetAsCommunicationDevice = false;
         if (mLeAudioDevice != null) {
             mBluetoothRouteManager.onAudioLost(mLeAudioDevice);
             mLeAudioDevice = null;
@@ -437,20 +479,20 @@
         }
 
         AudioDeviceInfo audioDeviceInfo = mAudioManager.getCommunicationDevice();
-        if (audioDeviceInfo != null && audioDeviceInfo.getType()
-                == AudioDeviceInfo.TYPE_BLE_HEADSET) {
-            mBluetoothRouteManager.onAudioLost(audioDeviceInfo.getAddress());
-            mAudioManager.clearCommunicationDevice();
+        if (audioDeviceInfo != null) {
+            if (audioDeviceInfo.getType() == AudioDeviceInfo.TYPE_BLE_HEADSET) {
+                mBluetoothRouteManager.onAudioLost(audioDeviceInfo.getAddress());
+                mAudioManager.clearCommunicationDevice();
+            } else if (audioDeviceInfo.getType() == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER) {
+                mAudioManager.clearCommunicationDevice();
+            }
         }
+        mLeAudioSetAsCommunicationDevice = false;
     }
 
-    public void clearHearingAidCommunicationDevice() {
+    public void clearHearingAidOrSpeakerCommunicationDevice() {
         Log.i(this, "clearHearingAidCommunicationDevice: mHearingAidSetAsCommunicationDevice = "
                 + mHearingAidSetAsCommunicationDevice);
-        if (!mHearingAidSetAsCommunicationDevice) {
-            return;
-        }
-        mHearingAidSetAsCommunicationDevice = false;
         if (mHearingAidDevice != null) {
             mBluetoothRouteManager.onAudioLost(mHearingAidDevice);
             mHearingAidDevice = null;
@@ -462,10 +504,15 @@
         }
 
         AudioDeviceInfo audioDeviceInfo = mAudioManager.getCommunicationDevice();
-        if (audioDeviceInfo != null && audioDeviceInfo.getType()
-                == AudioDeviceInfo.TYPE_HEARING_AID) {
-            mAudioManager.clearCommunicationDevice();
+        if (audioDeviceInfo != null) {
+            if (audioDeviceInfo.getType() == AudioDeviceInfo.TYPE_BLE_HEADSET) {
+                mBluetoothRouteManager.onAudioLost(audioDeviceInfo.getAddress());
+                mAudioManager.clearCommunicationDevice();
+            } else if (audioDeviceInfo.getType() == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER) {
+                mAudioManager.clearCommunicationDevice();
+            }
         }
+        mHearingAidSetAsCommunicationDevice = false;
     }
 
     public boolean setLeAudioCommunicationDevice() {
@@ -502,7 +549,7 @@
         }
 
         // clear hearing aid communication device if set
-        clearHearingAidCommunicationDevice();
+        clearHearingAidOrSpeakerCommunicationDevice();
 
         // Turn BLE_OUT_HEADSET ON.
         boolean result = mAudioManager.setCommunicationDevice(bleHeadset);
@@ -551,7 +598,7 @@
         }
 
         // clear LE audio communication device if set
-        clearLeAudioCommunicationDevice();
+        clearLeAudioOrSpeakerCommunicationDevice();
 
         // Turn hearing aid ON.
         boolean result = mAudioManager.setCommunicationDevice(hearingAid);
@@ -568,50 +615,74 @@
     // Connect audio to the bluetooth device at address, checking to see whether it's
     // le audio, hearing aid or a HFP device, and using the proper BT API.
     public boolean connectAudio(String address, boolean switchingBtDevices) {
+        int callProfile = BluetoothProfile.LE_AUDIO;
+        Log.i(this, "Telecomm connecting audio to device: " + address);
+        BluetoothDevice device = null;
         if (mLeAudioDevicesByAddress.containsKey(address)) {
+            Log.i(this, "Telecomm found LE Audio device for address: " + address);
             if (mBluetoothLeAudioService == null) {
                 Log.w(this, "Attempting to turn on audio when the le audio service is null");
                 return false;
             }
-            BluetoothDevice device = mLeAudioDevicesByAddress.get(address);
-            if (mBluetoothAdapter.setActiveDevice(
-                    device, BluetoothAdapter.ACTIVE_DEVICE_ALL)) {
-
-                /* ACTION_ACTIVE_DEVICE_CHANGED intent will trigger setting communication device.
-                 * Only after receiving ACTION_ACTIVE_DEVICE_CHANGED it is known that device that
-                 * will be audio switched to is available to be choose as communication device */
-                if (!switchingBtDevices) {
-                    return setLeAudioCommunicationDevice();
-                }
-
-                return true;
-            }
-            return false;
+            device = mLeAudioDevicesByAddress.get(address);
+            callProfile = BluetoothProfile.LE_AUDIO;
         } else if (mHearingAidDevicesByAddress.containsKey(address)) {
+            Log.i(this, "Telecomm found hearing aid device for address: " + address);
             if (mBluetoothHearingAid == null) {
                 Log.w(this, "Attempting to turn on audio when the hearing aid service is null");
                 return false;
             }
-            if (mBluetoothAdapter.setActiveDevice(
-                    mHearingAidDevicesByAddress.get(address),
-                    BluetoothAdapter.ACTIVE_DEVICE_ALL)) {
-
-                /* ACTION_ACTIVE_DEVICE_CHANGED intent will trigger setting communication device.
-                 * Only after receiving ACTION_ACTIVE_DEVICE_CHANGED it is known that device that
-                 * will be audio switched to is available to be choose as communication device */
-                if (!switchingBtDevices) {
-                    return setHearingAidCommunicationDevice();
-                }
-
-                return true;
-            }
-            return false;
+            device = mHearingAidDevicesByAddress.get(address);
+            callProfile = BluetoothProfile.HEARING_AID;
         } else if (mHfpDevicesByAddress.containsKey(address)) {
-            BluetoothDevice device = mHfpDevicesByAddress.get(address);
+            Log.i(this, "Telecomm found HFP device for address: " + address);
             if (mBluetoothHeadset == null) {
                 Log.w(this, "Attempting to turn on audio when the headset service is null");
                 return false;
             }
+            device = mHfpDevicesByAddress.get(address);
+            callProfile = BluetoothProfile.HEADSET;
+        }
+
+        if (device == null) {
+            Log.w(this, "No active profiles for Bluetooth address=" + address);
+            return false;
+        }
+
+        Bundle preferredAudioProfiles = mBluetoothAdapter.getPreferredAudioProfiles(device);
+        if (preferredAudioProfiles != null && !preferredAudioProfiles.isEmpty()
+            && preferredAudioProfiles.getInt(BluetoothAdapter.AUDIO_MODE_DUPLEX) != 0) {
+            Log.i(this, "Preferred duplex profile for device=" + address + " is "
+                + preferredAudioProfiles.getInt(BluetoothAdapter.AUDIO_MODE_DUPLEX));
+            callProfile = preferredAudioProfiles.getInt(BluetoothAdapter.AUDIO_MODE_DUPLEX);
+        }
+
+        if (callProfile == BluetoothProfile.LE_AUDIO) {
+            if (mBluetoothAdapter.setActiveDevice(
+                    device, BluetoothAdapter.ACTIVE_DEVICE_ALL)) {
+                /* ACTION_ACTIVE_DEVICE_CHANGED intent will trigger setting communication device.
+                 * Only after receiving ACTION_ACTIVE_DEVICE_CHANGED it is known that device that
+                 * will be audio switched to is available to be choose as communication device */
+                if (!switchingBtDevices) {
+                    return mCommunicationDeviceTracker.setCommunicationDevice(
+                            AudioDeviceInfo.TYPE_BLE_HEADSET, device);
+                }
+                return true;
+            }
+            return false;
+        } else if (callProfile == BluetoothProfile.HEARING_AID) {
+            if (mBluetoothAdapter.setActiveDevice(device, BluetoothAdapter.ACTIVE_DEVICE_ALL)) {
+                /* ACTION_ACTIVE_DEVICE_CHANGED intent will trigger setting communication device.
+                 * Only after receiving ACTION_ACTIVE_DEVICE_CHANGED it is known that device that
+                 * will be audio switched to is available to be choose as communication device */
+                if (!switchingBtDevices) {
+                    return mCommunicationDeviceTracker.setCommunicationDevice(
+                            AudioDeviceInfo.TYPE_HEARING_AID, null);
+                }
+                return true;
+            }
+            return false;
+        } else if (callProfile == BluetoothProfile.HEADSET) {
             boolean success = mBluetoothAdapter.setActiveDevice(device,
                 BluetoothAdapter.ACTIVE_DEVICE_PHONE_CALL);
             if (!success) {
@@ -647,7 +718,9 @@
     }
 
     public boolean isInbandRingingEnabled() {
-        BluetoothDevice activeDevice = mBluetoothRouteManager.getBluetoothAudioConnectedDevice();
+        // 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)) {
             if (mBluetoothLeAudioService == null) {
diff --git a/src/com/android/server/telecom/bluetooth/BluetoothRouteManager.java b/src/com/android/server/telecom/bluetooth/BluetoothRouteManager.java
index 7966f73..b411b25 100644
--- a/src/com/android/server/telecom/bluetooth/BluetoothRouteManager.java
+++ b/src/com/android/server/telecom/bluetooth/BluetoothRouteManager.java
@@ -20,9 +20,10 @@
 import android.bluetooth.BluetoothDevice;
 import android.bluetooth.BluetoothHeadset;
 import android.bluetooth.BluetoothHearingAid;
-import android.bluetooth.BluetoothProfile;
 import android.bluetooth.BluetoothLeAudio;
+import android.bluetooth.BluetoothProfile;
 import android.content.Context;
+import android.media.AudioDeviceInfo;
 import android.os.Message;
 import android.telecom.Log;
 import android.telecom.Logging.Session;
@@ -33,15 +34,14 @@
 import com.android.internal.util.IState;
 import com.android.internal.util.State;
 import com.android.internal.util.StateMachine;
+import com.android.server.telecom.CallAudioCommunicationDeviceTracker;
 import com.android.server.telecom.TelecomSystem;
 import com.android.server.telecom.Timeouts;
 
-import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.LinkedHashSet;
-import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Optional;
@@ -78,6 +78,7 @@
         void onBluetoothActiveDevicePresent();
         void onBluetoothActiveDeviceGone();
         void onBluetoothAudioConnected();
+        void onBluetoothAudioConnecting();
         void onBluetoothAudioDisconnected();
         /**
          * This gets called when we get an unexpected state change from Bluetooth. Their stack does
@@ -135,7 +136,7 @@
                 Log.w(LOG_TAG, "Entering AudioOff state but device %s appears to be connected. " +
                         "Switching to audio-on state for that device.", erroneouslyConnectedDevice);
                 // change this to just transition to the new audio on state
-                transitionToActualState();
+                transitionToActualState(null /* excludeAddress */);
             }
             cleanupStatesForDisconnectedDevices();
             if (mListener != null) {
@@ -231,8 +232,7 @@
             sendMessageDelayed(CONNECTION_TIMEOUT, args,
                     mTimeoutsAdapter.getBluetoothPendingTimeoutMillis(
                             mContext.getContentResolver()));
-            // Pretend like audio is connected when communicating w/ CARSM.
-            mListener.onBluetoothAudioConnected();
+            mListener.onBluetoothAudioConnecting();
         }
 
         @Override
@@ -259,7 +259,7 @@
                     case LOST_DEVICE:
                         removeDevice((String) args.arg2);
                         if (Objects.equals(address, mDeviceAddress)) {
-                            transitionToActualState();
+                            transitionToActualState(null /* excludeAddress */);
                         }
                         break;
                     case CONNECT_BT:
@@ -299,7 +299,7 @@
                     case CONNECTION_TIMEOUT:
                         Log.i(LOG_TAG, "Connection with device %s timed out.",
                                 mDeviceAddress);
-                        transitionToActualState();
+                        transitionToActualState(null /* excludeAddress */);
                         break;
                     case BT_AUDIO_IS_ON:
                         if (Objects.equals(mDeviceAddress, address)) {
@@ -316,7 +316,7 @@
                         if (Objects.equals(mDeviceAddress, address) || address == null) {
                             Log.i(LOG_TAG, "Connection with device %s failed.",
                                     mDeviceAddress);
-                            transitionToActualState();
+                            transitionToActualState(address);
                         } else {
                             Log.w(LOG_TAG, "Got BT lost message for device %s while" +
                                     " connecting to %s.", address, mDeviceAddress);
@@ -376,7 +376,7 @@
                     case LOST_DEVICE:
                         removeDevice((String) args.arg2);
                         if (Objects.equals(address, mDeviceAddress)) {
-                            transitionToActualState();
+                            transitionToActualState(null /* excludeAddress */);
                         }
                         break;
                     case CONNECT_BT:
@@ -433,7 +433,7 @@
                     case BT_AUDIO_LOST:
                         if (Objects.equals(mDeviceAddress, address) || address == null) {
                             Log.i(LOG_TAG, "BT connection with device %s lost.", mDeviceAddress);
-                            transitionToActualState();
+                            transitionToActualState(address);
                         } else {
                             Log.w(LOG_TAG, "Got BT lost message for device %s while" +
                                     " connected to %s.", address, mDeviceAddress);
@@ -469,15 +469,18 @@
     private BluetoothDevice mHearingAidActiveDeviceCache = null;
     private BluetoothDevice mLeAudioActiveDeviceCache = null;
     private BluetoothDevice mMostRecentlyReportedActiveDevice = null;
+    private CallAudioCommunicationDeviceTracker mCommunicationDeviceTracker;
 
     public BluetoothRouteManager(Context context, TelecomSystem.SyncRoot lock,
-            BluetoothDeviceManager deviceManager, Timeouts.Adapter timeoutsAdapter) {
+            BluetoothDeviceManager deviceManager, Timeouts.Adapter timeoutsAdapter,
+            CallAudioCommunicationDeviceTracker communicationDeviceTracker) {
         super(BluetoothRouteManager.class.getSimpleName());
         mContext = context;
         mLock = lock;
         mDeviceManager = deviceManager;
         mDeviceManager.setBluetoothRouteManager(this);
         mTimeoutsAdapter = timeoutsAdapter;
+        mCommunicationDeviceTracker = communicationDeviceTracker;
 
         mAudioOffState = new AudioOffState();
         addState(mAudioOffState);
@@ -621,12 +624,14 @@
         if (deviceType == BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO) {
             mLeAudioActiveDeviceCache = device;
             if (device == null) {
-                mDeviceManager.clearLeAudioCommunicationDevice();
+                mCommunicationDeviceTracker.clearCommunicationDevice(
+                        AudioDeviceInfo.TYPE_BLE_HEADSET);
             }
         } else if (deviceType == BluetoothDeviceManager.DEVICE_TYPE_HEARING_AID) {
             mHearingAidActiveDeviceCache = device;
             if (device == null) {
-                mDeviceManager.clearHearingAidCommunicationDevice();
+                mCommunicationDeviceTracker.clearCommunicationDevice(
+                        AudioDeviceInfo.TYPE_HEARING_AID);
             }
         } else if (deviceType == BluetoothDeviceManager.DEVICE_TYPE_HEADSET) {
             mHfpActiveDeviceCache = device;
@@ -645,6 +650,10 @@
         }
     }
 
+    public BluetoothDevice getMostRecentlyReportedActiveDevice() {
+        return mMostRecentlyReportedActiveDevice;
+    }
+
     public boolean hasBtActiveDevice() {
         return mLeAudioActiveDeviceCache != null ||
                 mHearingAidActiveDeviceCache != null ||
@@ -710,7 +719,7 @@
                 actualAddress)) {
             Log.i(this, "trying to connect to already connected device -- skipping connection"
                     + " and going into the actual connected state.");
-            transitionToActualState();
+            transitionToActualState(null /* excludeAddress */);
             return null;
         }
 
@@ -746,9 +755,10 @@
         return null;
     }
 
-    private void transitionToActualState() {
+    private void transitionToActualState(String excludeAddress) {
         BluetoothDevice possiblyAlreadyConnectedDevice = getBluetoothAudioConnectedDevice();
-        if (possiblyAlreadyConnectedDevice != null) {
+        if (possiblyAlreadyConnectedDevice != null
+                && !possiblyAlreadyConnectedDevice.getAddress().equals(excludeAddress)) {
             Log.i(LOG_TAG, "Device %s is already connected; going to AudioConnected.",
                     possiblyAlreadyConnectedDevice);
             transitionTo(getConnectedStateForAddress(
@@ -798,7 +808,8 @@
         }
 
         if (bluetoothHearingAid != null) {
-            if (mDeviceManager.isHearingAidSetAsCommunicationDevice()) {
+            if (mCommunicationDeviceTracker.isAudioDeviceSetForType(
+                    AudioDeviceInfo.TYPE_HEARING_AID)) {
                 for (BluetoothDevice device : bluetoothAdapter.getActiveDevices(
                         BluetoothProfile.HEARING_AID)) {
                     if (device != null) {
@@ -811,7 +822,8 @@
         }
 
         if (bluetoothLeAudio != null) {
-            if (mDeviceManager.isLeAudioCommunicationDevice()) {
+            if (mCommunicationDeviceTracker.isAudioDeviceSetForType(
+                    AudioDeviceInfo.TYPE_BLE_HEADSET)) {
                 for (BluetoothDevice device : bluetoothAdapter.getActiveDevices(
                         BluetoothProfile.LE_AUDIO)) {
                     if (device != null) {
diff --git a/src/com/android/server/telecom/bluetooth/BluetoothStateReceiver.java b/src/com/android/server/telecom/bluetooth/BluetoothStateReceiver.java
index 20af7b5..ec4f263 100644
--- a/src/com/android/server/telecom/bluetooth/BluetoothStateReceiver.java
+++ b/src/com/android/server/telecom/bluetooth/BluetoothStateReceiver.java
@@ -16,6 +16,7 @@
 
 package com.android.server.telecom.bluetooth;
 
+import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothDevice;
 import android.bluetooth.BluetoothHeadset;
 import android.bluetooth.BluetoothHearingAid;
@@ -25,10 +26,13 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
+import android.media.AudioDeviceInfo;
+import android.os.Bundle;
 import android.telecom.Log;
 import android.telecom.Logging.Session;
 
 import com.android.internal.os.SomeArgs;
+import com.android.server.telecom.CallAudioCommunicationDeviceTracker;
 
 import static com.android.server.telecom.bluetooth.BluetoothRouteManager.BT_AUDIO_IS_ON;
 import static com.android.server.telecom.bluetooth.BluetoothRouteManager.BT_AUDIO_LOST;
@@ -54,6 +58,7 @@
     private boolean mIsInCall = false;
     private final BluetoothRouteManager mBluetoothRouteManager;
     private final BluetoothDeviceManager mBluetoothDeviceManager;
+    private CallAudioCommunicationDeviceTracker mCommunicationDeviceTracker;
 
     public void onReceive(Context context, Intent intent) {
         Log.startSession("BSR.oR");
@@ -84,7 +89,7 @@
                 intent.getIntExtra(BluetoothHeadset.EXTRA_STATE,
                         BluetoothHeadset.STATE_AUDIO_DISCONNECTED);
         BluetoothDevice device =
-                intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
+                intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE, BluetoothDevice.class);
         if (device == null) {
             Log.w(LOG_TAG, "Got null device from broadcast. " +
                     "Ignoring.");
@@ -115,7 +120,7 @@
         int bluetoothHeadsetState = intent.getIntExtra(BluetoothHeadset.EXTRA_STATE,
                 BluetoothHeadset.STATE_DISCONNECTED);
         BluetoothDevice device =
-                intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
+                intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE, BluetoothDevice.class);
 
         if (device == null) {
             Log.w(LOG_TAG, "Got null device from broadcast. " +
@@ -149,7 +154,7 @@
 
     private void handleActiveDeviceChanged(Intent intent) {
         BluetoothDevice device =
-                intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
+                intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE, BluetoothDevice.class);
 
         int deviceType;
         if (BluetoothLeAudio.ACTION_LE_AUDIO_ACTIVE_DEVICE_CHANGED.equals(intent.getAction())) {
@@ -181,11 +186,32 @@
                 }
                 args.arg2 = device.getAddress();
 
+                boolean usePreferredAudioProfile = false;
+                BluetoothAdapter bluetoothAdapter = mBluetoothDeviceManager.getBluetoothAdapter();
+                int preferredDuplexProfile = BluetoothProfile.LE_AUDIO;
+                if (bluetoothAdapter != null) {
+                    Bundle preferredAudioProfiles = bluetoothAdapter.getPreferredAudioProfiles(
+                            device);
+                    if (preferredAudioProfiles != null && !preferredAudioProfiles.isEmpty()
+                            && preferredAudioProfiles.getInt(BluetoothAdapter.AUDIO_MODE_DUPLEX)
+                            != 0) {
+                        Log.i(this, "Preferred duplex profile for device=" + device + " is "
+                                + preferredAudioProfiles.getInt(
+                                BluetoothAdapter.AUDIO_MODE_DUPLEX));
+                        usePreferredAudioProfile = true;
+                        preferredDuplexProfile =
+                                preferredAudioProfiles.getInt(BluetoothAdapter.AUDIO_MODE_DUPLEX);
+                    }
+                }
+
                 if (deviceType == BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO) {
                     /* In Le Audio case, once device got Active, the Telecom needs to make sure it
                      * is set as communication device before we can say that BT_AUDIO_IS_ON
                      */
-                    if (!mBluetoothDeviceManager.setLeAudioCommunicationDevice()) {
+                    if ((!usePreferredAudioProfile
+                            || preferredDuplexProfile == BluetoothProfile.LE_AUDIO)
+                            && !mCommunicationDeviceTracker.setCommunicationDevice(
+                            AudioDeviceInfo.TYPE_BLE_HEADSET, device)) {
                         Log.w(LOG_TAG,
                                 "Device %s cannot be use as LE audio communication device.",
                                 device);
@@ -193,7 +219,8 @@
                     }
                 } else {
                     /* deviceType == BluetoothDeviceManager.DEVICE_TYPE_HEARING_AID */
-                    if (!mBluetoothDeviceManager.setHearingAidCommunicationDevice()) {
+                    if (!mCommunicationDeviceTracker.setCommunicationDevice(
+                            AudioDeviceInfo.TYPE_HEARING_AID, null)) {
                         Log.w(LOG_TAG,
                                 "Device %s cannot be use as hearing aid communication device.",
                                 device);
@@ -210,9 +237,11 @@
     }
 
     public BluetoothStateReceiver(BluetoothDeviceManager deviceManager,
-            BluetoothRouteManager routeManager) {
+            BluetoothRouteManager routeManager,
+            CallAudioCommunicationDeviceTracker communicationDeviceTracker) {
         mBluetoothDeviceManager = deviceManager;
         mBluetoothRouteManager = routeManager;
+        mCommunicationDeviceTracker = communicationDeviceTracker;
     }
 
     public void setIsInCall(boolean isInCall) {
diff --git a/src/com/android/server/telecom/callfiltering/BlockCheckerFilter.java b/src/com/android/server/telecom/callfiltering/BlockCheckerFilter.java
index 36f2077..64060c8 100644
--- a/src/com/android/server/telecom/callfiltering/BlockCheckerFilter.java
+++ b/src/com/android/server/telecom/callfiltering/BlockCheckerFilter.java
@@ -126,6 +126,7 @@
                     .setShouldReject(true)
                     .setShouldAddToCallLog(true)
                     .setShouldShowNotification(false)
+                    .setShouldSilence(true)
                     .setCallBlockReason(getBlockReason(blockStatus))
                     .setCallScreeningAppName(null)
                     .setCallScreeningComponentName(null)
diff --git a/src/com/android/server/telecom/callfiltering/IncomingCallFilterGraphProvider.java b/src/com/android/server/telecom/callfiltering/IncomingCallFilterGraphProvider.java
new file mode 100644
index 0000000..1501280
--- /dev/null
+++ b/src/com/android/server/telecom/callfiltering/IncomingCallFilterGraphProvider.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2023 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.callfiltering;
+
+import android.content.Context;
+
+import com.android.server.telecom.Call;
+import com.android.server.telecom.TelecomSystem;
+import com.android.server.telecom.Timeouts;
+
+/**
+ * Interface to provide a {@link IncomingCallFilterGraph}. This class serve for unit test purpose
+ * to mock an incoming call filter graph in test code.
+ */
+public interface IncomingCallFilterGraphProvider {
+
+
+    /**
+     * Provide a {@link  IncomingCallFilterGraph}
+     * @param call The call for the filters.
+     * @param listener Callback object to trigger when filtering is done.
+     * @param context An android context.
+     * @param timeoutsAdapter Adapter to provide timeout value for call filtering.
+     * @param lock Telecom lock.
+     * @return
+     */
+    IncomingCallFilterGraph createGraph(Call call, CallFilterResultCallback listener,
+            Context context,
+            Timeouts.Adapter timeoutsAdapter, TelecomSystem.SyncRoot lock);
+}
diff --git a/src/com/android/server/telecom/components/TelecomService.java b/src/com/android/server/telecom/components/TelecomService.java
index ef85fc7..9a5f2a7 100644
--- a/src/com/android/server/telecom/components/TelecomService.java
+++ b/src/com/android/server/telecom/components/TelecomService.java
@@ -61,6 +61,7 @@
 import com.android.server.telecom.TelecomWakeLock;
 import com.android.server.telecom.Timeouts;
 import com.android.server.telecom.callfiltering.BlockedNumbersAdapter;
+import com.android.server.telecom.flags.FeatureFlagsImpl;
 import com.android.server.telecom.settings.BlockedNumbersUtil;
 import com.android.server.telecom.ui.IncomingCallNotifier;
 import com.android.server.telecom.ui.MissedCallNotifierImpl;
@@ -215,6 +216,7 @@
                                 }
                             },
                             Executors.newCachedThreadPool(),
+                            Executors.newSingleThreadExecutor(),
                             new BlockedNumbersAdapter() {
                                 @Override
                                 public boolean shouldShowEmergencyCallNotification(Context
@@ -229,7 +231,8 @@
                                     BlockedNumbersUtil.updateEmergencyCallNotification(context,
                                             showNotification);
                                 }
-                            }));
+                            },
+                            new FeatureFlagsImpl()));
         }
     }
 
diff --git a/src/com/android/server/telecom/components/UserCallIntentProcessor.java b/src/com/android/server/telecom/components/UserCallIntentProcessor.java
index a4602c1..41232c2 100755
--- a/src/com/android/server/telecom/components/UserCallIntentProcessor.java
+++ b/src/com/android/server/telecom/components/UserCallIntentProcessor.java
@@ -105,47 +105,17 @@
             handle = Uri.fromParts(PhoneAccount.SCHEME_SIP, uriString, null);
         }
 
-       if(!isSelfManaged) {
-            // Check DISALLOW_OUTGOING_CALLS restriction. Note: We are skipping this
-            // check in a managed profile user because this check can always be bypassed
-            // by copying and pasting the phone number into the personal dialer.
-            if (!UserUtil.isManagedProfile(mContext, mUserHandle)) {
-                // Only emergency calls are allowed for users with the DISALLOW_OUTGOING_CALLS
-                // restriction.
-                if (!TelephonyUtil.shouldProcessAsEmergency(mContext, handle)) {
-                    final UserManager userManager =
-                            (UserManager) mContext.getSystemService(Context.USER_SERVICE);
-                    if (userManager.hasBaseUserRestriction(UserManager.DISALLOW_OUTGOING_CALLS,
-                            mUserHandle)) {
-                        showErrorDialogForRestrictedOutgoingCall(mContext,
-                                R.string.outgoing_call_not_allowed_user_restriction);
-                        Log.w(this, "Rejecting non-emergency phone call "
-                                + "due to DISALLOW_OUTGOING_CALLS restriction");
-                        return;
-                    } else if (userManager.hasUserRestriction(UserManager.DISALLOW_OUTGOING_CALLS,
-                            mUserHandle)) {
-                        final DevicePolicyManager dpm =
-                                mContext.getSystemService(DevicePolicyManager.class);
-                        if (dpm == null) {
-                            return;
-                        }
-                        final Intent adminSupportIntent = dpm.createAdminSupportIntent(
-                                UserManager.DISALLOW_OUTGOING_CALLS);
-                        if (adminSupportIntent != null) {
-                            mContext.startActivity(adminSupportIntent);
-                        }
-                        return;
-                    }
-                }
-            }
-        }
+       if (UserUtil.hasOutgoingCallsUserRestriction(mContext, mUserHandle,
+               handle, isSelfManaged, UserCallIntentProcessor.class.getCanonicalName())) {
+           return;
+       }
 
         if (!isSelfManaged && !canCallNonEmergency &&
                 !TelephonyUtil.shouldProcessAsEmergency(mContext, handle)) {
-            showErrorDialogForRestrictedOutgoingCall(mContext,
-                    R.string.outgoing_call_not_allowed_no_permission);
-            Log.w(this, "Rejecting non-emergency phone call because "
-                    + android.Manifest.permission.CALL_PHONE + " permission is not granted.");
+            String reason = android.Manifest.permission.CALL_PHONE + " permission is not granted.";
+            UserUtil.showErrorDialogForRestrictedOutgoingCall(mContext,
+                    R.string.outgoing_call_not_allowed_no_permission,
+                    this.getClass().getCanonicalName(), reason);
             return;
         }
 
@@ -187,11 +157,4 @@
         }
         return true;
     }
-
-    private static void showErrorDialogForRestrictedOutgoingCall(Context context, int stringId) {
-        final Intent intent = new Intent(context, ErrorDialogActivity.class);
-        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-        intent.putExtra(ErrorDialogActivity.ERROR_MESSAGE_ID_EXTRA, stringId);
-        context.startActivityAsUser(intent, UserHandle.CURRENT);
-    }
 }
diff --git a/src/com/android/server/telecom/voip/VerifyCallStateChangeTransaction.java b/src/com/android/server/telecom/voip/VerifyCallStateChangeTransaction.java
new file mode 100644
index 0000000..b17dedd
--- /dev/null
+++ b/src/com/android/server/telecom/voip/VerifyCallStateChangeTransaction.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright (C) 2023 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 com.android.internal.annotations.VisibleForTesting;
+import com.android.server.telecom.Call;
+import com.android.server.telecom.CallsManager;
+
+import android.telecom.DisconnectCause;
+import android.telecom.Log;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionStage;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * VerifyCallStateChangeTransaction is a transaction that verifies a CallState change and has
+ * the ability to disconnect if the CallState is not changed within the timeout window.
+ * <p>
+ * Note: This transaction has a timeout of 2 seconds.
+ */
+public class VerifyCallStateChangeTransaction extends VoipCallTransaction {
+    private static final String TAG = VerifyCallStateChangeTransaction.class.getSimpleName();
+    public static final int FAILURE_CODE = 0;
+    public static final int SUCCESS_CODE = 1;
+    public static final int TIMEOUT_SECONDS = 2;
+    private final Call mCall;
+    private final CallsManager mCallsManager;
+    private final int mTargetCallState;
+    private final boolean mShouldDisconnectUponFailure;
+    private final CompletableFuture<Integer> mCallStateOrTimeoutResult = new CompletableFuture<>();
+    private final CompletableFuture<VoipCallTransactionResult> mTransactionResult =
+            new CompletableFuture<>();
+
+    @VisibleForTesting
+    public Call.CallStateListener mCallStateListenerImpl = new Call.CallStateListener() {
+        @Override
+        public void onCallStateChanged(int newCallState) {
+            Log.d(TAG, "newState=[%d], expectedState=[%d]", newCallState, mTargetCallState);
+            if (newCallState == mTargetCallState) {
+                mCallStateOrTimeoutResult.complete(SUCCESS_CODE);
+            }
+            // NOTE:: keep listening to the call state until the timeout is reached. It's possible
+            // another call state is reached in between...
+        }
+    };
+
+    public VerifyCallStateChangeTransaction(CallsManager callsManager, Call call,
+            int targetCallState, boolean shouldDisconnectUponFailure) {
+        super(callsManager.getLock());
+        mCallsManager = callsManager;
+        mCall = call;
+        mTargetCallState = targetCallState;
+        mShouldDisconnectUponFailure = shouldDisconnectUponFailure;
+    }
+
+    @Override
+    public CompletionStage<VoipCallTransactionResult> processTransaction(Void v) {
+        Log.d(TAG, "processTransaction:");
+        // It's possible the Call is already in the expected call state
+        if (isNewCallStateTargetCallState()) {
+            mTransactionResult.complete(
+                    new VoipCallTransactionResult(VoipCallTransactionResult.RESULT_SUCCEED,
+                            TAG));
+            return mTransactionResult;
+        }
+        initCallStateListenerOnTimeout();
+        // At this point, the mCallStateOrTimeoutResult has been completed. There are 2 scenarios:
+        // (1) newCallState == targetCallState --> the transaction is successful
+        // (2) timeout is reached --> evaluate the current call state and complete the t accordingly
+        // also need to do cleanup for the transaction
+        evaluateCallStateUponChangeOrTimeout();
+
+        return mTransactionResult;
+    }
+
+    private boolean isNewCallStateTargetCallState() {
+        return mCall.getState() == mTargetCallState;
+    }
+
+    private void initCallStateListenerOnTimeout() {
+        mCall.addCallStateListener(mCallStateListenerImpl);
+        mCallStateOrTimeoutResult.completeOnTimeout(FAILURE_CODE, TIMEOUT_SECONDS,
+                TimeUnit.SECONDS);
+    }
+
+    private void evaluateCallStateUponChangeOrTimeout() {
+        mCallStateOrTimeoutResult.thenAcceptAsync((result) -> {
+            Log.i(TAG, "processTransaction: thenAcceptAsync: result=[%s]", result);
+            mCall.removeCallStateListener(mCallStateListenerImpl);
+            if (isNewCallStateTargetCallState()) {
+                mTransactionResult.complete(
+                        new VoipCallTransactionResult(VoipCallTransactionResult.RESULT_SUCCEED,
+                                TAG));
+            } else {
+                maybeDisconnectCall();
+                mTransactionResult.complete(
+                        new VoipCallTransactionResult(VoipCallTransactionResult.RESULT_FAILED,
+                                TAG));
+            }
+        }).exceptionally(exception -> {
+            Log.i(TAG, "hit exception=[%s] while completing future", exception);
+            mTransactionResult.complete(
+                    new VoipCallTransactionResult(VoipCallTransactionResult.RESULT_FAILED,
+                            TAG));
+            return null;
+        });
+    }
+
+    private void maybeDisconnectCall() {
+        if (mShouldDisconnectUponFailure) {
+            mCallsManager.markCallAsDisconnected(mCall,
+                    new DisconnectCause(DisconnectCause.ERROR,
+                            "did not hold in timeout window"));
+            mCallsManager.markCallAsRemoved(mCall);
+        }
+    }
+
+    @VisibleForTesting
+    public CompletableFuture<Integer> getCallStateOrTimeoutResult() {
+        return mCallStateOrTimeoutResult;
+    }
+
+    @VisibleForTesting
+    public CompletableFuture<VoipCallTransactionResult> getTransactionResult() {
+        return mTransactionResult;
+    }
+
+    @VisibleForTesting
+    public Call.CallStateListener getCallStateListenerImpl() {
+        return mCallStateListenerImpl;
+    }
+}
diff --git a/src/com/android/server/telecom/voip/VoipCallMonitor.java b/src/com/android/server/telecom/voip/VoipCallMonitor.java
index 3779a6d..8f6ad51 100644
--- a/src/com/android/server/telecom/voip/VoipCallMonitor.java
+++ b/src/com/android/server/telecom/voip/VoipCallMonitor.java
@@ -16,6 +16,12 @@
 
 package com.android.server.telecom.voip;
 
+import static android.app.ForegroundServiceDelegationOptions.DELEGATION_SERVICE_PHONE_CALL;
+import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_CAMERA;
+import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE;
+import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE;
+import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL;
+
 import android.app.ActivityManager;
 import android.app.ActivityManagerInternal;
 import android.app.ForegroundServiceDelegationOptions;
@@ -199,8 +205,11 @@
             ForegroundServiceDelegationOptions options = new ForegroundServiceDelegationOptions(pid,
                     uid, handle.getComponentName().getPackageName(), null /* clientAppThread */,
                     false /* isSticky */, String.valueOf(handle.hashCode()),
-                    0 /* foregroundServiceType */,
-                    ForegroundServiceDelegationOptions.DELEGATION_SERVICE_PHONE_CALL);
+                    FOREGROUND_SERVICE_TYPE_PHONE_CALL |
+                    FOREGROUND_SERVICE_TYPE_MICROPHONE |
+                    FOREGROUND_SERVICE_TYPE_CAMERA |
+                    FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE /* foregroundServiceTypes */,
+                    DELEGATION_SERVICE_PHONE_CALL /* delegationService */);
             ServiceConnection fgsConnection = new ServiceConnection() {
                 @Override
                 public void onServiceConnected(ComponentName name, IBinder service) {
diff --git a/tests/src/com/android/server/telecom/tests/BasicCallTests.java b/tests/src/com/android/server/telecom/tests/BasicCallTests.java
index bd81a2f..5bef130 100644
--- a/tests/src/com/android/server/telecom/tests/BasicCallTests.java
+++ b/tests/src/com/android/server/telecom/tests/BasicCallTests.java
@@ -39,10 +39,11 @@
 
 import android.content.Context;
 import android.content.IContentProvider;
-import android.content.pm.PackageManager;
-import android.media.AudioDeviceInfo;
 import android.content.Intent;
+import android.content.pm.PackageManager;
 import android.graphics.drawable.Icon;
+import android.media.AudioDeviceInfo;
+import android.media.AudioFocusRequest;
 import android.media.AudioManager;
 import android.net.Uri;
 import android.os.Binder;
@@ -110,6 +111,7 @@
         doReturn(mContext).when(mContext).createContextAsUser(any(UserHandle.class), anyInt());
         mPackageManager = mContext.getPackageManager();
         when(mPackageManager.getPackageUid(anyString(), eq(0))).thenReturn(Binder.getCallingUid());
+        when(mFeatureFlags.telecomResolveHiddenDependencies()).thenReturn(false);
     }
 
     @Override
@@ -651,8 +653,8 @@
                 .getCallAudioRouteStateMachine().getHandler(), TEST_TIMEOUT);
         ArgumentCaptor<AudioDeviceInfo> infoArgumentCaptor =
                 ArgumentCaptor.forClass(AudioDeviceInfo.class);
-        verify(audioManager, timeout(TEST_TIMEOUT)).setCommunicationDevice(
-                infoArgumentCaptor.capture());
+        verify(audioManager, timeout(TEST_TIMEOUT).atLeast(1))
+                .setCommunicationDevice(infoArgumentCaptor.capture());
         assertEquals(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER, infoArgumentCaptor.getValue().getType());
         mInCallServiceFixtureX.mInCallAdapter.setAudioRoute(CallAudioState.ROUTE_EARPIECE, null);
         waitForHandlerAction(mTelecomSystem.getCallsManager().getCallAudioManager()
@@ -1339,7 +1341,6 @@
     public void testValidateStatusHintsImage_addExistingConnection() throws Exception {
         IdPair outgoing = startAndMakeActiveOutgoingCall("650-555-1214",
                 mPhoneAccountA0.getAccountHandle(), mConnectionServiceFixtureA);
-        Connection existingConnection = mConnectionServiceFixtureA.mLatestConnection;
 
         // Modify existing connection with StatusHints image exploit
         Icon icon = Icon.createWithContentUri("content://10@media/external/images/media/");
diff --git a/tests/src/com/android/server/telecom/tests/BlockCheckerFilterTest.java b/tests/src/com/android/server/telecom/tests/BlockCheckerFilterTest.java
index 7df4f29..a98c1ee 100644
--- a/tests/src/com/android/server/telecom/tests/BlockCheckerFilterTest.java
+++ b/tests/src/com/android/server/telecom/tests/BlockCheckerFilterTest.java
@@ -72,6 +72,7 @@
     private static final CallFilteringResult BLOCK_RESULT = new CallFilteringResult.Builder()
             .setShouldAllowCall(false)
             .setShouldReject(true)
+            .setShouldSilence(true)
             .setShouldAddToCallLog(true)
             .setShouldShowNotification(false)
             .setCallBlockReason(CallLog.Calls.BLOCK_REASON_BLOCKED_NUMBER)
diff --git a/tests/src/com/android/server/telecom/tests/BluetoothDeviceManagerTest.java b/tests/src/com/android/server/telecom/tests/BluetoothDeviceManagerTest.java
index c37d136..da3f40c 100644
--- a/tests/src/com/android/server/telecom/tests/BluetoothDeviceManagerTest.java
+++ b/tests/src/com/android/server/telecom/tests/BluetoothDeviceManagerTest.java
@@ -24,13 +24,14 @@
 import android.bluetooth.BluetoothHearingAid;
 import android.bluetooth.BluetoothLeAudio;
 import android.bluetooth.BluetoothProfile;
-import android.content.BroadcastReceiver;
 import android.content.Intent;
 import android.media.AudioDeviceInfo;
 import android.media.AudioManager;
+import android.os.Bundle;
 import android.os.Parcel;
 import android.test.suitebuilder.annotation.SmallTest;
 
+import com.android.server.telecom.CallAudioCommunicationDeviceTracker;
 import com.android.server.telecom.bluetooth.BluetoothDeviceManager;
 import com.android.server.telecom.bluetooth.BluetoothRouteManager;
 import com.android.server.telecom.bluetooth.BluetoothStateReceiver;
@@ -76,6 +77,7 @@
     BluetoothDeviceManager mBluetoothDeviceManager;
     BluetoothProfile.ServiceListener serviceListenerUnderTest;
     BluetoothStateReceiver receiverUnderTest;
+    CallAudioCommunicationDeviceTracker mCommunicationDeviceTracker;
     ArgumentCaptor<BluetoothLeAudio.Callback> leAudioCallbacksTest;
 
     private BluetoothDevice device1;
@@ -103,8 +105,11 @@
         when(mBluetoothHearingAid.getHiSyncId(device4)).thenReturn(100L);
 
         mContext = mComponentContextFixture.getTestDouble().getApplicationContext();
-        mBluetoothDeviceManager = new BluetoothDeviceManager(mContext, mAdapter);
+        mCommunicationDeviceTracker = new CallAudioCommunicationDeviceTracker(mContext);
+        mBluetoothDeviceManager = new BluetoothDeviceManager(mContext, mAdapter,
+                mCommunicationDeviceTracker);
         mBluetoothDeviceManager.setBluetoothRouteManager(mRouteManager);
+        mCommunicationDeviceTracker.setBluetoothRouteManager(mRouteManager);
 
         mockAudioManager = mContext.getSystemService(AudioManager.class);
 
@@ -114,7 +119,8 @@
                 serviceCaptor.capture(), eq(BluetoothProfile.HEADSET));
         serviceListenerUnderTest = serviceCaptor.getValue();
 
-        receiverUnderTest = new BluetoothStateReceiver(mBluetoothDeviceManager, mRouteManager);
+        receiverUnderTest = new BluetoothStateReceiver(mBluetoothDeviceManager,
+                mRouteManager, mCommunicationDeviceTracker);
 
         mBluetoothDeviceManager.setHeadsetServiceForTesting(mBluetoothHeadset);
         mBluetoothDeviceManager.setHearingAidServiceForTesting(mBluetoothHearingAid);
@@ -178,6 +184,7 @@
                 buildConnectionActionIntent(BluetoothHeadset.STATE_CONNECTED, device5,
                         BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO));
         leAudioCallbacksTest.getValue().onGroupNodeAdded(device5, 1);
+        when(mBluetoothLeAudio.getGroupId(device5)).thenReturn(1);
         when(mBluetoothLeAudio.getConnectedGroupLeadDevice(1)).thenReturn(device5);
 
         receiverUnderTest.onReceive(mContext,
@@ -188,6 +195,7 @@
                         BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO));
         leAudioCallbacksTest.getValue().onGroupNodeAdded(device6, 2);
         when(mBluetoothLeAudio.getConnectedGroupLeadDevice(2)).thenReturn(device6);
+        when(mBluetoothLeAudio.getGroupId(device6)).thenReturn(1);
         receiverUnderTest.onReceive(mContext,
                 buildConnectionActionIntent(BluetoothHeadset.STATE_CONNECTED, device3,
                         BluetoothDeviceManager.DEVICE_TYPE_HEADSET));
@@ -263,17 +271,19 @@
     @Test
     public void testLeAudioDedup() {
         receiverUnderTest.onReceive(mContext,
-                buildConnectionActionIntent(BluetoothHeadset.STATE_CONNECTED, device1,
+                buildConnectionActionIntent(BluetoothProfile.STATE_CONNECTED, device1,
                         BluetoothDeviceManager.DEVICE_TYPE_HEADSET));
         receiverUnderTest.onReceive(mContext,
-                buildConnectionActionIntent(BluetoothHeadset.STATE_CONNECTED, device5,
+                buildConnectionActionIntent(BluetoothProfile.STATE_CONNECTED, device5,
                         BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO));
         leAudioCallbacksTest.getValue().onGroupNodeAdded(device5, 1);
         receiverUnderTest.onReceive(mContext,
-                buildConnectionActionIntent(BluetoothHeadset.STATE_CONNECTED, device6,
+                buildConnectionActionIntent(BluetoothProfile.STATE_CONNECTED, device6,
                         BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO));
         leAudioCallbacksTest.getValue().onGroupNodeAdded(device6, 1);
         when(mBluetoothLeAudio.getConnectedGroupLeadDevice(1)).thenReturn(device5);
+        when(mBluetoothLeAudio.getGroupId(device5)).thenReturn(1);
+        when(mBluetoothLeAudio.getGroupId(device6)).thenReturn(1);
         assertEquals(2, mBluetoothDeviceManager.getNumConnectedDevices());
         assertEquals(2, mBluetoothDeviceManager.getUniqueConnectedDevices().size());
     }
@@ -408,11 +418,12 @@
         when(mAdapter.setActiveDevice(nullable(BluetoothDevice.class),
                 eq(BluetoothAdapter.ACTIVE_DEVICE_ALL))).thenReturn(true);
 
-        AudioDeviceInfo mockAudioDeviceInfo = mock(AudioDeviceInfo.class);
-        when(mockAudioDeviceInfo.getType()).thenReturn(AudioDeviceInfo.TYPE_HEARING_AID);
+        AudioDeviceInfo mockAudioDeviceInfo = createMockAudioDeviceInfo(device5.getAddress(),
+                AudioDeviceInfo.TYPE_HEARING_AID);
         List<AudioDeviceInfo> devices = new ArrayList<>();
         devices.add(mockAudioDeviceInfo);
 
+        when(mockAudioManager.getCommunicationDevice()).thenReturn(mSpeakerInfo);
         when(mockAudioManager.getAvailableCommunicationDevices())
                 .thenReturn(devices);
         when(mockAudioManager.setCommunicationDevice(eq(mockAudioDeviceInfo)))
@@ -443,11 +454,12 @@
         when(mAdapter.setActiveDevice(nullable(BluetoothDevice.class),
                 eq(BluetoothAdapter.ACTIVE_DEVICE_ALL))).thenReturn(true);
 
-        AudioDeviceInfo mockAudioDeviceInfo = mock(AudioDeviceInfo.class);
-        when(mockAudioDeviceInfo.getType()).thenReturn(AudioDeviceInfo.TYPE_BLE_HEADSET);
+        AudioDeviceInfo mockAudioDeviceInfo = createMockAudioDeviceInfo(device5.getAddress(),
+                AudioDeviceInfo.TYPE_BLE_HEADSET);
         List<AudioDeviceInfo> devices = new ArrayList<>();
         devices.add(mockAudioDeviceInfo);
 
+        when(mockAudioManager.getCommunicationDevice()).thenReturn(mSpeakerInfo);
         when(mockAudioManager.getAvailableCommunicationDevices())
                         .thenReturn(devices);
         when(mockAudioManager.setCommunicationDevice(mockAudioDeviceInfo))
@@ -458,6 +470,8 @@
         verify(mBluetoothHeadset, never()).connectAudio();
         verify(mAdapter, never()).setActiveDevice(nullable(BluetoothDevice.class),
                 eq(BluetoothAdapter.ACTIVE_DEVICE_PHONE_CALL));
+        verify(mAdapter, never()).setActiveDevice(nullable(BluetoothDevice.class),
+                eq(BluetoothAdapter.ACTIVE_DEVICE_AUDIO));
 
         receiverUnderTest.onReceive(mContext, buildActiveDeviceChangeActionIntent(device5,
                 BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO));
@@ -485,6 +499,8 @@
         verify(mBluetoothHeadset, never()).connectAudio();
         verify(mAdapter, never()).setActiveDevice(nullable(BluetoothDevice.class),
                 eq(BluetoothAdapter.ACTIVE_DEVICE_PHONE_CALL));
+        verify(mAdapter, never()).setActiveDevice(nullable(BluetoothDevice.class),
+                eq(BluetoothAdapter.ACTIVE_DEVICE_PHONE_CALL));
 
         when(mAdapter.getActiveDevices(eq(BluetoothProfile.LE_AUDIO)))
                 .thenReturn(Arrays.asList(device5, device6));
@@ -499,6 +515,182 @@
 
     @SmallTest
     @Test
+    public void testConnectMultipleLeAudioDevices() {
+        receiverUnderTest.setIsInCall(true);
+        receiverUnderTest.onReceive(mContext,
+                buildConnectionActionIntent(BluetoothHeadset.STATE_CONNECTED, device1,
+                        BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO));
+        leAudioCallbacksTest.getValue().onGroupNodeAdded(device1, 1);
+        receiverUnderTest.onReceive(mContext,
+                buildConnectionActionIntent(BluetoothHeadset.STATE_CONNECTED, device2,
+                        BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO));
+        leAudioCallbacksTest.getValue().onGroupNodeAdded(device2, 1);
+        when(mAdapter.setActiveDevice(nullable(BluetoothDevice.class),
+                eq(BluetoothAdapter.ACTIVE_DEVICE_ALL))).thenReturn(true);
+
+        List<AudioDeviceInfo> devices = new ArrayList<>();
+        AudioDeviceInfo leAudioDevice1 = createMockAudioDeviceInfo(device1.getAddress(),
+                AudioDeviceInfo.TYPE_BLE_HEADSET);
+        AudioDeviceInfo leAudioDevice2 = createMockAudioDeviceInfo(device2.getAddress(),
+                AudioDeviceInfo.TYPE_BLE_HEADSET);
+        devices.add(leAudioDevice1);
+        devices.add(leAudioDevice2);
+
+        when(mockAudioManager.getAvailableCommunicationDevices())
+                .thenReturn(devices);
+        when(mockAudioManager.setCommunicationDevice(any(AudioDeviceInfo.class)))
+                .thenReturn(true);
+
+        // Connect LE audio device
+        mBluetoothDeviceManager.connectAudio(device1.getAddress(), false);
+        verify(mAdapter).setActiveDevice(device1, BluetoothAdapter.ACTIVE_DEVICE_ALL);
+        verify(mBluetoothHeadset, never()).connectAudio();
+        verify(mAdapter, never()).setActiveDevice(nullable(BluetoothDevice.class),
+                eq(BluetoothAdapter.ACTIVE_DEVICE_PHONE_CALL));
+        // Verify that we set the communication device for device 1
+        verify(mockAudioManager).setCommunicationDevice(leAudioDevice1);
+
+        // Change active device to other LE audio device
+        receiverUnderTest.onReceive(mContext, buildActiveDeviceChangeActionIntent(device2,
+                BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO));
+
+        // Verify call to clearLeAudioCommunicationDevice
+        verify(mRouteManager).onAudioLost(eq(DEVICE_ADDRESS_1));
+        // Verify that we set the communication device for device2
+        verify(mockAudioManager).setCommunicationDevice(leAudioDevice2);
+    }
+
+    @SmallTest
+    @Test
+    public void testClearCommunicationDeviceOnActiveDeviceChange() {
+        receiverUnderTest.setIsInCall(true);
+//        receiverUnderTest.onReceive(mContext,
+//                buildConnectionActionIntent(BluetoothHeadset.STATE_CONNECTED, device1,
+//                        BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO));
+//        leAudioCallbacksTest.getValue().onGroupNodeAdded(device1, 1);
+//        when(mAdapter.setActiveDevice(nullable(BluetoothDevice.class),
+//                eq(BluetoothAdapter.ACTIVE_DEVICE_ALL))).thenReturn(true);
+
+        List<AudioDeviceInfo> devices = new ArrayList<>();
+        AudioDeviceInfo leAudioDevice1 = createMockAudioDeviceInfo(device1.getAddress(),
+                AudioDeviceInfo.TYPE_BLE_HEADSET);
+        devices.add(leAudioDevice1);
+
+        when(mockAudioManager.getAvailableCommunicationDevices())
+                .thenReturn(devices);
+        when(mockAudioManager.setCommunicationDevice(any(AudioDeviceInfo.class)))
+                .thenReturn(true);
+
+        // Pretend that the speaker device is currently the requested device set for communication.
+        // This test ensures that the set/clear communication logic for audio switching in/out of BT
+        // is properly working when the receiver processes an active device change intent.
+        mCommunicationDeviceTracker.setTestCommunicationDevice(TYPE_BUILTIN_SPEAKER);
+
+        // Notify that LE audio device has been turned on
+        receiverUnderTest.onReceive(mContext, buildActiveDeviceChangeActionIntent(device1,
+                BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO));
+        // Verify call to clear speaker communication device
+        verify(mockAudioManager).clearCommunicationDevice();
+        // Verify that LE audio communication device was set after clearing the speaker device
+        verify(mockAudioManager).setCommunicationDevice(leAudioDevice1);
+    }
+
+    @SmallTest
+    @Test
+    public void testConnectDualModeEarbud() {
+        receiverUnderTest.setIsInCall(true);
+
+        // LE Audio earbuds connected
+        receiverUnderTest.onReceive(mContext,
+                buildConnectionActionIntent(BluetoothHeadset.STATE_CONNECTED, device5,
+                        BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO));
+        leAudioCallbacksTest.getValue().onGroupNodeAdded(device5, 1);
+        receiverUnderTest.onReceive(mContext,
+                buildConnectionActionIntent(BluetoothLeAudio.STATE_CONNECTED, device6,
+                        BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO));
+        leAudioCallbacksTest.getValue().onGroupNodeAdded(device6, 1);
+        // HFP device connected
+        receiverUnderTest.onReceive(mContext,
+                buildConnectionActionIntent(BluetoothHeadset.STATE_CONNECTED, device5,
+                        BluetoothDeviceManager.DEVICE_TYPE_HEADSET));
+        when(mAdapter.setActiveDevice(nullable(BluetoothDevice.class),
+                eq(BluetoothAdapter.ACTIVE_DEVICE_ALL))).thenReturn(true);
+
+        AudioDeviceInfo mockAudioDevice5Info = createMockAudioDeviceInfo(device5.getAddress(),
+                AudioDeviceInfo.TYPE_BLE_HEADSET);
+        AudioDeviceInfo mockAudioDevice6Info = createMockAudioDeviceInfo(device6.getAddress(),
+                AudioDeviceInfo.TYPE_BLE_HEADSET);
+        when(mockAudioDevice5Info.getType()).thenReturn(AudioDeviceInfo.TYPE_BLE_HEADSET);
+        when(mockAudioDevice6Info.getType()).thenReturn(AudioDeviceInfo.TYPE_BLE_HEADSET);
+        List<AudioDeviceInfo> devices = new ArrayList<>();
+        devices.add(mockAudioDevice5Info);
+        devices.add(mockAudioDevice6Info);
+
+        when(mockAudioManager.getAvailableCommunicationDevices())
+                .thenReturn(devices);
+        when(mockAudioManager.setCommunicationDevice(mockAudioDevice5Info))
+                .thenReturn(true);
+
+        Bundle hfpPreferred = new Bundle();
+        hfpPreferred.putInt(BluetoothAdapter.AUDIO_MODE_DUPLEX, BluetoothProfile.HEADSET);
+        Bundle leAudioPreferred = new Bundle();
+        leAudioPreferred.putInt(BluetoothAdapter.AUDIO_MODE_DUPLEX, BluetoothProfile.LE_AUDIO);
+
+        // TEST 1: LE Audio preferred for DUPLEX
+        when(mAdapter.getPreferredAudioProfiles(device5)).thenReturn(leAudioPreferred);
+        when(mAdapter.getPreferredAudioProfiles(device6)).thenReturn(leAudioPreferred);
+        mBluetoothDeviceManager.connectAudio(device5.getAddress(), false);
+        verify(mAdapter, times(1)).setActiveDevice(device5, BluetoothAdapter.ACTIVE_DEVICE_ALL);
+        verify(mBluetoothHeadset, never()).connectAudio();
+        verify(mAdapter, never()).setActiveDevice(nullable(BluetoothDevice.class),
+                eq(BluetoothAdapter.ACTIVE_DEVICE_PHONE_CALL));
+        verify(mockAudioManager).setCommunicationDevice(mockAudioDevice5Info);
+
+        when(mAdapter.getActiveDevices(eq(BluetoothProfile.LE_AUDIO)))
+                .thenReturn(Arrays.asList(device5, device6));
+
+        // Check disconnect during a call
+        devices.remove(mockAudioDevice5Info);
+        receiverUnderTest.onReceive(mContext,
+                buildConnectionActionIntent(BluetoothHeadset.STATE_DISCONNECTED, device5,
+                        BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO));
+        leAudioCallbacksTest.getValue().onGroupNodeRemoved(device5, 1);
+
+        mBluetoothDeviceManager.connectAudio(device6.getAddress(), false);
+        verify(mAdapter).setActiveDevice(device6, BluetoothAdapter.ACTIVE_DEVICE_ALL);
+        verify(mBluetoothHeadset, never()).connectAudio();
+        verify(mAdapter, never()).setActiveDevice(nullable(BluetoothDevice.class),
+                eq(BluetoothAdapter.ACTIVE_DEVICE_PHONE_CALL));
+        verify(mockAudioManager, times(1)).clearCommunicationDevice();
+
+        // Reconnect other LE Audio earbud
+        devices.add(mockAudioDevice5Info);
+        receiverUnderTest.onReceive(mContext,
+                buildConnectionActionIntent(BluetoothHeadset.STATE_CONNECTED, device5,
+                        BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO));
+        leAudioCallbacksTest.getValue().onGroupNodeAdded(device5, 1);
+
+        // Disconnects audio
+        mBluetoothDeviceManager.disconnectAudio();
+        verify(mockAudioManager, times(2)).clearCommunicationDevice();
+        verify(mBluetoothHeadset, times(1)).disconnectAudio();
+
+        // TEST 2: HFP preferred for DUPLEX
+        when(mAdapter.getPreferredAudioProfiles(device5)).thenReturn(hfpPreferred);
+        when(mAdapter.getPreferredAudioProfiles(device6)).thenReturn(hfpPreferred);
+        when(mAdapter.setActiveDevice(nullable(BluetoothDevice.class),
+                eq(BluetoothAdapter.ACTIVE_DEVICE_PHONE_CALL))).thenReturn(true);
+        mBluetoothDeviceManager.connectAudio(device5.getAddress(), false);
+        verify(mAdapter).setActiveDevice(device5, BluetoothAdapter.ACTIVE_DEVICE_PHONE_CALL);
+        verify(mAdapter, times(1)).setActiveDevice(device5,
+                BluetoothAdapter.ACTIVE_DEVICE_ALL);
+        verify(mBluetoothHeadset).connectAudio();
+        mBluetoothDeviceManager.disconnectAudio();
+        verify(mBluetoothHeadset, times(2)).disconnectAudio();
+    }
+
+    @SmallTest
+    @Test
     public void testClearHearingAidCommunicationDevice() {
         AudioDeviceInfo mockAudioDeviceInfo = mock(AudioDeviceInfo.class);
         when(mockAudioDeviceInfo.getAddress()).thenReturn(DEVICE_ADDRESS_1);
@@ -511,11 +703,52 @@
         when(mockAudioManager.setCommunicationDevice(eq(mockAudioDeviceInfo)))
                 .thenReturn(true);
 
-        mBluetoothDeviceManager.setHearingAidCommunicationDevice();
+        mCommunicationDeviceTracker.setCommunicationDevice(AudioDeviceInfo.TYPE_HEARING_AID, null);
         when(mockAudioManager.getCommunicationDevice()).thenReturn(mSpeakerInfo);
-        mBluetoothDeviceManager.clearHearingAidCommunicationDevice();
+        mCommunicationDeviceTracker.clearCommunicationDevice(AudioDeviceInfo.TYPE_HEARING_AID);
         verify(mRouteManager).onAudioLost(eq(DEVICE_ADDRESS_1));
-        assertFalse(mBluetoothDeviceManager.isHearingAidSetAsCommunicationDevice());
+        assertFalse(mCommunicationDeviceTracker.isAudioDeviceSetForType(
+                AudioDeviceInfo.TYPE_HEARING_AID));
+    }
+
+    @SmallTest
+    @Test
+    public void testClearLeAudioCommunicationDevice() {
+        AudioDeviceInfo mockAudioDeviceInfo = createMockAudioDeviceInfo(DEVICE_ADDRESS_1,
+                AudioDeviceInfo.TYPE_BLE_HEADSET);
+        List<AudioDeviceInfo> devices = new ArrayList<>();
+        devices.add(mockAudioDeviceInfo);
+
+        when(mockAudioManager.getAvailableCommunicationDevices())
+                .thenReturn(devices);
+        when(mockAudioManager.setCommunicationDevice(eq(mockAudioDeviceInfo)))
+                .thenReturn(true);
+
+        mCommunicationDeviceTracker.setCommunicationDevice(
+                AudioDeviceInfo.TYPE_BLE_HEADSET, device1);
+        when(mockAudioManager.getCommunicationDevice()).thenReturn(mSpeakerInfo);
+        mCommunicationDeviceTracker.clearCommunicationDevice(AudioDeviceInfo.TYPE_BLE_HEADSET);
+        verify(mRouteManager).onAudioLost(eq(DEVICE_ADDRESS_1));
+        assertFalse(mCommunicationDeviceTracker.isAudioDeviceSetForType(
+                AudioDeviceInfo.TYPE_BLE_HEADSET));
+    }
+
+    @SmallTest
+    @Test
+    public void testConnectedDevicesDoNotContainDuplicateDevices() {
+        BluetoothDevice hfpDevice = mock(BluetoothDevice.class);
+        when(hfpDevice.getAddress()).thenReturn("00:00:00:00:00:00");
+        when(hfpDevice.getType()).thenReturn(BluetoothDeviceManager.DEVICE_TYPE_HEADSET);
+        BluetoothDevice leDevice = mock(BluetoothDevice.class);
+        when(hfpDevice.getAddress()).thenReturn("00:00:00:00:00:00");
+        when(hfpDevice.getType()).thenReturn(BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO);
+
+        mBluetoothDeviceManager.onDeviceConnected(hfpDevice,
+                BluetoothDeviceManager.DEVICE_TYPE_HEADSET);
+        mBluetoothDeviceManager.onDeviceConnected(leDevice,
+                BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO);
+
+        assertEquals(1, mBluetoothDeviceManager.getNumConnectedDevices());
     }
 
     @SmallTest
@@ -525,22 +758,26 @@
         when(mBluetoothLeAudio.isInbandRingtoneEnabled(1)).thenReturn(true);
         when(mBluetoothLeAudio.getGroupId(eq(device3))).thenReturn(1);
         receiverUnderTest.onReceive(mContext,
-                buildConnectionActionIntent(BluetoothHeadset.STATE_CONNECTED, device1,
-                        BluetoothDeviceManager.DEVICE_TYPE_HEADSET));
-        receiverUnderTest.onReceive(mContext,
-                buildConnectionActionIntent(BluetoothHeadset.STATE_CONNECTED, device2,
-                        BluetoothDeviceManager.DEVICE_TYPE_HEADSET));
-        receiverUnderTest.onReceive(mContext,
                 buildConnectionActionIntent(BluetoothHeadset.STATE_CONNECTED, device3,
                         BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO));
         leAudioCallbacksTest.getValue().onGroupNodeAdded(device3, 1);
-        when(mBluetoothLeAudio.getConnectedGroupLeadDevice(1)).thenReturn(device3);
         when(mRouteManager.getBluetoothAudioConnectedDevice()).thenReturn(device3);
         when(mRouteManager.isCachedLeAudioDevice(eq(device3))).thenReturn(true);
-        assertEquals(3, mBluetoothDeviceManager.getNumConnectedDevices());
+        when(mBluetoothLeAudio.getConnectedGroupLeadDevice(1)).thenReturn(device3);
+        when(mRouteManager.getMostRecentlyReportedActiveDevice()).thenReturn(device3);
+        assertEquals(1, mBluetoothDeviceManager.getNumConnectedDevices());
         assertTrue(mBluetoothDeviceManager.isInbandRingingEnabled());
     }
 
+    private AudioDeviceInfo createMockAudioDeviceInfo(String address, int audioType) {
+        AudioDeviceInfo mockAudioDeviceInfo = mock(AudioDeviceInfo.class);
+        when(mockAudioDeviceInfo.getType()).thenReturn(audioType);
+        if (address != null) {
+            when(mockAudioDeviceInfo.getAddress()).thenReturn(address);
+        }
+        return mockAudioDeviceInfo;
+    }
+
     private Intent buildConnectionActionIntent(int state, BluetoothDevice device, int deviceType) {
         String intentString;
 
diff --git a/tests/src/com/android/server/telecom/tests/BluetoothRouteManagerTest.java b/tests/src/com/android/server/telecom/tests/BluetoothRouteManagerTest.java
index 1a6fb88..8e31f9c 100644
--- a/tests/src/com/android/server/telecom/tests/BluetoothRouteManagerTest.java
+++ b/tests/src/com/android/server/telecom/tests/BluetoothRouteManagerTest.java
@@ -16,6 +16,15 @@
 
 package com.android.server.telecom.tests;
 
+import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.nullable;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
 import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothDevice;
 import android.bluetooth.BluetoothHeadset;
@@ -29,6 +38,7 @@
 import android.test.suitebuilder.annotation.SmallTest;
 
 import com.android.internal.os.SomeArgs;
+import com.android.server.telecom.CallAudioCommunicationDeviceTracker;
 import com.android.server.telecom.TelecomSystem;
 import com.android.server.telecom.Timeouts;
 import com.android.server.telecom.bluetooth.BluetoothDeviceManager;
@@ -46,16 +56,6 @@
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
-import static org.junit.Assert.assertEquals;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyBoolean;
-import static org.mockito.ArgumentMatchers.nullable;
-import static org.mockito.Matchers.eq;
-import static org.mockito.Mockito.reset;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
 @RunWith(JUnit4.class)
 public class BluetoothRouteManagerTest extends TelecomTestCase {
     private static final int TEST_TIMEOUT = 1000;
@@ -71,6 +71,7 @@
     @Mock private BluetoothLeAudio mBluetoothLeAudio;
     @Mock private Timeouts.Adapter mTimeoutsAdapter;
     @Mock private BluetoothRouteManager.BluetoothStateListener mListener;
+    @Mock private CallAudioCommunicationDeviceTracker mCommunicationDeviceTracker;
 
     @Override
     @Before
@@ -171,11 +172,25 @@
         sm.quitNow();
     }
 
+    @SmallTest
+    @Test
+    public void testSkipInactiveBtDeviceWhenEvaluateActualState() {
+        BluetoothRouteManager sm = setupStateMachine(
+                BluetoothRouteManager.AUDIO_CONNECTED_STATE_NAME_PREFIX, HEARING_AID_DEVICE);
+        setupConnectedDevices(null, new BluetoothDevice[]{HEARING_AID_DEVICE},
+                null, null, HEARING_AID_DEVICE, null);
+        executeRoutingAction(sm, BluetoothRouteManager.BT_AUDIO_LOST,
+                HEARING_AID_DEVICE.getAddress());
+        assertEquals(BluetoothRouteManager.AUDIO_OFF_STATE_NAME, sm.getCurrentState().getName());
+        sm.quitNow();
+    }
+
     private BluetoothRouteManager setupStateMachine(String initialState,
             BluetoothDevice initialDevice) {
         resetMocks();
         BluetoothRouteManager sm = new BluetoothRouteManager(mContext,
-                new TelecomSystem.SyncRoot() { }, mDeviceManager, mTimeoutsAdapter);
+                new TelecomSystem.SyncRoot() { }, mDeviceManager,
+                mTimeoutsAdapter, mCommunicationDeviceTracker);
         sm.setListener(mListener);
         sm.setInitialStateForTesting(initialState, initialDevice);
         waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT);
diff --git a/tests/src/com/android/server/telecom/tests/BluetoothRouteTransitionTests.java b/tests/src/com/android/server/telecom/tests/BluetoothRouteTransitionTests.java
index 5eecccc..15a81d4 100644
--- a/tests/src/com/android/server/telecom/tests/BluetoothRouteTransitionTests.java
+++ b/tests/src/com/android/server/telecom/tests/BluetoothRouteTransitionTests.java
@@ -28,6 +28,7 @@
 import android.test.suitebuilder.annotation.SmallTest;
 
 import com.android.internal.os.SomeArgs;
+import com.android.server.telecom.CallAudioCommunicationDeviceTracker;
 import com.android.server.telecom.TelecomSystem;
 import com.android.server.telecom.Timeouts;
 import com.android.server.telecom.bluetooth.BluetoothDeviceManager;
@@ -66,7 +67,7 @@
 public class BluetoothRouteTransitionTests extends TelecomTestCase {
     private enum ListenerUpdate {
         DEVICE_LIST_CHANGED, ACTIVE_DEVICE_PRESENT, ACTIVE_DEVICE_GONE,
-        AUDIO_CONNECTED, AUDIO_DISCONNECTED, UNEXPECTED_STATE_CHANGE
+        AUDIO_CONNECTING, AUDIO_CONNECTED, AUDIO_DISCONNECTED, UNEXPECTED_STATE_CHANGE
     }
 
     private static class BluetoothRouteTestParametersBuilder {
@@ -263,6 +264,7 @@
     @Mock private BluetoothLeAudio mBluetoothLeAudio;
     @Mock private Timeouts.Adapter mTimeoutsAdapter;
     @Mock private BluetoothRouteManager.BluetoothStateListener mListener;
+    @Mock private CallAudioCommunicationDeviceTracker mCommunicationDeviceTracker;
 
     @Override
     @Before
@@ -348,6 +350,9 @@
                 case ACTIVE_DEVICE_GONE:
                     verify(mListener).onBluetoothActiveDeviceGone();
                     break;
+                case AUDIO_CONNECTING:
+                    verify(mListener).onBluetoothAudioConnecting();
+                    break;
                 case AUDIO_CONNECTED:
                     verify(mListener).onBluetoothAudioConnected();
                     break;
@@ -413,7 +418,8 @@
         when(mTimeoutsAdapter.getBluetoothPendingTimeoutMillis(
                 nullable(ContentResolver.class))).thenReturn(100000L);
         BluetoothRouteManager sm = new BluetoothRouteManager(mContext,
-                new TelecomSystem.SyncRoot() { }, mDeviceManager, mTimeoutsAdapter);
+                new TelecomSystem.SyncRoot() { }, mDeviceManager,
+                mTimeoutsAdapter, mCommunicationDeviceTracker);
         sm.setListener(mListener);
         sm.setInitialStateForTesting(initialState, initialDevice);
         waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT);
@@ -449,7 +455,7 @@
                 .setConnectedDevices(DEVICE2, DEVICE1)
                 .setActiveDevice(DEVICE1)
                 .setMessageType(BluetoothRouteManager.CONNECT_BT)
-                .setExpectedListenerUpdates(ListenerUpdate.AUDIO_CONNECTED)
+                .setExpectedListenerUpdates(ListenerUpdate.AUDIO_CONNECTING)
                 .setExpectedBluetoothInteraction(CONNECT)
                 .setExpectedConnectionDevice(DEVICE1)
                 .setExpectedFinalStateName(BluetoothRouteManager.AUDIO_CONNECTING_STATE_NAME_PREFIX
@@ -505,7 +511,7 @@
                 .setConnectedDevices(DEVICE2, DEVICE1, DEVICE3)
                 .setMessageType(BluetoothRouteManager.CONNECT_BT)
                 .setMessageDevice(DEVICE3)
-                .setExpectedListenerUpdates(ListenerUpdate.AUDIO_CONNECTED)
+                .setExpectedListenerUpdates(ListenerUpdate.AUDIO_CONNECTING)
                 .setExpectedBluetoothInteraction(CONNECT_SWITCH_DEVICE)
                 .setExpectedConnectionDevice(DEVICE3)
                 .setExpectedFinalStateName(BluetoothRouteManager.AUDIO_CONNECTING_STATE_NAME_PREFIX
@@ -519,7 +525,7 @@
                 .setConnectedDevices(DEVICE2, DEVICE1, DEVICE3)
                 .setMessageType(BluetoothRouteManager.CONNECT_BT)
                 .setMessageDevice(DEVICE3)
-                .setExpectedListenerUpdates(ListenerUpdate.AUDIO_CONNECTED)
+                .setExpectedListenerUpdates(ListenerUpdate.AUDIO_CONNECTING)
                 .setExpectedBluetoothInteraction(CONNECT_SWITCH_DEVICE)
                 .setExpectedConnectionDevice(DEVICE3)
                 .setExpectedFinalStateName(BluetoothRouteManager.AUDIO_CONNECTING_STATE_NAME_PREFIX
diff --git a/tests/src/com/android/server/telecom/tests/CallAudioManagerTest.java b/tests/src/com/android/server/telecom/tests/CallAudioManagerTest.java
index 3d06ad0..c8ceea9 100644
--- a/tests/src/com/android/server/telecom/tests/CallAudioManagerTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallAudioManagerTest.java
@@ -59,6 +59,7 @@
 import static org.mockito.Matchers.anyInt;
 import static org.mockito.Matchers.eq;
 import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.clearInvocations;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
@@ -277,7 +278,8 @@
         verify(mCallAudioModeStateMachine, times(2)).sendMessageWithArgs(
                 eq(CallAudioModeStateMachine.NEW_ACTIVE_OR_DIALING_CALL), captor.capture());
         assertMessageArgEquality(expectedArgs, captor.getValue());
-        verify(mCallAudioModeStateMachine, times(2)).sendMessageWithArgs(
+        // Expet another invocation due to audio mode change signal.
+        verify(mCallAudioModeStateMachine, times(3)).sendMessageWithArgs(
                 anyInt(), any(CallAudioModeStateMachine.MessageArgs.class));
 
 
@@ -286,7 +288,7 @@
         verify(mCallAudioModeStateMachine, times(3)).sendMessageWithArgs(
                 eq(CallAudioModeStateMachine.NEW_ACTIVE_OR_DIALING_CALL), captor.capture());
         assertMessageArgEquality(expectedArgs, captor.getValue());
-        verify(mCallAudioModeStateMachine, times(3)).sendMessageWithArgs(
+        verify(mCallAudioModeStateMachine, times(4)).sendMessageWithArgs(
                 anyInt(), any(CallAudioModeStateMachine.MessageArgs.class));
 
         disconnectCall(call);
@@ -327,7 +329,8 @@
         verify(mCallAudioModeStateMachine, times(2)).sendMessageWithArgs(
                 eq(CallAudioModeStateMachine.NEW_ACTIVE_OR_DIALING_CALL), captor.capture());
         assertMessageArgEquality(expectedArgs, captor.getValue());
-        verify(mCallAudioModeStateMachine, times(2)).sendMessageWithArgs(
+        // Expect an extra time due to audio mode change signal
+        verify(mCallAudioModeStateMachine, times(3)).sendMessageWithArgs(
                 anyInt(), any(CallAudioModeStateMachine.MessageArgs.class));
 
         // Ensure we started ringback.
@@ -702,6 +705,73 @@
         assertFalse(captor.getValue().isStreaming);
     }
 
+    @SmallTest
+    @Test
+    public void testTriggerAudioManagerModeChange() {
+        // Start with an incoming PSTN call
+        Call pstnCall = mock(Call.class);
+        when(pstnCall.getState()).thenReturn(CallState.RINGING);
+        when(pstnCall.getIsVoipAudioMode()).thenReturn(false);
+        ArgumentCaptor<CallAudioModeStateMachine.MessageArgs> captor = makeNewCaptor();
+
+        // Add the call
+        mCallAudioManager.onCallAdded(pstnCall);
+        verify(mCallAudioModeStateMachine).sendMessageWithArgs(
+                eq(CallAudioModeStateMachine.FOREGROUND_VOIP_MODE_CHANGE), captor.capture());
+        CallAudioModeStateMachine.MessageArgs expectedArgs =
+                new Builder()
+                        .setHasActiveOrDialingCalls(false)
+                        .setHasRingingCalls(true)
+                        .setHasHoldingCalls(false)
+                        .setIsTonePlaying(false)
+                        .setHasAudioProcessingCalls(false)
+                        .setForegroundCallIsVoip(false)
+                        .setSession(null)
+                        .setForegroundCallIsVoip(false)
+                        .build();
+        assertMessageArgEquality(expectedArgs, captor.getValue());
+        clearInvocations(mCallAudioModeStateMachine); // Avoid verifying for previous calls
+
+        // Make call active; don't expect there to be an audio mode transition.
+        when(pstnCall.getState()).thenReturn(CallState.ACTIVE);
+        mCallAudioManager.onCallStateChanged(pstnCall, CallState.RINGING, CallState.ACTIVE);
+        verify(mCallAudioModeStateMachine, never()).sendMessageWithArgs(
+                eq(CallAudioModeStateMachine.FOREGROUND_VOIP_MODE_CHANGE),
+                any(CallAudioModeStateMachine.MessageArgs.class));
+        clearInvocations(mCallAudioModeStateMachine); // Avoid verifying for previous calls
+
+        // Add a new Voip call in ringing state; this should not result in a direct audio mode
+        // change.
+        Call voipCall = mock(Call.class);
+        when(voipCall.getState()).thenReturn(CallState.RINGING);
+        when(voipCall.getIsVoipAudioMode()).thenReturn(true);
+        mCallAudioManager.onCallAdded(voipCall);
+        verify(mCallAudioModeStateMachine, never()).sendMessageWithArgs(
+                eq(CallAudioModeStateMachine.FOREGROUND_VOIP_MODE_CHANGE),
+                any(CallAudioModeStateMachine.MessageArgs.class));
+        clearInvocations(mCallAudioModeStateMachine); // Avoid verifying for previous calls
+
+        // Make voip call active and set the PSTN call to locally disconnecting; the new foreground
+        // call will be the voip call.
+        when(pstnCall.isLocallyDisconnecting()).thenReturn(true);
+        when(voipCall.getState()).thenReturn(CallState.ACTIVE);
+        mCallAudioManager.onCallStateChanged(voipCall, CallState.RINGING, CallState.ACTIVE);
+        verify(mCallAudioModeStateMachine).sendMessageWithArgs(
+                eq(CallAudioModeStateMachine.FOREGROUND_VOIP_MODE_CHANGE), captor.capture());
+        CallAudioModeStateMachine.MessageArgs expectedArgs2 =
+                new Builder()
+                        .setHasActiveOrDialingCalls(true)
+                        .setHasRingingCalls(false)
+                        .setHasHoldingCalls(false)
+                        .setIsTonePlaying(false)
+                        .setHasAudioProcessingCalls(false)
+                        .setForegroundCallIsVoip(false)
+                        .setSession(null)
+                        .setForegroundCallIsVoip(true)
+                        .build();
+        assertMessageArgEquality(expectedArgs2, captor.getValue());
+    }
+
     private Call createSimulatedRingingCall() {
         Call call = mock(Call.class);
         when(call.getState()).thenReturn(CallState.SIMULATED_RINGING);
diff --git a/tests/src/com/android/server/telecom/tests/CallAudioModeStateMachineTest.java b/tests/src/com/android/server/telecom/tests/CallAudioModeStateMachineTest.java
index d7854a5..8c6d84c 100644
--- a/tests/src/com/android/server/telecom/tests/CallAudioModeStateMachineTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallAudioModeStateMachineTest.java
@@ -16,14 +16,29 @@
 
 package com.android.server.telecom.tests;
 
+import static com.android.server.telecom.CallAudioModeStateMachine.CALL_AUDIO_FOCUS_REQUEST;
+import static com.android.server.telecom.CallAudioModeStateMachine.RING_AUDIO_FOCUS_REQUEST;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.nullable;
+import static org.mockito.Mockito.atLeast;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.media.AudioFocusRequest;
 import android.media.AudioManager;
 import android.os.HandlerThread;
 import android.test.suitebuilder.annotation.SmallTest;
 
 import com.android.server.telecom.CallAudioManager;
 import com.android.server.telecom.CallAudioModeStateMachine;
-import com.android.server.telecom.CallAudioRouteStateMachine;
 import com.android.server.telecom.CallAudioModeStateMachine.MessageArgs.Builder;
+import com.android.server.telecom.CallAudioRouteStateMachine;
 import com.android.server.telecom.SystemStateHelper;
 
 import org.junit.After;
@@ -31,18 +46,9 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
+import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
 
-import static org.junit.Assert.assertEquals;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.ArgumentMatchers.nullable;
-import static org.mockito.Matchers.anyInt;
-import static org.mockito.Mockito.clearInvocations;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
 @RunWith(JUnit4.class)
 public class CallAudioModeStateMachineTest extends TelecomTestCase {
     private static final int TEST_TIMEOUT = 1000;
@@ -62,6 +68,7 @@
         super.setUp();
         when(mCallAudioManager.getCallAudioRouteStateMachine())
                 .thenReturn(mCallAudioRouteStateMachine);
+        when(mFeatureFlags.telecomResolveHiddenDependencies()).thenReturn(false);
     }
 
     @Override
@@ -76,7 +83,7 @@
     @Test
     public void testNoFocusWhenRingerSilenced() throws Throwable {
         CallAudioModeStateMachine sm = new CallAudioModeStateMachine(mSystemStateHelper,
-                mAudioManager, mTestThread.getLooper());
+                mAudioManager, mTestThread.getLooper(), mFeatureFlags);
         sm.setCallAudioManager(mCallAudioManager);
         sm.sendMessage(CallAudioModeStateMachine.ABANDON_FOCUS_FOR_TESTING);
         waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT);
@@ -108,7 +115,7 @@
     @Test
     public void testSwitchToStreamingMode() {
         CallAudioModeStateMachine sm = new CallAudioModeStateMachine(mSystemStateHelper,
-                mAudioManager, mTestThread.getLooper());
+                mAudioManager, mTestThread.getLooper(), mFeatureFlags);
         sm.setCallAudioManager(mCallAudioManager);
         sm.sendMessage(CallAudioModeStateMachine.ABANDON_FOCUS_FOR_TESTING);
         waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT);
@@ -138,7 +145,7 @@
     @Test
     public void testExitStreamingMode() {
         CallAudioModeStateMachine sm = new CallAudioModeStateMachine(mSystemStateHelper,
-                mAudioManager, mTestThread.getLooper());
+                mAudioManager, mTestThread.getLooper(), mFeatureFlags);
         sm.setCallAudioManager(mCallAudioManager);
         sm.sendMessage(CallAudioModeStateMachine.ENTER_STREAMING_FOCUS_FOR_TESTING);
         waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT);
@@ -166,7 +173,7 @@
     @Test
     public void testNoRingWhenDeviceIsAtEar() {
         CallAudioModeStateMachine sm = new CallAudioModeStateMachine(mSystemStateHelper,
-                mAudioManager, mTestThread.getLooper());
+                mAudioManager, mTestThread.getLooper(), mFeatureFlags);
         sm.setCallAudioManager(mCallAudioManager);
         sm.sendMessage(CallAudioModeStateMachine.ABANDON_FOCUS_FOR_TESTING);
         sm.sendMessage(CallAudioModeStateMachine.NEW_HOLDING_CALL, new Builder()
@@ -202,7 +209,7 @@
     @Test
     public void testRegainFocusWhenHfpIsConnectedSilenced() throws Throwable {
         CallAudioModeStateMachine sm = new CallAudioModeStateMachine(mSystemStateHelper,
-                mAudioManager, mTestThread.getLooper());
+                mAudioManager, mTestThread.getLooper(), mFeatureFlags);
         sm.setCallAudioManager(mCallAudioManager);
         sm.sendMessage(CallAudioModeStateMachine.ABANDON_FOCUS_FOR_TESTING);
         waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT);
@@ -246,7 +253,7 @@
     @Test
     public void testDoNotRingTwiceWhenHfpConnected() {
         CallAudioModeStateMachine sm = new CallAudioModeStateMachine(mSystemStateHelper,
-                mAudioManager, mTestThread.getLooper());
+                mAudioManager, mTestThread.getLooper(), mFeatureFlags);
         sm.setCallAudioManager(mCallAudioManager);
         sm.sendMessage(CallAudioModeStateMachine.ABANDON_FOCUS_FOR_TESTING);
         waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT);
@@ -284,7 +291,7 @@
     @Test
     public void testStartRingingAfterHfpConnectedIfNotAlreadyPlaying() {
         CallAudioModeStateMachine sm = new CallAudioModeStateMachine(mSystemStateHelper,
-                mAudioManager, mTestThread.getLooper());
+                mAudioManager, mTestThread.getLooper(), mFeatureFlags);
         sm.setCallAudioManager(mCallAudioManager);
         sm.sendMessage(CallAudioModeStateMachine.ABANDON_FOCUS_FOR_TESTING);
         waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT);
@@ -318,7 +325,46 @@
         verify(mCallAudioManager, times(2)).startRinging();
     }
 
+    @SmallTest
+    @Test
+    public void testAudioFocusRequestWithResolveHiddenDependencies() {
+        CallAudioModeStateMachine sm = new CallAudioModeStateMachine(mSystemStateHelper,
+                mAudioManager, mTestThread.getLooper(), mFeatureFlags);
+        when(mFeatureFlags.telecomResolveHiddenDependencies()).thenReturn(true);
+        ArgumentCaptor<AudioFocusRequest> captor = ArgumentCaptor.forClass(AudioFocusRequest.class);
+        sm.setCallAudioManager(mCallAudioManager);
+
+        resetMocks();
+        when(mCallAudioManager.startRinging()).thenReturn(true);
+        when(mCallAudioManager.isRingtonePlaying()).thenReturn(false);
+
+        sm.sendMessage(CallAudioModeStateMachine.ENTER_RING_FOCUS_FOR_TESTING);
+        waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT);
+        verify(mAudioManager).requestAudioFocus(captor.capture());
+        assertTrue(areAudioFocusRequestsMatch(captor.getValue(), RING_AUDIO_FOCUS_REQUEST));
+
+        sm.sendMessage(CallAudioModeStateMachine.ENTER_CALL_FOCUS_FOR_TESTING);
+        waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT);
+        verify(mAudioManager, atLeast(1)).requestAudioFocus(captor.capture());
+        AudioFocusRequest request = captor.getValue();
+        assertTrue(areAudioFocusRequestsMatch(request, CALL_AUDIO_FOCUS_REQUEST));
+
+        sm.sendMessage(CallAudioModeStateMachine.ABANDON_FOCUS_FOR_TESTING);
+    }
+
     private void resetMocks() {
         clearInvocations(mCallAudioManager, mAudioManager);
     }
+
+    private boolean areAudioFocusRequestsMatch(AudioFocusRequest r1, AudioFocusRequest r2) {
+        if ((r1 == null) || (r2 == null)) {
+            return false;
+        }
+
+        if (r1.getFocusGain() != r2.getFocusGain()) {
+            return false;
+        }
+
+        return r1.getAudioAttributes().equals(r2.getAudioAttributes());
+    }
 }
diff --git a/tests/src/com/android/server/telecom/tests/CallAudioModeTransitionTests.java b/tests/src/com/android/server/telecom/tests/CallAudioModeTransitionTests.java
index c7e5aa9..21b5312 100644
--- a/tests/src/com/android/server/telecom/tests/CallAudioModeTransitionTests.java
+++ b/tests/src/com/android/server/telecom/tests/CallAudioModeTransitionTests.java
@@ -16,6 +16,10 @@
 
 package com.android.server.telecom.tests;
 
+import static com.android.server.telecom.CallAudioModeStateMachine.CALL_AUDIO_FOCUS_REQUEST;
+import static com.android.server.telecom.CallAudioModeStateMachine.RING_AUDIO_FOCUS_REQUEST;
+
+import android.media.AudioFocusRequest;
 import android.media.AudioManager;
 import android.os.HandlerThread;
 import android.test.suitebuilder.annotation.SmallTest;
@@ -37,6 +41,7 @@
 import java.util.List;
 
 import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.ArgumentMatchers.nullable;
@@ -130,13 +135,14 @@
     @SmallTest
     public void modeTransitionTest() {
         CallAudioModeStateMachine sm = new CallAudioModeStateMachine(mSystemStateHelper,
-                mAudioManager, mTestThread.getLooper());
+                mAudioManager, mTestThread.getLooper(), mFeatureFlags);
         sm.setCallAudioManager(mCallAudioManager);
         sm.sendMessage(mParams.initialAudioState);
         waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT);
 
         resetMocks();
         when(mCallAudioManager.startRinging()).thenReturn(true);
+        when(mFeatureFlags.telecomResolveHiddenDependencies()).thenReturn(false);
         if (mParams.initialAudioState
                 == CallAudioModeStateMachine.ENTER_AUDIO_PROCESSING_FOCUS_FOR_TESTING) {
             when(mAudioManager.getMode())
diff --git a/tests/src/com/android/server/telecom/tests/CallAudioRoutePeripheralAdapterTest.java b/tests/src/com/android/server/telecom/tests/CallAudioRoutePeripheralAdapterTest.java
index dfe1483..2fc6ec6 100644
--- a/tests/src/com/android/server/telecom/tests/CallAudioRoutePeripheralAdapterTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallAudioRoutePeripheralAdapterTest.java
@@ -26,6 +26,7 @@
 
 import android.test.suitebuilder.annotation.SmallTest;
 
+import com.android.server.telecom.AsyncRingtonePlayer;
 import com.android.server.telecom.CallAudioRoutePeripheralAdapter;
 import com.android.server.telecom.CallAudioRouteStateMachine;
 import com.android.server.telecom.DockManager;
@@ -47,6 +48,7 @@
     @Mock private BluetoothRouteManager mBluetoothRouteManager;
     @Mock private WiredHeadsetManager mWiredHeadsetManager;
     @Mock private DockManager mDockManager;
+    @Mock private AsyncRingtonePlayer mRingtonePlayer;
 
     @Override
     @Before
@@ -57,7 +59,8 @@
                 mCallAudioRouteStateMachine,
                 mBluetoothRouteManager,
                 mWiredHeadsetManager,
-                mDockManager);
+                mDockManager,
+                mRingtonePlayer);
     }
 
     @Override
@@ -126,6 +129,16 @@
         mAdapter.onBluetoothAudioConnected();
         verify(mCallAudioRouteStateMachine).sendMessageWithSessionInfo(
                 CallAudioRouteStateMachine.BT_AUDIO_CONNECTED);
+        verify(mRingtonePlayer).updateBtActiveState(true);
+    }
+
+    @SmallTest
+    @Test
+    public void testOnBluetoothAudioConnecting() {
+        mAdapter.onBluetoothAudioConnecting();
+        verify(mCallAudioRouteStateMachine).sendMessageWithSessionInfo(
+                CallAudioRouteStateMachine.BT_AUDIO_CONNECTED);
+        verify(mRingtonePlayer).updateBtActiveState(false);
     }
 
     @SmallTest
@@ -134,6 +147,7 @@
         mAdapter.onBluetoothAudioDisconnected();
         verify(mCallAudioRouteStateMachine).sendMessageWithSessionInfo(
                 CallAudioRouteStateMachine.BT_AUDIO_DISCONNECTED);
+        verify(mRingtonePlayer).updateBtActiveState(false);
     }
 
     @SmallTest
diff --git a/tests/src/com/android/server/telecom/tests/CallAudioRouteStateMachineTest.java b/tests/src/com/android/server/telecom/tests/CallAudioRouteStateMachineTest.java
index 569c487..3641405 100644
--- a/tests/src/com/android/server/telecom/tests/CallAudioRouteStateMachineTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallAudioRouteStateMachineTest.java
@@ -21,6 +21,7 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
+import android.content.pm.PackageManager;
 import android.media.AudioDeviceInfo;
 import android.media.AudioManager;
 import android.media.IAudioService;
@@ -29,6 +30,7 @@
 import android.test.suitebuilder.annotation.MediumTest;
 import android.test.suitebuilder.annotation.SmallTest;
 
+import com.android.server.telecom.CallAudioCommunicationDeviceTracker;
 import com.android.server.telecom.bluetooth.BluetoothRouteManager;
 import com.android.server.telecom.Call;
 import com.android.server.telecom.CallAudioRouteStateMachine;
@@ -50,6 +52,7 @@
 import org.mockito.invocation.InvocationOnMock;
 import org.mockito.stubbing.Answer;
 
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
@@ -58,8 +61,10 @@
 import java.util.Set;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.ArgumentMatchers.nullable;
 import static org.mockito.Matchers.any;
@@ -100,6 +105,7 @@
     private AudioManager mockAudioManager;
     private final TelecomSystem.SyncRoot mLock = new TelecomSystem.SyncRoot() { };
     private HandlerThread mThreadHandler;
+    CallAudioCommunicationDeviceTracker mCommunicationDeviceTracker;
 
     @Override
     @Before
@@ -110,6 +116,8 @@
         mThreadHandler.start();
         mContext = mComponentContextFixture.getTestDouble().getApplicationContext();
         mockAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
+        mCommunicationDeviceTracker = new CallAudioCommunicationDeviceTracker(mContext);
+        mCommunicationDeviceTracker.setBluetoothRouteManager(mockBluetoothRouteManager);
 
         mAudioServiceFactory = new CallAudioManager.AudioServiceFactory() {
             @Override
@@ -154,7 +162,8 @@
                 mAudioServiceFactory,
                 CallAudioRouteStateMachine.EARPIECE_AUTO_DETECT,
                 mThreadHandler.getLooper(),
-                Runnable::run /** do async stuff sync for test purposes */);
+                Runnable::run /** do async stuff sync for test purposes */,
+                mCommunicationDeviceTracker);
 
         // Since we don't know if we're on a platform with an earpiece or not, all we can do
         // is ensure the stateMachine construction didn't fail.  But at least we exercised the
@@ -174,7 +183,8 @@
                 mAudioServiceFactory,
                 CallAudioRouteStateMachine.EARPIECE_AUTO_DETECT,
                 mThreadHandler.getLooper(),
-                Runnable::run /** do async stuff sync for test purposes */);
+                Runnable::run /** do async stuff sync for test purposes */,
+                mCommunicationDeviceTracker);
         stateMachine.setCallAudioManager(mockCallAudioManager);
 
         Set<Call> trackedCalls = new HashSet<>(Arrays.asList(fakeCall, fakeSelfManagedCall));
@@ -208,6 +218,57 @@
                 .onCallAudioStateChanged(any(), any());
     }
 
+    @SmallTest
+    @Test
+    public void testSystemAudioStateIsUpdated() {
+        CallAudioRouteStateMachine stateMachine = new CallAudioRouteStateMachine(
+                mContext,
+                mockCallsManager,
+                mockBluetoothRouteManager,
+                mockWiredHeadsetManager,
+                mockStatusBarNotifier,
+                mAudioServiceFactory,
+                CallAudioRouteStateMachine.EARPIECE_AUTO_DETECT,
+                mThreadHandler.getLooper(),
+                Runnable::run /** do async stuff sync for test purposes */,
+                mCommunicationDeviceTracker);
+        stateMachine.setCallAudioManager(mockCallAudioManager);
+
+        Set<Call> trackedCalls = new HashSet<>(Arrays.asList(fakeCall, fakeSelfManagedCall));
+        when(mockCallsManager.getTrackedCalls()).thenReturn(trackedCalls);
+
+        // start state --> ROUTE_EARPIECE
+        CallAudioState initState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE,
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER);
+        stateMachine.initialize(initState);
+
+        stateMachine.setCallAudioManager(mockCallAudioManager);
+
+        assertEquals(stateMachine.getCurrentCallAudioState().getRoute(),
+                CallAudioRouteStateMachine.ROUTE_EARPIECE);
+
+        // ROUTE_EARPIECE  --> ROUTE_SPEAKER
+        stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.SWITCH_SPEAKER,
+                CallAudioRouteStateMachine.SPEAKER_ON);
+
+        stateMachine.sendMessageWithSessionInfo(
+                CallAudioRouteStateMachine.UPDATE_SYSTEM_AUDIO_ROUTE);
+
+        waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT);
+        waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT);
+
+        CallAudioState expectedCallAudioState = stateMachine.getLastKnownCallAudioState();
+
+        // assert expected end state
+        assertEquals(stateMachine.getCurrentCallAudioState().getRoute(),
+                CallAudioRouteStateMachine.ROUTE_SPEAKER);
+        // should update the audio route on all tracked calls ...
+        verify(mockConnectionServiceWrapper, times(trackedCalls.size()))
+                .onCallAudioStateChanged(any(), any());
+
+        assertEquals(expectedCallAudioState, stateMachine.getCurrentCallAudioState());
+    }
+
     @MediumTest
     @Test
     public void testStreamRingMuteChange() {
@@ -220,7 +281,8 @@
                 mAudioServiceFactory,
                 CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED,
                 mThreadHandler.getLooper(),
-                Runnable::run /** do async stuff sync for test purposes */);
+                Runnable::run /** do async stuff sync for test purposes */,
+                mCommunicationDeviceTracker);
         stateMachine.setCallAudioManager(mockCallAudioManager);
         CallAudioState initState = new CallAudioState(false, CallAudioState.ROUTE_SPEAKER,
                 CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER);
@@ -250,6 +312,7 @@
             foundValid = true;
         }
         assertTrue(foundValid);
+        verify(mockBluetoothRouteManager, timeout(1000L)).getBluetoothAudioConnectedDevice();
     }
 
     @MediumTest
@@ -264,7 +327,8 @@
                 mAudioServiceFactory,
                 CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED,
                 mThreadHandler.getLooper(),
-                Runnable::run /** do async stuff sync for test purposes */);
+                Runnable::run /** do async stuff sync for test purposes */,
+                mCommunicationDeviceTracker);
 
         when(mockBluetoothRouteManager.isBluetoothAudioConnectedOrPending()).thenReturn(false);
         when(mockBluetoothRouteManager.isBluetoothAvailable()).thenReturn(true);
@@ -310,7 +374,8 @@
                 mAudioServiceFactory,
                 CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED,
                 mThreadHandler.getLooper(),
-                Runnable::run /** do async stuff sync for test purposes */);
+                Runnable::run /** do async stuff sync for test purposes */,
+                mCommunicationDeviceTracker);
         stateMachine.setCallAudioManager(mockCallAudioManager);
 
         when(mockBluetoothRouteManager.isBluetoothAudioConnectedOrPending()).thenReturn(false);
@@ -355,7 +420,8 @@
                 mAudioServiceFactory,
                 CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED,
                 mThreadHandler.getLooper(),
-                Runnable::run /** do async stuff sync for test purposes */);
+                Runnable::run /** do async stuff sync for test purposes */,
+                mCommunicationDeviceTracker);
         stateMachine.setCallAudioManager(mockCallAudioManager);
         Collection<BluetoothDevice> availableDevices = Collections.singleton(bluetoothDevice1);
 
@@ -434,7 +500,8 @@
                 mAudioServiceFactory,
                 CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED,
                 mThreadHandler.getLooper(),
-                Runnable::run /** do async stuff sync for test purposes */);
+                Runnable::run /** do async stuff sync for test purposes */,
+                mCommunicationDeviceTracker);
         stateMachine.setCallAudioManager(mockCallAudioManager);
 
         when(mockBluetoothRouteManager.isBluetoothAudioConnectedOrPending()).thenReturn(false);
@@ -471,7 +538,8 @@
                 mAudioServiceFactory,
                 CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED,
                 mThreadHandler.getLooper(),
-                Runnable::run /** do async stuff sync for test purposes */);
+                Runnable::run /** do async stuff sync for test purposes */,
+                mCommunicationDeviceTracker);
         stateMachine.setCallAudioManager(mockCallAudioManager);
         setInBandRing(false);
         when(mockBluetoothRouteManager.isBluetoothAudioConnectedOrPending()).thenReturn(false);
@@ -512,7 +580,10 @@
                 .thenReturn(bluetoothDevice1);
         stateMachine.sendMessage(CallAudioRouteStateMachine.BT_AUDIO_CONNECTED);
         waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT);
-        verify(mockCallAudioManager, times(1)).onRingerModeChange();
+        // It is possible that this will be called twice from ActiveBluetoothRoute#enter. The extra
+        // call to setBluetoothOn will trigger BT_AUDIO_CONNECTED, which also ends up invoking
+        // CallAudioManager#onRingerModeChange.
+        verify(mockCallAudioManager, atLeastOnce()).onRingerModeChange();
     }
 
     @SmallTest
@@ -527,7 +598,8 @@
                 mAudioServiceFactory,
                 CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED,
                 mThreadHandler.getLooper(),
-                Runnable::run /** do async stuff sync for test purposes */);
+                Runnable::run /** do async stuff sync for test purposes */,
+                mCommunicationDeviceTracker);
         stateMachine.setCallAudioManager(mockCallAudioManager);
         List<BluetoothDevice> availableDevices =
                 Arrays.asList(bluetoothDevice1, bluetoothDevice2, bluetoothDevice3);
@@ -578,7 +650,8 @@
                 mAudioServiceFactory,
                 CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED,
                 mThreadHandler.getLooper(),
-                Runnable::run /** do async stuff sync for test purposes */);
+                Runnable::run /** do async stuff sync for test purposes */,
+                mCommunicationDeviceTracker);
         stateMachine.setCallAudioManager(mockCallAudioManager);
         when(mockAudioManager.isSpeakerphoneOn()).thenReturn(false);
         CallAudioState initState = new CallAudioState(false, CallAudioState.ROUTE_SPEAKER,
@@ -610,7 +683,8 @@
                 mAudioServiceFactory,
                 CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED,
                 mThreadHandler.getLooper(),
-                Runnable::run /** do async stuff sync for test purposes */);
+                Runnable::run /** do async stuff sync for test purposes */,
+                mCommunicationDeviceTracker);
         stateMachine.setCallAudioManager(mockCallAudioManager);
 
         when(mockAudioManager.isSpeakerphoneOn()).thenReturn(false);
@@ -645,7 +719,8 @@
                 mAudioServiceFactory,
                 CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED,
                 mThreadHandler.getLooper(),
-                Runnable::run /** do async stuff sync for test purposes */);
+                Runnable::run /** do async stuff sync for test purposes */,
+                mCommunicationDeviceTracker);
         stateMachine.setCallAudioManager(mockCallAudioManager);
         List<BluetoothDevice> availableDevices =
                 Arrays.asList(bluetoothDevice1, bluetoothDevice2);
@@ -674,6 +749,116 @@
         verify(mockBluetoothRouteManager, atLeastOnce())
                 .connectBluetoothAudio(eq(bluetoothDevice1.getAddress()));
         assertTrue(stateMachine.isInActiveState());
+
+        // Switch to inactive, pretending that the call disconnected.
+        stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.SWITCH_FOCUS,
+                CallAudioRouteStateMachine.NO_FOCUS);
+        waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT);
+
+        // Make sure that we've successfully switched to the quiescent BT route
+        assertEquals(CallAudioState.ROUTE_BLUETOOTH,
+                stateMachine.getCurrentCallAudioState().getRoute());
+        assertFalse(stateMachine.isInActiveState());
+    }
+
+    @SmallTest
+    @Test
+    public void testSetAndClearEarpieceCommunicationDevice() {
+        CallAudioRouteStateMachine stateMachine = new CallAudioRouteStateMachine(
+                mContext,
+                mockCallsManager,
+                mockBluetoothRouteManager,
+                mockWiredHeadsetManager,
+                mockStatusBarNotifier,
+                mAudioServiceFactory,
+                CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED,
+                mThreadHandler.getLooper(),
+                Runnable::run /** do async stuff sync for test purposes */,
+                mCommunicationDeviceTracker);
+        stateMachine.setCallAudioManager(mockCallAudioManager);
+
+        AudioDeviceInfo earpiece = mock(AudioDeviceInfo.class);
+        when(earpiece.getType()).thenReturn(AudioDeviceInfo.TYPE_BUILTIN_EARPIECE);
+        when(earpiece.getAddress()).thenReturn("");
+        List<AudioDeviceInfo> devices = new ArrayList<>();
+        devices.add(earpiece);
+
+        when(mockAudioManager.getAvailableCommunicationDevices())
+                .thenReturn(devices);
+        when(mockAudioManager.setCommunicationDevice(eq(earpiece)))
+                .thenReturn(true);
+        when(mockAudioManager.getCommunicationDevice()).thenReturn(earpiece);
+
+        CallAudioState initState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE,
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER |
+                        CallAudioState.ROUTE_WIRED_HEADSET);
+        stateMachine.initialize(initState);
+
+        // Switch to active
+        stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.SWITCH_FOCUS,
+                CallAudioRouteStateMachine.ACTIVE_FOCUS);
+        waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT);
+
+        // Make sure that we've successfully switched to the active earpiece and that we set the
+        // communication device.
+        assertTrue(stateMachine.isInActiveState());
+        ArgumentCaptor<AudioDeviceInfo> infoArgumentCaptor = ArgumentCaptor.forClass(
+                AudioDeviceInfo.class);
+        verify(mockAudioManager).setCommunicationDevice(infoArgumentCaptor.capture());
+        assertEquals(AudioDeviceInfo.TYPE_BUILTIN_EARPIECE,
+                infoArgumentCaptor.getValue().getType());
+
+        // Route earpiece to speaker
+        stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.SWITCH_SPEAKER,
+                CallAudioRouteStateMachine.SPEAKER_ON);
+        waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT);
+
+        // Assert that communication device was cleared
+        verify(mockAudioManager).clearCommunicationDevice();
+    }
+
+    @SmallTest
+    @Test
+    public void testSetAndClearWiredHeadsetCommunicationDevice() {
+        verifySetAndClearHeadsetCommunicationDevice(AudioDeviceInfo.TYPE_WIRED_HEADSET);
+    }
+
+    @SmallTest
+    @Test
+    public void testSetAndClearUsbHeadsetCommunicationDevice() {
+        verifySetAndClearHeadsetCommunicationDevice(AudioDeviceInfo.TYPE_USB_HEADSET);
+    }
+
+    @SmallTest
+    @Test
+    public void testActiveFocusRouteSwitchFromQuiescentBluetooth() {
+        CallAudioRouteStateMachine stateMachine = new CallAudioRouteStateMachine(
+                mContext,
+                mockCallsManager,
+                mockBluetoothRouteManager,
+                mockWiredHeadsetManager,
+                mockStatusBarNotifier,
+                mAudioServiceFactory,
+                CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED,
+                mThreadHandler.getLooper(),
+                Runnable::run /** do async stuff sync for test purposes */,
+                mCommunicationDeviceTracker);
+        stateMachine.setCallAudioManager(mockCallAudioManager);
+
+        // Start the route in quiescent and ensure that a switch to ACTIVE_FOCUS transitions to
+        // the corresponding active route even when there aren't any active BT devices available.
+        CallAudioState initState = new CallAudioState(false,
+                CallAudioState.ROUTE_BLUETOOTH,
+                CallAudioState.ROUTE_BLUETOOTH | CallAudioState.ROUTE_EARPIECE);
+        stateMachine.initialize(initState);
+
+        // Switch to active
+        stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.SWITCH_FOCUS,
+                CallAudioRouteStateMachine.ACTIVE_FOCUS);
+        waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT);
+
+        // Make sure that we've successfully switched to the active route on BT
+        assertTrue(stateMachine.isInActiveState());
     }
 
     @SmallTest
@@ -761,7 +946,8 @@
                 mAudioServiceFactory,
                 CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED,
                 mThreadHandler.getLooper(),
-                Runnable::run /** do async stuff sync for test purposes */);
+                Runnable::run /** do async stuff sync for test purposes */,
+                mCommunicationDeviceTracker);
         stateMachine.initialize();
         assertEquals(expectedState, stateMachine.getCurrentCallAudioState());
     }
@@ -778,7 +964,8 @@
                 mAudioServiceFactory,
                 CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED,
                 mThreadHandler.getLooper(),
-                Runnable::run /** do async stuff sync for test purposes */);
+                Runnable::run /** do async stuff sync for test purposes */,
+                mCommunicationDeviceTracker);
         stateMachine.setCallAudioManager(mockCallAudioManager);
 
         CallAudioState initState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE,
@@ -798,6 +985,48 @@
         assertEquals(initState, stateMachine.getCurrentCallAudioState());
     }
 
+    @SmallTest
+    @Test
+    public void testIgnoreSpeakerOffMessage() {
+        when(mockBluetoothRouteManager.isInbandRingingEnabled()).thenReturn(true);
+        when(mockBluetoothRouteManager.getBluetoothAudioConnectedDevice())
+                .thenReturn(bluetoothDevice1);
+        when(mockBluetoothRouteManager.isBluetoothAudioConnectedOrPending()).thenReturn(true);
+        CallAudioRouteStateMachine stateMachine = new CallAudioRouteStateMachine(
+                mContext,
+                mockCallsManager,
+                mockBluetoothRouteManager,
+                mockWiredHeadsetManager,
+                mockStatusBarNotifier,
+                mAudioServiceFactory,
+                CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED,
+                mThreadHandler.getLooper(),
+                Runnable::run /** do async stuff sync for test purposes */,
+                mCommunicationDeviceTracker);
+        stateMachine.setCallAudioManager(mockCallAudioManager);
+
+        CallAudioState initState = new CallAudioState(false, CallAudioState.ROUTE_SPEAKER,
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER
+                        | CallAudioState.ROUTE_BLUETOOTH);
+        stateMachine.initialize(initState);
+
+        doAnswer(
+                (address) -> {
+                    stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.SPEAKER_OFF);
+                    stateMachine.sendMessageDelayed(CallAudioRouteStateMachine.BT_AUDIO_CONNECTED,
+                            5000L);
+                    return null;
+        }).when(mockBluetoothRouteManager).connectBluetoothAudio(anyString());
+        stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.SWITCH_FOCUS,
+                CallAudioRouteStateMachine.ACTIVE_FOCUS);
+        stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.USER_SWITCH_BLUETOOTH);
+
+        CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_SPEAKER,
+                CallAudioState.ROUTE_SPEAKER | CallAudioState.ROUTE_BLUETOOTH
+                        | CallAudioState.ROUTE_EARPIECE);
+        assertEquals(expectedState, stateMachine.getCurrentCallAudioState());
+    }
+
     private void initializationTestHelper(CallAudioState expectedState,
             int earpieceControl) {
         when(mockWiredHeadsetManager.isPluggedIn()).thenReturn(
@@ -816,7 +1045,8 @@
                 mAudioServiceFactory,
                 earpieceControl,
                 mThreadHandler.getLooper(),
-                Runnable::run /** do async stuff sync for test purposes */);
+                Runnable::run /** do async stuff sync for test purposes */,
+                mCommunicationDeviceTracker);
         stateMachine.initialize();
         assertEquals(expectedState, stateMachine.getCurrentCallAudioState());
     }
@@ -856,4 +1086,58 @@
         doNothing().when(mockConnectionServiceWrapper).onCallAudioStateChanged(any(Call.class),
                 any(CallAudioState.class));
     }
+
+    private void verifySetAndClearHeadsetCommunicationDevice(int audioType) {
+        CallAudioRouteStateMachine stateMachine = new CallAudioRouteStateMachine(
+                mContext,
+                mockCallsManager,
+                mockBluetoothRouteManager,
+                mockWiredHeadsetManager,
+                mockStatusBarNotifier,
+                mAudioServiceFactory,
+                CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED,
+                mThreadHandler.getLooper(),
+                Runnable::run /** do async stuff sync for test purposes */,
+                mCommunicationDeviceTracker);
+        stateMachine.setCallAudioManager(mockCallAudioManager);
+
+        AudioDeviceInfo headset = mock(AudioDeviceInfo.class);
+        when(headset.getType()).thenReturn(audioType);
+        when(headset.getAddress()).thenReturn("");
+        List<AudioDeviceInfo> devices = new ArrayList<>();
+        devices.add(headset);
+
+        when(mockAudioManager.getAvailableCommunicationDevices())
+                .thenReturn(devices);
+        when(mockAudioManager.setCommunicationDevice(eq(headset)))
+                .thenReturn(true);
+        when(mockAudioManager.getCommunicationDevice()).thenReturn(headset);
+
+        CallAudioState initState = new CallAudioState(false,
+                CallAudioState.ROUTE_WIRED_HEADSET,
+                CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_EARPIECE);
+        stateMachine.initialize(initState);
+
+        // Switch to active
+        stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.SWITCH_FOCUS,
+                CallAudioRouteStateMachine.ACTIVE_FOCUS);
+        waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT);
+
+        // Make sure that we've successfully switched to the active headset and that we set the
+        // communication device.
+        assertTrue(stateMachine.isInActiveState());
+        ArgumentCaptor<AudioDeviceInfo> infoArgumentCaptor = ArgumentCaptor.forClass(
+                AudioDeviceInfo.class);
+        verify(mockAudioManager).setCommunicationDevice(infoArgumentCaptor.capture());
+        assertEquals(audioType, infoArgumentCaptor.getValue().getType());
+
+        // Route out of headset route
+        stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.SWITCH_FOCUS,
+                CallAudioRouteStateMachine.ACTIVE_FOCUS);
+        stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.USER_SWITCH_EARPIECE);
+        waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT);
+
+        // Assert that communication device was cleared
+        verify(mockAudioManager).clearCommunicationDevice();
+    }
 }
diff --git a/tests/src/com/android/server/telecom/tests/CallAudioRouteTransitionTests.java b/tests/src/com/android/server/telecom/tests/CallAudioRouteTransitionTests.java
index cf684de..804ef17 100644
--- a/tests/src/com/android/server/telecom/tests/CallAudioRouteTransitionTests.java
+++ b/tests/src/com/android/server/telecom/tests/CallAudioRouteTransitionTests.java
@@ -20,6 +20,7 @@
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.nullable;
 import static org.mockito.ArgumentMatchers.same;
+import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.doReturn;
@@ -40,6 +41,7 @@
 import android.test.suitebuilder.annotation.SmallTest;
 
 import com.android.server.telecom.Call;
+import com.android.server.telecom.CallAudioCommunicationDeviceTracker;
 import com.android.server.telecom.CallAudioManager;
 import com.android.server.telecom.CallAudioRouteStateMachine;
 import com.android.server.telecom.CallsManager;
@@ -155,6 +157,7 @@
     @Mock StatusBarNotifier mockStatusBarNotifier;
     @Mock Call fakeCall;
     @Mock CallAudioManager mockCallAudioManager;
+    private CallAudioCommunicationDeviceTracker mCommunicationDeviceTracker;
     private CallAudioManager.AudioServiceFactory mAudioServiceFactory;
     private static final int TEST_TIMEOUT = 500;
     private AudioManager mockAudioManager;
@@ -174,6 +177,8 @@
         mHandlerThread.start();
         mContext = mComponentContextFixture.getTestDouble().getApplicationContext();
         mockAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
+        mCommunicationDeviceTracker = new CallAudioCommunicationDeviceTracker(mContext);
+        mCommunicationDeviceTracker.setBluetoothRouteManager(mockBluetoothRouteManager);
 
         mAudioServiceFactory = new CallAudioManager.AudioServiceFactory() {
             @Override
@@ -270,7 +275,8 @@
                 mAudioServiceFactory,
                 mParams.earpieceControl,
                 mHandlerThread.getLooper(),
-                Runnable::run /** do async stuff sync for test purposes */);
+                Runnable::run /** do async stuff sync for test purposes */,
+                mCommunicationDeviceTracker);
         stateMachine.setCallAudioManager(mockCallAudioManager);
 
         setupMocksForParams(stateMachine, mParams);
@@ -311,7 +317,7 @@
                 break;
             case ON:
                 if (mParams.expectedBluetoothDevice == null) {
-                    verify(mockBluetoothRouteManager).connectBluetoothAudio(null);
+                    verify(mockBluetoothRouteManager, atLeastOnce()).connectBluetoothAudio(null);
                 } else {
                     verify(mockBluetoothRouteManager).connectBluetoothAudio(
                             mParams.expectedBluetoothDevice.getAddress());
@@ -367,7 +373,8 @@
                 mAudioServiceFactory,
                 mParams.earpieceControl,
                 mHandlerThread.getLooper(),
-                Runnable::run /** do async stuff sync for test purposes */);
+                Runnable::run /** do async stuff sync for test purposes */,
+                mCommunicationDeviceTracker);
         stateMachine.setCallAudioManager(mockCallAudioManager);
 
         // Set up bluetooth and speakerphone state
diff --git a/tests/src/com/android/server/telecom/tests/CallEndpointControllerTest.java b/tests/src/com/android/server/telecom/tests/CallEndpointControllerTest.java
index f4008aa..9101a19 100644
--- a/tests/src/com/android/server/telecom/tests/CallEndpointControllerTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallEndpointControllerTest.java
@@ -17,6 +17,7 @@
 package com.android.server.telecom.tests;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
@@ -50,7 +51,9 @@
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.HashSet;
+import java.util.List;
 import java.util.Set;
+import java.util.stream.Collectors;
 
 @RunWith(JUnit4.class)
 public class CallEndpointControllerTest extends TelecomTestCase {
@@ -81,6 +84,9 @@
             availableBluetooth1);
     private static final CallAudioState audioState7 = new CallAudioState(false,
             CallAudioState.ROUTE_STREAMING, CallAudioState.ROUTE_ALL, null, availableBluetooth1);
+    private static final CallAudioState audioState8 = new CallAudioState(false,
+            CallAudioState.ROUTE_EARPIECE, CallAudioState.ROUTE_ALL, bluetoothDevice1,
+            availableBluetooth2);
 
     private CallEndpointController mCallEndpointController;
 
@@ -177,6 +183,74 @@
         verify(mConnectionService, never()).onMuteStateChanged(any(), anyBoolean());
     }
 
+    /**
+     * Ensure that {@link CallAudioManager#setAudioRoute(int, String)} is invoked when the user
+     * requests to switch to a bluetooth CallEndpoint.  This is an edge case where bluetooth is not
+     * the current CallEndpoint but the CallAudioState shows the bluetooth device is
+     * active/available.
+     */
+    @Test
+    public void testSwitchFromEarpieceToBluetooth() {
+        // simulate an audio state where the EARPIECE is active but a bluetooth device is active.
+        mCallEndpointController.onCallAudioStateChanged(null, audioState8 /* Ear but BT active */);
+        CallEndpoint btEndpoint = mCallEndpointController.getAvailableEndpoints().stream()
+                .filter(e -> e.getEndpointType() == CallEndpoint.TYPE_BLUETOOTH)
+                .toList().get(0); // get the only available BT endpoint
+
+        // verify the CallEndpointController shows EARPIECE active + BT endpoint is active device
+        assertEquals(CallEndpoint.TYPE_EARPIECE,
+                mCallEndpointController.getCurrentCallEndpoint().getEndpointType());
+        assertNotNull(btEndpoint);
+
+        // request an endpoint change from earpiece to the bluetooth
+        doReturn(audioState8).when(mCallAudioManager).getCallAudioState();
+        mCallEndpointController.requestCallEndpointChange(btEndpoint, mResultReceiver);
+
+        // verify the transaction was successful and CallAudioManager#setAudioRoute was called
+        verify(mResultReceiver, never()).send(eq(CallEndpoint.ENDPOINT_OPERATION_FAILED), any());
+        verify(mCallAudioManager, times(1)).setAudioRoute(eq(CallAudioState.ROUTE_BLUETOOTH),
+                eq(bluetoothDevice1.getAddress()));
+    }
+
+
+    /**
+     * Ensure that {@link CallAudioManager#setAudioRoute(int, String)} is invoked when the user
+     * requests to switch to from one bluetooth device to another.
+     */
+    @Test
+    public void testBtDeviceSwitch() {
+        // bluetoothDevice1 should start as active and bluetoothDevice2 is available
+        mCallEndpointController.onCallAudioStateChanged(null, audioState2 /* BT active D1 */);
+        CallEndpoint currentEndpoint = mCallEndpointController.getCurrentCallEndpoint();
+        List<CallEndpoint> btEndpoints = mCallEndpointController.getAvailableEndpoints().stream()
+                .filter(e -> e.getEndpointType() == CallEndpoint.TYPE_BLUETOOTH)
+                .toList(); // get the only available BT endpoint
+
+        // verify the initial state of the test
+        assertEquals(2, btEndpoints.size());
+        assertEquals(CallEndpoint.TYPE_BLUETOOTH, currentEndpoint.getEndpointType());
+
+        CallEndpoint otherBluetoothEndpoint = null;
+        for (CallEndpoint e : btEndpoints) {
+            if (!e.equals(currentEndpoint)) {
+                otherBluetoothEndpoint = e;
+            }
+        }
+
+        assertNotNull(otherBluetoothEndpoint);
+        assertNotEquals(currentEndpoint, otherBluetoothEndpoint);
+
+        // request an endpoint change from BT D1 --> BT D2
+        doReturn(audioState2).when(mCallAudioManager).getCallAudioState();
+        mCallEndpointController.requestCallEndpointChange(otherBluetoothEndpoint, mResultReceiver);
+
+        // verify the transaction was successful and CallAudioManager#setAudioRoute was called
+        verify(mResultReceiver, never()).send(eq(CallEndpoint.ENDPOINT_OPERATION_FAILED), any());
+        verify(mCallAudioManager, times(1))
+                .setAudioRoute(eq(CallAudioState.ROUTE_BLUETOOTH),
+                        eq(bluetoothDevice2.getAddress()));
+    }
+
     @Test
     public void testAvailableEndpointChanged() throws Exception {
         mCallEndpointController.onCallAudioStateChanged(audioState1, audioState6);
diff --git a/tests/src/com/android/server/telecom/tests/CallLogManagerTest.java b/tests/src/com/android/server/telecom/tests/CallLogManagerTest.java
index 9466220..94709cd 100644
--- a/tests/src/com/android/server/telecom/tests/CallLogManagerTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallLogManagerTest.java
@@ -36,6 +36,7 @@
 import android.content.ContentResolver;
 import android.content.ContentValues;
 import android.content.Context;
+import android.content.pm.PackageManager;
 import android.content.pm.UserInfo;
 import android.content.res.Resources;
 import android.location.Country;
@@ -65,6 +66,7 @@
 import androidx.test.filters.FlakyTest;
 
 import com.android.server.telecom.Analytics;
+import com.android.server.telecom.AnomalyReporterAdapter;
 import com.android.server.telecom.Call;
 import com.android.server.telecom.CallLogManager;
 import com.android.server.telecom.CallState;
@@ -72,6 +74,7 @@
 import com.android.server.telecom.MissedCallNotifier;
 import com.android.server.telecom.PhoneAccountRegistrar;
 import com.android.server.telecom.TelephonyUtil;
+import com.android.server.telecom.flags.FeatureFlags;
 
 import org.junit.After;
 import org.junit.Before;
@@ -123,6 +126,11 @@
     PhoneAccountRegistrar mMockPhoneAccountRegistrar;
     @Mock
     MissedCallNotifier mMissedCallNotifier;
+    @Mock
+    AnomalyReporterAdapter mAnomalyReporterAdapter;
+
+    @Mock
+    FeatureFlags mFeatureFlags;
 
     @Override
     @Before
@@ -130,7 +138,7 @@
         super.setUp();
         mContext = mComponentContextFixture.getTestDouble().getApplicationContext();
         mCallLogManager = new CallLogManager(mContext, mMockPhoneAccountRegistrar,
-                mMissedCallNotifier);
+                mMissedCallNotifier, mAnomalyReporterAdapter, mFeatureFlags);
         mDefaultAccountHandle = new PhoneAccountHandle(
                 new ComponentName("com.android.server.telecom.tests", "CallLogManagerTest"),
                 TEST_PHONE_ACCOUNT_ID,
@@ -181,6 +189,9 @@
         when(userManager.getUserInfo(eq(CURRENT_USER_ID))).thenReturn(userInfo);
         when(userManager.getUserInfo(eq(OTHER_USER_ID))).thenReturn(otherUserInfo);
         when(userManager.getUserInfo(eq(MANAGED_USER_ID))).thenReturn(managedProfileUserInfo);
+        PackageManager packageManager = mContext.getPackageManager();
+        when(packageManager.hasSystemFeature(PackageManager.FEATURE_WATCH)).thenReturn(false);
+        when(mFeatureFlags.telecomLogExternalWearableCalls()).thenReturn(false);
     }
 
     @Override
@@ -788,6 +799,34 @@
         assertEquals(1, insertedValues.getAsInteger(Calls.IS_READ).intValue());
     }
 
+    @Test
+    public void testLogCallWhenExternalCallOnWatch() {
+        when(mMockPhoneAccountRegistrar.getPhoneAccountUnchecked(any(PhoneAccountHandle.class)))
+                .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, CURRENT_USER_ID));
+        PackageManager packageManager = mContext.getPackageManager();
+        when(packageManager.hasSystemFeature(PackageManager.FEATURE_WATCH)).thenReturn(true);
+        when(mFeatureFlags.telecomLogExternalWearableCalls()).thenReturn(true);
+        Call fakeMissedCall = makeFakeCall(
+                DisconnectCause.REJECTED, // disconnectCauseCode
+                false, // isConference
+                true, // isIncoming
+                1L, // creationTimeMillis
+                1000L, // ageMillis
+                TEL_PHONEHANDLE, // callHandle
+                mDefaultAccountHandle, // phoneAccountHandle
+                NO_VIDEO_STATE, // callVideoState
+                POST_DIAL_STRING, // postDialDigits
+                VIA_NUMBER_STRING, // viaNumber
+                null
+        );
+        when(fakeMissedCall.isExternalCall()).thenReturn(true);
+
+        mCallLogManager.onCallStateChanged(fakeMissedCall, CallState.ACTIVE,
+                CallState.DISCONNECTED);
+        verifyInsertionWithCapture(CURRENT_USER_ID);
+    }
+
+
     @SmallTest
     @Test
     public void testCountryIso_setCache() {
diff --git a/tests/src/com/android/server/telecom/tests/CallsManagerTest.java b/tests/src/com/android/server/telecom/tests/CallsManagerTest.java
index 9f46336..d4c62f7 100644
--- a/tests/src/com/android/server/telecom/tests/CallsManagerTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallsManagerTest.java
@@ -84,6 +84,7 @@
 import com.android.server.telecom.AsyncRingtonePlayer;
 import com.android.server.telecom.Call;
 import com.android.server.telecom.CallAnomalyWatchdog;
+import com.android.server.telecom.CallAudioCommunicationDeviceTracker;
 import com.android.server.telecom.CallAudioManager;
 import com.android.server.telecom.CallAudioModeStateMachine;
 import com.android.server.telecom.CallAudioRouteStateMachine;
@@ -123,6 +124,8 @@
 import com.android.server.telecom.bluetooth.BluetoothStateReceiver;
 import com.android.server.telecom.callfiltering.BlockedNumbersAdapter;
 import com.android.server.telecom.callfiltering.CallFilteringResult;
+import com.android.server.telecom.flags.FeatureFlags;
+import com.android.server.telecom.callfiltering.IncomingCallFilterGraph;
 import com.android.server.telecom.ui.AudioProcessingNotification;
 import com.android.server.telecom.ui.CallStreamingNotification;
 import com.android.server.telecom.ui.DisconnectedCallNotifier;
@@ -276,7 +279,11 @@
     @Mock private Ringer.AccessibilityManagerAdapter mAccessibilityManagerAdapter;
     @Mock private BlockedNumbersAdapter mBlockedNumbersAdapter;
     @Mock private PhoneCapability mPhoneCapability;
+    @Mock private CallAudioCommunicationDeviceTracker mCommunicationDeviceTracker;
     @Mock private CallStreamingNotification mCallStreamingNotification;
+    @Mock private FeatureFlags mFeatureFlags;
+
+    @Mock private IncomingCallFilterGraph mIncomingCallFilterGraph;
 
     private CallsManager mCallsManager;
 
@@ -296,8 +303,8 @@
         when(mCallEndpointControllerFactory.create(any(), any(), any())).thenReturn(
                 mCallEndpointController);
         when(mCallAudioRouteStateMachineFactory.create(any(), any(), any(), any(), any(), any(),
-                anyInt(), any())).thenReturn(mCallAudioRouteStateMachine);
-        when(mCallAudioModeStateMachineFactory.create(any(), any()))
+                anyInt(), any(), any())).thenReturn(mCallAudioRouteStateMachine);
+        when(mCallAudioModeStateMachineFactory.create(any(), any(), any()))
                 .thenReturn(mCallAudioModeStateMachine);
         when(mClockProxy.currentTimeMillis()).thenReturn(System.currentTimeMillis());
         when(mClockProxy.elapsedRealtime()).thenReturn(SystemClock.elapsedRealtime());
@@ -345,10 +352,15 @@
                 mAccessibilityManagerAdapter,
                 // Just do async tasks synchronously to support testing.
                 command -> command.run(),
+                // For call audio tasks
+                command -> command.run(),
                 mBlockedNumbersAdapter,
                 TransactionManager.getTestInstance(),
                 mEmergencyCallDiagnosticLogger,
-                mCallStreamingNotification);
+                mCommunicationDeviceTracker,
+                mCallStreamingNotification,
+                mFeatureFlags,
+                (call, listener, context, timeoutsAdapter, lock) -> mIncomingCallFilterGraph);
 
         when(mPhoneAccountRegistrar.getPhoneAccount(
                 eq(SELF_MANAGED_HANDLE), any())).thenReturn(SELF_MANAGED_ACCOUNT);
@@ -1318,8 +1330,9 @@
 
     @SmallTest
     @Test
-    public void testNoFilteringOfCallsWhenPhoneAccountRequestsSkipped() {
+    public void testDndFilterAppliesOfCallsWhenPhoneAccountRequestsSkipped() {
         // GIVEN an incoming call which is from a PhoneAccount that requested to skip filtering.
+        when(mFeatureFlags.skipFilterPhoneAccountPerformDndFilter()).thenReturn(true);
         Call incomingCall = addSpyCall(SIM_1_HANDLE, CallState.NEW);
         Bundle extras = new Bundle();
         extras.putBoolean(PhoneAccount.EXTRA_SKIP_CALL_FILTERING, true);
@@ -1339,7 +1352,35 @@
         // WHEN the incoming call is successfully added.
         mCallsManager.onSuccessfulIncomingCall(incomingCall);
 
-        // THEN the incoming call is not using call filtering
+        // THEN the incoming call is still applying Dnd filter.
+        verify(incomingCall).setIsUsingCallFiltering(eq(true));
+    }
+
+    @SmallTest
+    @Test
+    public void testNoFilterAppliesOfCallsWhenFlagNotEnabled() {
+        // Flag is not enabled.
+        when(mFeatureFlags.skipFilterPhoneAccountPerformDndFilter()).thenReturn(false);
+        Call incomingCall = addSpyCall(SIM_1_HANDLE, CallState.NEW);
+        Bundle extras = new Bundle();
+        extras.putBoolean(PhoneAccount.EXTRA_SKIP_CALL_FILTERING, true);
+        PhoneAccount skipRequestedAccount = new PhoneAccount.Builder(SIM_2_HANDLE, "Skipper")
+                .setCapabilities(PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION
+                        | PhoneAccount.CAPABILITY_CALL_PROVIDER)
+                .setExtras(extras)
+                .setIsEnabled(true)
+                .build();
+        when(mPhoneAccountRegistrar.getPhoneAccountUnchecked(SIM_1_HANDLE))
+                .thenReturn(skipRequestedAccount);
+        doReturn(false).when(incomingCall).can(Connection.CAPABILITY_HOLD);
+        doReturn(false).when(incomingCall).can(Connection.CAPABILITY_SUPPORT_HOLD);
+        doReturn(false).when(incomingCall).isSelfManaged();
+        doReturn(true).when(incomingCall).setState(anyInt(), any());
+
+        // WHEN the incoming call is successfully added.
+        mCallsManager.onSuccessfulIncomingCall(incomingCall);
+
+        // THEN the incoming call is not applying filter.
         verify(incomingCall).setIsUsingCallFiltering(eq(false));
     }
 
@@ -2500,6 +2541,30 @@
         assertEquals(DEFAULT_CALL_SCREENING_APP, outgoingCall.getPostCallPackageName());
     }
 
+    /**
+     * Verify the only call state set from calling onSuccessfulOutgoingCall is CallState.DIALING.
+     */
+    @SmallTest
+    @Test
+    public void testOutgoingCallStateIsSetToAPreviousStateAndIgnored() {
+        Call outgoingCall = addSpyCall(CallState.CONNECTING);
+        mCallsManager.onSuccessfulOutgoingCall(outgoingCall, CallState.NEW);
+        verify(outgoingCall, never()).setState(eq(CallState.NEW), any());
+        verify(outgoingCall, times(1)).setState(eq(CallState.DIALING), any());
+    }
+
+    /**
+     * Verify a ConnectionService can start the call in the active state and avoid the dialing state
+     */
+    @SmallTest
+    @Test
+    public void testOutgoingCallStateCanAvoidDialingAndGoStraightToActive() {
+        Call outgoingCall = addSpyCall(CallState.CONNECTING);
+        mCallsManager.onSuccessfulOutgoingCall(outgoingCall, CallState.ACTIVE);
+        verify(outgoingCall, never()).setState(eq(CallState.DIALING), any());
+        verify(outgoingCall, times(1)).setState(eq(CallState.ACTIVE), any());
+    }
+
     @SmallTest
     @Test
     public void testRejectIncomingCallOnPAHInactive_SecondaryUser() throws Exception {
@@ -2981,7 +3046,6 @@
         Call call = addSpyCall(CONNECTION_MGR_1_HANDLE, CallState.NEW);
         when(call.getHandoverDestinationCall()).thenReturn(destinationCall);
         when(call.getHandoverState()).thenReturn(HandoverState.HANDOVER_FROM_STARTED);
-
         mCallsManager.createActionSetCallStateAndPerformAction(
                 call, CallState.DISCONNECTED, "");
 
diff --git a/tests/src/com/android/server/telecom/tests/ComponentContextFixture.java b/tests/src/com/android/server/telecom/tests/ComponentContextFixture.java
index cc22de2..c732720 100644
--- a/tests/src/com/android/server/telecom/tests/ComponentContextFixture.java
+++ b/tests/src/com/android/server/telecom/tests/ComponentContextFixture.java
@@ -16,6 +16,7 @@
 
 package com.android.server.telecom.tests;
 
+import com.android.server.telecom.flags.FeatureFlags;
 import com.google.common.collect.ArrayListMultimap;
 import com.google.common.collect.Multimap;
 
@@ -27,6 +28,8 @@
 import org.mockito.stubbing.Answer;
 
 import android.Manifest;
+import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
 import android.app.AppOpsManager;
 import android.app.NotificationManager;
 import android.app.StatusBarManager;
@@ -55,6 +58,7 @@
 import android.location.LocationManager;
 import android.media.AudioDeviceInfo;
 import android.media.AudioManager;
+import android.net.Uri;
 import android.os.BugreportManager;
 import android.os.Bundle;
 import android.os.DropBoxManager;
@@ -81,8 +85,10 @@
 import android.util.DisplayMetrics;
 import android.view.accessibility.AccessibilityManager;
 
+import java.io.ByteArrayInputStream;
 import java.io.File;
 import java.io.IOException;
+import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashMap;
@@ -409,12 +415,23 @@
         }
 
         @Override
+        public void sendBroadcastAsUser(Intent intent, UserHandle user, String receiverPermission) {
+            // Override so that this can be verified via spy.
+        }
+
+        @Override
         public void sendBroadcastAsUser(Intent intent, UserHandle user, String receiverPermission,
                 Bundle options) {
             // Override so that this can be verified via spy.
         }
 
         @Override
+        public void sendBroadcastAsUser(Intent intent, UserHandle user, String receiverPermission,
+                int appOp) {
+            // Override so that this can be verified via spy.
+        }
+
+        @Override
         public void sendOrderedBroadcastAsUser(Intent intent, UserHandle user,
                 String receiverPermission, BroadcastReceiver resultReceiver, Handler scheduler,
                 int initialCode, String initialData, Bundle initialExtras) {
@@ -617,8 +634,9 @@
 
     private TelecomManager mTelecomManager = mock(TelecomManager.class);
 
-    public ComponentContextFixture() {
+    public ComponentContextFixture(FeatureFlags featureFlags) {
         MockitoAnnotations.initMocks(this);
+        when(featureFlags.telecomResolveHiddenDependencies()).thenReturn(true);
         when(mResources.getConfiguration()).thenReturn(mResourceConfiguration);
         when(mResources.getString(anyInt())).thenReturn("");
         when(mResources.getStringArray(anyInt())).thenReturn(new String[0]);
@@ -701,7 +719,7 @@
             }
         }).when(mAppOpsManager).checkPackage(anyInt(), anyString());
 
-        when(mNotificationManager.matchesCallFilter(any(Bundle.class))).thenReturn(true);
+        when(mNotificationManager.matchesCallFilter(any(Uri.class))).thenReturn(true);
 
         when(mCarrierConfigManager.getConfig()).thenReturn(new PersistableBundle());
         when(mCarrierConfigManager.getConfigForSubId(anyInt())).thenReturn(new PersistableBundle());
@@ -735,6 +753,14 @@
         mServiceInfoByComponentName.put(componentName, serviceInfo);
     }
 
+    public void removeConnectionService(
+            ComponentName componentName,
+            IConnectionService service)
+            throws Exception {
+        removeService(ConnectionService.SERVICE_INTERFACE, componentName, service);
+        mServiceInfoByComponentName.remove(componentName);
+    }
+
     public void addInCallService(
             ComponentName componentName,
             IInCallService service,
@@ -756,6 +782,8 @@
                 componentName.getPackageName() });
         when(mPackageManager.checkPermission(eq(Manifest.permission.CONTROL_INCALL_EXPERIENCE),
                 eq(componentName.getPackageName()))).thenReturn(PackageManager.PERMISSION_GRANTED);
+        when(mPackageManager.checkPermission(eq(Manifest.permission.INTERACT_ACROSS_USERS),
+                eq(componentName.getPackageName()))).thenReturn(PackageManager.PERMISSION_GRANTED);
         when(mPermissionCheckerManager.checkPermission(
                 eq(Manifest.permission.CONTROL_INCALL_EXPERIENCE),
                 any(AttributionSourceState.class), anyString(), anyBoolean(), anyBoolean(),
@@ -794,6 +822,11 @@
         when(mResources.getStringArray(eq(id))).thenReturn(value);
     }
 
+    public void putRawResource(int id, String content) {
+        when(mResources.openRawResource(id))
+                .thenReturn(new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8)));
+    }
+
     public void setTelecomManager(TelecomManager telecomManager) {
         mTelecomManager = telecomManager;
     }
@@ -828,6 +861,12 @@
         mComponentNameByService.put(service, name);
     }
 
+    private void removeService(String action, ComponentName name, IInterface service) {
+        mComponentNamesByAction.remove(action, name);
+        mServiceByComponentName.remove(name);
+        mComponentNameByService.remove(service);
+    }
+
     private List<ResolveInfo> doQueryIntentServices(Intent intent, int flags) {
         List<ResolveInfo> result = new ArrayList<>();
         for (ComponentName componentName : mComponentNamesByAction.get(intent.getAction())) {
diff --git a/tests/src/com/android/server/telecom/tests/InCallTonePlayerTest.java b/tests/src/com/android/server/telecom/tests/InCallTonePlayerTest.java
index f11afc1..1f1b939 100644
--- a/tests/src/com/android/server/telecom/tests/InCallTonePlayerTest.java
+++ b/tests/src/com/android/server/telecom/tests/InCallTonePlayerTest.java
@@ -35,6 +35,7 @@
 import android.media.ToneGenerator;
 import android.test.suitebuilder.annotation.SmallTest;
 
+import com.android.server.telecom.AsyncRingtonePlayer;
 import com.android.server.telecom.CallAudioManager;
 import com.android.server.telecom.CallAudioRoutePeripheralAdapter;
 import com.android.server.telecom.CallAudioRouteStateMachine;
@@ -69,6 +70,7 @@
     @Mock private InCallTonePlayer.ToneGeneratorFactory mToneGeneratorFactory;
     @Mock private WiredHeadsetManager mWiredHeadsetManager;
     @Mock private DockManager mDockManager;
+    @Mock private AsyncRingtonePlayer mRingtonePlayer;
     @Mock private BluetoothDevice mDevice;
     @Mock private BluetoothAdapter mBluetoothAdapter;
 
@@ -124,7 +126,7 @@
 
         mCallAudioRoutePeripheralAdapter = new CallAudioRoutePeripheralAdapter(
                 mCallAudioRouteStateMachine, mBluetoothRouteManager, mWiredHeadsetManager,
-                mDockManager);
+                mDockManager, mRingtonePlayer);
         mFactory = new InCallTonePlayer.Factory(mCallAudioRoutePeripheralAdapter, mLock,
                 mToneGeneratorFactory, mMediaPlayerFactory, mAudioManagerAdapter);
         mFactory.setCallAudioManager(mCallAudioManager);
diff --git a/tests/src/com/android/server/telecom/tests/MissedInformationTest.java b/tests/src/com/android/server/telecom/tests/MissedInformationTest.java
index 4af3de3..753c847 100644
--- a/tests/src/com/android/server/telecom/tests/MissedInformationTest.java
+++ b/tests/src/com/android/server/telecom/tests/MissedInformationTest.java
@@ -109,6 +109,7 @@
         mAdapter = new CallIntentProcessor.AdapterImpl(mCallsManager.getDefaultDialerCache());
         mNotificationManager = spy((NotificationManager) mContext.getSystemService(
                 Context.NOTIFICATION_SERVICE));
+        when(mFeatureFlags.telecomResolveHiddenDependencies()).thenReturn(true);
         when(mContentResolver.getPackageName()).thenReturn(PACKAGE_NAME);
         when(mContentResolver.acquireProvider(any(String.class))).thenReturn(mContentProvider);
         when(mContentProvider.call(any(String.class), any(String.class),
@@ -358,7 +359,7 @@
         setUpIncomingCall();
         doReturn(mNotificationManager).when(mSpyContext)
                 .getSystemService(Context.NOTIFICATION_SERVICE);
-        doReturn(false).when(mNotificationManager).matchesCallFilter(any(Bundle.class));
+        doReturn(false).when(mNotificationManager).matchesCallFilter(any(Uri.class));
         doReturn(false).when(mIncomingCall).wasDndCheckComputedForCall();
         mCallsManager.getRinger().setNotificationManager(mNotificationManager);
 
@@ -369,7 +370,7 @@
 
         // Wait for ringer attributes build completed
         verify(mNotificationManager, timeout(TEST_TIMEOUT_MILLIS))
-                .matchesCallFilter(any(Bundle.class));
+                .matchesCallFilter(any(Uri.class));
         mCallsManager.getRinger().waitForAttributesCompletion();
 
         mCallsManager.markCallAsDisconnected(mIncomingCall,
diff --git a/tests/src/com/android/server/telecom/tests/NewOutgoingCallIntentBroadcasterTest.java b/tests/src/com/android/server/telecom/tests/NewOutgoingCallIntentBroadcasterTest.java
index 33acd98..1ffcb76 100644
--- a/tests/src/com/android/server/telecom/tests/NewOutgoingCallIntentBroadcasterTest.java
+++ b/tests/src/com/android/server/telecom/tests/NewOutgoingCallIntentBroadcasterTest.java
@@ -53,6 +53,7 @@
 import android.telephony.TelephonyManager;
 import android.test.suitebuilder.annotation.SmallTest;
 
+import com.android.server.telecom.flags.FeatureFlags;
 import com.android.server.telecom.Call;
 import com.android.server.telecom.CallsManager;
 import com.android.server.telecom.DefaultDialerCache;
@@ -75,6 +76,8 @@
 
 @RunWith(JUnit4.class)
 public class NewOutgoingCallIntentBroadcasterTest extends TelecomTestCase {
+    private static final Uri TEST_URI = Uri.parse("tel:16505551212");
+
     private static class ReceiverIntentPair {
         public BroadcastReceiver receiver;
         public Intent intent;
@@ -93,6 +96,7 @@
     @Mock private PhoneAccountRegistrar mPhoneAccountRegistrar;
     @Mock private RoleManagerAdapter mRoleManagerAdapter;
     @Mock private DefaultDialerCache mDefaultDialerCache;
+    @Mock private FeatureFlags mFeatureFlags;
 
     @Mock private MmiUtils mMmiUtils;
     private PhoneNumberUtilsAdapter mPhoneNumberUtilsAdapter = new PhoneNumberUtilsAdapterImpl();
@@ -113,6 +117,7 @@
             any(PhoneAccountHandle.class))).thenReturn(mPhoneAccount);
         when(mPhoneAccount.isSelfManaged()).thenReturn(true);
         when(mSystemStateHelper.isCarModeOrProjectionActive()).thenReturn(false);
+        when(mFeatureFlags.isNewOutgoingCallBroadcastUnblocking()).thenReturn(false);
     }
 
     @Override
@@ -510,6 +515,84 @@
         testUnmodifiedRegularCall();
     }
 
+    /**
+     * Where the flag `isNewOutgoingCallBroadcastUnblocking` is off, verify that we sent an ordered
+     * broadcast and did not try to start the call immediately (legacy behavior).
+     */
+    @SmallTest
+    @Test
+    public void testSendBroadcastBlocking() {
+        when(mFeatureFlags.isNewOutgoingCallBroadcastUnblocking()).thenReturn(false);
+        Intent intent = new Intent(Intent.ACTION_CALL, TEST_URI);
+        NewOutgoingCallIntentBroadcaster nocib = new NewOutgoingCallIntentBroadcaster(
+                mContext, mCallsManager, intent, mPhoneNumberUtilsAdapter,
+                true /* isDefaultPhoneApp */, mDefaultDialerCache, mMmiUtils, mFeatureFlags);
+
+        NewOutgoingCallIntentBroadcaster.CallDisposition disposition = nocib.evaluateCall();
+        nocib.processCall(mCall, disposition);
+
+        // We should not have not short-circuited to place the outgoing call directly.
+        verify(mCall, never()).setNewOutgoingCallIntentBroadcastIsDone();
+        verify(mCallsManager, never()).placeOutgoingCall(any(Call.class), any(Uri.class),
+                any(GatewayInfo.class), anyBoolean(), anyInt());
+
+        // Ensure we did send the broadcast ordered
+        verifyBroadcastSent(TEST_URI.getSchemeSpecificPart(),
+                createNumberExtras(TEST_URI.getSchemeSpecificPart()));
+
+        // Ensure we did not try to directly send the broadcast unordered.
+        verify(mContext, never()).sendBroadcastAsUser(
+                any(Intent.class),
+                eq(UserHandle.CURRENT),
+                eq(android.Manifest.permission.PROCESS_OUTGOING_CALLS));
+    }
+
+    /**
+     * Where the flag `isNewOutgoingCallBroadcastUnblocking` is off, verify that we sent an ordered
+     * broadcast and did not try to start the call immediately.  Also ensure that the broadcast
+     * flags are correct.
+     */
+    @SmallTest
+    @Test
+    public void testSendBroadcastNonBlocking() {
+        when(mFeatureFlags.isNewOutgoingCallBroadcastUnblocking()).thenReturn(true);
+        Intent intent = new Intent(Intent.ACTION_CALL, TEST_URI);
+        NewOutgoingCallIntentBroadcaster nocib = new NewOutgoingCallIntentBroadcaster(
+                mContext, mCallsManager, intent, mPhoneNumberUtilsAdapter,
+                true /* isDefaultPhoneApp */, mDefaultDialerCache, mMmiUtils, mFeatureFlags);
+
+        NewOutgoingCallIntentBroadcaster.CallDisposition disposition = nocib.evaluateCall();
+        nocib.processCall(mCall, disposition);
+
+        // We should have started the outgoing call flow immediately.
+        verify(mCall).setNewOutgoingCallIntentBroadcastIsDone();
+        verify(mCallsManager).placeOutgoingCall(any(Call.class), any(Uri.class),
+                nullable(GatewayInfo.class), anyBoolean(), anyInt());
+
+        // Ensure we didn't send an ordered broadcast.
+        verify(mContext, never()).sendOrderedBroadcastAsUser(
+                any(Intent.class),
+                any(UserHandle.class),
+                anyString(),
+                anyInt(),
+                any(Bundle.class),
+                any(BroadcastReceiver.class),
+                any(Handler.class),
+                eq(Activity.RESULT_OK),
+                anyString(),
+                any(Bundle.class));
+
+        // But that we did send a regular broadcast.
+        ArgumentCaptor<Intent> intentArgumentCaptor = ArgumentCaptor.forClass(Intent.class);
+        verify(mContext).sendBroadcastAsUser(
+                intentArgumentCaptor.capture(),
+                eq(UserHandle.CURRENT),
+                eq(android.Manifest.permission.PROCESS_OUTGOING_CALLS),
+                eq(AppOpsManager.OP_PROCESS_OUTGOING_CALLS));
+        Intent capturedIntent = intentArgumentCaptor.getValue();
+        assertEquals(Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND, capturedIntent.getFlags());
+    }
+
     private ReceiverIntentPair regularCallTestHelper(Intent intent,
             Bundle expectedAdditionalExtras) {
         Uri handle = intent.getData();
@@ -542,7 +625,7 @@
             boolean isDefaultPhoneApp) {
         NewOutgoingCallIntentBroadcaster b = new NewOutgoingCallIntentBroadcaster(
                 mContext, mCallsManager, intent, mPhoneNumberUtilsAdapter,
-                isDefaultPhoneApp, mDefaultDialerCache, mMmiUtils);
+                isDefaultPhoneApp, mDefaultDialerCache, mMmiUtils, mFeatureFlags);
         NewOutgoingCallIntentBroadcaster.CallDisposition cd = b.evaluateCall();
         if (cd.disconnectCause == DisconnectCause.NOT_DISCONNECTED) {
             b.processCall(mCall, cd);
diff --git a/tests/src/com/android/server/telecom/tests/PhoneAccountRegistrarTest.java b/tests/src/com/android/server/telecom/tests/PhoneAccountRegistrarTest.java
index e573bb8..9fcb87a 100644
--- a/tests/src/com/android/server/telecom/tests/PhoneAccountRegistrarTest.java
+++ b/tests/src/com/android/server/telecom/tests/PhoneAccountRegistrarTest.java
@@ -353,6 +353,40 @@
                 PhoneAccount.SCHEME_TEL));
     }
 
+    /**
+     * Verify when a {@link android.telecom.ConnectionService} is disabled or cannot be resolved,
+     * all phone accounts are unregistered when calling
+     * {@link  PhoneAccountRegistrar#getAccountsForPackage_BypassResolveComp(String, UserHandle)}.
+     */
+    @Test
+    public void testCannotResolveServiceUnregistersAccounts() throws Exception {
+        ComponentName componentName = makeQuickConnectionServiceComponentName();
+        PhoneAccount account = makeQuickAccountBuilder("0", 0, USER_HANDLE_10)
+                .setCapabilities(PhoneAccount.CAPABILITY_CONNECTION_MANAGER
+                        | PhoneAccount.CAPABILITY_CALL_PROVIDER).build();
+        // add the ConnectionService and register a single phone account for it
+        mComponentContextFixture.addConnectionService(componentName,
+                Mockito.mock(IConnectionService.class));
+        registerAndEnableAccount(account);
+        // verify the start state
+        assertEquals(1,
+                mRegistrar.getAccountsForPackage_BypassResolveComp(componentName.getPackageName(),
+                        USER_HANDLE_10).size());
+        // remove the ConnectionService so that the account cannot be resolved anymore
+        mComponentContextFixture.removeConnectionService(componentName,
+                Mockito.mock(IConnectionService.class));
+        // verify the account is unregistered when fetching the phone accounts for the package
+        assertEquals(1,
+                mRegistrar.getAccountsForPackage_BypassResolveComp(componentName.getPackageName(),
+                        USER_HANDLE_10).size());
+        assertEquals(0,mRegistrar.cleanupUnresolvableConnectionServiceAccounts(
+                mRegistrar.getAccountsForPackage_BypassResolveComp(componentName.getPackageName(),
+                USER_HANDLE_10)).size());
+        assertEquals(0,
+                mRegistrar.getAccountsForPackage_BypassResolveComp(componentName.getPackageName(),
+                        USER_HANDLE_10).size());
+    }
+
     @MediumTest
     @Test
     public void testSimCallManager() throws Exception {
diff --git a/tests/src/com/android/server/telecom/tests/RingbackPlayerTest.java b/tests/src/com/android/server/telecom/tests/RingbackPlayerTest.java
new file mode 100644
index 0000000..8de5e28
--- /dev/null
+++ b/tests/src/com/android/server/telecom/tests/RingbackPlayerTest.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2022 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.tests;
+
+import static org.junit.Assert.assertFalse;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.when;
+
+import android.test.suitebuilder.annotation.SmallTest;
+
+import com.android.server.telecom.Call;
+import com.android.server.telecom.CallState;
+import com.android.server.telecom.InCallTonePlayer;
+import com.android.server.telecom.RingbackPlayer;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+
+import java.util.concurrent.CountDownLatch;
+
+@RunWith(JUnit4.class)
+public class RingbackPlayerTest extends TelecomTestCase {
+    @Mock InCallTonePlayer.Factory mFactory;
+    @Mock Call mCall;
+    @Mock InCallTonePlayer mTonePlayer;
+
+    private RingbackPlayer mRingbackPlayer;
+
+    @Before
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        when(mFactory.createPlayer(anyInt())).thenReturn(mTonePlayer);
+        mRingbackPlayer = new RingbackPlayer(mFactory);
+    }
+
+    @SmallTest
+    @Test
+    public void testPlayerSync() {
+        // make sure InCallTonePlayer try to start playing the tone after RingbackPlayer receives
+        // stop tone request.
+        CountDownLatch latch = new CountDownLatch(1);
+        doReturn(CallState.DIALING).when(mCall).getState();
+        doAnswer(x -> {
+            new Thread(() -> {
+                try {
+                    latch.await();
+                } catch (InterruptedException e) {
+                    // Ignore
+                }
+            }).start();
+            return true;
+        }).when(mTonePlayer).startTone();
+
+        mRingbackPlayer.startRingbackForCall(mCall);
+        mRingbackPlayer.stopRingbackForCall(mCall);
+        assertFalse(mRingbackPlayer.isRingbackPlaying());
+        latch.countDown();
+    }
+}
diff --git a/tests/src/com/android/server/telecom/tests/RingerTest.java b/tests/src/com/android/server/telecom/tests/RingerTest.java
index a4adf77..baa4f90 100644
--- a/tests/src/com/android/server/telecom/tests/RingerTest.java
+++ b/tests/src/com/android/server/telecom/tests/RingerTest.java
@@ -16,8 +16,9 @@
 
 package com.android.server.telecom.tests;
 
+import static android.os.VibrationEffect.EFFECT_CLICK;
 import static android.provider.Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS;
-
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
@@ -30,6 +31,8 @@
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.timeout;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyNoMoreInteractions;
@@ -50,6 +53,9 @@
 import android.os.VibrationAttributes;
 import android.os.VibrationEffect;
 import android.os.Vibrator;
+import android.os.VibratorInfo;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 import android.telecom.PhoneAccountHandle;
 import android.telecom.TelecomManager;
 import android.test.suitebuilder.annotation.SmallTest;
@@ -62,33 +68,47 @@
 import com.android.server.telecom.Ringer;
 import com.android.server.telecom.RingtoneFactory;
 import com.android.server.telecom.SystemSettingsUtil;
+import com.android.server.telecom.flags.FeatureFlags;
 
 import org.junit.After;
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
 import org.mockito.Mock;
 import org.mockito.Spy;
 
+import java.time.Duration;
 import java.util.concurrent.CompletableFuture;
 
 @RunWith(JUnit4.class)
 public class RingerTest extends TelecomTestCase {
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
     private static final Uri FAKE_RINGTONE_URI = Uri.parse("content://media/fake/audio/1729");
     // Returned when the a URI-based VibrationEffect is attempted, to avoid depending on actual
     // device configuration for ringtone URIs. The actual Uri can be verified via the
     // VibrationEffectProxy mock invocation.
     private static final VibrationEffect URI_VIBRATION_EFFECT =
             VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK);
+    private static final VibrationEffect EXPECTED_SIMPLE_VIBRATION_PATTERN =
+            VibrationEffect.createWaveform(
+                    new long[] {0, 1000, 1000}, new int[] {0, 255, 0}, 1);
+    private static final VibrationEffect EXPECTED_PULSE_VIBRATION_PATTERN =
+            VibrationEffect.createWaveform(
+                    Ringer.PULSE_PATTERN, Ringer.PULSE_AMPLITUDE, 5);
 
     @Mock InCallTonePlayer.Factory mockPlayerFactory;
     @Mock SystemSettingsUtil mockSystemSettingsUtil;
     @Mock RingtoneFactory mockRingtoneFactory;
     @Mock Vibrator mockVibrator;
+    @Mock VibratorInfo mockVibratorInfo;
     @Mock InCallController mockInCallController;
     @Mock NotificationManager mockNotificationManager;
     @Mock Ringer.AccessibilityManagerAdapter mockAccessibilityManagerAdapter;
+    @Mock private FeatureFlags mFeatureFlags;
 
     @Spy Ringer.VibrationEffectProxy spyVibrationEffectProxy;
 
@@ -110,21 +130,26 @@
     @Before
     public void setUp() throws Exception {
         super.setUp();
-        mContext = mComponentContextFixture.getTestDouble().getApplicationContext();
+        mContext = spy(mComponentContextFixture.getTestDouble().getApplicationContext());
+        when(mFeatureFlags.telecomResolveHiddenDependencies()).thenReturn(true);
         doReturn(URI_VIBRATION_EFFECT).when(spyVibrationEffectProxy).get(any(), any());
         when(mockPlayerFactory.createPlayer(anyInt())).thenReturn(mockTonePlayer);
         mockAudioManager = mContext.getSystemService(AudioManager.class);
         when(mockAudioManager.getRingerMode()).thenReturn(AudioManager.RINGER_MODE_NORMAL);
+        when(mockVibrator.getInfo()).thenReturn(mockVibratorInfo);
         when(mockSystemSettingsUtil.isHapticPlaybackSupported(any(Context.class)))
                 .thenAnswer((invocation) -> mIsHapticPlaybackSupported);
         mockNotificationManager =mContext.getSystemService(NotificationManager.class);
         when(mockTonePlayer.startTone()).thenReturn(true);
-        when(mockNotificationManager.matchesCallFilter(any(Bundle.class))).thenReturn(true);
+        when(mockNotificationManager.matchesCallFilter(any(Uri.class))).thenReturn(true);
         when(mockRingtoneFactory.hasHapticChannels(any(Ringtone.class))).thenReturn(false);
         when(mockCall1.getState()).thenReturn(CallState.RINGING);
         when(mockCall2.getState()).thenReturn(CallState.RINGING);
         when(mockCall1.getAssociatedUser()).thenReturn(PA_HANDLE.getUserHandle());
         when(mockCall2.getAssociatedUser()).thenReturn(PA_HANDLE.getUserHandle());
+        // Set BT active state in tests to ensure that we do not end up blocking tests for 1 sec
+        // waiting for BT to connect in unit tests by default.
+        asyncRingtonePlayer.updateBtActiveState(true);
 
         createRingerUnderTest();
     }
@@ -136,7 +161,8 @@
     private void createRingerUnderTest() {
         mRingerUnderTest = new Ringer(mockPlayerFactory, mContext, mockSystemSettingsUtil,
                 asyncRingtonePlayer, mockRingtoneFactory, mockVibrator, spyVibrationEffectProxy,
-                mockInCallController, mockNotificationManager, mockAccessibilityManagerAdapter);
+                mockInCallController, mockNotificationManager, mockAccessibilityManagerAdapter,
+                mFeatureFlags);
         // This future is used to wait for AsyncRingtonePlayer to finish its part.
         mRingerUnderTest.setBlockOnRingingFuture(mRingCompletionFuture);
     }
@@ -149,6 +175,151 @@
 
     @SmallTest
     @Test
+    public void testSimpleVibrationPrecedesValidSupportedDefaultRingVibrationOverride()
+            throws Exception {
+        when(mFeatureFlags.useDeviceProvidedSerializedRingerVibration()).thenReturn(true);
+        mockVibrationResourceValues(
+                """
+                    <vibration>
+                        <predefined-effect name="click"/>
+                    </vibration>
+                """,
+                /* useSimpleVibration= */ true);
+        when(mockVibratorInfo.areVibrationFeaturesSupported(any())).thenReturn(true);
+
+        createRingerUnderTest();
+
+        assertEquals(EXPECTED_SIMPLE_VIBRATION_PATTERN, mRingerUnderTest.mDefaultVibrationEffect);
+    }
+
+    @SmallTest
+    @Test
+    public void testDefaultRingVibrationOverrideNotUsedWhenFeatureIsDisabled()
+            throws Exception {
+        when(mFeatureFlags.useDeviceProvidedSerializedRingerVibration()).thenReturn(false);
+        mockVibrationResourceValues(
+                """
+                    <vibration>
+                        <waveform-effect>
+                            <waveform-entry durationMs="100" amplitude="0"/>
+                            <repeating>
+                                <waveform-entry durationMs="500" amplitude="default"/>
+                                <waveform-entry durationMs="700" amplitude="0"/>
+                            </repeating>
+                        </waveform-effect>
+                    </vibration>
+                """,
+                /* useSimpleVibration= */ false);
+        when(mockVibratorInfo.areVibrationFeaturesSupported(any())).thenReturn(true);
+
+        createRingerUnderTest();
+
+        assertEquals(EXPECTED_PULSE_VIBRATION_PATTERN, mRingerUnderTest.mDefaultVibrationEffect);
+    }
+
+    @SmallTest
+    @Test
+    public void testValidSupportedRepeatingDefaultRingVibrationOverride() throws Exception {
+        when(mFeatureFlags.useDeviceProvidedSerializedRingerVibration()).thenReturn(true);
+        mockVibrationResourceValues(
+                """
+                    <vibration>
+                        <waveform-effect>
+                            <waveform-entry durationMs="100" amplitude="0"/>
+                            <repeating>
+                                <waveform-entry durationMs="500" amplitude="default"/>
+                                <waveform-entry durationMs="700" amplitude="0"/>
+                            </repeating>
+                        </waveform-effect>
+                    </vibration>
+                """,
+                /* useSimpleVibration= */ false);
+        when(mockVibratorInfo.areVibrationFeaturesSupported(any())).thenReturn(true);
+
+        createRingerUnderTest();
+
+        assertEquals(
+                VibrationEffect.createWaveform(new long[]{100, 500, 700}, /* repeat= */ 1),
+                mRingerUnderTest.mDefaultVibrationEffect);
+    }
+
+    @SmallTest
+    @Test
+    public void testValidSupportedNonRepeatingDefaultRingVibrationOverride() throws Exception {
+        when(mFeatureFlags.useDeviceProvidedSerializedRingerVibration()).thenReturn(true);
+        mockVibrationResourceValues(
+                """
+                    <vibration>
+                        <predefined-effect name="click"/>
+                    </vibration>
+                """,
+                /* useSimpleVibration= */ false);
+        when(mockVibratorInfo.areVibrationFeaturesSupported(any())).thenReturn(true);
+
+        createRingerUnderTest();
+
+        assertEquals(
+                VibrationEffect
+                        .startComposition()
+                        .repeatEffectIndefinitely(
+                                VibrationEffect
+                                        .startComposition()
+                                        .addEffect(VibrationEffect.createPredefined(EFFECT_CLICK))
+                                        .addOffDuration(Duration.ofSeconds(1))
+                                        .compose()
+                        )
+                        .compose(),
+                mRingerUnderTest.mDefaultVibrationEffect);
+    }
+
+    @SmallTest
+    @Test
+    public void testValidButUnsupportedDefaultRingVibrationOverride() throws Exception {
+        when(mFeatureFlags.useDeviceProvidedSerializedRingerVibration()).thenReturn(true);
+        mockVibrationResourceValues(
+                """
+                    <vibration>
+                        <predefined-effect name="click"/>
+                    </vibration>
+                """,
+                /* useSimpleVibration= */ false);
+        when(mockVibratorInfo.areVibrationFeaturesSupported(
+                eq(VibrationEffect.createPredefined(EFFECT_CLICK)))).thenReturn(false);
+
+        createRingerUnderTest();
+
+        assertEquals(EXPECTED_SIMPLE_VIBRATION_PATTERN, mRingerUnderTest.mDefaultVibrationEffect);
+    }
+
+    @SmallTest
+    @Test
+    public void testInvalidDefaultRingVibrationOverride() throws Exception {
+        when(mFeatureFlags.useDeviceProvidedSerializedRingerVibration()).thenReturn(true);
+        mockVibrationResourceValues(
+                /* defaultVibrationContent= */ "bad serialization",
+                /* useSimpleVibration= */ false);
+        when(mockVibratorInfo.areVibrationFeaturesSupported(any())).thenReturn(true);
+
+        createRingerUnderTest();
+
+        assertEquals(EXPECTED_SIMPLE_VIBRATION_PATTERN, mRingerUnderTest.mDefaultVibrationEffect);
+    }
+
+    @SmallTest
+    @Test
+    public void testEmptyDefaultRingVibrationOverride() throws Exception {
+        when(mFeatureFlags.useDeviceProvidedSerializedRingerVibration()).thenReturn(true);
+        mockVibrationResourceValues(
+                /* defaultVibrationContent= */ "", /* useSimpleVibration= */ false);
+        when(mockVibratorInfo.areVibrationFeaturesSupported(any())).thenReturn(true);
+
+        createRingerUnderTest();
+
+        assertEquals(EXPECTED_SIMPLE_VIBRATION_PATTERN, mRingerUnderTest.mDefaultVibrationEffect);
+    }
+
+    @SmallTest
+    @Test
     public void testNoActionInTheaterMode() throws Exception {
         // Start call waiting to make sure that it doesn't stop when we start ringing
         mRingerUnderTest.startCallWaiting(mockCall1);
@@ -227,7 +398,7 @@
     @SmallTest
     @Test
     public void testCallWaitingButNoRingForSpecificContacts() throws Exception {
-        when(mockNotificationManager.matchesCallFilter(any(Bundle.class))).thenReturn(false);
+        when(mockNotificationManager.matchesCallFilter(any(Uri.class))).thenReturn(false);
         // Start call waiting to make sure that it does stop when we start ringing
         mRingerUnderTest.startCallWaiting(mockCall1);
         verify(mockTonePlayer).startTone();
@@ -434,6 +605,48 @@
         verify(mockRingtone).play();
     }
 
+    @SmallTest
+    @Test
+    public void testDelayRingerForBtHfpDevices() throws Exception {
+        asyncRingtonePlayer.updateBtActiveState(false);
+        Ringtone mockRingtone = ensureRingtoneMocked();
+
+        ensureRingerIsAudible();
+        assertTrue(mRingerUnderTest.startRinging(mockCall1, true));
+        assertTrue(mRingerUnderTest.isRinging());
+        // We should not have the ringtone play until BT moves active
+        verify(mockRingtone, never()).play();
+
+        asyncRingtonePlayer.updateBtActiveState(true);
+        mRingCompletionFuture.get();
+        verify(mockRingtoneFactory, times(1))
+                .getRingtone(any(Call.class), nullable(VolumeShaper.Configuration.class),
+                        anyBoolean());
+        verifyNoMoreInteractions(mockRingtoneFactory);
+        verify(mockRingtone).play();
+
+        mRingerUnderTest.stopRinging();
+        verify(mockRingtone, timeout(1000/*ms*/)).stop();
+        assertFalse(mRingerUnderTest.isRinging());
+    }
+
+    @SmallTest
+    @Test
+    public void testUnblockRingerForStopCommand() throws Exception {
+        asyncRingtonePlayer.updateBtActiveState(false);
+        Ringtone mockRingtone = ensureRingtoneMocked();
+
+        ensureRingerIsAudible();
+        assertTrue(mRingerUnderTest.startRinging(mockCall1, true));
+        // We should not have the ringtone play until BT moves active
+        verify(mockRingtone, never()).play();
+
+        // We are not setting BT active, but calling stop ringing while the other thread is waiting
+        // for BT active should also unblock it.
+        mRingerUnderTest.stopRinging();
+        verify(mockRingtone, timeout(1000/*ms*/)).stop();
+    }
+
     /**
      * test shouldRingForContact will suppress the incoming call if matchesCallFilter returns
      * false (meaning DND is ON and the caller cannot bypass the settings)
@@ -446,7 +659,7 @@
         when(mContext.getSystemService(NotificationManager.class)).thenReturn(
                 mockNotificationManager);
         // suppress the call
-        when(mockNotificationManager.matchesCallFilter(any(Bundle.class))).thenReturn(false);
+        when(mockNotificationManager.matchesCallFilter(any(Uri.class))).thenReturn(false);
 
         // run the method under test
         assertFalse(mRingerUnderTest.shouldRingForContact(mockCall1));
@@ -455,7 +668,7 @@
         // verify we never set the call object and matchesCallFilter is called
         verify(mockCall1, never()).setCallIsSuppressedByDoNotDisturb(true);
         verify(mockNotificationManager, times(1))
-                .matchesCallFilter(any(Bundle.class));
+                .matchesCallFilter(any(Uri.class));
     }
 
     /**
@@ -468,7 +681,6 @@
         when(mockCall1.wasDndCheckComputedForCall()).thenReturn(false);
         when(mockCall1.getHandle()).thenReturn(Uri.parse(""));
         // alert the user of the call
-        when(mockNotificationManager.matchesCallFilter(any(Bundle.class))).thenReturn(true);
 
         // run the method under test
         assertTrue(mRingerUnderTest.shouldRingForContact(mockCall1));
@@ -477,7 +689,7 @@
         // verify we never set the call object and matchesCallFilter is called
         verify(mockCall1, never()).setCallIsSuppressedByDoNotDisturb(false);
         verify(mockNotificationManager, times(1))
-                .matchesCallFilter(any(Bundle.class));
+                .matchesCallFilter(any(Uri.class));
     }
 
     /**
@@ -493,7 +705,7 @@
         // THEN
         assertFalse(mRingerUnderTest.shouldRingForContact(mockCall1));
         verify(mockCall1, never()).setCallIsSuppressedByDoNotDisturb(false);
-        verify(mockNotificationManager, never()).matchesCallFilter(any(Bundle.class));
+        verify(mockNotificationManager, never()).matchesCallFilter(any(Uri.class));
     }
 
     @Test
@@ -503,7 +715,7 @@
         mRingerUnderTest.startCallWaiting(mockCall1);
         when(mockCall2.wasDndCheckComputedForCall()).thenReturn(false);
         when(mockCall2.getHandle()).thenReturn(Uri.parse(""));
-        when(mockNotificationManager.matchesCallFilter(any(Bundle.class))).thenReturn(false);
+        when(mockNotificationManager.matchesCallFilter(any(Uri.class))).thenReturn(false);
 
         assertFalse(mRingerUnderTest.shouldRingForContact(mockCall2));
         assertFalse(startRingingAndWaitForAsync(mockCall2, false));
@@ -518,7 +730,6 @@
         mRingerUnderTest.startCallWaiting(mockCall1);
         when(mockCall2.wasDndCheckComputedForCall()).thenReturn(false);
         when(mockCall2.getHandle()).thenReturn(Uri.parse(""));
-        when(mockNotificationManager.matchesCallFilter(any(Bundle.class))).thenReturn(true);
 
         assertTrue(mRingerUnderTest.shouldRingForContact(mockCall2));
         assertTrue(startRingingAndWaitForAsync(mockCall2, false));
@@ -545,7 +756,6 @@
         mRingerUnderTest.startCallWaiting(mockCall1);
         when(mockCall2.wasDndCheckComputedForCall()).thenReturn(false);
         when(mockCall2.getHandle()).thenReturn(Uri.parse(""));
-        when(mockNotificationManager.matchesCallFilter(any(Bundle.class))).thenReturn(true);
 
         assertTrue(mRingerUnderTest.shouldRingForContact(mockCall2));
         assertTrue(mRingerUnderTest.startRinging(mockCall2, false));
@@ -624,4 +834,13 @@
         when(mockRingtoneFactory.getHapticOnlyRingtone()).thenReturn(mockRingtone);
         return mockRingtone;
     }
+
+    private void mockVibrationResourceValues(
+            String defaultVibrationContent, boolean useSimpleVibration) {
+        mComponentContextFixture.putRawResource(
+                com.android.internal.R.raw.default_ringtone_vibration_effect,
+                defaultVibrationContent);
+        mComponentContextFixture.putBooleanResource(
+                R.bool.use_simple_vibration_pattern, useSimpleVibration);
+    }
 }
diff --git a/tests/src/com/android/server/telecom/tests/TelecomServiceImplTest.java b/tests/src/com/android/server/telecom/tests/TelecomServiceImplTest.java
index 8bc1f2a..e9466ee 100644
--- a/tests/src/com/android/server/telecom/tests/TelecomServiceImplTest.java
+++ b/tests/src/com/android/server/telecom/tests/TelecomServiceImplTest.java
@@ -58,11 +58,13 @@
 import com.android.server.telecom.CallState;
 import com.android.server.telecom.CallsManager;
 import com.android.server.telecom.DefaultDialerCache;
+import com.android.server.telecom.InCallController;
 import com.android.server.telecom.PhoneAccountRegistrar;
 import com.android.server.telecom.TelecomServiceImpl;
 import com.android.server.telecom.TelecomSystem;
 import com.android.server.telecom.components.UserCallIntentProcessor;
 import com.android.server.telecom.components.UserCallIntentProcessorFactory;
+import com.android.server.telecom.flags.FeatureFlags;
 import com.android.server.telecom.voip.IncomingCallTransaction;
 import com.android.server.telecom.voip.OutgoingCallTransaction;
 import com.android.server.telecom.voip.TransactionManager;
@@ -76,6 +78,7 @@
 import org.mockito.ArgumentMatcher;
 import org.mockito.Mock;
 
+import java.lang.reflect.Method;
 import java.util.Collection;
 import java.util.List;
 import java.util.concurrent.Executor;
@@ -86,6 +89,7 @@
 import static android.Manifest.permission.WRITE_SECURE_SETTINGS;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
@@ -122,7 +126,7 @@
     public static class CallIntentProcessAdapterFake implements CallIntentProcessor.Adapter {
         @Override
         public void processOutgoingCallIntent(Context context, CallsManager callsManager,
-                Intent intent, String callingPackage) {
+                Intent intent, String callingPackage, FeatureFlags flags) {
 
         }
 
@@ -192,6 +196,9 @@
     @Mock private ICallEventCallback mICallEventCallback;
     @Mock private TransactionManager mTransactionManager;
     @Mock private AnomalyReporterAdapter mAnomalyReporterAdapter;
+    @Mock private FeatureFlags mFeatureFlags;
+
+    @Mock private InCallController mInCallController;
 
     private final TelecomSystem.SyncRoot mLock = new TelecomSystem.SyncRoot() { };
 
@@ -219,6 +226,7 @@
 
         doReturn(mContext).when(mContext).getApplicationContext();
         doReturn(mContext).when(mContext).createContextAsUser(any(UserHandle.class), anyInt());
+        when(mFakeCallsManager.getInCallController()).thenReturn(mInCallController);
         doNothing().when(mContext).sendBroadcastAsUser(any(Intent.class), any(UserHandle.class),
                 anyString());
         when(mContext.checkCallingOrSelfPermission(Manifest.permission.INTERACT_ACROSS_USERS))
@@ -242,6 +250,7 @@
                 mDefaultDialerCache,
                 mSubscriptionManagerAdapter,
                 mSettingsSecureAdapter,
+                mFeatureFlags,
                 mLock);
         telecomServiceImpl.setTransactionManager(mTransactionManager);
         telecomServiceImpl.setAnomalyReporterAdapter(mAnomalyReporterAdapter);
@@ -260,6 +269,7 @@
 
         mPackageManager = mContext.getPackageManager();
         when(mPackageManager.getPackageUid(anyString(), eq(0))).thenReturn(Binder.getCallingUid());
+        when(mFeatureFlags.earlyBindingToIncallService()).thenReturn(true);
     }
 
     @Override
@@ -1040,6 +1050,7 @@
 
         verify(mFakePhoneAccountRegistrar).getPhoneAccount(
                 TEL_PA_HANDLE_16, TEL_PA_HANDLE_16.getUserHandle());
+        verify(mInCallController, never()).bindToServices(any());
         addCallTestHelper(TelecomManager.ACTION_INCOMING_CALL,
                 CallIntentProcessor.KEY_IS_INCOMING_CALL, extras,
                 TEL_PA_HANDLE_16, false);
@@ -1047,6 +1058,81 @@
 
     @SmallTest
     @Test
+    public void testAddNewIncomingFlagDisabledNoEarlyBinding() throws Exception {
+        when(mFeatureFlags.earlyBindingToIncallService()).thenReturn(false);
+        PhoneAccount phoneAccount = makeSkipCallFilteringPhoneAccount(TEL_PA_HANDLE_16).build();
+        phoneAccount.setIsEnabled(true);
+        doReturn(phoneAccount).when(mFakePhoneAccountRegistrar).getPhoneAccount(
+                eq(TEL_PA_HANDLE_16), any(UserHandle.class));
+        doReturn(phoneAccount).when(mFakePhoneAccountRegistrar).getPhoneAccountUnchecked(
+                eq(TEL_PA_HANDLE_16));
+        doNothing().when(mAppOpsManager).checkPackage(anyInt(), anyString());
+        when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WATCH)).thenReturn(true);
+        Bundle extras = createSampleExtras();
+
+        mTSIBinder.addNewIncomingCall(TEL_PA_HANDLE_16, extras, CALLING_PACKAGE);
+
+        verify(mInCallController, never()).bindToServices(null);
+    }
+
+    @SmallTest
+    @Test
+    public void testAddNewIncomingCallEarlyBindingForNoCallFilterCalls() throws Exception {
+        PhoneAccount phoneAccount = makeSkipCallFilteringPhoneAccount(TEL_PA_HANDLE_16).build();
+        phoneAccount.setIsEnabled(true);
+        doReturn(phoneAccount).when(mFakePhoneAccountRegistrar).getPhoneAccount(
+                eq(TEL_PA_HANDLE_16), any(UserHandle.class));
+        doReturn(phoneAccount).when(mFakePhoneAccountRegistrar).getPhoneAccountUnchecked(
+                eq(TEL_PA_HANDLE_16));
+        doNothing().when(mAppOpsManager).checkPackage(anyInt(), anyString());
+        when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WATCH)).thenReturn(true);
+        Bundle extras = createSampleExtras();
+
+        mTSIBinder.addNewIncomingCall(TEL_PA_HANDLE_16, extras, CALLING_PACKAGE);
+
+        verify(mInCallController).bindToServices(null);
+    }
+
+    @SmallTest
+    @Test
+    public void testAddNewIncomingCallEarlyBindingNotEnableForNonWatchDevices() throws Exception {
+        PhoneAccount phoneAccount = makeSkipCallFilteringPhoneAccount(TEL_PA_HANDLE_16).build();
+        phoneAccount.setIsEnabled(true);
+        doReturn(phoneAccount).when(mFakePhoneAccountRegistrar).getPhoneAccount(
+                eq(TEL_PA_HANDLE_16), any(UserHandle.class));
+        doReturn(phoneAccount).when(mFakePhoneAccountRegistrar).getPhoneAccountUnchecked(
+                eq(TEL_PA_HANDLE_16));
+        doNothing().when(mAppOpsManager).checkPackage(anyInt(), anyString());
+        when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WATCH)).thenReturn(false);
+        Bundle extras = createSampleExtras();
+
+        mTSIBinder.addNewIncomingCall(TEL_PA_HANDLE_16, extras, CALLING_PACKAGE);
+
+        verify(mInCallController, never()).bindToServices(null);
+    }
+
+    @SmallTest
+    @Test
+    public void testAddNewIncomingCallEarlyBindingNotEnableForPhoneAccountHasCallFilters()
+            throws Exception {
+        PhoneAccount phoneAccount = makePhoneAccount(TEL_PA_HANDLE_16).build();
+        phoneAccount.setIsEnabled(true);
+        doReturn(phoneAccount).when(mFakePhoneAccountRegistrar).getPhoneAccount(
+                eq(TEL_PA_HANDLE_16), any(UserHandle.class));
+        doReturn(phoneAccount).when(mFakePhoneAccountRegistrar).getPhoneAccountUnchecked(
+                eq(TEL_PA_HANDLE_16));
+        doNothing().when(mAppOpsManager).checkPackage(anyInt(), anyString());
+        when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WATCH)).thenReturn(true);
+        Bundle extras = createSampleExtras();
+
+        mTSIBinder.addNewIncomingCall(TEL_PA_HANDLE_16, extras, CALLING_PACKAGE);
+
+        verify(mInCallController, never()).bindToServices(null);
+    }
+
+
+    @SmallTest
+    @Test
     public void testAddNewIncomingCallFailure() throws Exception {
         try {
             mTSIBinder.addNewIncomingCall(TEL_PA_HANDLE_16, null, CALLING_PACKAGE);
@@ -1703,6 +1789,28 @@
         verify(mContext, never()).sendBroadcastAsUser(any(Intent.class), any(UserHandle.class));
     }
 
+    /**
+     * FeatureFlags is autogenerated code, so there could be a situation where something changes
+     * outside of Telecom control that breaks reflection. This test attempts to ensure that changes
+     * to auto-generated FeatureFlags code that breaks reflection are caught early.
+     */
+    @SmallTest
+    @Test
+    public void testFlagConfigReflectionWorks() {
+        try {
+            Method[] methods = FeatureFlags.class.getMethods();
+            for (Method m : methods) {
+                // test getting the name and invoking the flag code
+                String name = m.getName();
+                Object val = m.invoke(mFeatureFlags);
+                assertNotNull(name);
+                assertNotNull(val);
+            }
+        } catch (Exception e) {
+            fail("Reflection failed for FeatureFlags with error: " + e);
+        }
+    }
+
     @SmallTest
     @Test
     public void testIsVoicemailNumber() throws Exception {
@@ -2144,6 +2252,12 @@
         return new PhoneAccount.Builder(paHandle, "testLabel");
     }
 
+    private PhoneAccount.Builder makeSkipCallFilteringPhoneAccount(PhoneAccountHandle paHandle) {
+        Bundle extras = new Bundle();
+        extras.putBoolean(PhoneAccount.EXTRA_SKIP_CALL_FILTERING, true);
+        return new PhoneAccount.Builder(paHandle, "testLabel").setExtras(extras);
+    }
+
     private Bundle createSampleExtras() {
         Bundle extras = new Bundle();
         extras.putString("test_key", "test_value");
diff --git a/tests/src/com/android/server/telecom/tests/TelecomSystemTest.java b/tests/src/com/android/server/telecom/tests/TelecomSystemTest.java
index b962b2a..07cb9e2 100644
--- a/tests/src/com/android/server/telecom/tests/TelecomSystemTest.java
+++ b/tests/src/com/android/server/telecom/tests/TelecomSystemTest.java
@@ -69,6 +69,7 @@
 
 import com.android.internal.telecom.IInCallAdapter;
 import com.android.server.telecom.AsyncRingtonePlayer;
+import com.android.server.telecom.CallAudioCommunicationDeviceTracker;
 import com.android.server.telecom.CallAudioManager;
 import com.android.server.telecom.CallAudioModeStateMachine;
 import com.android.server.telecom.CallAudioRouteStateMachine;
@@ -98,6 +99,7 @@
 import com.android.server.telecom.bluetooth.BluetoothRouteManager;
 import com.android.server.telecom.callfiltering.BlockedNumbersAdapter;
 import com.android.server.telecom.components.UserCallIntentProcessor;
+import com.android.server.telecom.flags.FeatureFlags;
 import com.android.server.telecom.ui.IncomingCallNotifier;
 
 import com.google.common.base.Predicate;
@@ -214,6 +216,10 @@
     @Mock Ringer.AccessibilityManagerAdapter mAccessibilityManagerAdapter;
     @Mock
     BlockedNumbersAdapter mBlockedNumbersAdapter;
+    @Mock
+    CallAudioCommunicationDeviceTracker mCommunicationDeviceTracker;
+    @Mock
+    FeatureFlags mFeatureFlags;
 
     final ComponentName mInCallServiceComponentNameX =
             new ComponentName(
@@ -518,7 +524,8 @@
                             StatusBarNotifier statusBarNotifier,
                             CallAudioManager.AudioServiceFactory audioServiceFactory,
                             int earpieceControl,
-                            Executor asyncTaskExecutor) {
+                            Executor asyncTaskExecutor,
+                            CallAudioCommunicationDeviceTracker communicationDeviceTracker) {
                         return new CallAudioRouteStateMachine(context,
                                 callsManager,
                                 bluetoothManager,
@@ -528,15 +535,16 @@
                                 // Force enable an earpiece for the end-to-end tests
                                 CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED,
                                 mHandlerThread.getLooper(),
-                                Runnable::run /* async tasks as now sync for testing! */);
+                                Runnable::run /* async tasks as now sync for testing! */,
+                                communicationDeviceTracker);
                     }
                 },
                 new CallAudioModeStateMachine.Factory() {
                     @Override
                     public CallAudioModeStateMachine create(SystemStateHelper systemStateHelper,
-                            AudioManager am) {
+                            AudioManager am, FeatureFlags featureFlags) {
                         return new CallAudioModeStateMachine(systemStateHelper, am,
-                                mHandlerThread.getLooper());
+                                mHandlerThread.getLooper(), featureFlags);
                     }
                 },
                 mClockProxy,
@@ -549,7 +557,9 @@
                     }
                 }, mDeviceIdleControllerAdapter, mAccessibilityManagerAdapter,
                 Runnable::run,
-                mBlockedNumbersAdapter);
+                Runnable::run,
+                mBlockedNumbersAdapter,
+                mFeatureFlags);
 
         mComponentContextFixture.setTelecomManager(new TelecomManager(
                 mComponentContextFixture.getTestDouble(),
diff --git a/tests/src/com/android/server/telecom/tests/TelecomTestCase.java b/tests/src/com/android/server/telecom/tests/TelecomTestCase.java
index 5353bc6..e8389a0 100644
--- a/tests/src/com/android/server/telecom/tests/TelecomTestCase.java
+++ b/tests/src/com/android/server/telecom/tests/TelecomTestCase.java
@@ -22,6 +22,9 @@
 
 import androidx.test.InstrumentationRegistry;
 
+import com.android.server.telecom.flags.FeatureFlags;
+
+import org.mockito.Mock;
 import org.mockito.Mockito;
 import org.mockito.MockitoAnnotations;
 
@@ -33,6 +36,8 @@
 public abstract class TelecomTestCase {
     protected static final String TESTING_TAG = "Telecom-TEST";
     protected Context mContext;
+    @Mock
+    FeatureFlags mFeatureFlags;
 
     MockitoHelper mMockitoHelper = new MockitoHelper();
     ComponentContextFixture mComponentContextFixture;
@@ -42,11 +47,12 @@
         Log.setIsExtendedLoggingEnabled(true);
         Log.setUnitTestingEnabled(true);
         mMockitoHelper.setUp(InstrumentationRegistry.getContext(), getClass());
-        mComponentContextFixture = new ComponentContextFixture();
+        MockitoAnnotations.initMocks(this);
+
+        mComponentContextFixture = new ComponentContextFixture(mFeatureFlags);
         mContext = mComponentContextFixture.getTestDouble().getApplicationContext();
         Log.setSessionContext(mComponentContextFixture.getTestDouble().getApplicationContext());
         Log.getSessionManager().mCleanStaleSessions = null;
-        MockitoAnnotations.initMocks(this);
     }
 
     public void tearDown() throws Exception {
diff --git a/tests/src/com/android/server/telecom/tests/TransactionTests.java b/tests/src/com/android/server/telecom/tests/TransactionTests.java
index 3fc87a9..d733d9d 100644
--- a/tests/src/com/android/server/telecom/tests/TransactionTests.java
+++ b/tests/src/com/android/server/telecom/tests/TransactionTests.java
@@ -16,7 +16,11 @@
 
 package com.android.server.telecom.tests;
 
+import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
@@ -39,11 +43,14 @@
 import android.telecom.DisconnectCause;
 import android.telecom.PhoneAccountHandle;
 
+import androidx.test.filters.SmallTest;
+
 import com.android.server.telecom.Call;
 import com.android.server.telecom.CallState;
 import com.android.server.telecom.CallerInfoLookupHelper;
 import com.android.server.telecom.CallsManager;
 import com.android.server.telecom.ClockProxy;
+import com.android.server.telecom.ConnectionServiceWrapper;
 import com.android.server.telecom.PhoneNumberUtilsAdapter;
 import com.android.server.telecom.TelecomSystem;
 import com.android.server.telecom.ui.ToastFactory;
@@ -53,6 +60,8 @@
 import com.android.server.telecom.voip.OutgoingCallTransaction;
 import com.android.server.telecom.voip.MaybeHoldCallForNewCallTransaction;
 import com.android.server.telecom.voip.RequestNewActiveCallTransaction;
+import com.android.server.telecom.voip.VerifyCallStateChangeTransaction;
+import com.android.server.telecom.voip.VoipCallTransactionResult;
 
 import org.junit.After;
 import org.junit.Before;
@@ -62,6 +71,11 @@
 import org.mockito.Mockito;
 import org.mockito.MockitoAnnotations;
 
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
 
 public class TransactionTests extends TelecomTestCase {
 
@@ -250,6 +264,63 @@
                         isA(Boolean.class));
     }
 
+    /**
+     * This test verifies if the ConnectionService call is NOT transitioned to the desired call
+     * state (within timeout period), Telecom will disconnect the call.
+     */
+    @SmallTest
+    @Test
+    public void testCallStateChangeTimesOut()
+            throws ExecutionException, InterruptedException, TimeoutException {
+        VerifyCallStateChangeTransaction t = new VerifyCallStateChangeTransaction(mCallsManager,
+                mMockCall1, CallState.ON_HOLD, true);
+        // WHEN
+        setupHoldableCall();
+
+        // simulate the transaction being processed and the CompletableFuture timing out
+        t.processTransaction(null);
+        CompletableFuture<Integer> timeoutFuture = t.getCallStateOrTimeoutResult();
+        timeoutFuture.complete(VerifyCallStateChangeTransaction.FAILURE_CODE);
+
+        // THEN
+        verify(mMockCall1, times(1)).addCallStateListener(t.getCallStateListenerImpl());
+        assertEquals(timeoutFuture.get().intValue(), VerifyCallStateChangeTransaction.FAILURE_CODE);
+        assertEquals(VoipCallTransactionResult.RESULT_FAILED,
+                t.getTransactionResult().get(2, TimeUnit.SECONDS).getResult());
+        verify(mMockCall1, atLeastOnce()).removeCallStateListener(any());
+        verify(mCallsManager, times(1)).markCallAsDisconnected(eq(mMockCall1), any());
+        verify(mCallsManager, times(1)).markCallAsRemoved(eq(mMockCall1));
+    }
+
+    /**
+     * This test verifies that when an application transitions a call to the requested state,
+     * Telecom does not disconnect the call and transaction completes successfully.
+     */
+    @SmallTest
+    @Test
+    public void testCallStateIsSuccessfullyChanged()
+            throws ExecutionException, InterruptedException, TimeoutException {
+        VerifyCallStateChangeTransaction t = new VerifyCallStateChangeTransaction(mCallsManager,
+                mMockCall1, CallState.ON_HOLD, true);
+        // WHEN
+        setupHoldableCall();
+
+        // simulate the transaction being processed and the setOnHold() being called / state change
+        t.processTransaction(null);
+        t.getCallStateListenerImpl().onCallStateChanged(CallState.ON_HOLD);
+        when(mMockCall1.getState()).thenReturn(CallState.ON_HOLD);
+
+        // THEN
+        verify(mMockCall1, times(1)).addCallStateListener(t.getCallStateListenerImpl());
+        assertEquals(t.getCallStateOrTimeoutResult().get().intValue(),
+                VerifyCallStateChangeTransaction.SUCCESS_CODE);
+        assertEquals(VoipCallTransactionResult.RESULT_SUCCEED,
+                t.getTransactionResult().get(2, TimeUnit.SECONDS).getResult());
+        verify(mMockCall1, atLeastOnce()).removeCallStateListener(any());
+        verify(mCallsManager, never()).markCallAsDisconnected(eq(mMockCall1), any());
+        verify(mCallsManager, never()).markCallAsRemoved(eq(mMockCall1));
+    }
+
     private Call createSpyCall(PhoneAccountHandle targetPhoneAccount, int initialState, String id) {
         when(mCallsManager.getCallerInfoLookupHelper()).thenReturn(mCallerInfoLookupHelper);
 
@@ -280,4 +351,12 @@
 
         return callSpy;
     }
+
+    private void setupHoldableCall(){
+        when(mMockCall1.getState()).thenReturn(CallState.ACTIVE);
+        when(mMockCall1.getConnectionServiceWrapper()).thenReturn(
+                mock(ConnectionServiceWrapper.class));
+        doNothing().when(mMockCall1).addCallStateListener(any());
+        doReturn(true).when(mMockCall1).removeCallStateListener(any());
+    }
 }
\ No newline at end of file
diff --git a/tests/src/com/android/server/telecom/tests/VoipCallMonitorTest.java b/tests/src/com/android/server/telecom/tests/VoipCallMonitorTest.java
index c66b0f7..ddea231 100644
--- a/tests/src/com/android/server/telecom/tests/VoipCallMonitorTest.java
+++ b/tests/src/com/android/server/telecom/tests/VoipCallMonitorTest.java
@@ -16,6 +16,12 @@
 
 package com.android.server.telecom.tests;
 
+import static android.app.ForegroundServiceDelegationOptions.DELEGATION_SERVICE_PHONE_CALL;
+import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_CAMERA;
+import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE;
+import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE;
+import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL;
+
 import static org.junit.Assert.assertEquals;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
@@ -86,6 +92,31 @@
                 .thenReturn(true);
     }
 
+    /**
+     * This test ensures VoipCallMonitor is passing the correct foregroundServiceTypes when starting
+     * foreground service delegation on behalf of a client.
+     */
+    @SmallTest
+    @Test
+    public void testVerifyForegroundServiceTypesBeingPassedToActivityManager() {
+        Call call = createTestCall("testCall", mHandle1User1);
+        ArgumentCaptor<ForegroundServiceDelegationOptions> optionsCaptor =
+                ArgumentCaptor.forClass(ForegroundServiceDelegationOptions.class);
+
+        mMonitor.onCallAdded(call);
+
+        verify(mActivityManagerInternal, timeout(TIMEOUT)).startForegroundServiceDelegate(
+                 optionsCaptor.capture(), any(ServiceConnection.class));
+
+        assertEquals( FOREGROUND_SERVICE_TYPE_PHONE_CALL |
+                FOREGROUND_SERVICE_TYPE_MICROPHONE |
+                FOREGROUND_SERVICE_TYPE_CAMERA |
+                FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE,
+                optionsCaptor.getValue().mForegroundServiceTypes);
+
+        mMonitor.onCallRemoved(call);
+    }
+
     @SmallTest
     @Test
     public void testStartMonitorForOneCall() {