fix toggling hold for a single transactional call

I discovered in manual testing calls that were set to inactive (held)
were not able to be set active again. This was due to the fact that the
requestFocusTransaction asserts the focusManager has the desired new
active call as the focusCall.  FocusManager will not set the focusCall
to a held call which would error out the transaction.

If there are no active calls, then the requested new active call does
not need to wait for a hold on another call and the focusManager will
update after the call is marked as active.

Fixes: 266621488
Test: atest android.telecom.cts.TransactionalApisTest
                                       #testToggleActiveAndInactive
Change-Id: Ife3d3b3b73ca40237f9a0a22e7c369b2b63e2c3a
diff --git a/src/com/android/server/telecom/CallsManager.java b/src/com/android/server/telecom/CallsManager.java
index 0dc37fa..91c6079 100644
--- a/src/com/android/server/telecom/CallsManager.java
+++ b/src/com/android/server/telecom/CallsManager.java
@@ -6075,34 +6075,53 @@
     }
 
     /**
-     * Intended for ongoing or new calls that would like to go active/answered and need to
-     * update the mConnectionSvrFocusMgr before setting the state
+     * This helper mainly requests mConnectionSvrFocusMgr to update the call focus via a
+     * {@link TransactionalFocusRequestCallback}.  However, in the case of a held call, the
+     * state must be set first and then a request must be made.
+     *
+     * @param newCallFocus          to set active/answered
+     * @param resultCallback        that back propagates the focusManager result
+     *
+     * Note: This method should only be called if there are no active calls.
      */
-    public void transactionRequestNewFocusCall(Call call, int newCallState,
-            OutcomeReceiver<Boolean, CallException> callback) {
-        Log.d(this, "transactionRequestNewFocusCall");
-        PendingAction pendingAction = new ActionSetCallState(call, newCallState,
-                "transactional ActionSetCallState");
+    public void requestNewCallFocusAndVerify(Call newCallFocus,
+            OutcomeReceiver<Boolean, CallException> resultCallback) {
+        int currentCallState = newCallFocus.getState();
+        PendingAction pendingAction = null;
+
+        // if the current call is in a state that can become the new call focus, we can set the
+        // state afterwards...
+        if (ConnectionServiceFocusManager.PRIORITY_FOCUS_CALL_STATE.contains(currentCallState)) {
+            pendingAction = new ActionSetCallState(newCallFocus, CallState.ACTIVE,
+                    "vCFC: pending action set state");
+        } else {
+            // However, HELD calls need to be set to ACTIVE before requesting call focus.
+            setCallState(newCallFocus, CallState.ACTIVE, "vCFC: immediately set active");
+        }
+
         mConnectionSvrFocusMgr
-                .requestFocus(call,
-                        new TransactionalFocusRequestCallback(pendingAction, call, callback));
+                .requestFocus(newCallFocus,
+                        new TransactionalFocusRequestCallback(pendingAction, currentCallState,
+                                newCallFocus, resultCallback));
     }
 
     /**
      * Request a new call focus and ensure the request was successful via an OutcomeReceiver. Also,
-     * include a PendingAction that will execute if the call focus change is successful.
+     * conditionally include a PendingAction that will execute if and only if the call focus change
+     * is successful.
      */
     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
     public class TransactionalFocusRequestCallback implements
             ConnectionServiceFocusManager.RequestFocusCallback {
         private PendingAction mPendingAction;
-        @NonNull
-        private Call mTargetCallFocus;
+        private int mPreviousCallState;
+        @NonNull private Call mTargetCallFocus;
         private OutcomeReceiver<Boolean, CallException> mCallback;
 
-        TransactionalFocusRequestCallback(PendingAction pendingAction, @NonNull Call call,
-                OutcomeReceiver<Boolean, CallException> callback) {
+        TransactionalFocusRequestCallback(PendingAction pendingAction, int previousState,
+                @NonNull Call call, OutcomeReceiver<Boolean, CallException> callback) {
             mPendingAction = pendingAction;
+            mPreviousCallState = previousState;
             mTargetCallFocus = call;
             mCallback = callback;
         }
@@ -6115,12 +6134,18 @@
                     mTargetCallFocus, currentCallFocus);
             if (currentCallFocus == null ||
                     !currentCallFocus.getId().equals(mTargetCallFocus.getId())) {
+                // possibly reset the call state
+                if (mTargetCallFocus.getState() != mPreviousCallState) {
+                    mTargetCallFocus.setState(mPreviousCallState, "resetting call state");
+                }
                 mCallback.onError(new CallException("failed to switch focus to requested call",
                         CallException.CODE_CALL_CANNOT_BE_SET_TO_ACTIVE));
                 return;
             }
             // at this point, we know the FocusManager is able to update successfully
-            mPendingAction.performAction(); // set the call state
+            if (mPendingAction != null) {
+                mPendingAction.performAction(); // set the call state
+            }
             mCallback.onResult(true); // complete the transaction
         }
     }
diff --git a/src/com/android/server/telecom/ConnectionServiceFocusManager.java b/src/com/android/server/telecom/ConnectionServiceFocusManager.java
index 8db98e9..6fbc494 100644
--- a/src/com/android/server/telecom/ConnectionServiceFocusManager.java
+++ b/src/com/android/server/telecom/ConnectionServiceFocusManager.java
@@ -32,6 +32,7 @@
 import java.util.List;
 import java.util.Objects;
 import java.util.Optional;
+import java.util.Set;
 import java.util.concurrent.BlockingQueue;
 import java.util.concurrent.LinkedBlockingQueue;
 import java.util.concurrent.TimeUnit;
@@ -153,10 +154,9 @@
         void setCallsManagerListener(CallsManager.CallsManagerListener listener);
     }
 
-    private static final int[] PRIORITY_FOCUS_CALL_STATE = new int[] {
-            CallState.ACTIVE, CallState.CONNECTING, CallState.DIALING, CallState.AUDIO_PROCESSING,
-            CallState.RINGING
-    };
+    public static final Set<Integer> PRIORITY_FOCUS_CALL_STATE
+            = Set.of(CallState.ACTIVE, CallState.CONNECTING, CallState.DIALING,
+            CallState.AUDIO_PROCESSING, CallState.RINGING);
 
     private static final int MSG_REQUEST_FOCUS = 1;
     private static final int MSG_RELEASE_CONNECTION_FOCUS = 2;
@@ -374,17 +374,15 @@
                         && call.isFocusable())
                 .collect(Collectors.toList());
 
-        for (int i = 0; i < PRIORITY_FOCUS_CALL_STATE.length; i++) {
-            for (CallFocus call : calls) {
-                if (call.getState() == PRIORITY_FOCUS_CALL_STATE[i]) {
-                    mCurrentFocusCall = call;
-                    Log.d(this, "updateCurrentFocusCall %s", mCurrentFocusCall);
-                    return;
-                }
+        for (CallFocus call : calls) {
+            if (PRIORITY_FOCUS_CALL_STATE.contains(call.getState())) {
+                mCurrentFocusCall = call;
+                Log.i(this, "updateCurrentFocusCall %s", mCurrentFocusCall);
+                return;
             }
         }
 
-        Log.d(this, "updateCurrentFocusCall = null");
+        Log.i(this, "updateCurrentFocusCall = null");
     }
 
     private void onRequestFocusDone(FocusRequest focusRequest) {
diff --git a/src/com/android/server/telecom/TransactionalServiceWrapper.java b/src/com/android/server/telecom/TransactionalServiceWrapper.java
index 90acba8..1e6403e 100644
--- a/src/com/android/server/telecom/TransactionalServiceWrapper.java
+++ b/src/com/android/server/telecom/TransactionalServiceWrapper.java
@@ -38,14 +38,13 @@
 
 import com.android.internal.telecom.ICallControl;
 import com.android.internal.telecom.ICallEventCallback;
-import com.android.server.telecom.voip.AnswerCallTransaction;
 import com.android.server.telecom.voip.CallEventCallbackAckTransaction;
 import com.android.server.telecom.voip.EndpointChangeTransaction;
 import com.android.server.telecom.voip.HoldCallTransaction;
 import com.android.server.telecom.voip.EndCallTransaction;
-import com.android.server.telecom.voip.HoldActiveCallForNewCallTransaction;
+import com.android.server.telecom.voip.MaybeHoldCallForNewCallTransaction;
 import com.android.server.telecom.voip.ParallelTransaction;
-import com.android.server.telecom.voip.RequestFocusTransaction;
+import com.android.server.telecom.voip.RequestNewActiveCallTransaction;
 import com.android.server.telecom.voip.SerialTransaction;
 import com.android.server.telecom.voip.TransactionManager;
 import com.android.server.telecom.voip.VoipCallTransaction;
@@ -247,11 +246,12 @@
             if (call != null) {
                 switch (action) {
                     case SET_ACTIVE:
-                        addTransactionsToManager(createSetActiveTransactions(call), callback);
+                        handleCallControlNewCallFocusTransactions(call, SET_ACTIVE,
+                                false /* isAnswer */, 0/*VideoState (ignored)*/, callback);
                         break;
                     case ANSWER:
-                        addTransactionsToManager(createSetAnswerTransactions(call,
-                                (int) objects[0]), callback);
+                        handleCallControlNewCallFocusTransactions(call, ANSWER,
+                                true /* isAnswer */, (int) objects[0] /*VideoState*/, callback);
                         break;
                     case DISCONNECT:
                         addTransactionsToManager(new EndCallTransaction(mCallsManager,
@@ -278,6 +278,32 @@
             }
         }
 
+        // The client is request their VoIP call state go ACTIVE/ANSWERED.
+        // This request is originating from the VoIP application.
+        private void handleCallControlNewCallFocusTransactions(Call call, String action,
+                boolean isAnswer, int potentiallyNewVideoState, ResultReceiver callback) {
+            mTransactionManager.addTransaction(createSetActiveTransactions(call),
+                    new OutcomeReceiver<>() {
+                        @Override
+                        public void onResult(VoipCallTransactionResult result) {
+                            Log.i(TAG, String.format(Locale.US,
+                                    "%s: onResult: callId=[%s]", action, call.getId()));
+                            if (isAnswer) {
+                                call.setVideoState(potentiallyNewVideoState);
+                            }
+                            callback.send(TELECOM_TRANSACTION_SUCCESS, new Bundle());
+                        }
+
+                        @Override
+                        public void onError(CallException exception) {
+                            Bundle extras = new Bundle();
+                            extras.putParcelable(TRANSACTION_EXCEPTION_KEY, exception);
+                            callback.send(exception == null ? CallException.CODE_ERROR_UNKNOWN :
+                                    exception.getCode(), extras);
+                        }
+                    });
+        }
+
         @Override
         public void requestCallEndpointChange(CallEndpoint endpoint, ResultReceiver callback) {
             try {
@@ -299,14 +325,12 @@
                 Call call = mTrackedCalls.get(callId);
                 if (call != null) {
                     call.onConnectionEvent(event, extras);
-                }
-                else{
+                } else {
                     Log.i(TAG,
                             "sendEvent: was called but there is no call with id=[%s] cannot be "
                                     + "found. Most likely the call has been disconnected");
                 }
-            }
-            finally {
+            } finally {
                 Log.endSession();
             }
         }
@@ -348,7 +372,8 @@
         try {
             Log.startSession("TSW.oSA");
             Log.d(TAG, String.format(Locale.US, "onSetActive: callId=[%s]", call.getId()));
-            handleNewActiveCallCallbacks(call, ON_SET_ACTIVE, 0);
+            handleCallEventCallbackNewFocus(call, ON_SET_ACTIVE, false /*isAnswerRequest*/,
+                    0 /*VideoState*/);
         } finally {
             Log.endSession();
         }
@@ -358,42 +383,51 @@
         try {
             Log.startSession("TSW.oA");
             Log.d(TAG, String.format(Locale.US, "onAnswer: callId=[%s]", call.getId()));
-            handleNewActiveCallCallbacks(call, ON_ANSWER, videoState);
+            handleCallEventCallbackNewFocus(call, ON_ANSWER, true /*isAnswerRequest*/,
+                    videoState /*VideoState*/);
         } finally {
             Log.endSession();
         }
     }
 
-    // need to create multiple transactions for onSetActive and onAnswer which both seek to set
-    // the call to active
-    private void handleNewActiveCallCallbacks(Call call, String action, int videoState) {
+    // handle a CallEventCallback to set a call ACTIVE/ANSWERED. Must get ack from client since the
+    // request has come from another source (ex. Android Auto is requesting a call to go active)
+    private void handleCallEventCallbackNewFocus(Call call, String action, boolean isAnswerRequest,
+            int potentiallyNewVideoState) {
         // save CallsManager state before sending client state changes
         Call foregroundCallBeforeSwap = mCallsManager.getForegroundCall();
         boolean wasActive = foregroundCallBeforeSwap != null && foregroundCallBeforeSwap.isActive();
 
-        // create 3 serial transactions:
-        // -- hold active
-        // -- set newCall as active
-        // -- ack from client
         SerialTransaction serialTransactions = createSetActiveTransactions(call);
-        serialTransactions.appendTransaction(
-                new CallEventCallbackAckTransaction(mICallEventCallback,
-                        action, call.getId(), videoState, mLock));
+        // 3. get ack from client (that the requested call can go active)
+        if (isAnswerRequest) {
+            serialTransactions.appendTransaction(
+                    new CallEventCallbackAckTransaction(mICallEventCallback,
+                            action, call.getId(), potentiallyNewVideoState, mLock));
+        } else {
+            serialTransactions.appendTransaction(
+                    new CallEventCallbackAckTransaction(mICallEventCallback,
+                            action, call.getId(), mLock));
+        }
 
         // do CallsManager workload before asking client and
         //   reset CallsManager state if client does NOT ack
-        mTransactionManager.addTransaction(serialTransactions, new OutcomeReceiver<>() {
-            @Override
-            public void onResult(VoipCallTransactionResult result) {
-                Log.i(TAG, String.format(Locale.US,
-                        "%s: onResult: callId=[%s]", action, call.getId()));
-            }
+        mTransactionManager.addTransaction(serialTransactions,
+                new OutcomeReceiver<>() {
+                    @Override
+                    public void onResult(VoipCallTransactionResult result) {
+                        Log.i(TAG, String.format(Locale.US,
+                                "%s: onResult: callId=[%s]", action, call.getId()));
+                        if (isAnswerRequest) {
+                            call.setVideoState(potentiallyNewVideoState);
+                        }
+                    }
 
-            @Override
-            public void onError(CallException exception) {
-                maybeResetForegroundCall(foregroundCallBeforeSwap, wasActive);
-            }
-        });
+                    @Override
+                    public void onError(CallException exception) {
+                        maybeResetForegroundCall(foregroundCallBeforeSwap, wasActive);
+                    }
+                });
     }
 
 
@@ -519,7 +553,7 @@
         }
     }
 
-    public void onEvent(Call call, String event, Bundle extras){
+    public void onEvent(Call call, String event, Bundle extras) {
         if (call != null) {
             try {
                 mICallEventCallback.onEvent(call.getId(), event, extras);
@@ -553,27 +587,11 @@
         // create list for multiple transactions
         List<VoipCallTransaction> transactions = new ArrayList<>();
 
-        // add t1. hold potential active call
-        transactions.add(new HoldActiveCallForNewCallTransaction(mCallsManager, call));
+        // potentially hold the current active call in order to set a new call (active/answered)
+        transactions.add(new MaybeHoldCallForNewCallTransaction(mCallsManager, call));
+        // And request a new focus call update
+        transactions.add(new RequestNewActiveCallTransaction(mCallsManager, call));
 
-        // add t2. send request to set the current call active
-        transactions.add(new RequestFocusTransaction(mCallsManager, call));
-
-        // send off to Transaction Manager to process
-        return new SerialTransaction(transactions, mLock);
-    }
-
-    private SerialTransaction createSetAnswerTransactions(Call call, int videoState) {
-        // create list for multiple transactions
-        List<VoipCallTransaction> transactions = new ArrayList<>();
-
-        // add t1. hold potential active call
-        transactions.add(new HoldActiveCallForNewCallTransaction(mCallsManager, call));
-
-        // add t2. answer current call
-        transactions.add(new AnswerCallTransaction(mCallsManager, call, videoState));
-
-        // send off to Transaction Manager to process
         return new SerialTransaction(transactions, mLock);
     }
 
diff --git a/src/com/android/server/telecom/voip/AnswerCallTransaction.java b/src/com/android/server/telecom/voip/AnswerCallTransaction.java
deleted file mode 100644
index efd2343..0000000
--- a/src/com/android/server/telecom/voip/AnswerCallTransaction.java
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.telecom.voip;
-
-import android.os.OutcomeReceiver;
-import android.telecom.CallException;
-import android.util.Log;
-
-import com.android.server.telecom.Call;
-import com.android.server.telecom.CallState;
-import com.android.server.telecom.CallsManager;
-
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.CompletionStage;
-
-/**
- * This transaction should be created for new incoming calls that request to go from
- * CallState.Ringing to CallState.Answered.  Before changing the CallState, the focus manager must
- * be updated. Once the focus manager updates, the call state will be set.  If there is an issue
- * answering the call, the transaction will fail.
- */
-public class AnswerCallTransaction extends VoipCallTransaction {
-
-    private static final String TAG = AnswerCallTransaction.class.getSimpleName();
-    private final CallsManager mCallsManager;
-    private final Call mCall;
-    private final int mVideoState;
-
-    public AnswerCallTransaction(CallsManager callsManager, Call call, int videoState) {
-        super(callsManager.getLock());
-        mCallsManager = callsManager;
-        mCall = call;
-        mVideoState = videoState;
-    }
-
-    @Override
-    public CompletionStage<VoipCallTransactionResult> processTransaction(Void v) {
-        Log.d(TAG, "processTransaction");
-        CompletableFuture<VoipCallTransactionResult> future = new CompletableFuture<>();
-
-        mCall.setVideoState(mVideoState);
-
-        mCallsManager.transactionRequestNewFocusCall(mCall, CallState.ANSWERED,
-                new OutcomeReceiver<>() {
-            @Override
-            public void onResult(Boolean result) {
-                Log.d(TAG, "processTransaction: onResult");
-                future.complete(new VoipCallTransactionResult(
-                        VoipCallTransactionResult.RESULT_SUCCEED, null));
-            }
-
-            @Override
-            public void onError(CallException exception) {
-                Log.d(TAG, "processTransaction: onError");
-                future.complete(new VoipCallTransactionResult(
-                        exception.getCode(), exception.getMessage()));
-            }
-        });
-
-        return future;
-    }
-}
\ No newline at end of file
diff --git a/src/com/android/server/telecom/voip/CallEventCallbackAckTransaction.java b/src/com/android/server/telecom/voip/CallEventCallbackAckTransaction.java
index 3d59ed3..8b4ffed 100644
--- a/src/com/android/server/telecom/voip/CallEventCallbackAckTransaction.java
+++ b/src/com/android/server/telecom/voip/CallEventCallbackAckTransaction.java
@@ -22,6 +22,7 @@
 import android.os.Bundle;
 import android.os.RemoteException;
 import android.os.ResultReceiver;
+import android.telecom.CallAttributes;
 import android.telecom.DisconnectCause;
 import android.util.Log;
 
@@ -44,7 +45,7 @@
     private final String mAction;
     private final String mCallId;
     // optional values
-    private int mVideoState = 0;
+    private int mVideoState = CallAttributes.AUDIO_CALL;
     private DisconnectCause mDisconnectCause = null;
 
     private final VoipCallTransactionResult TRANSACTION_FAILED = new VoipCallTransactionResult(
diff --git a/src/com/android/server/telecom/voip/HoldActiveCallForNewCallTransaction.java b/src/com/android/server/telecom/voip/MaybeHoldCallForNewCallTransaction.java
similarity index 88%
rename from src/com/android/server/telecom/voip/HoldActiveCallForNewCallTransaction.java
rename to src/com/android/server/telecom/voip/MaybeHoldCallForNewCallTransaction.java
index ab203ad..a245c1c 100644
--- a/src/com/android/server/telecom/voip/HoldActiveCallForNewCallTransaction.java
+++ b/src/com/android/server/telecom/voip/MaybeHoldCallForNewCallTransaction.java
@@ -26,13 +26,13 @@
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.CompletionStage;
 
-public class HoldActiveCallForNewCallTransaction extends VoipCallTransaction {
+public class MaybeHoldCallForNewCallTransaction extends VoipCallTransaction {
 
-    private static final String TAG = HoldActiveCallForNewCallTransaction.class.getSimpleName();
+    private static final String TAG = MaybeHoldCallForNewCallTransaction.class.getSimpleName();
     private final CallsManager mCallsManager;
     private final Call mCall;
 
-    public HoldActiveCallForNewCallTransaction(CallsManager callsManager, Call call) {
+    public MaybeHoldCallForNewCallTransaction(CallsManager callsManager, Call call) {
         super(callsManager.getLock());
         mCallsManager = callsManager;
         mCall = call;
diff --git a/src/com/android/server/telecom/voip/RequestFocusTransaction.java b/src/com/android/server/telecom/voip/RequestFocusTransaction.java
deleted file mode 100644
index cb4ee37..0000000
--- a/src/com/android/server/telecom/voip/RequestFocusTransaction.java
+++ /dev/null
@@ -1,66 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.telecom.voip;
-
-import android.os.OutcomeReceiver;
-import android.telecom.CallException;
-import android.util.Log;
-
-import com.android.server.telecom.Call;
-import com.android.server.telecom.CallState;
-import com.android.server.telecom.CallsManager;
-
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.CompletionStage;
-
-public class RequestFocusTransaction extends VoipCallTransaction {
-
-    private static final String TAG = RequestFocusTransaction.class.getSimpleName();
-    private final CallsManager mCallsManager;
-    private final Call mCall;
-
-    public RequestFocusTransaction(CallsManager callsManager, Call call) {
-        super(callsManager.getLock());
-        mCallsManager = callsManager;
-        mCall = call;
-    }
-
-    @Override
-    public CompletionStage<VoipCallTransactionResult> processTransaction(Void v) {
-        Log.d(TAG, "processTransaction");
-        CompletableFuture<VoipCallTransactionResult> future = new CompletableFuture<>();
-
-        mCallsManager.transactionRequestNewFocusCall(mCall, CallState.ACTIVE,
-                new OutcomeReceiver<>() {
-            @Override
-            public void onResult(Boolean result) {
-                Log.d(TAG, "processTransaction: onResult");
-                future.complete(new VoipCallTransactionResult(
-                        VoipCallTransactionResult.RESULT_SUCCEED, null));
-            }
-
-            @Override
-            public void onError(CallException exception) {
-                Log.d(TAG, "processTransaction: onError");
-                future.complete(new VoipCallTransactionResult(
-                        exception.getCode(), exception.getMessage()));
-            }
-        });
-
-        return future;
-    }
-}
\ No newline at end of file
diff --git a/src/com/android/server/telecom/voip/RequestNewActiveCallTransaction.java b/src/com/android/server/telecom/voip/RequestNewActiveCallTransaction.java
new file mode 100644
index 0000000..f586cc3
--- /dev/null
+++ b/src/com/android/server/telecom/voip/RequestNewActiveCallTransaction.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.telecom.voip;
+
+import android.os.OutcomeReceiver;
+import android.telecom.CallAttributes;
+import android.telecom.CallException;
+import android.util.Log;
+
+import com.android.server.telecom.Call;
+import com.android.server.telecom.CallState;
+import com.android.server.telecom.CallsManager;
+import com.android.server.telecom.ConnectionServiceFocusManager;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionStage;
+
+/**
+ * This transaction should be created when a requesting call would like to go from a valid inactive
+ * state (ex. HELD, RINGING, DIALING) to ACTIVE.
+ *
+ * This class performs some pre-checks to spot a failure in requesting a new call focus and sends
+ * the official request to transition the requested call to ACTIVE.
+ *
+ * Note:
+ * - This Transaction is used for CallControl and CallEventCallbacks, do not put logic in the
+ * onResult/onError that pertains to one direction.
+ * - MaybeHoldCallForNewCallTransaction was performed before this so any potential active calls
+ * should be held now.
+ */
+public class RequestNewActiveCallTransaction extends VoipCallTransaction {
+
+    private static final String TAG = RequestNewActiveCallTransaction.class.getSimpleName();
+    private final CallsManager mCallsManager;
+    private final Call mCall;
+
+    public RequestNewActiveCallTransaction(CallsManager callsManager, Call call) {
+        super(callsManager.getLock());
+        mCallsManager = callsManager;
+        mCall = call;
+    }
+
+    @Override
+    public CompletionStage<VoipCallTransactionResult> processTransaction(Void v) {
+        Log.d(TAG, "processTransaction");
+        CompletableFuture<VoipCallTransactionResult> future = new CompletableFuture<>();
+        int currentCallState = mCall.getState();
+
+        // certain calls cannot go active/answered (ex. disconnect calls, etc.)
+        if (!canBecomeNewCallFocus(currentCallState)) {
+            future.complete(new VoipCallTransactionResult(
+                    CallException.CODE_CALL_CANNOT_BE_SET_TO_ACTIVE,
+                    "CallState cannot be set to active or answered due to current call"
+                            + " state being in invalid state"));
+            return future;
+        }
+
+        if (mCallsManager.getActiveCall() != null) {
+            future.complete(new VoipCallTransactionResult(
+                    CallException.CODE_CALL_CANNOT_BE_SET_TO_ACTIVE,
+                    "Already an active call. Request hold on current active call."));
+            return future;
+        }
+
+        mCallsManager.requestNewCallFocusAndVerify(mCall, new OutcomeReceiver<>() {
+                    @Override
+                    public void onResult(Boolean result) {
+                        Log.d(TAG, "processTransaction: onResult");
+                        future.complete(new VoipCallTransactionResult(
+                                VoipCallTransactionResult.RESULT_SUCCEED, null));
+                    }
+
+                    @Override
+                    public void onError(CallException exception) {
+                        Log.d(TAG, "processTransaction: onError");
+                        future.complete(new VoipCallTransactionResult(
+                                exception.getCode(), exception.getMessage()));
+                    }
+                });
+
+        return future;
+    }
+
+    private boolean isPriorityCallingState(int currentCallState) {
+        return ConnectionServiceFocusManager.PRIORITY_FOCUS_CALL_STATE.contains(currentCallState);
+    }
+
+    private boolean canBecomeNewCallFocus(int currentCallState) {
+        return isPriorityCallingState(currentCallState) || currentCallState == CallState.ON_HOLD;
+    }
+}
\ No newline at end of file
diff --git a/tests/src/com/android/server/telecom/tests/TransactionTests.java b/tests/src/com/android/server/telecom/tests/TransactionTests.java
index 1e6734b..3fc87a9 100644
--- a/tests/src/com/android/server/telecom/tests/TransactionTests.java
+++ b/tests/src/com/android/server/telecom/tests/TransactionTests.java
@@ -47,13 +47,12 @@
 import com.android.server.telecom.PhoneNumberUtilsAdapter;
 import com.android.server.telecom.TelecomSystem;
 import com.android.server.telecom.ui.ToastFactory;
-import com.android.server.telecom.voip.AnswerCallTransaction;
 import com.android.server.telecom.voip.EndCallTransaction;
 import com.android.server.telecom.voip.HoldCallTransaction;
 import com.android.server.telecom.voip.IncomingCallTransaction;
 import com.android.server.telecom.voip.OutgoingCallTransaction;
-import com.android.server.telecom.voip.HoldActiveCallForNewCallTransaction;
-import com.android.server.telecom.voip.RequestFocusTransaction;
+import com.android.server.telecom.voip.MaybeHoldCallForNewCallTransaction;
+import com.android.server.telecom.voip.RequestNewActiveCallTransaction;
 
 import org.junit.After;
 import org.junit.Before;
@@ -143,40 +142,56 @@
     }
 
     @Test
-    public void testTransactionalRequestFocus() throws Exception {
+    public void testRequestNewCallFocusWithDialingCall() throws Exception {
         // GIVEN
-        RequestFocusTransaction transaction =
-                new RequestFocusTransaction(mCallsManager, mMockCall1);
+        RequestNewActiveCallTransaction transaction =
+                new RequestNewActiveCallTransaction(mCallsManager, mMockCall1);
 
         // WHEN
+        when(mMockCall1.getState()).thenReturn(CallState.DIALING);
         transaction.processTransaction(null);
 
         // THEN
         verify(mCallsManager, times(1))
-                .transactionRequestNewFocusCall(eq(mMockCall1), eq(CallState.ACTIVE),
-                        isA(OutcomeReceiver.class));
+                .requestNewCallFocusAndVerify(eq(mMockCall1), isA(OutcomeReceiver.class));
     }
 
     @Test
-    public void testAnswerCallTransaction() throws Exception {
+    public void testRequestNewCallFocusWithRingingCall() throws Exception {
         // GIVEN
-        AnswerCallTransaction transaction =
-                new AnswerCallTransaction(mCallsManager, mMockCall1, 0);
+        RequestNewActiveCallTransaction transaction =
+                new RequestNewActiveCallTransaction(mCallsManager, mMockCall1);
 
         // WHEN
+        when(mMockCall1.getState()).thenReturn(CallState.RINGING);
         transaction.processTransaction(null);
 
         // THEN
         verify(mCallsManager, times(1))
-                .transactionRequestNewFocusCall(eq(mMockCall1), eq(CallState.ANSWERED),
-                        isA(OutcomeReceiver.class));
+                .requestNewCallFocusAndVerify(eq(mMockCall1), isA(OutcomeReceiver.class));
+    }
+
+    @Test
+    public void testRequestNewCallFocusFailure() throws Exception {
+        // GIVEN
+        RequestNewActiveCallTransaction transaction =
+                new RequestNewActiveCallTransaction(mCallsManager, mMockCall1);
+
+        // WHEN
+        when(mMockCall1.getState()).thenReturn(CallState.DISCONNECTING);
+        when(mCallsManager.getActiveCall()).thenReturn(null);
+        transaction.processTransaction(null);
+
+        // THEN
+        verify(mCallsManager, times(0))
+                .requestNewCallFocusAndVerify( eq(mMockCall1), isA(OutcomeReceiver.class));
     }
 
     @Test
     public void testTransactionalHoldActiveCallForNewCall() throws Exception {
         // GIVEN
-        HoldActiveCallForNewCallTransaction transaction =
-                new HoldActiveCallForNewCallTransaction(mCallsManager, mMockCall1);
+        MaybeHoldCallForNewCallTransaction transaction =
+                new MaybeHoldCallForNewCallTransaction(mCallsManager, mMockCall1);
 
         // WHEN
         transaction.processTransaction(null);