Merge "Formalize Telecom BlockedNumbers APIs into manager." into main
diff --git a/flags/telecom_anomaly_report_flags.aconfig b/flags/telecom_anomaly_report_flags.aconfig
index 6879d86..296b300 100644
--- a/flags/telecom_anomaly_report_flags.aconfig
+++ b/flags/telecom_anomaly_report_flags.aconfig
@@ -1,6 +1,7 @@
 package: "com.android.server.telecom.flags"
 container: "system"
 
+# OWNER=tjstuart TARGET=24Q3
 flag {
   name: "gen_anom_report_on_focus_timeout"
   namespace: "telecom"
diff --git a/flags/telecom_api_flags.aconfig b/flags/telecom_api_flags.aconfig
index c0f4cba..75efdfa 100644
--- a/flags/telecom_api_flags.aconfig
+++ b/flags/telecom_api_flags.aconfig
@@ -1,58 +1,74 @@
 package: "com.android.server.telecom.flags"
 container: "system"
 
+# OWNER=grantmenke TARGET=24Q3
 flag {
   name: "voip_app_actions_support"
+  is_exported: true
   namespace: "telecom"
   description: "When set, Telecom support for additional VOIP application actions is active."
   bug: "296934278"
 }
 
+# OWNER=grantmenke TARGET=24Q3
 flag {
   name: "call_details_id_changes"
+  is_exported: true
   namespace: "telecom"
   description: "When set, call details/extras id updates to Telecom APIs for Android V are active."
   bug: "301713560"
 }
 
-flag{
+# OWNER=kunduz TARGET=24Q2
+flag {
   name: "add_call_uri_for_missed_calls"
+  is_exported: true
   namespace: "telecom"
   description: "The key is used for dialer apps to mark missed calls as read when it gets the notification on reboot."
   bug: "292597423"
 }
 
-flag{
+# OWNER=tjstuart TARGET=24Q3
+flag {
   name: "set_mute_state"
+  is_exported: true
   namespace: "telecom"
   description: "transactional calls need the ability to mute the call audio input"
   bug: "310669304"
 }
 
-flag{
+# OWNER=tjstuart TARGET=24Q3
+flag {
   name: "get_registered_phone_accounts"
+  is_exported: true
   namespace: "telecom"
   description: "When set, self-managed clients can get their own phone accounts"
   bug: "317132586"
 }
 
-flag{
+# OWNER=tjstuart TARGET=24Q3
+flag {
   name: "transactional_video_state"
+  is_exported: true
   namespace: "telecom"
   description: "when set, clients using transactional implementations will be able to set & get the video state"
   bug: "311265260"
 }
 
-flag{
+# OWNER=tjstuart TARGET=24Q3
+flag {
   name: "business_call_composer"
+  is_exported: true
   namespace: "telecom"
   description: "Enables enriched calling features (e.g. Business name will show for a call)"
   bug: "311688497"
   is_exported: true
 }
 
-flag{
+# OWNER=tgunn TARGET=25Q3
+flag {
   name: "get_last_known_cell_identity"
+  is_exported: true
   namespace: "telecom"
   description: "Formalizes the getLastKnownCellIdentity API that Telecom reliees on as a system api"
   bug: "327454165"
diff --git a/flags/telecom_bluetoothroutemanager_flags.aconfig b/flags/telecom_bluetoothroutemanager_flags.aconfig
index 1df1e9b..dc69dd5 100644
--- a/flags/telecom_bluetoothroutemanager_flags.aconfig
+++ b/flags/telecom_bluetoothroutemanager_flags.aconfig
@@ -1,10 +1,10 @@
 package: "com.android.server.telecom.flags"
 container: "system"
 
+# OWNER=tgunn TARGET=24Q3
 flag {
   name: "use_actual_address_to_enter_connecting_state"
   namespace: "telecom"
   description: "Fix bugs that may add bluetooth device with null address."
   bug: "306113816"
 }
-
diff --git a/flags/telecom_broadcast_flags.aconfig b/flags/telecom_broadcast_flags.aconfig
index de8dd27..8314376 100644
--- a/flags/telecom_broadcast_flags.aconfig
+++ b/flags/telecom_broadcast_flags.aconfig
@@ -1,6 +1,7 @@
 package: "com.android.server.telecom.flags"
 container: "system"
 
+# OWNER=tgunn TARGET=24Q3
 flag {
   name: "is_new_outgoing_call_broadcast_unblocking"
   namespace: "telecom"
diff --git a/flags/telecom_call_filtering_flags.aconfig b/flags/telecom_call_filtering_flags.aconfig
index 72f9db3..d80cfa3 100644
--- a/flags/telecom_call_filtering_flags.aconfig
+++ b/flags/telecom_call_filtering_flags.aconfig
@@ -1,6 +1,7 @@
 package: "com.android.server.telecom.flags"
 container: "system"
 
+# OWNER=qingzhong TARGET=24Q2
 flag {
   name: "skip_filter_phone_account_perform_dnd_filter"
   namespace: "telecom"
diff --git a/flags/telecom_call_flags.aconfig b/flags/telecom_call_flags.aconfig
index 27a4b22..5cb9dbd 100644
--- a/flags/telecom_call_flags.aconfig
+++ b/flags/telecom_call_flags.aconfig
@@ -1,6 +1,7 @@
 package: "com.android.server.telecom.flags"
 container: "system"
 
+# OWNER=tjstuart TARGET=24Q3
 flag {
   name: "transactional_cs_verifier"
   namespace: "telecom"
diff --git a/flags/telecom_callaudiomodestatemachine_flags.aconfig b/flags/telecom_callaudiomodestatemachine_flags.aconfig
index 1d81535..63761ec 100644
--- a/flags/telecom_callaudiomodestatemachine_flags.aconfig
+++ b/flags/telecom_callaudiomodestatemachine_flags.aconfig
@@ -1,6 +1,7 @@
 package: "com.android.server.telecom.flags"
 container: "system"
 
+# OWNER=pmadapurmath TARGET=24Q3
 flag {
   name: "set_audio_mode_before_abandon_focus"
   namespace: "telecom"
diff --git a/flags/telecom_callaudioroutestatemachine_flags.aconfig b/flags/telecom_callaudioroutestatemachine_flags.aconfig
index f5da045..1608869 100644
--- a/flags/telecom_callaudioroutestatemachine_flags.aconfig
+++ b/flags/telecom_callaudioroutestatemachine_flags.aconfig
@@ -1,6 +1,7 @@
 package: "com.android.server.telecom.flags"
 container: "system"
 
+# OWNER=kunduz TARGET=24Q2
 flag {
   name: "available_routes_never_updated_after_set_system_audio_state"
   namespace: "telecom"
@@ -8,6 +9,7 @@
   bug: "292599751"
 }
 
+# OWNER=pmadapurmath TARGET=24Q3
 flag {
   name: "use_refactored_audio_route_switching"
   namespace: "telecom"
@@ -15,6 +17,7 @@
   bug: "306395598"
 }
 
+# OWNER=tgunn TARGET=24Q3
 flag {
   name: "ensure_audio_mode_updates_on_foreground_call_change"
   namespace: "telecom"
@@ -22,6 +25,7 @@
   bug: "289861657"
 }
 
+# OWNER=pmadapurmath TARGET=24Q1
 flag {
   name: "ignore_auto_route_to_watch_device"
   namespace: "telecom"
@@ -29,6 +33,7 @@
   bug: "294378768"
 }
 
+# OWNER=pmadapurmath TARGET=24Q3
 flag {
   name: "transit_route_before_audio_disconnect_bt"
   namespace: "telecom"
@@ -36,6 +41,7 @@
   bug: "306113816"
 }
 
+# OWNER=pmadapurmath TARGET=24Q3
 flag {
   name: "call_audio_communication_device_refactor"
   namespace: "telecom"
@@ -43,6 +49,7 @@
   bug: "308968392"
 }
 
+# OWNER=pmadapurmath TARGET=24Q3
 flag {
   name: "communication_device_protected_by_lock"
   namespace: "telecom"
@@ -50,6 +57,7 @@
   bug: "303001133"
 }
 
+# OWNER=pmadapurmath TARGET=24Q3
 flag {
   name: "reset_mute_when_entering_quiescent_bt_route"
   namespace: "telecom"
@@ -57,6 +65,7 @@
   bug: "311313250"
 }
 
+# OWNER=pmadapurmath TARGET=24Q3
 flag {
   name: "update_route_mask_when_bt_connected"
   namespace: "telecom"
@@ -64,6 +73,7 @@
   bug: "301695370"
 }
 
+# OWNER=pmadapurmath TARGET=24Q3
 flag {
   name: "clear_communication_device_after_audio_ops_complete"
   namespace: "telecom"
diff --git a/flags/telecom_calllog_flags.aconfig b/flags/telecom_calllog_flags.aconfig
index 593b7e5..c0eebf1 100644
--- a/flags/telecom_calllog_flags.aconfig
+++ b/flags/telecom_calllog_flags.aconfig
@@ -1,6 +1,7 @@
 package: "com.android.server.telecom.flags"
 container: "system"
 
+# OWNER=qingzhong TARGET=24Q2
 flag {
   name: "telecom_log_external_wearable_calls"
   namespace: "telecom"
@@ -8,6 +9,7 @@
   bug: "292600751"
 }
 
+# OWNER=ranamouawi TARGET=24Q2
 flag {
   name: "telecom_skip_log_based_on_extra"
   namespace: "telecom"
diff --git a/flags/telecom_calls_manager_flags.aconfig b/flags/telecom_calls_manager_flags.aconfig
index f329ca6..28e9dd8 100644
--- a/flags/telecom_calls_manager_flags.aconfig
+++ b/flags/telecom_calls_manager_flags.aconfig
@@ -1,6 +1,7 @@
 package: "com.android.server.telecom.flags"
 container: "system"
 
+# OWNER=pmadapurmath TARGET=24Q3
 flag {
   name: "use_improved_listener_order"
   namespace: "telecom"
@@ -8,6 +9,7 @@
   bug: "24244713"
 }
 
+# OWNER=tjstuart TARGET=24Q3
 flag {
   name: "fix_audio_flicker_for_outgoing_calls"
   namespace: "telecom"
@@ -15,6 +17,7 @@
   bug: "309540769"
 }
 
+# OWNER=breadley TARGET=24Q3
 flag {
   name: "enable_call_sequencing"
   namespace: "telecom"
diff --git a/flags/telecom_connection_service_wrapper_flags.aconfig b/flags/telecom_connection_service_wrapper_flags.aconfig
index 80a8dfe..38e5e13 100644
--- a/flags/telecom_connection_service_wrapper_flags.aconfig
+++ b/flags/telecom_connection_service_wrapper_flags.aconfig
@@ -1,6 +1,7 @@
 package: "com.android.server.telecom.flags"
 container: "system"
 
+# OWNER=grantmenke TARGET=24Q2
 flag {
   name: "updated_rcs_call_count_tracking"
   namespace: "telecom"
diff --git a/flags/telecom_default_phone_account_flags.aconfig b/flags/telecom_default_phone_account_flags.aconfig
index e6badde..161b674 100644
--- a/flags/telecom_default_phone_account_flags.aconfig
+++ b/flags/telecom_default_phone_account_flags.aconfig
@@ -1,6 +1,7 @@
 package: "com.android.server.telecom.flags"
 container: "system"
 
+# OWNER=tjstuart TARGET=24Q3
 flag {
   name: "only_update_telephony_on_valid_sub_ids"
   namespace: "telecom"
@@ -8,6 +9,7 @@
   bug: "234846282"
 }
 
+# OWNER=tjstuart TARGET=24Q3
 flag {
   name: "telephony_has_default_but_telecom_does_not"
   namespace: "telecom"
diff --git a/flags/telecom_incallservice_flags.aconfig b/flags/telecom_incallservice_flags.aconfig
index 08a82ba..ea842ac 100644
--- a/flags/telecom_incallservice_flags.aconfig
+++ b/flags/telecom_incallservice_flags.aconfig
@@ -1,6 +1,7 @@
 package: "com.android.server.telecom.flags"
 container: "system"
 
+# OWNER=qingzhong TARGET=24Q2
 flag {
   name: "early_binding_to_incall_service"
   namespace: "telecom"
@@ -8,6 +9,7 @@
   bug: "282113261"
 }
 
+# OWNER=pmadapurmath TARGET=24Q2
 flag {
   name: "ecc_keyguard"
   namespace: "telecom"
@@ -15,6 +17,7 @@
   bug: "306582821"
 }
 
+# OWNER=pmadapurmath TARGET=24Q3
 flag {
   name: "separately_bind_to_bt_incall_service"
   namespace: "telecom"
diff --git a/flags/telecom_profile_user_flags.aconfig b/flags/telecom_profile_user_flags.aconfig
index c046de8..feee07d 100644
--- a/flags/telecom_profile_user_flags.aconfig
+++ b/flags/telecom_profile_user_flags.aconfig
@@ -1,6 +1,7 @@
 package: "com.android.server.telecom.flags"
 container: "system"
 
+# OWNER=huiwang TARGET=24Q3
 flag {
   name: "profile_user_support"
   namespace: "telecom"
diff --git a/flags/telecom_remote_connection_service.aconfig b/flags/telecom_remote_connection_service.aconfig
index 55c7536..a30f0b2 100644
--- a/flags/telecom_remote_connection_service.aconfig
+++ b/flags/telecom_remote_connection_service.aconfig
@@ -1,5 +1,7 @@
 package: "com.android.server.telecom.flags"
+container: "system"
 
+# OWNER=pmadapurmath TARGET=24Q3
 flag {
   name: "set_remote_connection_call_id"
   namespace: "telecom"
diff --git a/flags/telecom_resolve_hidden_dependencies.aconfig b/flags/telecom_resolve_hidden_dependencies.aconfig
index c9b6612..a120b85 100644
--- a/flags/telecom_resolve_hidden_dependencies.aconfig
+++ b/flags/telecom_resolve_hidden_dependencies.aconfig
@@ -1,8 +1,10 @@
 package: "com.android.server.telecom.flags"
 container: "system"
 
+# OWNER=tgunn TARGET=24Q3
 flag {
     name: "telecom_resolve_hidden_dependencies"
+    is_exported: true
     namespace: "telecom"
     description: "Mainland cleanup for hidden dependencies"
     bug: "323414215"
diff --git a/flags/telecom_ringer_flag_declarations.aconfig b/flags/telecom_ringer_flag_declarations.aconfig
index 13577bb..f126bf3 100644
--- a/flags/telecom_ringer_flag_declarations.aconfig
+++ b/flags/telecom_ringer_flag_declarations.aconfig
@@ -1,6 +1,7 @@
 package: "com.android.server.telecom.flags"
 container: "system"
 
+# OWNER=yeabkal TARGET=24Q2
 flag {
   name: "use_device_provided_serialized_ringer_vibration"
   namespace: "telecom"
diff --git a/flags/telecom_work_profile_flags.aconfig b/flags/telecom_work_profile_flags.aconfig
index 854568b..1891423 100644
--- a/flags/telecom_work_profile_flags.aconfig
+++ b/flags/telecom_work_profile_flags.aconfig
@@ -1,6 +1,7 @@
 package: "com.android.server.telecom.flags"
 container: "system"
 
+# OWNER=pmadapurmath TARGET=24Q3
 flag {
   name: "associated_user_refactor_for_work_profile"
   namespace: "telecom"
diff --git a/res/values-cs/strings.xml b/res/values-cs/strings.xml
index ab74d61..25534e4 100644
--- a/res/values-cs/strings.xml
+++ b/res/values-cs/strings.xml
@@ -63,7 +63,7 @@
     <string name="change_default_call_screening_dialog_affirmative" msgid="7162433828280058647">"Nastavit jako výchozí"</string>
     <string name="change_default_call_screening_dialog_negative" msgid="1839266125623106342">"Zrušit"</string>
     <string name="blocked_numbers" msgid="8322134197039865180">"Blokovaná čísla"</string>
-    <string name="blocked_numbers_msg" msgid="2797422132329662697">"Ze zablokovaných čísel už nebudete přijímat hovory ani zprávy SMS."</string>
+    <string name="blocked_numbers_msg" msgid="2797422132329662697">"Od zablokovaných čísel už nebudete přijímat hovory ani zprávy SMS."</string>
     <string name="block_number" msgid="3784343046852802722">"Přidat číslo"</string>
     <string name="unblock_dialog_body" msgid="2723393535797217261">"Odblokovat číslo <xliff:g id="NUMBER_TO_BLOCK">%1$s</xliff:g>?"</string>
     <string name="unblock_button" msgid="8732021675729981781">"Odblokovat"</string>
@@ -113,7 +113,7 @@
     <string name="phone_settings_private_num_summary_txt" msgid="6755758240544021037">"Blokovat volající, kteří skrývají své číslo"</string>
     <string name="phone_settings_payphone_txt" msgid="5003987966052543965">"Z veřejných telefonů"</string>
     <string name="phone_settings_payphone_summary_txt" msgid="3936631076065563665">"Blokovat hovory z veřejných telefonů"</string>
-    <string name="phone_settings_unknown_txt" msgid="3577926178354772728">"Z nerozpoznaných čísel"</string>
+    <string name="phone_settings_unknown_txt" msgid="3577926178354772728">"Nerozpoznaná čísla"</string>
     <string name="phone_settings_unknown_summary_txt" msgid="5446657192535779645">"Blokovat hovory od nerozpoznaných volajících"</string>
     <string name="phone_settings_unavailable_txt" msgid="825918186053980858">"Neznámé"</string>
     <string name="phone_settings_unavailable_summary_txt" msgid="8221686031038282633">"Blokovat hovory z neznámých čísel"</string>
diff --git a/src/com/android/server/telecom/Call.java b/src/com/android/server/telecom/Call.java
index ed72a3f..cdf7cd9 100644
--- a/src/com/android/server/telecom/Call.java
+++ b/src/com/android/server/telecom/Call.java
@@ -3053,16 +3053,24 @@
     public void awaitCallStateChangeAndMaybeDisconnectCall(int targetCallState,
             boolean shouldDisconnectUponTimeout, String callingMethod) {
         TransactionManager tm = TransactionManager.getInstance();
-        tm.addTransaction(new VerifyCallStateChangeTransaction(mCallsManager,
-                this, targetCallState, shouldDisconnectUponTimeout), new OutcomeReceiver<>() {
+        tm.addTransaction(new VerifyCallStateChangeTransaction(mCallsManager.getLock(),
+                this, targetCallState), new OutcomeReceiver<>() {
             @Override
             public void onResult(VoipCallTransactionResult result) {
+                Log.i(this, "awaitCallStateChangeAndMaybeDisconnectCall: %s: onResult:"
+                        + " due to CallException=[%s]", callingMethod, result);
             }
 
             @Override
             public void onError(CallException e) {
                 Log.i(this, "awaitCallStateChangeAndMaybeDisconnectCall: %s: onError"
                         + " due to CallException=[%s]", callingMethod, e);
+                if (shouldDisconnectUponTimeout) {
+                    mCallsManager.markCallAsDisconnected(Call.this,
+                            new DisconnectCause(DisconnectCause.ERROR,
+                                    "did not hold in timeout window"));
+                    mCallsManager.markCallAsRemoved(Call.this);
+                }
             }
         });
     }
diff --git a/src/com/android/server/telecom/InCallController.java b/src/com/android/server/telecom/InCallController.java
index fb2ca3e..f350d22 100644
--- a/src/com/android/server/telecom/InCallController.java
+++ b/src/com/android/server/telecom/InCallController.java
@@ -2428,7 +2428,9 @@
         }
 
         String bluetoothPackage = mDefaultDialerCache.getBTInCallServicePackage();
-        if (serviceInfo.packageName != null && serviceInfo.packageName.equals(bluetoothPackage)
+        if (mFeatureFlags.separatelyBindToBtIncallService()
+                && serviceInfo.packageName != null
+                && serviceInfo.packageName.equals(bluetoothPackage)
                 && (hasControlInCallPermission || hasAppOpsPermittedManageOngoingCalls)) {
             return IN_CALL_SERVICE_TYPE_BLUETOOTH;
         }
diff --git a/src/com/android/server/telecom/TelecomServiceImpl.java b/src/com/android/server/telecom/TelecomServiceImpl.java
index 774a2bf..fe7c0ae 100644
--- a/src/com/android/server/telecom/TelecomServiceImpl.java
+++ b/src/com/android/server/telecom/TelecomServiceImpl.java
@@ -96,8 +96,10 @@
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
 import java.lang.reflect.Method;
+import java.util.Arrays;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Locale;
 import java.util.Objects;
 import java.util.Set;
 import java.util.UUID;
@@ -2119,12 +2121,24 @@
                 // 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();
+                int maxLength = Arrays.stream(methods)
+                        .map(Method::getName)
+                        .map(String::length)
+                        .max(Integer::compare)
+                        .get();
+                String format = "\t%s: %-" + maxLength + "s %s";
+
                 if (methods.length == 0) {
                     pw.println("NONE");
                     return;
                 }
+
                 for (Method m : methods) {
-                    pw.println(m.getName() + "-> " + m.invoke(mFeatureFlags));
+                    String flagEnabled = (Boolean) m.invoke(mFeatureFlags) ? "[✅]": "[❌]";
+                    String methodName = m.getName();
+                    String camelCaseName = methodName.replaceAll("([a-z])([A-Z]+)", "$1_$2")
+                            .toLowerCase(Locale.US);
+                    pw.println(String.format(format, flagEnabled, methodName, camelCaseName));
                 }
             } catch (Exception e) {
                 pw.println("[ERROR]");
diff --git a/src/com/android/server/telecom/voip/CallEventCallbackAckTransaction.java b/src/com/android/server/telecom/voip/CallEventCallbackAckTransaction.java
index 93d9836..9e140a7 100644
--- a/src/com/android/server/telecom/voip/CallEventCallbackAckTransaction.java
+++ b/src/com/android/server/telecom/voip/CallEventCallbackAckTransaction.java
@@ -125,7 +125,7 @@
 
         try {
             // wait for the client to ack that CallEventCallback
-            boolean success = latch.await(VoipCallTransaction.TIMEOUT_LIMIT, TimeUnit.MILLISECONDS);
+            boolean success = latch.await(mTransactionTimeoutMs, TimeUnit.MILLISECONDS);
             if (!success) {
                 // client send onError and failed to complete transaction
                 Log.i(TAG, String.format("CallEventCallbackAckTransaction:"
diff --git a/src/com/android/server/telecom/voip/ParallelTransaction.java b/src/com/android/server/telecom/voip/ParallelTransaction.java
index 621892a..79a940b 100644
--- a/src/com/android/server/telecom/voip/ParallelTransaction.java
+++ b/src/com/android/server/telecom/voip/ParallelTransaction.java
@@ -33,78 +33,62 @@
     }
 
     @Override
-    public void start() {
-        if (mStats != null) mStats.markStarted();
-        // post timeout work
-        CompletableFuture<Void> future = new CompletableFuture<>();
-        mHandler.postDelayed(() -> future.complete(null), TIMEOUT_LIMIT);
-        future.thenApplyAsync((x) -> {
-            if (mCompleted.getAndSet(true)) {
-                return null;
-            }
-            if (mCompleteListener != null) {
-                mCompleteListener.onTransactionTimeout(mTransactionName);
-            }
-            timeout();
-            return null;
-        }, new LoggedHandlerExecutor(mHandler, mTransactionName + "@" + hashCode()
-                + ".s", mLock));
+    public void processTransactions() {
+        if (mSubTransactions == null || mSubTransactions.isEmpty()) {
+            scheduleTransaction();
+            return;
+        }
+        TransactionManager.TransactionCompleteListener subTransactionListener =
+                new TransactionManager.TransactionCompleteListener() {
+                    private final AtomicInteger mCount = new AtomicInteger(mSubTransactions.size());
 
-        if (mSubTransactions != null && mSubTransactions.size() > 0) {
-            TransactionManager.TransactionCompleteListener subTransactionListener =
-                    new TransactionManager.TransactionCompleteListener() {
-                        private final AtomicInteger mCount = new AtomicInteger(mSubTransactions.size());
-
-                        @Override
-                        public void onTransactionCompleted(VoipCallTransactionResult result,
-                                String transactionName) {
-                            if (result.getResult() != VoipCallTransactionResult.RESULT_SUCCEED) {
-                                CompletableFuture.completedFuture(null).thenApplyAsync(
-                                        (x) -> {
-                                            VoipCallTransactionResult mainResult =
-                                                    new VoipCallTransactionResult(
-                                                            VoipCallTransactionResult.RESULT_FAILED,
-                                                            String.format(
-                                                                    "sub transaction %s failed",
-                                                                    transactionName));
-                                            mCompleteListener.onTransactionCompleted(mainResult,
-                                                    mTransactionName);
-                                            finish(mainResult);
-                                            return null;
-                                        }, new LoggedHandlerExecutor(mHandler,
-                                                mTransactionName + "@" + hashCode()
-                                                        + ".oTC", mLock));
-                            } else {
-                                if (mCount.decrementAndGet() == 0) {
-                                    scheduleTransaction();
-                                }
-                            }
-                        }
-
-                        @Override
-                        public void onTransactionTimeout(String transactionName) {
+                    @Override
+                    public void onTransactionCompleted(VoipCallTransactionResult result,
+                            String transactionName) {
+                        if (result.getResult() != VoipCallTransactionResult.RESULT_SUCCEED) {
                             CompletableFuture.completedFuture(null).thenApplyAsync(
                                     (x) -> {
                                         VoipCallTransactionResult mainResult =
                                                 new VoipCallTransactionResult(
-                                                VoipCallTransactionResult.RESULT_FAILED,
-                                                String.format("sub transaction %s timed out",
-                                                        transactionName));
+                                                        VoipCallTransactionResult.RESULT_FAILED,
+                                                        String.format(
+                                                                "sub transaction %s failed",
+                                                                transactionName));
+                                        finish(mainResult);
                                         mCompleteListener.onTransactionCompleted(mainResult,
                                                 mTransactionName);
-                                        finish(mainResult);
                                         return null;
                                     }, new LoggedHandlerExecutor(mHandler,
                                             mTransactionName + "@" + hashCode()
-                                                    + ".oTT", mLock));
+                                                    + ".oTC", mLock));
+                        } else {
+                            if (mCount.decrementAndGet() == 0) {
+                                scheduleTransaction();
+                            }
                         }
-                    };
-            for (VoipCallTransaction transaction : mSubTransactions) {
-                transaction.setCompleteListener(subTransactionListener);
-                transaction.start();
-            }
-        } else {
-            scheduleTransaction();
+                    }
+
+                    @Override
+                    public void onTransactionTimeout(String transactionName) {
+                        CompletableFuture.completedFuture(null).thenApplyAsync(
+                                (x) -> {
+                                    VoipCallTransactionResult mainResult =
+                                            new VoipCallTransactionResult(
+                                            VoipCallTransactionResult.RESULT_FAILED,
+                                            String.format("sub transaction %s timed out",
+                                                    transactionName));
+                                    finish(mainResult);
+                                    mCompleteListener.onTransactionCompleted(mainResult,
+                                            mTransactionName);
+                                    return null;
+                                }, new LoggedHandlerExecutor(mHandler,
+                                        mTransactionName + "@" + hashCode()
+                                                + ".oTT", mLock));
+                    }
+                };
+        for (VoipCallTransaction transaction : mSubTransactions) {
+            transaction.setCompleteListener(subTransactionListener);
+            transaction.start();
         }
     }
 }
diff --git a/src/com/android/server/telecom/voip/SerialTransaction.java b/src/com/android/server/telecom/voip/SerialTransaction.java
index 7d5a178..55d2065 100644
--- a/src/com/android/server/telecom/voip/SerialTransaction.java
+++ b/src/com/android/server/telecom/voip/SerialTransaction.java
@@ -37,86 +37,71 @@
     }
 
     @Override
-    public void start() {
-        if (mStats != null) mStats.markStarted();
-        // post timeout work
-        CompletableFuture<Void> future = new CompletableFuture<>();
-        mHandler.postDelayed(() -> future.complete(null), TIMEOUT_LIMIT);
-        future.thenApplyAsync((x) -> {
-            if (mCompleted.getAndSet(true)) {
-                return null;
-            }
-            if (mCompleteListener != null) {
-                mCompleteListener.onTransactionTimeout(mTransactionName);
-            }
-            timeout();
-            return null;
-        }, new LoggedHandlerExecutor(mHandler, mTransactionName + "@" + hashCode()
-                + ".s", mLock));
+    public void processTransactions() {
+        if (mSubTransactions == null || mSubTransactions.isEmpty()) {
+            scheduleTransaction();
+            return;
+        }
+        TransactionManager.TransactionCompleteListener subTransactionListener =
+                new TransactionManager.TransactionCompleteListener() {
+                    private final AtomicInteger mTransactionIndex = new AtomicInteger(0);
 
-        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,
-                                String transactionName) {
-                            if (result.getResult() != VoipCallTransactionResult.RESULT_SUCCEED) {
-                                handleTransactionFailure();
-                                CompletableFuture.completedFuture(null).thenApplyAsync(
-                                        (x) -> {
-                                            VoipCallTransactionResult mainResult =
-                                                    new VoipCallTransactionResult(
-                                                            VoipCallTransactionResult.RESULT_FAILED,
-                                                            String.format(
-                                                                    "sub transaction %s failed",
-                                                                    transactionName));
-                                            mCompleteListener.onTransactionCompleted(mainResult,
-                                                    mTransactionName);
-                                            finish(mainResult);
-                                            return null;
-                                        }, new LoggedHandlerExecutor(mHandler,
-                                                mTransactionName + "@" + hashCode()
-                                                        + ".oTC", mLock));
-                            } else {
-                                int currTransactionIndex = mTransactionIndex.incrementAndGet();
-                                if (currTransactionIndex < mSubTransactions.size()) {
-                                    VoipCallTransaction transaction = mSubTransactions.get(
-                                            currTransactionIndex);
-                                    transaction.setCompleteListener(this);
-                                    transaction.start();
-                                } else {
-                                    scheduleTransaction();
-                                }
-                            }
-                        }
-
-                        @Override
-                        public void onTransactionTimeout(String transactionName) {
+                    @Override
+                    public void onTransactionCompleted(VoipCallTransactionResult result,
+                            String transactionName) {
+                        if (result.getResult() != VoipCallTransactionResult.RESULT_SUCCEED) {
                             handleTransactionFailure();
                             CompletableFuture.completedFuture(null).thenApplyAsync(
                                     (x) -> {
                                         VoipCallTransactionResult mainResult =
                                                 new VoipCallTransactionResult(
-                                                VoipCallTransactionResult.RESULT_FAILED,
-                                                String.format("sub transaction %s timed out",
-                                                        transactionName));
+                                                        VoipCallTransactionResult.RESULT_FAILED,
+                                                        String.format(
+                                                                "sub transaction %s failed",
+                                                                transactionName));
+                                        finish(mainResult);
                                         mCompleteListener.onTransactionCompleted(mainResult,
                                                 mTransactionName);
-                                        finish(mainResult);
                                         return null;
                                     }, new LoggedHandlerExecutor(mHandler,
                                             mTransactionName + "@" + hashCode()
-                                                    + ".oTT", mLock));
+                                                    + ".oTC", mLock));
+                        } else {
+                            int currTransactionIndex = mTransactionIndex.incrementAndGet();
+                            if (currTransactionIndex < mSubTransactions.size()) {
+                                VoipCallTransaction transaction = mSubTransactions.get(
+                                        currTransactionIndex);
+                                transaction.setCompleteListener(this);
+                                transaction.start();
+                            } else {
+                                scheduleTransaction();
+                            }
                         }
-                    };
-            VoipCallTransaction transaction = mSubTransactions.get(0);
-            transaction.setCompleteListener(subTransactionListener);
-            transaction.start();
-        } else {
-            scheduleTransaction();
-        }
+                    }
+
+                    @Override
+                    public void onTransactionTimeout(String transactionName) {
+                        handleTransactionFailure();
+                        CompletableFuture.completedFuture(null).thenApplyAsync(
+                                (x) -> {
+                                    VoipCallTransactionResult mainResult =
+                                            new VoipCallTransactionResult(
+                                            VoipCallTransactionResult.RESULT_FAILED,
+                                            String.format("sub transaction %s timed out",
+                                                    transactionName));
+                                    finish(mainResult);
+                                    mCompleteListener.onTransactionCompleted(mainResult,
+                                            mTransactionName);
+                                    return null;
+                                }, new LoggedHandlerExecutor(mHandler,
+                                        mTransactionName + "@" + hashCode()
+                                                + ".oTT", mLock));
+                    }
+                };
+        VoipCallTransaction transaction = mSubTransactions.get(0);
+        transaction.setCompleteListener(subTransactionListener);
+        transaction.start();
+
     }
 
     public void handleTransactionFailure() {}
diff --git a/src/com/android/server/telecom/voip/VerifyCallStateChangeTransaction.java b/src/com/android/server/telecom/voip/VerifyCallStateChangeTransaction.java
index b17dedd..5de4b1d 100644
--- a/src/com/android/server/telecom/voip/VerifyCallStateChangeTransaction.java
+++ b/src/com/android/server/telecom/voip/VerifyCallStateChangeTransaction.java
@@ -18,14 +18,12 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.telecom.Call;
-import com.android.server.telecom.CallsManager;
+import com.android.server.telecom.TelecomSystem;
 
-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
@@ -35,37 +33,30 @@
  */
 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 static final long CALL_STATE_TIMEOUT_MILLISECONDS = 2000L;
     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() {
+    private final 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);
+                mTransactionResult.complete(new VoipCallTransactionResult(
+                        VoipCallTransactionResult.RESULT_SUCCEED, TAG));
             }
             // 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;
+    public VerifyCallStateChangeTransaction(TelecomSystem.SyncRoot lock,  Call call,
+            int targetCallState) {
+        super(lock, CALL_STATE_TIMEOUT_MILLISECONDS);
         mCall = call;
         mTargetCallState = targetCallState;
-        mShouldDisconnectUponFailure = shouldDisconnectUponFailure;
     }
 
     @Override
@@ -73,68 +64,23 @@
         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));
+            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();
-
+        mCall.addCallStateListener(mCallStateListenerImpl);
         return mTransactionResult;
     }
 
+    @Override
+    public void finishTransaction() {
+        mCall.removeCallStateListener(mCallStateListenerImpl);
+    }
+
     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;
diff --git a/src/com/android/server/telecom/voip/VoipCallTransaction.java b/src/com/android/server/telecom/voip/VoipCallTransaction.java
index 3c91158..ceb8d55 100644
--- a/src/com/android/server/telecom/voip/VoipCallTransaction.java
+++ b/src/com/android/server/telecom/voip/VoipCallTransaction.java
@@ -20,6 +20,7 @@
 import android.os.HandlerThread;
 import android.telecom.Log;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.telecom.LoggedHandlerExecutor;
 import com.android.server.telecom.TelecomSystem;
 import com.android.server.telecom.flags.Flags;
@@ -34,7 +35,7 @@
 
 public class VoipCallTransaction {
     //TODO: add log events
-    protected static final long TIMEOUT_LIMIT = 5000L;
+    private static final long DEFAULT_TRANSACTION_TIMEOUT_MS = 5000L;
 
     /**
      * Tracks stats about a transaction for logging purposes.
@@ -129,58 +130,80 @@
 
     protected final AtomicBoolean mCompleted = new AtomicBoolean(false);
     protected final String mTransactionName = this.getClass().getSimpleName();
-    private HandlerThread mHandlerThread;
-    protected Handler mHandler;
+    private final HandlerThread mHandlerThread;
+    protected final Handler mHandler;
     protected TransactionManager.TransactionCompleteListener mCompleteListener;
-    protected List<VoipCallTransaction> mSubTransactions;
-    protected TelecomSystem.SyncRoot mLock;
+    protected final List<VoipCallTransaction> mSubTransactions;
+    protected final TelecomSystem.SyncRoot mLock;
+    protected final long mTransactionTimeoutMs;
     protected final Stats mStats;
 
     public VoipCallTransaction(
-            List<VoipCallTransaction> subTransactions, TelecomSystem.SyncRoot lock) {
+            List<VoipCallTransaction> subTransactions, TelecomSystem.SyncRoot lock,
+            long timeoutMs) {
         mSubTransactions = subTransactions;
         mHandlerThread = new HandlerThread(this.toString());
         mHandlerThread.start();
         mHandler = new Handler(mHandlerThread.getLooper());
         mLock = lock;
+        mTransactionTimeoutMs = timeoutMs;
         mStats = Flags.enableCallSequencing() ? new Stats() : null;
     }
 
-    public VoipCallTransaction(TelecomSystem.SyncRoot lock) {
-        this(null /** mSubTransactions */, lock);
+    public VoipCallTransaction(List<VoipCallTransaction> subTransactions,
+            TelecomSystem.SyncRoot lock) {
+        this(subTransactions, lock, DEFAULT_TRANSACTION_TIMEOUT_MS);
+    }
+    public VoipCallTransaction(TelecomSystem.SyncRoot lock, long timeoutMs) {
+        this(null /* mSubTransactions */, lock, timeoutMs);
     }
 
-    public void start() {
+    public VoipCallTransaction(TelecomSystem.SyncRoot lock) {
+        this(null /* mSubTransactions */, lock);
+    }
+
+    public final void start() {
         if (mStats != null) mStats.markStarted();
         // post timeout work
         CompletableFuture<Void> future = new CompletableFuture<>();
-        mHandler.postDelayed(() -> future.complete(null), TIMEOUT_LIMIT);
+        mHandler.postDelayed(() -> future.complete(null), mTransactionTimeoutMs);
         future.thenApplyAsync((x) -> {
-            if (mCompleted.getAndSet(true)) {
-                return null;
-            }
-            if (mCompleteListener != null) {
-                mCompleteListener.onTransactionTimeout(mTransactionName);
-            }
             timeout();
             return null;
         }, new LoggedHandlerExecutor(mHandler, mTransactionName + "@" + hashCode()
                 + ".s", mLock));
 
+        processTransactions();
+    }
+
+    /**
+     * By default, this processes this transaction. For VoipCallTransactions with sub-transactions,
+     * this implementation should be overwritten to handle also processing sub-transactions.
+     */
+    protected void processTransactions() {
         scheduleTransaction();
     }
 
-    protected void scheduleTransaction() {
+    /**
+     * This method is called when the transaction has finished either successfully or exceptionally.
+     * VoipCallTransactions that are extending this class should override this method to clean up
+     * any leftover state.
+     */
+    protected void finishTransaction() {
+
+    }
+
+    protected final void scheduleTransaction() {
         LoggedHandlerExecutor executor = new LoggedHandlerExecutor(mHandler,
                 mTransactionName + "@" + hashCode() + ".pT", mLock);
         CompletableFuture<Void> future = CompletableFuture.completedFuture(null);
         future.thenComposeAsync(this::processTransaction, executor)
                 .thenApplyAsync((Function<VoipCallTransactionResult, Void>) result -> {
                     mCompleted.set(true);
+                    finish(result);
                     if (mCompleteListener != null) {
                         mCompleteListener.onTransactionCompleted(result, mTransactionName);
                     }
-                    finish(result);
                     return null;
                     }, executor)
                 .exceptionallyAsync((throwable -> {
@@ -189,25 +212,38 @@
                 }), executor);
     }
 
-    public CompletionStage<VoipCallTransactionResult> processTransaction(Void v) {
+    protected CompletionStage<VoipCallTransactionResult> processTransaction(Void v) {
         return CompletableFuture.completedFuture(
                 new VoipCallTransactionResult(VoipCallTransactionResult.RESULT_SUCCEED, null));
     }
 
-    public void setCompleteListener(TransactionManager.TransactionCompleteListener listener) {
+    public final void setCompleteListener(TransactionManager.TransactionCompleteListener listener) {
         mCompleteListener = listener;
     }
 
-    public void timeout() {
+    @VisibleForTesting
+    public final void timeout() {
+        if (mCompleted.getAndSet(true)) {
+            return;
+        }
         finish(true, null);
+        if (mCompleteListener != null) {
+            mCompleteListener.onTransactionTimeout(mTransactionName);
+        }
     }
 
-    public void finish(VoipCallTransactionResult result) {
+    @VisibleForTesting
+    public final Handler getHandler() {
+        return mHandler;
+    }
+
+    public final void finish(VoipCallTransactionResult result) {
         finish(false, result);
     }
 
-    public void finish(boolean isTimedOut, VoipCallTransactionResult result) {
+    private void finish(boolean isTimedOut, VoipCallTransactionResult result) {
         if (mStats != null) mStats.markComplete(isTimedOut, result);
+        finishTransaction();
         // finish all sub transactions
         if (mSubTransactions != null && !mSubTransactions.isEmpty()) {
             mSubTransactions.forEach( t -> t.finish(isTimedOut, result));
@@ -218,7 +254,7 @@
     /**
      * @return Stats related to this transaction if stats are enabled, null otherwise.
      */
-    public Stats getStats() {
+    public final Stats getStats() {
         return mStats;
     }
 }
diff --git a/tests/src/com/android/server/telecom/tests/TransactionTests.java b/tests/src/com/android/server/telecom/tests/TransactionTests.java
index e58c6c4..b5a0c26 100644
--- a/tests/src/com/android/server/telecom/tests/TransactionTests.java
+++ b/tests/src/com/android/server/telecom/tests/TransactionTests.java
@@ -17,11 +17,13 @@
 package com.android.server.telecom.tests;
 
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
 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.timeout;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.isA;
@@ -61,6 +63,7 @@
 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.TransactionManager;
 import com.android.server.telecom.voip.VerifyCallStateChangeTransaction;
 import com.android.server.telecom.voip.VoipCallTransactionResult;
 
@@ -271,27 +274,24 @@
      */
     @SmallTest
     @Test
-    public void testCallStateChangeTimesOut()
-            throws ExecutionException, InterruptedException, TimeoutException {
+    public void testCallStateChangeTimesOut() {
         when(mFeatureFlags.transactionalCsVerifier()).thenReturn(true);
-        VerifyCallStateChangeTransaction t = new VerifyCallStateChangeTransaction(mCallsManager,
-                mMockCall1, CallState.ON_HOLD, true);
+        VerifyCallStateChangeTransaction t = new VerifyCallStateChangeTransaction(
+                mLock, mMockCall1, CallState.ON_HOLD);
+        TransactionManager.TransactionCompleteListener listener =
+                mock(TransactionManager.TransactionCompleteListener.class);
+        t.setCompleteListener(listener);
         // 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);
+        t.timeout();
 
         // 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(listener).onTransactionTimeout(anyString());
         verify(mMockCall1, atLeastOnce()).removeCallStateListener(any());
-        verify(mCallsManager, times(1)).markCallAsDisconnected(eq(mMockCall1), any());
-        verify(mCallsManager, times(1)).markCallAsRemoved(eq(mMockCall1));
     }
 
     /**
@@ -303,25 +303,23 @@
     public void testCallStateIsSuccessfullyChanged()
             throws ExecutionException, InterruptedException, TimeoutException {
         when(mFeatureFlags.transactionalCsVerifier()).thenReturn(true);
-        VerifyCallStateChangeTransaction t = new VerifyCallStateChangeTransaction(mCallsManager,
-                mMockCall1, CallState.ON_HOLD, true);
+        VerifyCallStateChangeTransaction t = new VerifyCallStateChangeTransaction(
+                mLock, mMockCall1, CallState.ON_HOLD);
         // WHEN
         setupHoldableCall();
 
         // simulate the transaction being processed and the setOnHold() being called / state change
         t.processTransaction(null);
+        doReturn(CallState.ON_HOLD).when(mMockCall1).getState();
         t.getCallStateListenerImpl().onCallStateChanged(CallState.ON_HOLD);
-        when(mMockCall1.getState()).thenReturn(CallState.ON_HOLD);
+        t.finish(null);
+
 
         // 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) {
diff --git a/tests/src/com/android/server/telecom/tests/VoipCallTransactionTest.java b/tests/src/com/android/server/telecom/tests/VoipCallTransactionTest.java
index b7848a2..30cfc2e 100644
--- a/tests/src/com/android/server/telecom/tests/VoipCallTransactionTest.java
+++ b/tests/src/com/android/server/telecom/tests/VoipCallTransactionTest.java
@@ -61,6 +61,7 @@
         private long mSleepTime;
         private String mName;
         private int mType;
+        public boolean isFinished = false;
 
         public TestVoipCallTransaction(String name, long sleepTime, int type) {
             super(VoipCallTransactionTest.this.mLock);
@@ -96,6 +97,11 @@
             }, mSleepTime);
             return resultFuture;
         }
+
+        @Override
+        public void finishTransaction() {
+            isFinished = true;
+        }
     }
 
     @Override
@@ -109,7 +115,6 @@
     @Override
     @After
     public void tearDown() throws Exception {
-        Log.i("Grace", mLog.toString());
         mTransactionManager.clear();
         super.tearDown();
     }
@@ -119,11 +124,11 @@
     public void testSerialTransactionSuccess()
             throws ExecutionException, InterruptedException, TimeoutException {
         List<VoipCallTransaction> subTransactions = new ArrayList<>();
-        VoipCallTransaction t1 = new TestVoipCallTransaction("t1", 1000L,
+        TestVoipCallTransaction t1 = new TestVoipCallTransaction("t1", 1000L,
                 TestVoipCallTransaction.SUCCESS);
-        VoipCallTransaction t2 = new TestVoipCallTransaction("t2", 1000L,
+        TestVoipCallTransaction t2 = new TestVoipCallTransaction("t2", 1000L,
                 TestVoipCallTransaction.SUCCESS);
-        VoipCallTransaction t3 = new TestVoipCallTransaction("t3", 1000L,
+        TestVoipCallTransaction t3 = new TestVoipCallTransaction("t3", 1000L,
                 TestVoipCallTransaction.SUCCESS);
         subTransactions.add(t1);
         subTransactions.add(t2);
@@ -137,6 +142,7 @@
         assertEquals(VoipCallTransactionResult.RESULT_SUCCEED,
                 resultFuture.get(5000L, TimeUnit.MILLISECONDS).getResult());
         assertEquals(expectedLog, mLog.toString());
+        verifyTransactionsFinished(t1, t2, t3);
     }
 
     @SmallTest
@@ -144,11 +150,11 @@
     public void testSerialTransactionFailed()
             throws ExecutionException, InterruptedException, TimeoutException {
         List<VoipCallTransaction> subTransactions = new ArrayList<>();
-        VoipCallTransaction t1 = new TestVoipCallTransaction("t1", 1000L,
+        TestVoipCallTransaction t1 = new TestVoipCallTransaction("t1", 1000L,
                 TestVoipCallTransaction.SUCCESS);
-        VoipCallTransaction t2 = new TestVoipCallTransaction("t2", 1000L,
+        TestVoipCallTransaction t2 = new TestVoipCallTransaction("t2", 1000L,
                 TestVoipCallTransaction.FAILED);
-        VoipCallTransaction t3 = new TestVoipCallTransaction("t3", 1000L,
+        TestVoipCallTransaction t3 = new TestVoipCallTransaction("t3", 1000L,
                 TestVoipCallTransaction.SUCCESS);
         subTransactions.add(t1);
         subTransactions.add(t2);
@@ -171,6 +177,7 @@
         exceptionFuture.get(5000L, TimeUnit.MILLISECONDS);
         String expectedLog = "t1 success;\nt2 failed;\n";
         assertEquals(expectedLog, mLog.toString());
+        verifyTransactionsFinished(t1, t2, t3);
     }
 
     @SmallTest
@@ -178,11 +185,11 @@
     public void testParallelTransactionSuccess()
             throws ExecutionException, InterruptedException, TimeoutException {
         List<VoipCallTransaction> subTransactions = new ArrayList<>();
-        VoipCallTransaction t1 = new TestVoipCallTransaction("t1", 1000L,
+        TestVoipCallTransaction t1 = new TestVoipCallTransaction("t1", 1000L,
                 TestVoipCallTransaction.SUCCESS);
-        VoipCallTransaction t2 = new TestVoipCallTransaction("t2", 500L,
+        TestVoipCallTransaction t2 = new TestVoipCallTransaction("t2", 500L,
                 TestVoipCallTransaction.SUCCESS);
-        VoipCallTransaction t3 = new TestVoipCallTransaction("t3", 200L,
+        TestVoipCallTransaction t3 = new TestVoipCallTransaction("t3", 200L,
                 TestVoipCallTransaction.SUCCESS);
         subTransactions.add(t1);
         subTransactions.add(t2);
@@ -198,6 +205,7 @@
         assertTrue(log.contains("t1 success;\n"));
         assertTrue(log.contains("t2 success;\n"));
         assertTrue(log.contains("t3 success;\n"));
+        verifyTransactionsFinished(t1, t2, t3);
     }
 
     @SmallTest
@@ -205,11 +213,11 @@
     public void testParallelTransactionFailed()
             throws ExecutionException, InterruptedException, TimeoutException {
         List<VoipCallTransaction> subTransactions = new ArrayList<>();
-        VoipCallTransaction t1 = new TestVoipCallTransaction("t1", 1000L,
+        TestVoipCallTransaction t1 = new TestVoipCallTransaction("t1", 1000L,
                 TestVoipCallTransaction.SUCCESS);
-        VoipCallTransaction t2 = new TestVoipCallTransaction("t2", 500L,
+        TestVoipCallTransaction t2 = new TestVoipCallTransaction("t2", 500L,
                 TestVoipCallTransaction.FAILED);
-        VoipCallTransaction t3 = new TestVoipCallTransaction("t3", 200L,
+        TestVoipCallTransaction t3 = new TestVoipCallTransaction("t3", 200L,
                 TestVoipCallTransaction.SUCCESS);
         subTransactions.add(t1);
         subTransactions.add(t2);
@@ -231,13 +239,14 @@
                 outcomeReceiver);
         exceptionFuture.get(5000L, TimeUnit.MILLISECONDS);
         assertTrue(mLog.toString().contains("t2 failed;\n"));
+        verifyTransactionsFinished(t1, t2, t3);
     }
 
     @SmallTest
     @Test
     public void testTransactionTimeout()
             throws ExecutionException, InterruptedException, TimeoutException {
-        VoipCallTransaction t = new TestVoipCallTransaction("t", 10000L,
+        TestVoipCallTransaction t = new TestVoipCallTransaction("t", 10000L,
                 TestVoipCallTransaction.SUCCESS);
         CompletableFuture<String> exceptionFuture = new CompletableFuture<>();
         OutcomeReceiver<VoipCallTransactionResult, CallException> outcomeReceiver =
@@ -255,15 +264,16 @@
         mTransactionManager.addTransaction(t, outcomeReceiver);
         String message = exceptionFuture.get(7000L, TimeUnit.MILLISECONDS);
         assertTrue(message.contains("timeout"));
+        verifyTransactionsFinished(t);
     }
 
     @SmallTest
     @Test
     public void testTransactionException()
             throws ExecutionException, InterruptedException, TimeoutException {
-        VoipCallTransaction t1 = new TestVoipCallTransaction("t1", 1000L,
+        TestVoipCallTransaction t1 = new TestVoipCallTransaction("t1", 1000L,
                 TestVoipCallTransaction.EXCEPTION);
-        VoipCallTransaction t2 = new TestVoipCallTransaction("t2", 1000L,
+        TestVoipCallTransaction t2 = new TestVoipCallTransaction("t2", 1000L,
                 TestVoipCallTransaction.SUCCESS);
         CompletableFuture<String> exceptionFuture = new CompletableFuture<>();
         OutcomeReceiver<VoipCallTransactionResult, CallException> outcomeExceptionReceiver =
@@ -290,17 +300,18 @@
         assertEquals(VoipCallTransactionResult.RESULT_SUCCEED,
                 resultFuture.get(5000L, TimeUnit.MILLISECONDS).getResult());
         assertEquals(expectedLog, mLog.toString());
+        verifyTransactionsFinished(t1, t2);
     }
 
     @SmallTest
     @Test
     public void testTransactionResultException()
             throws ExecutionException, InterruptedException, TimeoutException {
-        VoipCallTransaction t1 = new TestVoipCallTransaction("t1", 1000L,
+        TestVoipCallTransaction t1 = new TestVoipCallTransaction("t1", 1000L,
                 TestVoipCallTransaction.SUCCESS);
-        VoipCallTransaction t2 = new TestVoipCallTransaction("t2", 1000L,
+        TestVoipCallTransaction t2 = new TestVoipCallTransaction("t2", 1000L,
                 TestVoipCallTransaction.SUCCESS);
-        VoipCallTransaction t3 = new TestVoipCallTransaction("t3", 1000L,
+        TestVoipCallTransaction t3 = new TestVoipCallTransaction("t3", 1000L,
                 TestVoipCallTransaction.SUCCESS);
         OutcomeReceiver<VoipCallTransactionResult, CallException> outcomeExceptionReceiver =
                 new OutcomeReceiver<>() {
@@ -335,5 +346,13 @@
         assertEquals(VoipCallTransactionResult.RESULT_SUCCEED,
                 resultFuture.get(5000L, TimeUnit.MILLISECONDS).getResult());
         assertEquals(expectedLog, mLog.toString());
+        verifyTransactionsFinished(t1, t2, t3);
+    }
+
+    public void verifyTransactionsFinished(TestVoipCallTransaction... transactions) {
+        for (TestVoipCallTransaction t : transactions) {
+            assertTrue("TestVoipCallTransaction[" + t.mName + "] never called finishTransaction",
+                    t.isFinished);
+        }
     }
 }