Merge "DSDA: Handle call resume failure" into main
diff --git a/flags/telecom_call_flags.aconfig b/flags/telecom_call_flags.aconfig
index 8e15910..0000f32 100644
--- a/flags/telecom_call_flags.aconfig
+++ b/flags/telecom_call_flags.aconfig
@@ -77,3 +77,11 @@
       purpose: PURPOSE_BUGFIX
     }
 }
+
+# OWNER=pmadapurmath TARGET=25Q4
+flag {
+  name: "call_sequencing_call_resume_failed"
+  namespace: "telecom"
+  description: "Connection event received when a call resume fails"
+  bug: "390116261"
+}
diff --git a/src/com/android/server/telecom/Call.java b/src/com/android/server/telecom/Call.java
index 40f3aa7..a54a3b6 100644
--- a/src/com/android/server/telecom/Call.java
+++ b/src/com/android/server/telecom/Call.java
@@ -205,6 +205,7 @@
         default void onHoldToneRequested(Call call) {};
         default void onCallHoldFailed(Call call) {};
         default void onCallSwitchFailed(Call call) {};
+        default void onCallResumeFailed(Call call) {};
         default void onConnectionEvent(Call call, String event, Bundle extras) {};
         default void onCallStreamingStateChanged(Call call, boolean isStreaming) {}
         default void onExternalCallChanged(Call call, boolean isExternalCall) {};
@@ -295,6 +296,8 @@
         @Override
         public void onCallSwitchFailed(Call call) {}
         @Override
+        public void onCallResumeFailed(Call call) {}
+        @Override
         public void onConnectionEvent(Call call, String event, Bundle extras) {}
         @Override
         public void onCallStreamingStateChanged(Call call, boolean isStreaming) {}
@@ -4545,6 +4548,10 @@
             for (Listener l : mListeners) {
                 l.onCallSwitchFailed(this);
             }
+        } else if (Connection.EVENT_CALL_RESUME_FAILED.equals(event)) {
+            for (Listener l : mListeners) {
+                l.onCallResumeFailed(this);
+            }
         } else if (Connection.EVENT_DEVICE_TO_DEVICE_MESSAGE.equals(event)
                 && extras != null && extras.containsKey(
                 Connection.EXTRA_DEVICE_TO_DEVICE_MESSAGE_TYPE)
diff --git a/src/com/android/server/telecom/CallsManager.java b/src/com/android/server/telecom/CallsManager.java
index 247fd0b..4d59fc8 100644
--- a/src/com/android/server/telecom/CallsManager.java
+++ b/src/com/android/server/telecom/CallsManager.java
@@ -1477,6 +1477,14 @@
         markAllAnsweredCallAsRinging(call, "switch");
     }
 
+    @Override
+    public void onCallResumeFailed(Call call) {
+        Call heldCall = getFirstCallWithState(call, true /* skipSelfManaged */, CallState.ON_HOLD);
+        if (heldCall != null) {
+            mCallSequencingAdapter.handleCallResumeFailed(call, heldCall);
+        }
+    }
+
     private void markAllAnsweredCallAsRinging(Call call, String actionName) {
         // Normally, we don't care whether a call hold or switch has failed.
         // However, if a call was held or switched in order to answer an incoming call, that
diff --git a/src/com/android/server/telecom/callsequencing/CallsManagerCallSequencingAdapter.java b/src/com/android/server/telecom/callsequencing/CallsManagerCallSequencingAdapter.java
index f9ee8d7..611bb9e 100644
--- a/src/com/android/server/telecom/callsequencing/CallsManagerCallSequencingAdapter.java
+++ b/src/com/android/server/telecom/callsequencing/CallsManagerCallSequencingAdapter.java
@@ -364,6 +364,20 @@
         });
     }
 
+    /**
+     * Upon a call resume failure, we will auto-unhold the foreground call that was held. Note that
+     * this should only apply for calls across phone accounts as the ImsPhoneCallTracker handles
+     * this for a single phone.
+     * @param callResumeFailed The call that failed to resume.
+     * @param callToUnhold The fg call that was held.
+     */
+    public void handleCallResumeFailed(Call callResumeFailed, Call callToUnhold) {
+        if (mIsCallSequencingEnabled && !mSequencingController.arePhoneAccountsSame(
+                callResumeFailed, callToUnhold)) {
+            unholdCall(callToUnhold);
+        }
+    }
+
     public Handler getHandler() {
         return mHandler;
     }
diff --git a/src/com/android/server/telecom/callsequencing/VerifyCallStateChangeTransaction.java b/src/com/android/server/telecom/callsequencing/VerifyCallStateChangeTransaction.java
index 7bebb55..b7e4f04 100644
--- a/src/com/android/server/telecom/callsequencing/VerifyCallStateChangeTransaction.java
+++ b/src/com/android/server/telecom/callsequencing/VerifyCallStateChangeTransaction.java
@@ -18,8 +18,10 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.telecom.Call;
+import com.android.server.telecom.CallState;
 import com.android.server.telecom.TelecomSystem;
 
+import android.telecom.CallException;
 import android.telecom.Log;
 
 import java.util.Set;
@@ -56,6 +58,26 @@
         }
     };
 
+    private final Call.ListenerBase mCallListenerImpl = new Call.ListenerBase() {
+        @Override
+        public void onCallHoldFailed(Call call) {
+            if (call.equals(mCall) && mTargetCallStates.contains(CallState.ON_HOLD)) {
+                // Fail the transaction if a call hold failure is received.
+                mTransactionResult.complete(new CallTransactionResult(
+                        CallException.CODE_CANNOT_HOLD_CURRENT_ACTIVE_CALL, "error holding call"));
+            }
+        }
+        @Override
+        public void onCallResumeFailed(Call call) {
+            if (call.equals(mCall) && mTargetCallStates.contains(CallState.ACTIVE)) {
+                // Fail the transaction if a call resume failure is received (this means that the
+                // current call could not be unheld).
+                mTransactionResult.complete(new CallTransactionResult(
+                        CallException.CODE_CALL_CANNOT_BE_SET_TO_ACTIVE, "error unholding call"));
+            }
+        }
+    };
+
     public VerifyCallStateChangeTransaction(TelecomSystem.SyncRoot lock,  Call call,
             int... targetCallStates) {
         super(lock, CALL_STATE_TIMEOUT_MILLISECONDS);
@@ -73,12 +95,14 @@
             return mTransactionResult;
         }
         mCall.addCallStateListener(mCallStateListenerImpl);
+        mCall.addListener(mCallListenerImpl);
         return mTransactionResult;
     }
 
     @Override
     public void finishTransaction() {
         mCall.removeCallStateListener(mCallStateListenerImpl);
+        mCall.removeListener(mCallListenerImpl);
     }
 
     private boolean isNewCallStateTargetCallState() {
diff --git a/tests/src/com/android/server/telecom/tests/CallTest.java b/tests/src/com/android/server/telecom/tests/CallTest.java
index 3a7a822..b2cdd7d 100644
--- a/tests/src/com/android/server/telecom/tests/CallTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallTest.java
@@ -999,6 +999,7 @@
         @Test
     @SmallTest
     public void testOnConnectionEventNotifiesListener() {
+        when(mFeatureFlags.enableCallSequencing()).thenReturn(true);
         Call.Listener listener = mock(Call.Listener.class);
         Call call = createCall("1");
         call.addListener(listener);
@@ -1017,6 +1018,9 @@
         call.onConnectionEvent(Connection.EVENT_CALL_SWITCH_FAILED, null);
         verify(listener).onCallSwitchFailed(call);
 
+        call.onConnectionEvent(Connection.EVENT_CALL_RESUME_FAILED, null);
+        verify(listener).onCallResumeFailed(call);
+
         final int d2dType = 1;
         final int d2dValue = 2;
         final Bundle d2dExtras = new Bundle();