Adds TransactionManager dumpsys info and cleanups

1) Adds new call sequencing flag for general DSDA development in Telecom
2) Adds a new Transaction History in TransactionManager
  - Keeps track of when a transaction was added/started/completed
  - prints the state of pending and completed transactions for
  dumpsys purposes. Limits the completed transaction queue to 20
  items for now.
  - Modifies SerialTransaction to not remove sub-transactions so that
  we can collect histry about the transaction when finished
3) Cleans up the visibility of some methods and adds flag propogation
changes for testing

Fixes: 315879576
Test: atest TelecomUnitTests:VoipCallTransactionTest
Change-Id: Ifac05d24f70c7e12b1dd5d8f89308d550d3e3ae5
diff --git a/flags/telecom_calls_manager_flags.aconfig b/flags/telecom_calls_manager_flags.aconfig
index 1a19480..a68042e 100644
--- a/flags/telecom_calls_manager_flags.aconfig
+++ b/flags/telecom_calls_manager_flags.aconfig
@@ -13,3 +13,10 @@
   description: "This fix ensures the MO calls won't switch from Active to Quite b/c setDialing was not called"
   bug: "309540769"
 }
+
+flag {
+  name: "enable_call_sequencing"
+  namespace: "telecom"
+  description: "Enables simultaneous call sequencing for SIM PhoneAccounts"
+  bug: "297446980"
+}
diff --git a/src/com/android/server/telecom/CallsManager.java b/src/com/android/server/telecom/CallsManager.java
index de601a5..aa8eb57 100644
--- a/src/com/android/server/telecom/CallsManager.java
+++ b/src/com/android/server/telecom/CallsManager.java
@@ -700,7 +700,8 @@
         mCallLogManager = new CallLogManager(context, phoneAccountRegistrar, mMissedCallNotifier,
                 mAnomalyReporter, featureFlags);
         mConnectionServiceRepository =
-                new ConnectionServiceRepository(mPhoneAccountRegistrar, mContext, mLock, this);
+                new ConnectionServiceRepository(mPhoneAccountRegistrar, mContext, mLock, this,
+                        featureFlags);
         mInCallWakeLockController = inCallWakeLockControllerFactory.create(context, this);
         mClockProxy = clockProxy;
         mToastFactory = toastFactory;
@@ -5880,8 +5881,7 @@
             return;
         }
         ConnectionServiceWrapper service = mConnectionServiceRepository.getService(
-                phoneAccountHandle.getComponentName(), phoneAccountHandle.getUserHandle(),
-                mFeatureFlags);
+                phoneAccountHandle.getComponentName(), phoneAccountHandle.getUserHandle());
         if (service == null) {
             Log.i(this, "Found no connection service.");
             return;
@@ -5906,8 +5906,7 @@
             return;
         }
         ConnectionServiceWrapper service = mConnectionServiceRepository.getService(
-                phoneAccountHandle.getComponentName(), phoneAccountHandle.getUserHandle(),
-                mFeatureFlags);
+                phoneAccountHandle.getComponentName(), phoneAccountHandle.getUserHandle());
         if (service == null) {
             Log.i(this, "Found no connection service.");
             return;
diff --git a/src/com/android/server/telecom/ConnectionServiceRepository.java b/src/com/android/server/telecom/ConnectionServiceRepository.java
index d6a78d0..e4ed220 100644
--- a/src/com/android/server/telecom/ConnectionServiceRepository.java
+++ b/src/com/android/server/telecom/ConnectionServiceRepository.java
@@ -38,6 +38,7 @@
     private final Context mContext;
     private final TelecomSystem.SyncRoot mLock;
     private final CallsManager mCallsManager;
+    private final FeatureFlags mFeatureFlags;
 
     private final ServiceBinder.Listener<ConnectionServiceWrapper> mUnbindListener =
             new ServiceBinder.Listener<ConnectionServiceWrapper>() {
@@ -54,18 +55,19 @@
             PhoneAccountRegistrar phoneAccountRegistrar,
             Context context,
             TelecomSystem.SyncRoot lock,
-            CallsManager callsManager) {
+            CallsManager callsManager,
+            FeatureFlags featureFlags) {
         mPhoneAccountRegistrar = phoneAccountRegistrar;
         mContext = context;
         mLock = lock;
         mCallsManager = callsManager;
+        mFeatureFlags = featureFlags;
     }
 
     @VisibleForTesting
     public ConnectionServiceWrapper getService(
             ComponentName componentName,
-            UserHandle userHandle,
-            FeatureFlags featureFlags) {
+            UserHandle userHandle) {
         Pair<ComponentName, UserHandle> cacheKey = Pair.create(componentName, userHandle);
         ConnectionServiceWrapper service = mServiceCache.get(cacheKey);
         if (service == null) {
@@ -77,7 +79,7 @@
                     mContext,
                     mLock,
                     userHandle,
-                    featureFlags);
+                    mFeatureFlags);
             service.addListener(mUnbindListener);
             mServiceCache.put(cacheKey, service);
         }
diff --git a/src/com/android/server/telecom/ConnectionServiceWrapper.java b/src/com/android/server/telecom/ConnectionServiceWrapper.java
index 43ceff3..53da8ff 100644
--- a/src/com/android/server/telecom/ConnectionServiceWrapper.java
+++ b/src/com/android/server/telecom/ConnectionServiceWrapper.java
@@ -1351,7 +1351,6 @@
     private final CallsManager mCallsManager;
     private final AppOpsManager mAppOpsManager;
     private final Context mContext;
-    private final FeatureFlags mFlags;
 
     private ConnectionServiceFocusManager.ConnectionServiceFocusListener mConnSvrFocusListener;
 
@@ -1386,7 +1385,6 @@
         mCallsManager = callsManager;
         mAppOpsManager = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
         mContext = context;
-        mFlags = featureFlags;
     }
 
     /** See {@link IConnectionService#addConnectionServiceAdapter}. */
@@ -2540,7 +2538,7 @@
                 isCallerConnectionManager = true;
             }
             ConnectionServiceWrapper service = mConnectionServiceRepository.getService(
-                    handle.getComponentName(), handle.getUserHandle(), mFlags);
+                    handle.getComponentName(), handle.getUserHandle());
             if (service != null && service != this) {
                 simServices.add(service);
             } else {
diff --git a/src/com/android/server/telecom/CreateConnectionProcessor.java b/src/com/android/server/telecom/CreateConnectionProcessor.java
index f5b257d..bcb2d2f 100644
--- a/src/com/android/server/telecom/CreateConnectionProcessor.java
+++ b/src/com/android/server/telecom/CreateConnectionProcessor.java
@@ -244,7 +244,7 @@
             Log.i(this, "Trying attempt %s", attempt);
             PhoneAccountHandle phoneAccount = attempt.connectionManagerPhoneAccount;
             mService = mRepository.getService(phoneAccount.getComponentName(),
-                    phoneAccount.getUserHandle(), mFlags);
+                    phoneAccount.getUserHandle());
             if (mService == null) {
                 Log.i(this, "Found no connection service for attempt %s", attempt);
                 attemptNextPhoneAccount();
@@ -260,7 +260,7 @@
                         PhoneAccountHandle remotePhoneAccount = attempt.targetPhoneAccount;
                         ConnectionServiceWrapper mRemoteService =
                                 mRepository.getService(remotePhoneAccount.getComponentName(),
-                                remotePhoneAccount.getUserHandle(), mFlags);
+                                remotePhoneAccount.getUserHandle());
                         if (mRemoteService == null) {
                             mCall.setConnectionService(mService);
                         } else {
diff --git a/src/com/android/server/telecom/TelecomServiceImpl.java b/src/com/android/server/telecom/TelecomServiceImpl.java
index b704d33..2932fb7 100644
--- a/src/com/android/server/telecom/TelecomServiceImpl.java
+++ b/src/com/android/server/telecom/TelecomServiceImpl.java
@@ -2027,6 +2027,11 @@
                 pw.increaseIndent();
                 reflectAndPrintFlagConfigs(pw);
                 pw.decreaseIndent();
+
+                pw.println("TransactionManager: ");
+                pw.increaseIndent();
+                TransactionManager.getInstance().dump(pw);
+                pw.decreaseIndent();
             }
             if (isTimeLineView) {
                 Log.dumpEventsTimeline(pw);
diff --git a/src/com/android/server/telecom/TransactionalServiceRepository.java b/src/com/android/server/telecom/TransactionalServiceRepository.java
index 15278e1..793840e 100644
--- a/src/com/android/server/telecom/TransactionalServiceRepository.java
+++ b/src/com/android/server/telecom/TransactionalServiceRepository.java
@@ -61,11 +61,11 @@
         return service;
     }
 
-    public TransactionalServiceWrapper getTransactionalServiceWrapper(PhoneAccountHandle pah) {
+    private TransactionalServiceWrapper getTransactionalServiceWrapper(PhoneAccountHandle pah) {
         return mServiceLookupTable.get(pah);
     }
 
-    public boolean hasExistingServiceWrapper(PhoneAccountHandle pah) {
+    private boolean hasExistingServiceWrapper(PhoneAccountHandle pah) {
         return mServiceLookupTable.containsKey(pah);
     }
 
diff --git a/src/com/android/server/telecom/TransactionalServiceWrapper.java b/src/com/android/server/telecom/TransactionalServiceWrapper.java
index 938ee58..98f2c9c 100644
--- a/src/com/android/server/telecom/TransactionalServiceWrapper.java
+++ b/src/com/android/server/telecom/TransactionalServiceWrapper.java
@@ -130,6 +130,7 @@
         return mTransactionManager;
     }
 
+    @VisibleForTesting
     public PhoneAccountHandle getPhoneAccountHandle() {
         return mPhoneAccountHandle;
     }
@@ -166,7 +167,7 @@
         return callCount;
     }
 
-    public void cleanupTransactionalServiceWrapper() {
+    private void cleanupTransactionalServiceWrapper() {
         for (Call call : mTrackedCalls.values()) {
             mCallsManager.markCallAsDisconnected(call,
                     new DisconnectCause(DisconnectCause.ERROR, "process died"));
@@ -179,7 +180,7 @@
      **                        ICallControl: Client --> Server                                **
      **********************************************************************************************
      */
-    public final ICallControl mICallControl = new ICallControl.Stub() {
+    private final ICallControl mICallControl = new ICallControl.Stub() {
         @Override
         public void setActive(String callId, android.os.ResultReceiver callback)
                 throws RemoteException {
@@ -346,7 +347,7 @@
         }
     };
 
-    public void addTransactionsToManager(VoipCallTransaction transaction,
+    private void addTransactionsToManager(VoipCallTransaction transaction,
             ResultReceiver callback) {
         Log.d(TAG, "addTransactionsToManager");
 
diff --git a/src/com/android/server/telecom/voip/ParallelTransaction.java b/src/com/android/server/telecom/voip/ParallelTransaction.java
index 6176087..621892a 100644
--- a/src/com/android/server/telecom/voip/ParallelTransaction.java
+++ b/src/com/android/server/telecom/voip/ParallelTransaction.java
@@ -34,6 +34,7 @@
 
     @Override
     public void start() {
+        if (mStats != null) mStats.markStarted();
         // post timeout work
         CompletableFuture<Void> future = new CompletableFuture<>();
         mHandler.postDelayed(() -> future.complete(null), TIMEOUT_LIMIT);
@@ -44,7 +45,7 @@
             if (mCompleteListener != null) {
                 mCompleteListener.onTransactionTimeout(mTransactionName);
             }
-            finish();
+            timeout();
             return null;
         }, new LoggedHandlerExecutor(mHandler, mTransactionName + "@" + hashCode()
                 + ".s", mLock));
@@ -68,7 +69,7 @@
                                                                     transactionName));
                                             mCompleteListener.onTransactionCompleted(mainResult,
                                                     mTransactionName);
-                                            finish();
+                                            finish(mainResult);
                                             return null;
                                         }, new LoggedHandlerExecutor(mHandler,
                                                 mTransactionName + "@" + hashCode()
@@ -91,7 +92,7 @@
                                                         transactionName));
                                         mCompleteListener.onTransactionCompleted(mainResult,
                                                 mTransactionName);
-                                        finish();
+                                        finish(mainResult);
                                         return null;
                                     }, new LoggedHandlerExecutor(mHandler,
                                             mTransactionName + "@" + hashCode()
diff --git a/src/com/android/server/telecom/voip/SerialTransaction.java b/src/com/android/server/telecom/voip/SerialTransaction.java
index b35b471..7d5a178 100644
--- a/src/com/android/server/telecom/voip/SerialTransaction.java
+++ b/src/com/android/server/telecom/voip/SerialTransaction.java
@@ -21,6 +21,7 @@
 
 import java.util.List;
 import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.atomic.AtomicInteger;
 
 /**
  * A VoipCallTransaction implementation that its sub transactions will be executed in serial
@@ -37,6 +38,7 @@
 
     @Override
     public void start() {
+        if (mStats != null) mStats.markStarted();
         // post timeout work
         CompletableFuture<Void> future = new CompletableFuture<>();
         mHandler.postDelayed(() -> future.complete(null), TIMEOUT_LIMIT);
@@ -47,7 +49,7 @@
             if (mCompleteListener != null) {
                 mCompleteListener.onTransactionTimeout(mTransactionName);
             }
-            finish();
+            timeout();
             return null;
         }, new LoggedHandlerExecutor(mHandler, mTransactionName + "@" + hashCode()
                 + ".s", mLock));
@@ -55,6 +57,7 @@
         if (mSubTransactions != null && mSubTransactions.size() > 0) {
             TransactionManager.TransactionCompleteListener subTransactionListener =
                     new TransactionManager.TransactionCompleteListener() {
+                        private final AtomicInteger mTransactionIndex = new AtomicInteger(0);
 
                         @Override
                         public void onTransactionCompleted(VoipCallTransactionResult result,
@@ -71,14 +74,16 @@
                                                                     transactionName));
                                             mCompleteListener.onTransactionCompleted(mainResult,
                                                     mTransactionName);
-                                            finish();
+                                            finish(mainResult);
                                             return null;
                                         }, new LoggedHandlerExecutor(mHandler,
                                                 mTransactionName + "@" + hashCode()
                                                         + ".oTC", mLock));
                             } else {
-                                if (mSubTransactions.size() > 0) {
-                                    VoipCallTransaction transaction = mSubTransactions.remove(0);
+                                int currTransactionIndex = mTransactionIndex.incrementAndGet();
+                                if (currTransactionIndex < mSubTransactions.size()) {
+                                    VoipCallTransaction transaction = mSubTransactions.get(
+                                            currTransactionIndex);
                                     transaction.setCompleteListener(this);
                                     transaction.start();
                                 } else {
@@ -99,14 +104,14 @@
                                                         transactionName));
                                         mCompleteListener.onTransactionCompleted(mainResult,
                                                 mTransactionName);
-                                        finish();
+                                        finish(mainResult);
                                         return null;
                                     }, new LoggedHandlerExecutor(mHandler,
                                             mTransactionName + "@" + hashCode()
                                                     + ".oTT", mLock));
                         }
                     };
-            VoipCallTransaction transaction = mSubTransactions.remove(0);
+            VoipCallTransaction transaction = mSubTransactions.get(0);
             transaction.setCompleteListener(subTransactionListener);
             transaction.start();
         } else {
diff --git a/src/com/android/server/telecom/voip/TransactionManager.java b/src/com/android/server/telecom/voip/TransactionManager.java
index 228bdde..76d83cc 100644
--- a/src/com/android/server/telecom/voip/TransactionManager.java
+++ b/src/com/android/server/telecom/voip/TransactionManager.java
@@ -21,20 +21,25 @@
 import android.os.OutcomeReceiver;
 import android.telecom.TelecomManager;
 import android.telecom.CallException;
+import android.util.IndentingPrintWriter;
 import android.util.Log;
 
 import com.android.internal.annotations.VisibleForTesting;
-
+import com.android.server.telecom.flags.Flags;
 import java.util.ArrayDeque;
 import java.util.ArrayList;
+import java.util.Deque;
 import java.util.List;
+import java.util.Locale;
 import java.util.Queue;
 
 public class TransactionManager {
     private static final String TAG = "VoipCallTransactionManager";
+    private static final int TRANSACTION_HISTORY_SIZE = 20;
     private static TransactionManager INSTANCE = null;
     private static final Object sLock = new Object();
-    private Queue<VoipCallTransaction> mTransactions;
+    private final Queue<VoipCallTransaction> mTransactions;
+    private final Deque<VoipCallTransaction> mCompletedTransactions;
     private VoipCallTransaction mCurrentTransaction;
 
     public interface TransactionCompleteListener {
@@ -45,6 +50,10 @@
     private TransactionManager() {
         mTransactions = new ArrayDeque<>();
         mCurrentTransaction = null;
+        if (Flags.enableCallSequencing()) {
+            mCompletedTransactions = new ArrayDeque<>();
+        } else
+            mCompletedTransactions = null;
     }
 
     public static TransactionManager getInstance() {
@@ -69,7 +78,7 @@
         transaction.setCompleteListener(new TransactionCompleteListener() {
             @Override
             public void onTransactionCompleted(VoipCallTransactionResult result,
-                    String transactionName){
+                    String transactionName) {
                 Log.i(TAG, String.format("transaction %s completed: with result=[%d]",
                         transactionName, result.getResult()));
                 if (result.getResult() == TelecomManager.TELECOM_TRANSACTION_SUCCESS) {
@@ -112,7 +121,10 @@
 
     private void finishTransaction() {
         synchronized (sLock) {
-            mCurrentTransaction = null;
+            if (mCurrentTransaction != null) {
+                addTransactionToHistory(mCurrentTransaction);
+                mCurrentTransaction = null;
+            }
         }
         startTransactions();
     }
@@ -123,8 +135,115 @@
         synchronized (sLock) {
             pendingTransactions = new ArrayList<>(mTransactions);
         }
-        for (VoipCallTransaction transaction : pendingTransactions) {
-            transaction.finish();
+        for (VoipCallTransaction t : pendingTransactions) {
+            t.finish(new VoipCallTransactionResult(VoipCallTransactionResult.RESULT_FAILED,
+                    "clear called"));
         }
     }
+
+    private void addTransactionToHistory(VoipCallTransaction t) {
+        if (!Flags.enableCallSequencing()) return;
+
+        mCompletedTransactions.add(t);
+        if (mCompletedTransactions.size() > TRANSACTION_HISTORY_SIZE) {
+            mCompletedTransactions.poll();
+        }
+    }
+
+    /**
+     * Called when the dumpsys is created for telecom to capture the current state.
+     */
+    public void dump(IndentingPrintWriter pw) {
+        if (!Flags.enableCallSequencing()) {
+            pw.println("<<Flag not enabled>>");
+            return;
+        }
+        synchronized (sLock) {
+            pw.println("Pending Transactions:");
+            pw.increaseIndent();
+            for (VoipCallTransaction t : mTransactions) {
+                printPendingTransactionStats(t, pw);
+            }
+            pw.decreaseIndent();
+
+            pw.println("Ongoing Transaction:");
+            pw.increaseIndent();
+            if (mCurrentTransaction != null) {
+                printPendingTransactionStats(mCurrentTransaction, pw);
+            }
+            pw.decreaseIndent();
+
+            pw.println("Completed Transactions:");
+            pw.increaseIndent();
+            for (VoipCallTransaction t : mCompletedTransactions) {
+                printCompleteTransactionStats(t, pw);
+            }
+            pw.decreaseIndent();
+        }
+    }
+
+    /**
+     * Recursively print the pending {@link VoipCallTransaction} stats for logging purposes.
+     * @param t The transaction that stats should be printed for
+     * @param pw The IndentingPrintWriter to print the result to
+     */
+    private void printPendingTransactionStats(VoipCallTransaction t, IndentingPrintWriter pw) {
+        VoipCallTransaction.Stats s = t.getStats();
+        if (s == null) {
+            pw.println(String.format(Locale.getDefault(), "%s: <NO STATS>", t.mTransactionName));
+            return;
+        }
+        pw.println(String.format(Locale.getDefault(),
+                "[%s] %s: (result=[%s]), (created -> now : [%+d] mS),"
+                        + " (created -> started : [%+d] mS),"
+                        + " (started -> now : [%+d] mS)",
+                s.addedTimeStamp, t.mTransactionName, parseTransactionResult(s),
+                s.measureTimeSinceCreatedMs(), s.measureCreatedToStartedMs(),
+                s.measureTimeSinceStartedMs()));
+
+        if (t.mSubTransactions == null || t.mSubTransactions.isEmpty()) {
+            return;
+        }
+        pw.increaseIndent();
+        for (VoipCallTransaction subTransaction : t.mSubTransactions) {
+            printPendingTransactionStats(subTransaction, pw);
+        }
+        pw.decreaseIndent();
+    }
+
+    /**
+     * Recursively print the complete Transaction stats for logging purposes.
+     * @param t The transaction that stats should be printed for
+     * @param pw The IndentingPrintWriter to print the result to
+     */
+    private void printCompleteTransactionStats(VoipCallTransaction t, IndentingPrintWriter pw) {
+        VoipCallTransaction.Stats s = t.getStats();
+        if (s == null) {
+            pw.println(String.format(Locale.getDefault(), "%s: <NO STATS>", t.mTransactionName));
+            return;
+        }
+        pw.println(String.format(Locale.getDefault(),
+                "[%s] %s: (result=[%s]), (created -> started : [%+d] mS), "
+                        + "(started -> completed : [%+d] mS)",
+                s.addedTimeStamp, t.mTransactionName, parseTransactionResult(s),
+                s.measureCreatedToStartedMs(), s.measureStartedToCompletedMs()));
+
+        if (t.mSubTransactions == null || t.mSubTransactions.isEmpty()) {
+            return;
+        }
+        pw.increaseIndent();
+        for (VoipCallTransaction subTransaction : t.mSubTransactions) {
+            printCompleteTransactionStats(subTransaction, pw);
+        }
+        pw.decreaseIndent();
+    }
+
+    private String parseTransactionResult(VoipCallTransaction.Stats s) {
+        if (s.isTimedOut()) return "TIMED OUT";
+        if (s.getTransactionResult() == null) return "PENDING";
+        if (s.getTransactionResult().getResult() == VoipCallTransactionResult.RESULT_SUCCEED) {
+            return "SUCCESS";
+        }
+        return s.getTransactionResult().toString();
+    }
 }
diff --git a/src/com/android/server/telecom/voip/VoipCallTransaction.java b/src/com/android/server/telecom/voip/VoipCallTransaction.java
index a952eb1..3c91158 100644
--- a/src/com/android/server/telecom/voip/VoipCallTransaction.java
+++ b/src/com/android/server/telecom/voip/VoipCallTransaction.java
@@ -22,23 +22,119 @@
 
 import com.android.server.telecom.LoggedHandlerExecutor;
 import com.android.server.telecom.TelecomSystem;
+import com.android.server.telecom.flags.Flags;
 
+import java.time.LocalDateTime;
 import java.util.List;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.CompletionStage;
+import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.function.Function;
 
 public class VoipCallTransaction {
     //TODO: add log events
     protected static final long TIMEOUT_LIMIT = 5000L;
+
+    /**
+     * Tracks stats about a transaction for logging purposes.
+     */
+    public static class Stats {
+        // the logging visible timestamp for ease of debugging
+        public final LocalDateTime addedTimeStamp;
+        // the time in nS that the transaction was first created
+        private final long mCreatedTimeNs;
+        // the time that the transaction was started.
+        private long mStartedTimeNs = -1L;
+        // the time that the transaction was finished.
+        private long mFinishedTimeNs = -1L;
+        // If finished, did this transaction finish because it timed out?
+        private boolean mIsTimedOut = false;
+        private VoipCallTransactionResult  mTransactionResult = null;
+
+        public Stats() {
+            addedTimeStamp = LocalDateTime.now();
+            mCreatedTimeNs = System.nanoTime();
+        }
+
+        /**
+         * Mark the transaction as started and record the time.
+         */
+        public void markStarted() {
+            if (mStartedTimeNs > -1) return;
+            mStartedTimeNs = System.nanoTime();
+        }
+
+        /**
+         * Mark the transaction as completed and record the time.
+         */
+        public void markComplete(boolean isTimedOut, VoipCallTransactionResult result) {
+            if (mFinishedTimeNs > -1) return;
+            mFinishedTimeNs = System.nanoTime();
+            mIsTimedOut = isTimedOut;
+            mTransactionResult = result;
+        }
+
+        /**
+         * @return Time in mS since the transaction was created.
+         */
+        public long measureTimeSinceCreatedMs() {
+            return TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - mCreatedTimeNs);
+        }
+
+        /**
+         * @return Time in mS between when transaction was created and when it was marked as
+         * started. Returns -1 if the transaction was not started yet.
+         */
+        public long measureCreatedToStartedMs() {
+            return mStartedTimeNs > 0 ?
+                    TimeUnit.NANOSECONDS.toMillis(mStartedTimeNs - mCreatedTimeNs) : -1;
+        }
+
+        /**
+         * @return Time in mS since the transaction was marked started to the TransactionManager.
+         * Returns -1 if the transaction hasn't been started yet.
+         */
+        public long measureTimeSinceStartedMs() {
+            return mStartedTimeNs > 0 ?
+                    TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - mStartedTimeNs) : -1;
+        }
+
+        /**
+         * @return Time in mS between when the transaction was marked as started and when it was
+         * marked as completed. Returns -1 if the transaction hasn't started or finished yet.
+         */
+        public long measureStartedToCompletedMs() {
+            return (mStartedTimeNs > 0 && mFinishedTimeNs > 0) ?
+                    TimeUnit.NANOSECONDS.toMillis(mFinishedTimeNs - mStartedTimeNs) : -1;
+
+        }
+
+        /**
+         * @return true if this transaction completed due to timing out, false if the transaction
+         * hasn't completed yet or it completed and did not time out.
+         */
+        public boolean isTimedOut() {
+            return mIsTimedOut;
+        }
+
+        /**
+         * @return the result if the transaction completed, null if it timed out or hasn't completed
+         * yet.
+         */
+        public VoipCallTransactionResult getTransactionResult() {
+            return mTransactionResult;
+        }
+    }
+
     protected final AtomicBoolean mCompleted = new AtomicBoolean(false);
-    protected String mTransactionName = this.getClass().getSimpleName();
+    protected final String mTransactionName = this.getClass().getSimpleName();
     private HandlerThread mHandlerThread;
     protected Handler mHandler;
     protected TransactionManager.TransactionCompleteListener mCompleteListener;
     protected List<VoipCallTransaction> mSubTransactions;
     protected TelecomSystem.SyncRoot mLock;
+    protected final Stats mStats;
 
     public VoipCallTransaction(
             List<VoipCallTransaction> subTransactions, TelecomSystem.SyncRoot lock) {
@@ -47,6 +143,7 @@
         mHandlerThread.start();
         mHandler = new Handler(mHandlerThread.getLooper());
         mLock = lock;
+        mStats = Flags.enableCallSequencing() ? new Stats() : null;
     }
 
     public VoipCallTransaction(TelecomSystem.SyncRoot lock) {
@@ -54,6 +151,7 @@
     }
 
     public void start() {
+        if (mStats != null) mStats.markStarted();
         // post timeout work
         CompletableFuture<Void> future = new CompletableFuture<>();
         mHandler.postDelayed(() -> future.complete(null), TIMEOUT_LIMIT);
@@ -64,7 +162,7 @@
             if (mCompleteListener != null) {
                 mCompleteListener.onTransactionTimeout(mTransactionName);
             }
-            finish();
+            timeout();
             return null;
         }, new LoggedHandlerExecutor(mHandler, mTransactionName + "@" + hashCode()
                 + ".s", mLock));
@@ -82,7 +180,7 @@
                     if (mCompleteListener != null) {
                         mCompleteListener.onTransactionCompleted(result, mTransactionName);
                     }
-                    finish();
+                    finish(result);
                     return null;
                     }, executor)
                 .exceptionallyAsync((throwable -> {
@@ -100,11 +198,27 @@
         mCompleteListener = listener;
     }
 
-    public void finish() {
+    public void timeout() {
+        finish(true, null);
+    }
+
+    public void finish(VoipCallTransactionResult result) {
+        finish(false, result);
+    }
+
+    public void finish(boolean isTimedOut, VoipCallTransactionResult result) {
+        if (mStats != null) mStats.markComplete(isTimedOut, result);
         // finish all sub transactions
-        if (mSubTransactions != null && mSubTransactions.size() > 0) {
-            mSubTransactions.forEach(VoipCallTransaction::finish);
+        if (mSubTransactions != null && !mSubTransactions.isEmpty()) {
+            mSubTransactions.forEach( t -> t.finish(isTimedOut, result));
         }
         mHandlerThread.quit();
     }
+
+    /**
+     * @return Stats related to this transaction if stats are enabled, null otherwise.
+     */
+    public Stats getStats() {
+        return mStats;
+    }
 }
diff --git a/tests/src/com/android/server/telecom/tests/CreateConnectionProcessorTest.java b/tests/src/com/android/server/telecom/tests/CreateConnectionProcessorTest.java
index e973992..2f27bb5 100644
--- a/tests/src/com/android/server/telecom/tests/CreateConnectionProcessorTest.java
+++ b/tests/src/com/android/server/telecom/tests/CreateConnectionProcessorTest.java
@@ -838,9 +838,10 @@
 
     private ConnectionServiceWrapper makeConnectionServiceWrapper() {
         ConnectionServiceWrapper wrapper = mock(ConnectionServiceWrapper.class);
+
         when(mMockConnectionServiceRepository.getService(
-                eq(makeQuickConnectionServiceComponentName()),
-                any(UserHandle.class), any(FeatureFlags.class))).thenReturn(wrapper);
+                eq(makeQuickConnectionServiceComponentName()), any(UserHandle.class)))
+                .thenReturn(wrapper);
         return wrapper;
     }