Merge "Add notification cache for VoipCallMonitor to avoid match failure between notifications and calls." into udc-dev
diff --git a/src/com/android/server/telecom/CallsManager.java b/src/com/android/server/telecom/CallsManager.java
index 9503c10..e195fd2 100644
--- a/src/com/android/server/telecom/CallsManager.java
+++ b/src/com/android/server/telecom/CallsManager.java
@@ -3656,40 +3656,57 @@
      * @param call The call.
      */
     private void performRemoval(Call call) {
-        mInCallController.getBindingFuture().thenRunAsync(() -> {
-            call.maybeCleanupHandover();
-            removeCall(call);
-            Call foregroundCall = mCallAudioManager.getPossiblyHeldForegroundCall();
-            if (mLocallyDisconnectingCalls.contains(call)) {
-                boolean isDisconnectingChildCall = call.isDisconnectingChildCall();
-                Log.v(this, "performRemoval: isDisconnectingChildCall = "
-                        + isDisconnectingChildCall + "call -> %s", call);
-                mLocallyDisconnectingCalls.remove(call);
-                // Auto-unhold the foreground call due to a locally disconnected call, except if the
-                // call which was disconnected is a member of a conference (don't want to auto
-                // un-hold the conference if we remove a member of the conference).
-                if (!isDisconnectingChildCall && foregroundCall != null
-                        && foregroundCall.getState() == CallState.ON_HOLD) {
-                    foregroundCall.unhold();
-                }
-            } else if (foregroundCall != null &&
-                    !foregroundCall.can(Connection.CAPABILITY_SUPPORT_HOLD) &&
-                    foregroundCall.getState() == CallState.ON_HOLD) {
+        if (mInCallController.getBindingFuture() != null) {
+            mInCallController.getBindingFuture().thenRunAsync(() -> {
+                        doRemoval(call);
+                    }, new LoggedHandlerExecutor(mHandler, "CM.pR", mLock))
+                    .exceptionally((throwable) -> {
+                        Log.e(TAG, throwable, "Error while executing call removal");
+                        mAnomalyReporter.reportAnomaly(CALL_REMOVAL_EXECUTION_ERROR_UUID,
+                                CALL_REMOVAL_EXECUTION_ERROR_MSG);
+                        return null;
+                    });
+        } else {
+            doRemoval(call);
+        }
+    }
 
-                // The new foreground call is on hold, however the carrier does not display the hold
-                // button in the UI.  Therefore, we need to auto unhold the held call since the user
-                // has no means of unholding it themselves.
-                Log.i(this, "performRemoval: Auto-unholding held foreground call (call doesn't "
-                        + "support hold)");
+    /**
+     * Code to perform removal of a call.  Called above from {@link #performRemoval(Call)} either
+     * async (in live code) or sync (in testing).
+     * @param call the call to remove.
+     */
+    private void doRemoval(Call call) {
+        call.maybeCleanupHandover();
+        removeCall(call);
+        Call foregroundCall = mCallAudioManager.getPossiblyHeldForegroundCall();
+        if (mLocallyDisconnectingCalls.contains(call)) {
+            boolean isDisconnectingChildCall = call.isDisconnectingChildCall();
+            Log.v(this, "performRemoval: isDisconnectingChildCall = "
+                    + isDisconnectingChildCall + "call -> %s", call);
+            mLocallyDisconnectingCalls.remove(call);
+            // Auto-unhold the foreground call due to a locally disconnected call, except if the
+            // call which was disconnected is a member of a conference (don't want to auto
+            // un-hold the conference if we remove a member of the conference).
+            // Also, ensure that the call we're removing is from the same ConnectionService as
+            // the one we're removing.  We don't want to auto-unhold between ConnectionService
+            // implementations, especially if one is managed and the other is a VoIP CS.
+            if (!isDisconnectingChildCall && foregroundCall != null
+                    && foregroundCall.getState() == CallState.ON_HOLD
+                    && areFromSameSource(foregroundCall, call)) {
                 foregroundCall.unhold();
             }
-        }, new LoggedHandlerExecutor(mHandler, "CM.pR", mLock))
-                .exceptionally((throwable) -> {
-                    Log.e(TAG, throwable, "Error while executing call removal");
-                    mAnomalyReporter.reportAnomaly(CALL_REMOVAL_EXECUTION_ERROR_UUID,
-                            CALL_REMOVAL_EXECUTION_ERROR_MSG);
-                    return null;
-                });
+        } else if (foregroundCall != null &&
+                !foregroundCall.can(Connection.CAPABILITY_SUPPORT_HOLD) &&
+                foregroundCall.getState() == CallState.ON_HOLD) {
+
+            // The new foreground call is on hold, however the carrier does not display the hold
+            // button in the UI.  Therefore, we need to auto unhold the held call since the user
+            // has no means of unholding it themselves.
+            Log.i(this, "performRemoval: Auto-unholding held foreground call (call doesn't "
+                    + "support hold)");
+            foregroundCall.unhold();
+        }
     }
 
     /**
diff --git a/tests/src/com/android/server/telecom/tests/CallsManagerTest.java b/tests/src/com/android/server/telecom/tests/CallsManagerTest.java
index 7ea3568..129bba2 100644
--- a/tests/src/com/android/server/telecom/tests/CallsManagerTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallsManagerTest.java
@@ -688,6 +688,54 @@
         verify(heldCall).unhold(any());
     }
 
+    /**
+     * Ensures we don't auto-unhold a call from a different app when we locally disconnect a call.
+     */
+    @SmallTest
+    @Test
+    public void testDontUnholdCallsBetweenConnectionServices() {
+        // GIVEN a CallsManager with ongoing call
+        Call ongoingCall = addSpyCall(SIM_1_HANDLE, CallState.ACTIVE);
+        when(ongoingCall.isDisconnectHandledViaFuture()).thenReturn(false);
+        doReturn(true).when(ongoingCall).can(Connection.CAPABILITY_HOLD);
+        doReturn(true).when(ongoingCall).can(Connection.CAPABILITY_SUPPORT_HOLD);
+        when(mConnectionSvrFocusMgr.getCurrentFocusCall()).thenReturn(ongoingCall);
+
+        // and a held call which has different ConnectionService
+        Call heldCall = addSpyCall(VOIP_1_HANDLE, CallState.ON_HOLD);
+
+        // Disconnect and cleanup the active ongoing call.
+        mCallsManager.disconnectCall(ongoingCall);
+        mCallsManager.markCallAsRemoved(ongoingCall);
+
+        // Should not unhold the held call since its in another app.
+        verify(heldCall, never()).unhold();
+    }
+
+    /**
+     * Ensures we do auto-unhold a call from the same app when we locally disconnect a call.
+     */
+    @SmallTest
+    @Test
+    public void testUnholdCallWhenDisconnectingInSameApp() {
+        // GIVEN a CallsManager with ongoing call
+        Call ongoingCall = addSpyCall(SIM_1_HANDLE, CallState.ACTIVE);
+        when(ongoingCall.isDisconnectHandledViaFuture()).thenReturn(false);
+        doReturn(true).when(ongoingCall).can(Connection.CAPABILITY_HOLD);
+        doReturn(true).when(ongoingCall).can(Connection.CAPABILITY_SUPPORT_HOLD);
+        when(mConnectionSvrFocusMgr.getCurrentFocusCall()).thenReturn(ongoingCall);
+
+        // and a held call which has same ConnectionService
+        Call heldCall = addSpyCall(SIM_1_HANDLE, CallState.ON_HOLD);
+
+        // Disconnect and cleanup the active ongoing call.
+        mCallsManager.disconnectCall(ongoingCall);
+        mCallsManager.markCallAsRemoved(ongoingCall);
+
+        // Should auto-unhold the held call since its in the same app.
+        verify(heldCall).unhold();
+    }
+
     @SmallTest
     @Test
     public void testUnholdCallWhenOngoingEmergCallCanNotBeHeldAndFromDifferentConnectionService() {