Merge "Track non-telecom calls in telecom metrics." into main
diff --git a/flags/telecom_anomaly_report_flags.aconfig b/flags/telecom_anomaly_report_flags.aconfig
index 5d42b86..bc248c8 100644
--- a/flags/telecom_anomaly_report_flags.aconfig
+++ b/flags/telecom_anomaly_report_flags.aconfig
@@ -27,3 +27,11 @@
     purpose: PURPOSE_BUGFIX
   }
 }
+
+# OWNER=tjstuart TARGET=25Q2
+flag {
+  name: "enable_call_exception_anom_reports"
+  namespace: "telecom"
+  description: "When a new CallException is created, generate an anomaly report for metrics"
+  bug: "308932906"
+}
diff --git a/res/values-kn/strings.xml b/res/values-kn/strings.xml
index 3b6234d..dbdc258 100644
--- a/res/values-kn/strings.xml
+++ b/res/values-kn/strings.xml
@@ -135,5 +135,5 @@
     <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"ಹ್ಯಾಂಗ್ ಅಪ್"</string>
     <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"ಇಲ್ಲಿಗೆ ಬದಲಾಯಿಸಿ"</string>
     <string name="callFailed_too_many_calls" msgid="4249997210954876420">"ಈಗಾಗಲೇ ಎರಡು ಕರೆಗಳು ಪ್ರಗತಿಯಲ್ಲಿರುವುದರಿಂದ, ಕರೆ ಮಾಡಲು ಸಾಧ್ಯವಾಗುವುದಿಲ್ಲ. ಒಂದು ಕರೆಯನ್ನು ಡಿಸ್‌ಕನೆಕ್ಟ್ ಮಾಡಿ ಅಥವಾ ಹೊಸ ಕರೆಯನ್ನು ಮಾಡುವ ಮೊದಲು ಎರಡು ಕರೆಗಳನ್ನು ಒಂದೇ ಕಾನ್ಫರೆನ್ಸ್‌ನಲ್ಲಿ ವಿಲೀನಗೊಳಿಸಿ."</string>
-    <string name="callFailed_unholdable_call" msgid="7580834131274566524">"ಈಗಾಗಲೇ ಪ್ರಗತಿಯಲ್ಲಿರುವ ಕರೆಯನ್ನು ಹೋಲ್ಡ್ ಮಾಡಲು ಸಾಧ್ಯವಿಲ್ಲದ ಕಾರಣ ಕರೆ ಮಾಡಲು ಸಾಧ್ಯವಿಲ್ಲ. ಹೊಸ ಕರೆಯನ್ನು ಮಾಡುವ ಮೊದಲು ಕರೆಯನ್ನು ಡಿಸ್‌ಕನೆಕ್ಟ್ ಮಾಡಿ."</string>
+    <string name="callFailed_unholdable_call" msgid="7580834131274566524">"ಈಗಾಗಲೇ ಪ್ರಗತಿಯಲ್ಲಿರುವ ಕರೆಯನ್ನು ಹೋಲ್ಡ್ ಮಾಡಲು ಸಾಧ್ಯವಿಲ್ಲದ ಕಾರಣ, ಕರೆ ಮಾಡಲು ಸಾಧ್ಯವಿಲ್ಲ. ಹೊಸ ಕರೆಯನ್ನು ಮಾಡುವ ಮೊದಲು ಕರೆಯನ್ನು ಡಿಸ್‌ಕನೆಕ್ಟ್ ಮಾಡಿ."</string>
 </resources>
diff --git a/src/com/android/server/telecom/Call.java b/src/com/android/server/telecom/Call.java
index 75d8d7d..42d5943 100644
--- a/src/com/android/server/telecom/Call.java
+++ b/src/com/android/server/telecom/Call.java
@@ -3486,7 +3486,8 @@
         return Contacts.getLookupUri(mCallerInfo.getContactId(), mCallerInfo.lookupKey);
     }
 
-    Uri getRingtone() {
+    @VisibleForTesting
+    public Uri getRingtone() {
         return mCallerInfo == null ? null : mCallerInfo.contactRingtoneUri;
     }
 
diff --git a/src/com/android/server/telecom/CallsManager.java b/src/com/android/server/telecom/CallsManager.java
index 854fa3b..5cc868b 100644
--- a/src/com/android/server/telecom/CallsManager.java
+++ b/src/com/android/server/telecom/CallsManager.java
@@ -329,6 +329,10 @@
             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";
+    public static final UUID CANNOT_HOLD_CURRENT_ACTIVE_CALL_ERROR_UUID =
+            UUID.fromString("1b6a9b88-5049-4ffa-a52a-134d7c3a40e6");
+    public static final UUID FAILED_TO_SWITCH_FOCUS_ERROR_UUID =
+            UUID.fromString("a1b2c3d4-e5f6-7890-1234-567890abcdef");
 
     public static final int[] OUTGOING_CALL_STATES =
             {CallState.CONNECTING, CallState.SELECT_PHONE_ACCOUNT, CallState.DIALING,
@@ -772,7 +776,10 @@
         mCallStreamingNotification = callStreamingNotification;
         mFeatureFlags = featureFlags;
         if (mFeatureFlags.voipCallMonitorRefactor()) {
-            mVoipCallMonitor = new VoipCallMonitor(mContext, mLock);
+            mVoipCallMonitor = new VoipCallMonitor(
+                    mContext,
+                    new Handler(Looper.getMainLooper()),
+                    mLock);
             mVoipCallMonitorLegacy = null;
         } else {
             mVoipCallMonitor = null;
@@ -4090,11 +4097,14 @@
             if (activeCall.isLocallyDisconnecting()) {
                 callback.onResult(true);
             } else {
-                Log.i(this, "transactionHoldPotentialActiveCallForNewCallOld: active call could "
-                        + "not be held or disconnected");
+                String msg = "active call could not be held or disconnected";
+                Log.i(this, "transactionHoldPotentialActiveCallForNewCallOld: " + msg);
                 callback.onError(
-                        new CallException("activeCall could not be held or disconnected",
+                        new CallException(msg,
                                 CallException.CODE_CANNOT_HOLD_CURRENT_ACTIVE_CALL));
+                if (mFeatureFlags.enableCallExceptionAnomReports()) {
+                    mAnomalyReporter.reportAnomaly(CANNOT_HOLD_CURRENT_ACTIVE_CALL_ERROR_UUID, msg);
+                }
             }
         }
     }
@@ -4110,19 +4120,25 @@
         // early
         if (!canHold(activeCall) &&
                 !(supportsHold(activeCall) && areFromSameSource(activeCall, newCall))) {
-            Log.i(this, "transactionHoldPotentialActiveCallForNewCall: "
-                    + "conditions show the call cannot be held.");
-            callback.onError(new CallException("call does not support hold",
+            String msg = "call does not support hold";
+            Log.i(this, "transactionHoldPotentialActiveCallForNewCall: " + msg);
+            callback.onError(new CallException(msg,
                     CallException.CODE_CANNOT_HOLD_CURRENT_ACTIVE_CALL));
+            if (mFeatureFlags.enableCallExceptionAnomReports()) {
+                mAnomalyReporter.reportAnomaly(CANNOT_HOLD_CURRENT_ACTIVE_CALL_ERROR_UUID, msg);
+            }
             return;
         }
 
         // attempt to hold the active call
         if (!holdActiveCallForNewCall(newCall)) {
-            Log.i(this, "transactionHoldPotentialActiveCallForNewCall: "
-                    + "attempted to hold call but failed.");
-            callback.onError(new CallException("cannot hold active call failed",
+            String msg = "cannot hold active call failed";
+            Log.i(this, "transactionHoldPotentialActiveCallForNewCall: " + msg);
+            callback.onError(new CallException(msg,
                     CallException.CODE_CANNOT_HOLD_CURRENT_ACTIVE_CALL));
+            if (mFeatureFlags.enableCallExceptionAnomReports()) {
+                mAnomalyReporter.reportAnomaly(CANNOT_HOLD_CURRENT_ACTIVE_CALL_ERROR_UUID, msg);
+            }
             return;
         }
 
@@ -6835,8 +6851,12 @@
                 if (mTargetCallFocus.getState() != mPreviousCallState) {
                     mTargetCallFocus.setState(mPreviousCallState, "resetting call state");
                 }
-                mCallback.onError(new CallException("failed to switch focus to requested call",
+                String msg = "failed to switch focus to requested call";
+                mCallback.onError(new CallException(msg,
                         CallException.CODE_CALL_CANNOT_BE_SET_TO_ACTIVE));
+                if (mFeatureFlags.enableCallExceptionAnomReports()) {
+                    mAnomalyReporter.reportAnomaly(FAILED_TO_SWITCH_FOCUS_ERROR_UUID, msg);
+                }
                 return;
             }
             // at this point, we know the FocusManager is able to update successfully
diff --git a/src/com/android/server/telecom/RingtoneFactory.java b/src/com/android/server/telecom/RingtoneFactory.java
index c740c24..3e71dc2 100644
--- a/src/com/android/server/telecom/RingtoneFactory.java
+++ b/src/com/android/server/telecom/RingtoneFactory.java
@@ -74,12 +74,18 @@
         Ringtone ringtone = null;
 
         if (ringtoneUri != null && userContext != null) {
-            // Ringtone URI is explicitly specified. First, try to create a Ringtone with that.
-            try {
-                ringtone = RingtoneManager.getRingtone(
-                        userContext, ringtoneUri, volumeShaperConfig, audioAttrs);
-            } catch (Exception e) {
-                Log.e(this, e, "getRingtone: exception while getting ringtone.");
+            if (currentUserOwnsRingtone(ringtoneUri, incomingCall)) {
+                // Ringtone URI is explicitly specified and owned by the current user - try to
+                // create a Ringtone with that.
+                try {
+                    ringtone = RingtoneManager.getRingtone(
+                            userContext, ringtoneUri, volumeShaperConfig, audioAttrs);
+                } catch (Exception e) {
+                    Log.e(this, e, "getRingtone: exception while getting ringtone.");
+                }
+            } else {
+                Log.w(this, "getRingtone: Failed to verify that the custom ringtone URI"
+                        + " is owned by the current user. Falling back to the default ringtone.");
             }
         }
         if (ringtone == null) {
@@ -119,6 +125,21 @@
         return new Pair(ringtoneUri, ringtone);
     }
 
+    private static boolean currentUserOwnsRingtone(Uri ringtoneUri, Call incomingCall) {
+        if (TextUtils.isEmpty(ringtoneUri.getUserInfo()) ||
+                incomingCall.getAssociatedUser() == null) {
+            return false;
+        }
+
+        UserHandle associatedUser = incomingCall.getAssociatedUser();
+        if (associatedUser == null) {
+            return false;
+        }
+
+        String currentUserId = String.valueOf(associatedUser.getIdentifier());
+        return currentUserId.equals(ringtoneUri.getUserInfo());
+    }
+
     private AudioAttributes getDefaultRingtoneAudioAttributes(boolean hapticChannelsMuted) {
         return new AudioAttributes.Builder()
             .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
diff --git a/src/com/android/server/telecom/TelecomServiceImpl.java b/src/com/android/server/telecom/TelecomServiceImpl.java
index af094b7..b9841ba 100644
--- a/src/com/android/server/telecom/TelecomServiceImpl.java
+++ b/src/com/android/server/telecom/TelecomServiceImpl.java
@@ -27,8 +27,6 @@
 import static android.Manifest.permission.READ_SMS;
 import static android.Manifest.permission.REGISTER_SIM_SUBSCRIPTION;
 import static android.Manifest.permission.WRITE_SECURE_SETTINGS;
-import static android.telecom.CallAttributes.DIRECTION_INCOMING;
-import static android.telecom.CallAttributes.DIRECTION_OUTGOING;
 import static android.telecom.CallException.CODE_ERROR_UNKNOWN;
 import static android.telecom.TelecomManager.TELECOM_TRANSACTION_SUCCESS;
 
@@ -52,8 +50,6 @@
 import android.os.Binder;
 import android.os.Build;
 import android.os.Bundle;
-import android.os.Handler;
-import android.os.Looper;
 import android.os.OutcomeReceiver;
 import android.os.ParcelFileDescriptor;
 import android.os.Process;
@@ -84,15 +80,12 @@
 import com.android.internal.telecom.ICallEventCallback;
 import com.android.internal.telecom.ITelecomService;
 import com.android.internal.util.IndentingPrintWriter;
-import com.android.server.telecom.callsequencing.voip.OutgoingCallTransactionSequencing;
 import com.android.server.telecom.callsequencing.voip.VoipCallMonitor;
 import com.android.server.telecom.components.UserCallIntentProcessorFactory;
 import com.android.server.telecom.flags.FeatureFlags;
 import com.android.server.telecom.metrics.ApiStats;
 import com.android.server.telecom.metrics.TelecomMetricsController;
 import com.android.server.telecom.settings.BlockedNumbersActivity;
-import com.android.server.telecom.callsequencing.voip.IncomingCallTransaction;
-import com.android.server.telecom.callsequencing.voip.OutgoingCallTransaction;
 import com.android.server.telecom.callsequencing.TransactionManager;
 import com.android.server.telecom.callsequencing.CallTransaction;
 import com.android.server.telecom.callsequencing.CallTransactionResult;
@@ -147,6 +140,13 @@
             UUID.fromString("4edf6c8d-1e43-4c94-b0fc-a40c8d80cfe8");
     public static final String PLACE_CALL_SECURITY_EXCEPTION_ERROR_MSG =
             "Security exception thrown while placing an outgoing call.";
+    public static final UUID CALL_IS_NULL_OR_ID_MISMATCH_UUID =
+            UUID.fromString("b11f3251-474c-4f90-96d6-a256aebc3c19");
+    public static final String CALL_IS_NULL_OR_ID_MISMATCH_MSG =
+            "call is null or id mismatch";
+    public static final UUID ADD_CALL_ON_ERROR_UUID =
+            UUID.fromString("f8e7d6c5-b4a3-9210-8765-432109abcdef");
+
     private static final String TAG = "TelecomServiceImpl";
     private static final String TIME_LINE_ARG = "timeline";
     private static final int DEFAULT_VIDEO_STATE = -1;
@@ -239,6 +239,11 @@
                                     onAddCallControl(callId, callEventCallback, null,
                                             new CallException(ADD_CALL_ERR_MSG,
                                                     CODE_ERROR_UNKNOWN));
+                                    if (mFeatureFlags.enableCallExceptionAnomReports()) {
+                                        mAnomalyReporter.reportAnomaly(
+                                                CALL_IS_NULL_OR_ID_MISMATCH_UUID,
+                                                CALL_IS_NULL_OR_ID_MISMATCH_MSG);
+                                    }
                                     return;
                                 }
 
@@ -268,6 +273,11 @@
                             public void onError(@NonNull CallException exception) {
                                 Log.d(TAG, "addCall: onError: e=[%s]", exception.toString());
                                 onAddCallControl(callId, callEventCallback, null, exception);
+                                if (mFeatureFlags.enableCallExceptionAnomReports()) {
+                                    mAnomalyReporter.reportAnomaly(
+                                            ADD_CALL_ON_ERROR_UUID,
+                                            exception.getMessage());
+                                }
                             }
                         });
                     }
@@ -3024,7 +3034,10 @@
         });
 
         mTransactionManager = TransactionManager.getInstance();
-        mTransactionalServiceRepository = new TransactionalServiceRepository(mFeatureFlags);
+        mTransactionManager.setFeatureFlag(mFeatureFlags);
+        mTransactionManager.setAnomalyReporter(mAnomalyReporter);
+        mTransactionalServiceRepository = new TransactionalServiceRepository(mFeatureFlags,
+                mAnomalyReporter);
         mBlockedNumbersManager = mFeatureFlags.telecomMainlineBlockedNumbersManager()
                 ? mContext.getSystemService(BlockedNumbersManager.class)
                 : null;
diff --git a/src/com/android/server/telecom/TransactionalServiceRepository.java b/src/com/android/server/telecom/TransactionalServiceRepository.java
index 5ae459e..954307a 100644
--- a/src/com/android/server/telecom/TransactionalServiceRepository.java
+++ b/src/com/android/server/telecom/TransactionalServiceRepository.java
@@ -35,9 +35,13 @@
     private static final Map<PhoneAccountHandle, TransactionalServiceWrapper> mServiceLookupTable =
             new HashMap<>();
     private final FeatureFlags mFlags;
+    private final AnomalyReporterAdapter mAnomalyReporter;
 
-    public TransactionalServiceRepository(FeatureFlags flags) {
+    public TransactionalServiceRepository(
+            FeatureFlags flags,
+            AnomalyReporterAdapter anomalyReporter) {
         mFlags = flags;
+        mAnomalyReporter = anomalyReporter;
     }
 
     public TransactionalServiceWrapper addNewCallForTransactionalServiceWrapper
@@ -50,7 +54,8 @@
             Log.d(TAG, "creating a new TSW; handle=[%s]", phoneAccountHandle);
             service = new TransactionalServiceWrapper(callEventCallback,
                     callsManager, phoneAccountHandle, call, this,
-                    TransactionManager.getInstance(), mFlags.enableCallSequencing());
+                    TransactionManager.getInstance(), mFlags.enableCallSequencing(),
+                    mFlags, mAnomalyReporter);
         } else {
             Log.d(TAG, "add a new call to an existing TSW; handle=[%s]", phoneAccountHandle);
             service = getTransactionalServiceWrapper(phoneAccountHandle);
diff --git a/src/com/android/server/telecom/TransactionalServiceWrapper.java b/src/com/android/server/telecom/TransactionalServiceWrapper.java
index d63a0bd..cc0d547 100644
--- a/src/com/android/server/telecom/TransactionalServiceWrapper.java
+++ b/src/com/android/server/telecom/TransactionalServiceWrapper.java
@@ -47,9 +47,11 @@
 import com.android.server.telecom.callsequencing.TransactionManager;
 import com.android.server.telecom.callsequencing.CallTransaction;
 import com.android.server.telecom.callsequencing.CallTransactionResult;
+import com.android.server.telecom.flags.FeatureFlags;
 
 import java.util.Locale;
 import java.util.Set;
+import java.util.UUID;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ConcurrentHashMap;
 
@@ -92,7 +94,12 @@
     private TransactionManager mTransactionManager;
     private CallStreamingController mStreamingController;
     private final TransactionalCallSequencingAdapter mCallSequencingAdapter;
-
+    private final FeatureFlags mFeatureFlags;
+    private final AnomalyReporterAdapter mAnomalyReporter;
+    public static final UUID CALL_IS_NO_LONGER_BEING_TRACKED_ERROR_UUID =
+            UUID.fromString("8187cd59-97a7-4e9f-a772-638dda4b69bb");
+    public static final String CALL_IS_NO_LONGER_BEING_TRACKED_ERROR_MSG =
+            "A call update was attempted for a call no longer being tracked";
 
     // Each TransactionalServiceWrapper should have their own Binder.DeathRecipient to clean up
     // any calls in the event the application crashes or is force stopped.
@@ -108,7 +115,8 @@
     public TransactionalServiceWrapper(ICallEventCallback callEventCallback,
             CallsManager callsManager, PhoneAccountHandle phoneAccountHandle, Call call,
             TransactionalServiceRepository repo, TransactionManager transactionManager,
-            boolean isCallSequencingEnabled) {
+            boolean isCallSequencingEnabled, FeatureFlags featureFlags,
+            AnomalyReporterAdapter anomalyReporterAdapter) {
         // passed args
         mICallEventCallback = callEventCallback;
         mCallsManager = callsManager;
@@ -123,6 +131,8 @@
         mCallSequencingAdapter = new TransactionalCallSequencingAdapter(mTransactionManager,
                 mCallsManager, isCallSequencingEnabled);
         setDeathRecipient(callEventCallback);
+        mFeatureFlags = featureFlags;
+        mAnomalyReporter = anomalyReporterAdapter;
     }
 
     public TransactionManager getTransactionManager() {
@@ -307,6 +317,11 @@
                                 + " via TelecomManager#addCall", action, callId),
                                 CODE_CALL_IS_NOT_BEING_TRACKED));
                 callback.send(CODE_CALL_IS_NOT_BEING_TRACKED, exceptionBundle);
+                if (mFeatureFlags.enableCallExceptionAnomReports()) {
+                    mAnomalyReporter.reportAnomaly(
+                            CALL_IS_NO_LONGER_BEING_TRACKED_ERROR_UUID,
+                            CALL_IS_NO_LONGER_BEING_TRACKED_ERROR_MSG);
+                }
             }
         }
 
diff --git a/src/com/android/server/telecom/callsequencing/CallSequencingController.java b/src/com/android/server/telecom/callsequencing/CallSequencingController.java
index 67ef358..034f02a 100644
--- a/src/com/android/server/telecom/callsequencing/CallSequencingController.java
+++ b/src/com/android/server/telecom/callsequencing/CallSequencingController.java
@@ -42,7 +42,6 @@
 import android.telecom.PhoneAccountHandle;
 import android.telephony.AnomalyReporter;
 import android.telephony.CarrierConfigManager;
-import android.util.Pair;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.telecom.AnomalyReporterAdapter;
@@ -62,6 +61,7 @@
 import com.android.server.telecom.stats.CallFailureCause;
 
 import java.util.Objects;
+import java.util.UUID;
 import java.util.concurrent.CompletableFuture;
 
 /**
@@ -81,6 +81,10 @@
     private final Context mContext;
     private final FeatureFlags mFeatureFlags;
     private static String TAG = CallSequencingController.class.getSimpleName();
+    public static final UUID SEQUENCING_CANNOT_HOLD_ACTIVE_CALL_UUID =
+            UUID.fromString("ea094d77-6ea9-4e40-891e-14bff5d485d7");
+    public static final String SEQUENCING_CANNOT_HOLD_ACTIVE_CALL_MSG =
+            "Cannot hold active call";
 
     public CallSequencingController(CallsManager callsManager, Context context,
             ClockProxy clockProxy, AnomalyReporterAdapter anomalyReporter,
@@ -217,6 +221,12 @@
                         callback.onError(
                                 new CallException("activeCall could not be held or disconnected",
                                 CallException.CODE_CANNOT_HOLD_CURRENT_ACTIVE_CALL));
+                        if (mFeatureFlags.enableCallExceptionAnomReports()) {
+                            mAnomalyReporter.reportAnomaly(
+                                    SEQUENCING_CANNOT_HOLD_ACTIVE_CALL_UUID,
+                                    SEQUENCING_CANNOT_HOLD_ACTIVE_CALL_MSG
+                            );
+                        }
                     }
                     return CompletableFuture.completedFuture(result);
                 }, new LoggedHandlerExecutor(mHandler, "CM.mCAA", mCallsManager.getLock()));
diff --git a/src/com/android/server/telecom/callsequencing/TransactionManager.java b/src/com/android/server/telecom/callsequencing/TransactionManager.java
index 2a6431b..98d54da 100644
--- a/src/com/android/server/telecom/callsequencing/TransactionManager.java
+++ b/src/com/android/server/telecom/callsequencing/TransactionManager.java
@@ -25,6 +25,8 @@
 import android.util.Log;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.telecom.AnomalyReporterAdapter;
+import com.android.server.telecom.flags.FeatureFlags;
 import com.android.server.telecom.flags.Flags;
 import java.util.ArrayDeque;
 import java.util.ArrayList;
@@ -32,6 +34,7 @@
 import java.util.List;
 import java.util.Locale;
 import java.util.Queue;
+import java.util.UUID;
 import java.util.concurrent.CompletableFuture;
 
 public class TransactionManager {
@@ -43,6 +46,12 @@
     private final Deque<CallTransaction> mCompletedTransactions;
     private CallTransaction mCurrentTransaction;
     private boolean mProcessingCallSequencing;
+    private AnomalyReporterAdapter mAnomalyReporter;
+    private FeatureFlags mFeatureFlags;
+    public static final UUID TRANSACTION_MANAGER_TIMEOUT_UUID =
+            UUID.fromString("9ccce52e-6694-4357-9e5e-516a9531b062");
+    public static final String TRANSACTION_MANAGER_TIMEOUT_MSG =
+            "TransactionManager hit a timeout while processing a transaction";
 
     public interface TransactionCompleteListener {
         void onTransactionCompleted(CallTransactionResult result, String transactionName);
@@ -67,6 +76,14 @@
         return INSTANCE;
     }
 
+    public void setFeatureFlag(FeatureFlags flag){
+       mFeatureFlags = flag;
+    }
+
+    public void setAnomalyReporter(AnomalyReporterAdapter callAnomalyReporter){
+        mAnomalyReporter = callAnomalyReporter;
+    }
+
     @VisibleForTesting
     public static TransactionManager getTestInstance() {
         return new TransactionManager();
@@ -109,6 +126,12 @@
                     receiver.onError(new CallException(transactionName + " timeout",
                             CODE_OPERATION_TIMED_OUT));
                     transactionCompleteFuture.complete(false);
+                    if (mFeatureFlags != null && mAnomalyReporter != null &&
+                            mFeatureFlags.enableCallExceptionAnomReports()) {
+                        mAnomalyReporter.reportAnomaly(
+                                TRANSACTION_MANAGER_TIMEOUT_UUID,
+                                TRANSACTION_MANAGER_TIMEOUT_MSG);
+                    }
                 } catch (Exception e) {
                     Log.e(TAG, String.format("onTransactionTimeout: Notifying transaction "
                             + " %s resulted in an Exception.", transactionName), e);
diff --git a/src/com/android/server/telecom/callsequencing/voip/VoipCallMonitor.java b/src/com/android/server/telecom/callsequencing/voip/VoipCallMonitor.java
index bd0b932..8c74510 100644
--- a/src/com/android/server/telecom/callsequencing/voip/VoipCallMonitor.java
+++ b/src/com/android/server/telecom/callsequencing/voip/VoipCallMonitor.java
@@ -30,9 +30,7 @@
 import android.content.Context;
 import android.content.ServiceConnection;
 import android.os.Handler;
-import android.os.HandlerThread;
 import android.os.IBinder;
-import android.os.Looper;
 import android.os.RemoteException;
 import android.service.notification.NotificationListenerService;
 import android.service.notification.StatusBarNotification;
@@ -53,14 +51,16 @@
 import java.util.Objects;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentLinkedQueue;
 
 public class VoipCallMonitor extends CallsManagerListenerBase {
-    private static final long NOTIFICATION_NOT_POSTED_IN_TIME_TIMEOUT = 5000L;
-    private static final long NOTIFICATION_REMOVED_BUT_CALL_IS_STILL_ONGOING_TIMEOUT = 5000L;
+    public static final long NOTIFICATION_NOT_POSTED_IN_TIME_TIMEOUT = 5000L;
+    public static final long NOTIFICATION_REMOVED_BUT_CALL_IS_STILL_ONGOING_TIMEOUT = 5000L;
+    private static final String TAG = VoipCallMonitor.class.getSimpleName();
     private static final String DElIMITER = "#";
     // This list caches calls that are added to the VoipCallMonitor and need an accompanying
     // Call-Style Notification!
-    private final List<Call> mNewCallsMissingCallStyleNotification;
+    private final ConcurrentLinkedQueue<Call> mNewCallsMissingCallStyleNotification;
     private final ConcurrentHashMap<String, Call> mNotificationIdToCall;
     private final ConcurrentHashMap<PhoneAccountHandle, Set<Call>> mAccountHandleToCallMap;
     private final ConcurrentHashMap<PhoneAccountHandle, ServiceConnection> mServices;
@@ -70,11 +70,11 @@
     private final Context mContext;
     private final TelecomSystem.SyncRoot mSyncRoot;
 
-    public VoipCallMonitor(Context context, TelecomSystem.SyncRoot lock) {
+    public VoipCallMonitor(Context context, Handler handler, TelecomSystem.SyncRoot lock) {
         mSyncRoot = lock;
         mContext = context;
-        mHandlerForClass = new Handler(Looper.getMainLooper());
-        mNewCallsMissingCallStyleNotification = new ArrayList<>();
+        mHandlerForClass = handler;
+        mNewCallsMissingCallStyleNotification = new ConcurrentLinkedQueue<>();
         mNotificationIdToCall = new ConcurrentHashMap<>();
         mServices = new ConcurrentHashMap<>();
         mAccountHandleToCallMap = new ConcurrentHashMap<>();
@@ -83,21 +83,18 @@
             @Override
             public void onNotificationPosted(StatusBarNotification sbn) {
                 if (isCallStyleNotification(sbn)) {
-                    Log.i(this, "onNotificationPosted: sbn=[%s]", sbn);
-                    boolean foundCallForNotification = false;
+                    Log.i(TAG, "onNotificationPosted: sbn=[%s]", sbn);
                     // Case 1: Call added to this class (via onCallAdded) BEFORE Call-Style
                     //         Notification is posted by the app (only supported scenario)
-                    // --> remove the newly added call from
-                    //     mNewCallsMissingCallStyleNotification so FGS is not revoked.
-                    for (Call call : new ArrayList<>(mNewCallsMissingCallStyleNotification)) {
+                    Call newCallNoLongerAwaitingNotification = null;
+                    for (Call call : mNewCallsMissingCallStyleNotification) {
                         if (isNotificationForCall(sbn, call)) {
-                            Log.i(this, "onNotificationPosted: found a pending "
+                            Log.i(TAG, "onNotificationPosted: found a pending "
                                     + "call=[%s] for sbn.id=[%s]", call, sbn.getId());
                             mNotificationIdToCall.put(
                                     getNotificationIdToCallKey(sbn),
                                     call);
-                            removeFromNotificationTracking(call);
-                            foundCallForNotification = true;
+                            newCallNoLongerAwaitingNotification = call;
                             break;
                         }
                     }
@@ -105,11 +102,19 @@
                     // --> Currently do not support this
                     // Case 3: Call-Style Notification was updated (ex. incoming -> ongoing)
                     // --> do nothing
-                    if (!foundCallForNotification) {
-                        Log.i(this, "onNotificationPosted: could not find a call for the"
+                    if (newCallNoLongerAwaitingNotification == null) {
+                        Log.i(TAG, "onNotificationPosted: could not find a call for the"
                                 + " sbn.id=[%s]. This could mean the notification posted"
                                 + " BEFORE the call is added (error) or it's an update from"
                                 + " incoming to ongoing (ok).", sbn.getId());
+                    } else {
+                        // --> remove the newly added call from
+                        // mNewCallsMissingCallStyleNotification so FGS is not revoked when the
+                        // timeout is hit in VoipCallMonitor#startMonitoringNotification(...). The
+                        // timeout ensures the voip app posts a call-style notification within
+                        // 5 seconds!
+                        mNewCallsMissingCallStyleNotification
+                                .remove(newCallNoLongerAwaitingNotification);
                     }
                 }
             }
@@ -119,14 +124,17 @@
                 if (!isCallStyleNotification(sbn)) {
                     return;
                 }
-                Log.i(this, "onNotificationRemoved: Call-Style notification=[%s] removed", sbn);
+                Log.i(TAG, "onNotificationRemoved: Call-Style notification=[%s] removed", sbn);
                 Call call = getCallFromStatusBarNotificationId(sbn);
                 if (call != null) {
-                    PhoneAccountHandle handle = getTargetPhoneAccount(call);
                     if (!isCallDisconnected(call)) {
                         mHandlerForClass.postDelayed(() -> {
                             if (isCallStillBeingTracked(call)) {
-                                stopFGSDelegation(call, handle);
+                                Log.w(TAG,
+                                        "onNotificationRemoved: notification has been removed for"
+                                                + " more than 5 seconds but call still ongoing "
+                                                + "c=[%s]", call);
+                                // TODO:: stopFGSDelegation(call, handle) when b/383403913 is fixed
                             }
                         }, NOTIFICATION_REMOVED_BUT_CALL_IS_STILL_ONGOING_TIMEOUT);
                     }
@@ -185,7 +193,7 @@
                     new ComponentName(this.getClass().getPackageName(),
                             this.getClass().getCanonicalName()), ActivityManager.getCurrentUser());
         } catch (RemoteException e) {
-            Log.e(this, e, "Cannot register notification listener");
+            Log.e(TAG, e, "Cannot register notification listener");
         }
     }
 
@@ -193,7 +201,7 @@
         try {
             mNotificationListener.unregisterAsSystemService();
         } catch (RemoteException e) {
-            Log.e(this, e, "Cannot unregister notification listener");
+            Log.e(TAG, e, "Cannot unregister notification listener");
         }
     }
 
@@ -217,11 +225,10 @@
         if (!isTransactional(call) || handle == null) {
             return;
         }
-        removeFromNotificationTracking(call);
         Set<Call> ongoingCalls = mAccountHandleToCallMap
                 .computeIfAbsent(handle, k -> new HashSet<>());
         ongoingCalls.remove(call);
-        Log.d(this, "onCallRemoved: callList.size=[%d]", ongoingCalls.size());
+        Log.d(TAG, "onCallRemoved: callList.size=[%d]", ongoingCalls.size());
         if (ongoingCalls.isEmpty()) {
             stopFGSDelegation(call, handle);
         } else {
@@ -230,7 +237,7 @@
     }
 
     private void maybeStartFGSDelegation(int pid, int uid, PhoneAccountHandle handle, Call call) {
-        Log.i(this, "maybeStartFGSDelegation for call=[%s]", call);
+        Log.i(TAG, "maybeStartFGSDelegation for call=[%s]", call);
         if (mActivityManagerInternal != null) {
             if (mServices.containsKey(handle)) {
                 Log.addEvent(call, LogUtils.Events.ALREADY_HAS_FGS_DELEGATION);
@@ -262,34 +269,38 @@
             try {
                 if (mActivityManagerInternal
                         .startForegroundServiceDelegate(options, fgsConnection)) {
-                    Log.i(this, "maybeStartFGSDelegation: startForegroundServiceDelegate success");
+                    Log.i(TAG, "maybeStartFGSDelegation: startForegroundServiceDelegate success");
                 } else {
                     Log.addEvent(call, LogUtils.Events.GAIN_FGS_DELEGATION_FAILED);
                 }
             } catch (Exception e) {
-                Log.i(this, "startForegroundServiceDelegate failed due to: " + e);
+                Log.i(TAG, "startForegroundServiceDelegate failed due to: " + e);
             }
         }
     }
 
     @VisibleForTesting
     public void stopFGSDelegation(Call call, PhoneAccountHandle handle) {
-        Log.i(this, "stopFGSDelegation of call=[%s]", call);
+        Log.i(TAG, "stopFGSDelegation of call=[%s]", call);
         if (handle == null) {
             return;
         }
-        // In the event this class is waiting for any new calls to post a notification, remove
-        // the call from the notification tracking container!
-        Set<Call> ongoingCalls = mAccountHandleToCallMap.get(handle);
-        if (ongoingCalls != null) {
-            for (Call c : new ArrayList<>(ongoingCalls)) {
-                removeFromNotificationTracking(c);
+
+        // In the event this class is waiting for any new calls to post a notification, cleanup
+        List<Call> toRemove = new ArrayList<>();
+        for (Call callAwaitingNotification : mNewCallsMissingCallStyleNotification) {
+            if (handle.equals(callAwaitingNotification.getTargetPhoneAccount())) {
+                Log.d(TAG, "stopFGSDelegation: removing call from notification tracking c=[%s]",
+                        callAwaitingNotification);
+                toRemove.add(callAwaitingNotification);
             }
         }
+        mNewCallsMissingCallStyleNotification.removeAll(toRemove);
+
         if (mActivityManagerInternal != null) {
             ServiceConnection fgsConnection = mServices.get(handle);
             if (fgsConnection != null) {
-                Log.i(this, "stopFGSDelegation: requesting stopForegroundServiceDelegate");
+                Log.i(TAG, "stopFGSDelegation: requesting stopForegroundServiceDelegate");
                 mActivityManagerInternal.stopForegroundServiceDelegate(fgsConnection);
             }
         }
@@ -301,17 +312,17 @@
         String callId = getCallId(call);
         // Wait 5 seconds for a CallStyle notification to be posted for the call.
         // If the Call-Style Notification is not posted, FGS delegation needs to be revoked!
-        Log.i(this, "startMonitoringNotification: starting timeout for call.id=[%s]", callId);
-        addToNotificationTracking(call);
+        Log.i(TAG, "startMonitoringNotification: starting timeout for call.id=[%s]", callId);
+        mNewCallsMissingCallStyleNotification.add(call);
         // If no notification is posted, stop foreground service delegation!
         mHandlerForClass.postDelayed(() -> {
-            if (isStillMissingNotification(call)) {
-                Log.i(this, "startMonitoringNotification: A Call-Style-Notification"
+            if (mNewCallsMissingCallStyleNotification.contains(call)) {
+                Log.i(TAG, "startMonitoringNotification: A Call-Style-Notification"
                         + " for voip-call=[%s] hasn't posted in time,"
                         + " stopping delegation for app=[%s].", call, packageName);
                 stopFGSDelegation(call, handle);
             } else {
-                Log.i(this, "startMonitoringNotification: found a call-style"
+                Log.i(TAG, "startMonitoringNotification: found a call-style"
                         + " notification for call.id[%s] at timeout", callId);
             }
         }, NOTIFICATION_NOT_POSTED_IN_TIME_TIMEOUT);
@@ -321,24 +332,6 @@
      * Helpers
      */
 
-    private void addToNotificationTracking(Call call) {
-        synchronized (mNewCallsMissingCallStyleNotification) {
-            mNewCallsMissingCallStyleNotification.add(call);
-        }
-    }
-
-    private boolean isStillMissingNotification(Call call) {
-        synchronized (mNewCallsMissingCallStyleNotification) {
-           return mNewCallsMissingCallStyleNotification.contains(call);
-        }
-    }
-
-    private void removeFromNotificationTracking(Call call) {
-        synchronized (mNewCallsMissingCallStyleNotification) {
-            mNewCallsMissingCallStyleNotification.remove(call);
-        }
-    }
-
     private PhoneAccountHandle getTargetPhoneAccount(Call call) {
         synchronized (mSyncRoot) {
             if (call == null) {
@@ -426,7 +419,7 @@
 
     public boolean hasForegroundServiceDelegation(PhoneAccountHandle handle) {
         boolean hasFgs = mServices.containsKey(handle);
-        Log.i(this, "hasForegroundServiceDelegation: handle=[%s], hasFgs=[%b]", handle, hasFgs);
+        Log.i(TAG, "hasForegroundServiceDelegation: handle=[%s], hasFgs=[%b]", handle, hasFgs);
         return hasFgs;
     }
 
@@ -434,4 +427,9 @@
     public ConcurrentHashMap<PhoneAccountHandle, Set<Call>> getAccountToCallsMapping() {
         return mAccountHandleToCallMap;
     }
+
+    @VisibleForTesting
+    public  ConcurrentLinkedQueue<Call> getNewCallsMissingCallStyleNotificationQueue(){
+        return mNewCallsMissingCallStyleNotification;
+    }
 }
diff --git a/tests/src/com/android/server/telecom/tests/RingtoneFactoryTest.java b/tests/src/com/android/server/telecom/tests/RingtoneFactoryTest.java
new file mode 100644
index 0000000..2a951aa
--- /dev/null
+++ b/tests/src/com/android/server/telecom/tests/RingtoneFactoryTest.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2025 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.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.when;
+
+import android.media.Ringtone;
+import android.net.Uri;
+import android.os.UserHandle;
+import android.util.Pair;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.server.telecom.CallsManager;
+import com.android.server.telecom.Call;
+import com.android.server.telecom.RingtoneFactory;
+import com.android.server.telecom.flags.FeatureFlags;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+
+@RunWith(JUnit4.class)
+public class RingtoneFactoryTest extends TelecomTestCase {
+    @Mock private Uri mockCustomRingtoneUri;
+    @Mock private CallsManager mockCallsManager;
+    @Mock private FeatureFlags mockFeatureFlags;
+    @Mock Call mockCall;
+    private RingtoneFactory ringtoneFactory;
+
+    @Override
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+        mContext = spy(mComponentContextFixture.getTestDouble().getApplicationContext());
+        ringtoneFactory = new RingtoneFactory(mockCallsManager, mContext, mockFeatureFlags);
+    }
+
+    @Override
+    @After
+    public void tearDown() throws Exception {
+        super.tearDown();
+    }
+
+    @SmallTest
+    @Test
+    public void testCustomRingtoneAccessibleWhenUserOwnsCustomRingtone() throws Exception {
+        // Current User is User 10:
+        when(mockCall.getAssociatedUser()).thenReturn(new UserHandle(10));
+
+        // Custom ringtone is owned by User 10:
+        when(mockCall.getRingtone()).thenReturn(mockCustomRingtoneUri);
+        when(mockCustomRingtoneUri.getUserInfo()).thenReturn("10");
+
+        // Ensure access to the custom ringtone is allowed:
+        Pair<Uri, Ringtone> ringtonePair = ringtoneFactory.getRingtone(mockCall, null, false);
+        assertEquals(mockCustomRingtoneUri, ringtonePair.first);
+    }
+
+    @SmallTest
+    @Test
+    public void testCustomRingtoneNotAccessibleByOtherUser() throws Exception {
+        // Current User is User 10:
+        when(mockCall.getAssociatedUser()).thenReturn(new UserHandle(0));
+
+        // Custom ringtone is owned by User 10:
+        when(mockCall.getRingtone()).thenReturn(mockCustomRingtoneUri);
+        when(mockCustomRingtoneUri.getUserInfo()).thenReturn("10");
+
+        // Ensure access to the custom ringtone is NOT allowed:
+        Pair<Uri, Ringtone> ringtonePair = ringtoneFactory.getRingtone(mockCall, null, false);
+        assertNotEquals(mockCustomRingtoneUri, ringtonePair.first);
+    }
+}
+
diff --git a/tests/src/com/android/server/telecom/tests/TransactionalServiceWrapperTest.java b/tests/src/com/android/server/telecom/tests/TransactionalServiceWrapperTest.java
index fea6135..16b6e44 100644
--- a/tests/src/com/android/server/telecom/tests/TransactionalServiceWrapperTest.java
+++ b/tests/src/com/android/server/telecom/tests/TransactionalServiceWrapperTest.java
@@ -34,6 +34,7 @@
 
 import com.android.internal.telecom.ICallControl;
 import com.android.internal.telecom.ICallEventCallback;
+import com.android.server.telecom.AnomalyReporterAdapter;
 import com.android.server.telecom.Call;
 import com.android.server.telecom.CallsManager;
 import com.android.server.telecom.TelecomSystem;
@@ -70,6 +71,7 @@
     @Mock private TransactionManager mTransactionManager;
     @Mock private ICallEventCallback mCallEventCallback;
     @Mock private TransactionalServiceRepository mRepository;
+    @Mock private AnomalyReporterAdapter mAnomalyReporterAdapter;
     @Mock private IBinder mIBinder;
     private final TelecomSystem.SyncRoot mLock = new TelecomSystem.SyncRoot() {};
 
@@ -84,7 +86,7 @@
         Mockito.when(mCallEventCallback.asBinder()).thenReturn(mIBinder);
         mTransactionalServiceWrapper = new TransactionalServiceWrapper(mCallEventCallback,
                 mCallsManager, SERVICE_HANDLE, mMockCall1, mRepository, mTransactionManager,
-                false /*call sequencing*/);
+                false /*call sequencing*/, mFeatureFlags, mAnomalyReporterAdapter);
     }
 
     @Override
@@ -98,7 +100,7 @@
         TransactionalServiceWrapper service =
                 new TransactionalServiceWrapper(mCallEventCallback,
                         mCallsManager, SERVICE_HANDLE, mMockCall1, mRepository, mTransactionManager,
-                        false /*call sequencing*/);
+                        false /*call sequencing*/, mFeatureFlags, mAnomalyReporterAdapter);
 
         assertEquals(SERVICE_HANDLE, service.getPhoneAccountHandle());
         assertEquals(1, service.getNumberOfTrackedCalls());
@@ -109,7 +111,7 @@
         TransactionalServiceWrapper service =
                 new TransactionalServiceWrapper(mCallEventCallback,
                         mCallsManager, SERVICE_HANDLE, mMockCall1, mRepository, mTransactionManager,
-                        false /*call sequencing*/);
+                        false /*call sequencing*/, mFeatureFlags, mAnomalyReporterAdapter);
 
         assertEquals(1, service.getNumberOfTrackedCalls());
         service.trackCall(mMockCall2);
diff --git a/tests/src/com/android/server/telecom/tests/VoipCallMonitorTest.java b/tests/src/com/android/server/telecom/tests/VoipCallMonitorTest.java
index 1444b09..1b3856c 100644
--- a/tests/src/com/android/server/telecom/tests/VoipCallMonitorTest.java
+++ b/tests/src/com/android/server/telecom/tests/VoipCallMonitorTest.java
@@ -22,9 +22,13 @@
 import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.timeout;
 import static org.mockito.Mockito.times;
@@ -40,6 +44,7 @@
 import android.content.Intent;
 import android.content.ServiceConnection;
 import android.os.Bundle;
+import android.os.Handler;
 import android.os.IBinder;
 import android.os.UserHandle;
 import android.service.notification.StatusBarNotification;
@@ -54,6 +59,7 @@
 
 import org.junit.After;
 import org.junit.Before;
+import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
@@ -75,6 +81,7 @@
     private static final UserHandle USER_HANDLE_1 = new UserHandle(1);
     private static final long TIMEOUT = 6000L;
 
+    @Mock private Handler mHandler;
     @Mock private TelecomSystem.SyncRoot mLock;
     @Mock private ActivityManagerInternal mActivityManagerInternal;
     @Mock private IBinder mServiceConnection;
@@ -88,7 +95,8 @@
     @Before
     public void setUp() throws Exception {
         super.setUp();
-        mMonitor = new VoipCallMonitor(mContext, mLock);
+        mHandler = mock(Handler.class);
+        mMonitor = new VoipCallMonitor(mContext, mHandler, mLock);
         mActivityManagerInternal = mock(ActivityManagerInternal.class);
         mMonitor.setActivityManagerInternal(mActivityManagerInternal);
         mMonitor.registerNotificationListener();
@@ -158,16 +166,18 @@
     public void testStartMonitorForOneCall() {
         // GIVEN - a single call and notification for a voip app
         Call call = createTestCall("testCall", mHandle1User1);
-        StatusBarNotification sbn = createStatusBarNotificationFromHandle(mHandle1User1);
+        StatusBarNotification sbn = createStatusBarNotificationFromHandle(mHandle1User1, 1);
 
         // WHEN - the Voip call is added and a notification is posted, verify FGS is gained
         addCallAndVerifyFgsIsGained(call);
         mMonitor.postNotification(sbn);
+        assertNotificationTimeoutTriggered();
+        assertFalse(mMonitor.getNewCallsMissingCallStyleNotificationQueue().contains(call));
 
         // THEN - when the Voip call is removed, verify that FGS is revoked for the app
         mMonitor.onCallRemoved(call);
         mMonitor.removeNotification(sbn);
-        verify(mActivityManagerInternal, timeout(TIMEOUT))
+        verify(mActivityManagerInternal, times(1))
                 .stopForegroundServiceDelegate(any(ServiceConnection.class));
     }
 
@@ -179,25 +189,34 @@
     public void testStopDelegation_SameApp() {
         // GIVEN - 2 consecutive calls for a single Voip app
         Call call1 = createTestCall("testCall1", mHandle1User1);
-        StatusBarNotification sbn1 = createStatusBarNotificationFromHandle(mHandle1User1);
+        StatusBarNotification sbn1 = createStatusBarNotificationFromHandle(mHandle1User1, 1);
         Call call2 = createTestCall("testCall2", mHandle1User1);
-        StatusBarNotification sbn2 = createStatusBarNotificationFromHandle(mHandle1User1);
+        StatusBarNotification sbn2 = createStatusBarNotificationFromHandle(mHandle1User1, 2);
 
         // WHEN - the second call is added and the first is disconnected
-        mMonitor.postNotification(sbn1);
+        // -- add the first all and post the corresponding notification
         addCallAndVerifyFgsIsGained(call1);
-        mMonitor.postNotification(sbn2);
+        assertTrue(mMonitor.getNewCallsMissingCallStyleNotificationQueue().contains(call1));
+        mMonitor.postNotification(sbn1);
+        assertNotificationTimeoutTriggered();
+        assertFalse(mMonitor.getNewCallsMissingCallStyleNotificationQueue().contains(call1));
+        // -- add the second call and post the corresponding notification
         mMonitor.onCallAdded(call2);
-        mMonitor.onCallRemoved(call1);
+        assertTrue(mMonitor.getNewCallsMissingCallStyleNotificationQueue().contains(call2));
+        mMonitor.postNotification(sbn2);
+        assertNotificationTimeoutTriggered();
+        assertFalse(mMonitor.getNewCallsMissingCallStyleNotificationQueue().contains(call2));
 
         // THEN - assert FGS is maintained for the process since there is still an ongoing call
-        verify(mActivityManagerInternal, timeout(TIMEOUT).times(0))
-                .stopForegroundServiceDelegate(any(ServiceConnection.class));
+        mMonitor.onCallRemoved(call1);
         mMonitor.removeNotification(sbn1);
+        assertNotificationTimeoutTriggered();
+        verify(mActivityManagerInternal, times(0))
+                .stopForegroundServiceDelegate(any(ServiceConnection.class));
         // once all calls are removed, verify FGS is stopped
         mMonitor.onCallRemoved(call2);
         mMonitor.removeNotification(sbn2);
-        verify(mActivityManagerInternal, timeout(TIMEOUT).times(1))
+        verify(mActivityManagerInternal, times(1))
                 .stopForegroundServiceDelegate(any(ServiceConnection.class));
     }
 
@@ -245,9 +264,10 @@
      */
     @SmallTest
     @Test
+    @Ignore("b/383403913") // when b/383403913 is fixed, remove the @Ignore
     public void testStopFgsIfCallNotificationIsRemoved_PostedAfterFgsIsGained() {
         // GIVEN
-        StatusBarNotification sbn = createStatusBarNotificationFromHandle(mHandle1User1);
+        StatusBarNotification sbn = createStatusBarNotificationFromHandle(mHandle1User1, 1);
 
         // WHEN
         // FGS is gained after the call is added to VoipCallMonitor
@@ -259,7 +279,65 @@
         // shortly after posting the notification, simulate the user dismissing it
         mMonitor.removeNotification(sbn);
         // FGS should be removed once the notification is removed
-        verify(mActivityManagerInternal, timeout(TIMEOUT)).stopForegroundServiceDelegate(c);
+        assertNotificationTimeoutTriggered();
+        verify(mActivityManagerInternal, times(1)).stopForegroundServiceDelegate(c);
+    }
+
+
+    /**
+     * Tests the behavior of foreground service (FGS) delegation for a VoIP app during a scenario
+     * with two consecutive calls.  In this scenario, the first call is disconnected shortly after
+     * being created but the second call continues.  The apps foreground service should be
+     * maintained.
+     *
+     * GIVEN: Two calls (call1 and call2) are created for the same VoIP app.
+     * WHEN:
+     *  - call1 is added, starting the FGS.
+     *  - call2 is added immediately after.
+     *  - call1 is removed.
+     *  - call1 notification is finally posted (late)
+     *  - call1 notification is removed shortly after since the call was disconnected
+     * THEN:
+     *  - Verifies that the FGS is NOT stopped while call2 is still active.
+     *  - Verifies that the FGS IS stopped after call2 is removed and its notification is gone.
+     */
+    @SmallTest
+    @Test
+    public void test2CallsInQuickSuccession() {
+        // GIVEN - 2 consecutive calls for a single Voip app
+        Call call1 = createTestCall("testCall1", mHandle1User1);
+        StatusBarNotification sbn1 = createStatusBarNotificationFromHandle(mHandle1User1, 1);
+        Call call2 = createTestCall("testCall2", mHandle1User1);
+        StatusBarNotification sbn2 = createStatusBarNotificationFromHandle(mHandle1User1, 2);
+
+        // WHEN - add the calls to the VoipCallMonitor class
+        addCallAndVerifyFgsIsGained(call1);
+        mMonitor.onCallAdded(call2);
+        assertTrue(mMonitor.getNewCallsMissingCallStyleNotificationQueue().contains(call1));
+        assertTrue(mMonitor.getNewCallsMissingCallStyleNotificationQueue().contains(call2));
+        // -- mock the app disconnecting the first
+        mMonitor.onCallRemoved(call1);
+        // Shortly after, simulate the notification updates coming in to the class
+        // -- post and remove the first call-style notification
+        mMonitor.postNotification(sbn1);
+        assertFalse(mMonitor.getNewCallsMissingCallStyleNotificationQueue().contains(call1));
+        mMonitor.removeNotification(sbn1);
+        assertNotificationTimeoutTriggered();
+
+        // -- keep the second notification up since the call will continue
+        mMonitor.postNotification(sbn2);
+        assertFalse(mMonitor.getNewCallsMissingCallStyleNotificationQueue().contains(call2));
+
+        // THEN - assert FGS is maintained for the process since there is still an ongoing call
+        assertNotificationTimeoutTriggered();
+        verify(mActivityManagerInternal, times(0))
+                .stopForegroundServiceDelegate(any(ServiceConnection.class));
+
+        // once all calls are removed, verify FGS is stopped
+        mMonitor.onCallRemoved(call2);
+        mMonitor.removeNotification(sbn2);
+        verify(mActivityManagerInternal, timeout(TIMEOUT).times(1))
+                .stopForegroundServiceDelegate(any(ServiceConnection.class));
     }
 
     /**
@@ -291,9 +369,10 @@
                 .build();
     }
 
-    private StatusBarNotification createStatusBarNotificationFromHandle(PhoneAccountHandle handle) {
+    private StatusBarNotification createStatusBarNotificationFromHandle(
+            PhoneAccountHandle handle, int id) {
         return new StatusBarNotification(
-                handle.getComponentName().getPackageName(), "", 0, "", 0, 0,
+                handle.getComponentName().getPackageName(), "", id, "", 0, 0,
                 createCallStyleNotification(), handle.getUserHandle(), "", 0);
     }
 
@@ -314,4 +393,17 @@
                 mServiceConnection);
         return serviceConnection;
     }
+
+    /**
+     * Verifies that a delayed runnable is posted to the handler to handle the notification timeout.
+     * This also executes the captured runnable to simulate the timeout occurring.
+     */
+    private void assertNotificationTimeoutTriggered() {
+        ArgumentCaptor<Runnable> runnableCaptor = ArgumentCaptor.forClass(Runnable.class);
+        verify(mHandler, atLeastOnce()).postDelayed(
+                runnableCaptor.capture(),
+                eq(VoipCallMonitor.NOTIFICATION_NOT_POSTED_IN_TIME_TIMEOUT));
+        Runnable capturedRunnable = runnableCaptor.getValue();
+        capturedRunnable.run();
+    }
 }