Clear IMS call information when SRVCC has completed

Bug: 255450284
Test: atest ImsCallInfoNotifierTest
Change-Id: I82bd36dd736b2770e4ada47c4cb59e36318640a3
diff --git a/src/java/com/android/internal/telephony/imsphone/ImsCallInfo.java b/src/java/com/android/internal/telephony/imsphone/ImsCallInfo.java
index 718a988..79ab9c5 100644
--- a/src/java/com/android/internal/telephony/imsphone/ImsCallInfo.java
+++ b/src/java/com/android/internal/telephony/imsphone/ImsCallInfo.java
@@ -78,6 +78,11 @@
         return changed;
     }
 
+    /** Called when clearing orphaned connection. */
+    public void onDisconnect() {
+        mState = Call.State.DISCONNECTED;
+    }
+
     /** @return the call index. */
     public int getIndex() {
         return mIndex;
diff --git a/src/java/com/android/internal/telephony/imsphone/ImsCallInfoTracker.java b/src/java/com/android/internal/telephony/imsphone/ImsCallInfoTracker.java
index aabe8d3..5783e48 100644
--- a/src/java/com/android/internal/telephony/imsphone/ImsCallInfoTracker.java
+++ b/src/java/com/android/internal/telephony/imsphone/ImsCallInfoTracker.java
@@ -25,6 +25,7 @@
 import com.android.internal.telephony.Call;
 import com.android.internal.telephony.Connection;
 import com.android.internal.telephony.Phone;
+import com.android.telephony.Rlog;
 
 import java.util.ArrayList;
 import java.util.Collection;
@@ -39,6 +40,8 @@
  * Contains the state of all IMS calls.
  */
 public class ImsCallInfoTracker {
+    private static final String LOG_TAG = "ImsCallInfoTracker";
+    private static final boolean DBG = false;
 
     private final Phone mPhone;
     private final List<ImsCallInfo> mQueue = new ArrayList<>();
@@ -56,6 +59,8 @@
      * @param c The instance of {@link ImsPhoneConnection}.
      */
     public void addImsCallStatus(@NonNull ImsPhoneConnection c) {
+        if (DBG) Rlog.d(LOG_TAG, "addImsCallStatus");
+
         synchronized (mImsCallInfo) {
             if (mQueue.isEmpty()) {
                 mQueue.add(new ImsCallInfo(mNextIndex++));
@@ -69,6 +74,8 @@
             mImsCallInfo.put(c, imsCallInfo);
 
             notifyImsCallStatus();
+
+            if (DBG) dump();
         }
     }
 
@@ -90,16 +97,26 @@
      */
     public void updateImsCallStatus(@NonNull ImsPhoneConnection c,
             boolean holdReceived, boolean resumeReceived) {
+        if (DBG) {
+            Rlog.d(LOG_TAG, "updateImsCallStatus holdReceived=" + holdReceived
+                    + ", resumeReceived=" + resumeReceived);
+        }
 
         synchronized (mImsCallInfo) {
             ImsCallInfo info = mImsCallInfo.get(c);
 
+            if (info == null) {
+                // This happens when the user tries to hangup the call after handover has completed.
+                return;
+            }
+
             boolean changed = info.update(c, holdReceived, resumeReceived);
 
             if (changed) notifyImsCallStatus();
 
             Call.State state = c.getState();
 
+            if (DBG) Rlog.d(LOG_TAG, "updateImsCallStatus state=" + state);
             // Call is disconnected. There are 2 cases in disconnected state:
             // if silent redial, state == IDLE, otherwise, state == DISCONNECTED.
             if (state == DISCONNECTED || state == IDLE) {
@@ -113,6 +130,42 @@
                     mNextIndex--;
                 }
             }
+
+            if (DBG) dump();
+        }
+    }
+
+    /** Clears all orphaned IMS call information. */
+    public void clearAllOrphanedConnections() {
+        if (DBG) Rlog.d(LOG_TAG, "clearAllOrphanedConnections");
+
+        Collection<ImsCallInfo> infos = mImsCallInfo.values();
+        infos.stream().forEach(info -> { info.onDisconnect(); });
+        notifyImsCallStatus();
+        clearAllCallInfo();
+
+        if (DBG) dump();
+    }
+
+    /** Notifies that SRVCC has completed. */
+    public void notifySrvccCompleted() {
+        if (DBG) Rlog.d(LOG_TAG, "notifySrvccCompleted");
+
+        clearAllCallInfo();
+        notifyImsCallStatus();
+
+        if (DBG) dump();
+    }
+
+    private void clearAllCallInfo() {
+        try {
+            Collection<ImsCallInfo> infos = mImsCallInfo.values();
+            infos.stream().forEach(info -> { info.reset(); });
+            mImsCallInfo.clear();
+            mQueue.clear();
+            mNextIndex = 1;
+        } catch (UnsupportedOperationException e) {
+            Rlog.e(LOG_TAG, "e=" + e);
         }
     }
 
@@ -142,4 +195,11 @@
             }
         });
     }
+
+    private void dump() {
+        Collection<ImsCallInfo> infos = mImsCallInfo.values();
+        ArrayList<ImsCallInfo> imsCallInfo = new ArrayList<ImsCallInfo>(infos);
+        sort(imsCallInfo);
+        Rlog.d(LOG_TAG, "imsCallInfos=" + imsCallInfo);
+    }
 }
diff --git a/src/java/com/android/internal/telephony/imsphone/ImsPhoneCallTracker.java b/src/java/com/android/internal/telephony/imsphone/ImsPhoneCallTracker.java
index 59f01ec..6840f4b 100644
--- a/src/java/com/android/internal/telephony/imsphone/ImsPhoneCallTracker.java
+++ b/src/java/com/android/internal/telephony/imsphone/ImsPhoneCallTracker.java
@@ -1479,6 +1479,7 @@
         // above. Remove all references to it.
         mPendingMO = null;
         updatePhoneState();
+        mImsCallInfoTracker.clearAllOrphanedConnections();
     }
 
     /**
@@ -4658,6 +4659,7 @@
                 transferHandoverConnections(mBackgroundCall);
                 transferHandoverConnections(mRingingCall);
                 updatePhoneState();
+                mImsCallInfoTracker.notifySrvccCompleted();
                 break;
 
             case TelephonyManager.SRVCC_STATE_HANDOVER_FAILED:
diff --git a/tests/telephonytests/src/com/android/internal/telephony/imsphone/ImsCallInfoTrackerTest.java b/tests/telephonytests/src/com/android/internal/telephony/imsphone/ImsCallInfoTrackerTest.java
index a0fc4b9..e3fc6d3 100644
--- a/tests/telephonytests/src/com/android/internal/telephony/imsphone/ImsCallInfoTrackerTest.java
+++ b/tests/telephonytests/src/com/android/internal/telephony/imsphone/ImsCallInfoTrackerTest.java
@@ -363,6 +363,60 @@
         assertEquals(2, imsCallInfos.get(1).getIndex());
     }
 
+    @Test
+    public void testSrvccCompleted() throws Exception {
+        ArgumentCaptor<List<ImsCallInfo>> captor = ArgumentCaptor.forClass(List.class);
+
+        ImsPhoneConnection c = getConnection(Call.State.DIALING, false);
+        mImsCallInfoTracker.addImsCallStatus(c);
+
+        verify(mImsPhone, times(1)).updateImsCallStatus(captor.capture(), any());
+
+        List<ImsCallInfo> imsCallInfos = captor.getValue();
+
+        assertNotNull(imsCallInfos);
+        assertEquals(1, imsCallInfos.size());
+
+        mImsCallInfoTracker.notifySrvccCompleted();
+
+        verify(mImsPhone, times(2)).updateImsCallStatus(captor.capture(), any());
+
+        imsCallInfos = captor.getValue();
+
+        assertNotNull(imsCallInfos);
+        assertEquals(0, imsCallInfos.size());
+    }
+
+    @Test
+    public void testClearAllOrphanedConnections() throws Exception {
+        ArgumentCaptor<List<ImsCallInfo>> captor = ArgumentCaptor.forClass(List.class);
+
+        ImsPhoneConnection c = getConnection(Call.State.DIALING, false);
+        mImsCallInfoTracker.addImsCallStatus(c);
+
+        verify(mImsPhone, times(1)).updateImsCallStatus(captor.capture(), any());
+
+        List<ImsCallInfo> imsCallInfos = captor.getValue();
+
+        assertNotNull(imsCallInfos);
+        assertEquals(1, imsCallInfos.size());
+
+        mImsCallInfoTracker.clearAllOrphanedConnections();
+
+        verify(mImsPhone, times(2)).updateImsCallStatus(captor.capture(), any());
+
+        imsCallInfos = captor.getValue();
+
+        assertNotNull(imsCallInfos);
+        assertEquals(1, imsCallInfos.size());
+
+        ImsCallInfo info = imsCallInfos.get(0);
+
+        assertNotNull(info);
+        assertEquals(1, info.getIndex());
+        assertEquals(Call.State.IDLE, info.getCallState());
+    }
+
     private ImsPhoneConnection getConnection(Call.State state, boolean isEmergency) {
         ImsPhoneConnection c = mock(ImsPhoneConnection.class);
         doReturn(isEmergency).when(c).isEmergencyCall();
diff --git a/tests/telephonytests/src/com/android/internal/telephony/imsphone/ImsPhoneCallTrackerTest.java b/tests/telephonytests/src/com/android/internal/telephony/imsphone/ImsPhoneCallTrackerTest.java
index 6ab7651..0af95e8 100644
--- a/tests/telephonytests/src/com/android/internal/telephony/imsphone/ImsPhoneCallTrackerTest.java
+++ b/tests/telephonytests/src/com/android/internal/telephony/imsphone/ImsPhoneCallTrackerTest.java
@@ -2464,6 +2464,41 @@
         verify(mImsPhone, times(7)).updateImsCallStatus(any(), any());
     }
 
+    @Test
+    public void testUpdateImsCallStatusSrvccCompleted() throws Exception {
+        // Incoming call
+        setupRingingConnection();
+
+        verify(mImsPhone, times(1)).updateImsCallStatus(any(), any());
+
+        // no interaction when SRVCC has started, failed, or canceled.
+        mCTUT.notifySrvccState(SRVCC_STATE_HANDOVER_STARTED);
+
+        verify(mImsPhone, times(1)).updateImsCallStatus(any(), any());
+
+        mCTUT.notifySrvccState(SRVCC_STATE_HANDOVER_FAILED);
+
+        verify(mImsPhone, times(1)).updateImsCallStatus(any(), any());
+
+        mCTUT.notifySrvccState(SRVCC_STATE_HANDOVER_CANCELED);
+
+        verify(mImsPhone, times(1)).updateImsCallStatus(any(), any());
+
+        // interaction when SRVCC has completed
+        mCTUT.notifySrvccState(SRVCC_STATE_HANDOVER_COMPLETED);
+
+        verify(mImsPhone, times(2)).updateImsCallStatus(any(), any());
+    }
+
+    @Test
+    public void testClearAllOrphanedConnectionInfo() throws Exception {
+        verify(mImsPhone, times(0)).updateImsCallStatus(any(), any());
+
+        mConnectorListener.connectionUnavailable(FeatureConnector.UNAVAILABLE_REASON_DISCONNECTED);
+
+        verify(mImsPhone, times(1)).updateImsCallStatus(any(), any());
+    }
+
     /** Verifies that the request from ImsService is passed to ImsPhone as expected. */
     @Test
     @SmallTest