Merge "Add notif. and foreground service to transactional test app" into udc-dev
diff --git a/src/com/android/server/telecom/CallsManager.java b/src/com/android/server/telecom/CallsManager.java
index e195fd2..d775350 100644
--- a/src/com/android/server/telecom/CallsManager.java
+++ b/src/com/android/server/telecom/CallsManager.java
@@ -6090,34 +6090,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;
         }
@@ -6130,12 +6149,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/src/com/android/server/telecom/voip/TransactionManager.java b/src/com/android/server/telecom/voip/TransactionManager.java
index a0955c8..98faf3d 100644
--- a/src/com/android/server/telecom/voip/TransactionManager.java
+++ b/src/com/android/server/telecom/voip/TransactionManager.java
@@ -16,6 +16,8 @@
 
 package com.android.server.telecom.voip;
 
+import static android.telecom.CallException.CODE_OPERATION_TIMED_OUT;
+
 import android.os.OutcomeReceiver;
 import android.telecom.TelecomManager;
 import android.telecom.CallException;
@@ -79,8 +81,8 @@
 
                 @Override
                 public void onTransactionTimeout(String transactionName){
-                    receiver.onResult(new VoipCallTransactionResult(
-                            VoipCallTransactionResult.RESULT_FAILED, transactionName + " timeout"));
+                    receiver.onError(new CallException(transactionName + " timeout",
+                            CODE_OPERATION_TIMED_OUT));
                     finishTransaction();
                 }
             });
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);
diff --git a/tests/src/com/android/server/telecom/tests/VoipCallTransactionTest.java b/tests/src/com/android/server/telecom/tests/VoipCallTransactionTest.java
index 62b8bc4..e2c7b7b 100644
--- a/tests/src/com/android/server/telecom/tests/VoipCallTransactionTest.java
+++ b/tests/src/com/android/server/telecom/tests/VoipCallTransactionTest.java
@@ -211,7 +211,7 @@
         subTransactions.add(t3);
         CompletableFuture<String> exceptionFuture = new CompletableFuture<>();
         OutcomeReceiver<VoipCallTransactionResult, CallException> outcomeReceiver =
-                new OutcomeReceiver<VoipCallTransactionResult, CallException>() {
+                new OutcomeReceiver<>() {
             @Override
             public void onResult(VoipCallTransactionResult result) {
 
@@ -234,12 +234,20 @@
             throws ExecutionException, InterruptedException, TimeoutException {
         VoipCallTransaction t = new TestVoipCallTransaction("t", 10000L,
                 TestVoipCallTransaction.SUCCESS);
-        CompletableFuture<VoipCallTransactionResult> resultFuture = new CompletableFuture<>();
+        CompletableFuture<String> exceptionFuture = new CompletableFuture<>();
         OutcomeReceiver<VoipCallTransactionResult, CallException> outcomeReceiver =
-                resultFuture::complete;
-        mTransactionManager.addTransaction(t, outcomeReceiver);
-        VoipCallTransactionResult result = resultFuture.get(7000L, TimeUnit.MILLISECONDS);
-        assertEquals(VoipCallTransactionResult.RESULT_FAILED, result.getResult());
-        assertTrue(result.getMessage().contains("timeout"));
+                new OutcomeReceiver<>() {
+                    @Override
+                    public void onResult(VoipCallTransactionResult result) {
+
+                    }
+
+                    @Override
+                    public void onError(CallException e) {
+                        exceptionFuture.complete(e.getMessage());
+                    }
+                };        mTransactionManager.addTransaction(t, outcomeReceiver);
+        String message = exceptionFuture.get(7000L, TimeUnit.MILLISECONDS);
+        assertTrue(message.contains("timeout"));
     }
 }