Complete implementation of ImsCallSession merge()

This CL implements the rest of the logic around the various callbacks
that are issued from the ImsCallSession to implement the merging of
two calls into a conference.

Bug: 18056632
Change-Id: I183a3b2c49d4192e14813333339440d746febdbf
diff --git a/src/java/com/android/ims/ImsCall.java b/src/java/com/android/ims/ImsCall.java
index 3bebbfa..3ecf1b3 100644
--- a/src/java/com/android/ims/ImsCall.java
+++ b/src/java/com/android/ims/ImsCall.java
@@ -412,7 +412,19 @@
     // Media session to control media (audio/video) operations for an IMS call
     private ImsStreamMediaSession mMediaSession = null;
 
+    // The temporary ImsCallSession that could represent the merged call once
+    // we receive notification that the merge was successful.
     private ImsCallSession mTransientConferenceSession = null;
+    // While a merge is progressing, we bury any session termination requests
+    // made on the original ImsCallSession until we have closure on the merge request
+    // If the request ultimately fails, we need to act on the termination request
+    // that we buried temporarily. We do this because we feel that timing issues could
+    // cause the termination request to occur just because the merge is succeeding.
+    private boolean mSessionEndDuringMerge = false;
+    // Just like mSessionEndDuringMerge, we need to keep track of the reason why the
+    // termination request was made on the original session in case we need to act
+    // on it in the case of a merge failure.
+    private ImsReasonInfo mSessionEndDuringMergeReasonInfo = null;
 
     /**
      * Create an IMS call object.
@@ -1080,7 +1092,17 @@
             if (mHold || (mContext.getResources().getBoolean(
                     com.android.internal.R.bool.skipHoldBeforeMerge))) {
                 mSession.merge();
-                mUpdateRequest = UPDATE_MERGE;
+
+                // Check to see if there is an owner to a valid call group.  If this is the
+                // case, then we already have a conference call.
+                if (mCallGroup != null && mCallGroup.getOwner() == null) {
+                    // We only set UPDATE_MERGE when we are adding the first
+                    // calls to the Conference.  If there is already a conference
+                    // no special handling is needed.The existing conference
+                    // session will just go active and any other sessions will be terminated
+                    // if needed.  There will be no merge failed callback.
+                    mUpdateRequest = UPDATE_MERGE;
+                }
             } else {
                 // This code basically says, we need to explicitly hold before requesting a merge
                 // when we get the callback that the hold was successful (or failed), we should
@@ -1618,6 +1640,59 @@
         }
     }
 
+    /**
+     * Perform all cleanup and notification around the termination of a session.
+     * Note that there are 2 distinct modes of operation.  The first is when
+     * we receive a session termination on the primary session when we are
+     * in the processing of merging.  The second is when we are not merging anything
+     * and the call is terminated.
+     *
+     * @param reasonInfo The reason for the session termination
+     */
+    private void processCallTerminated(ImsReasonInfo reasonInfo) {
+        if (DBG) {
+            String sessionString = mSession != null ? mSession.toString() : "null";
+            String transientSessionString = mTransientConferenceSession != null ?
+                    mTransientConferenceSession.toString() : "null";
+            String reasonString = reasonInfo != null ? reasonInfo.toString() : "null";
+            log("processCallTerminated :: session=" + sessionString + " transientSession=" +
+                    transientSessionString + " reason=" + reasonString);
+        }
+
+        ImsCall.Listener listener = null;
+
+        synchronized(ImsCall.this) {
+            if (mUpdateRequest == UPDATE_MERGE) {
+                // Since we are in the process of a merge, this trigger means something
+                // else because it is probably due to the merge happening vs. the
+                // session is really terminated. Let's flag this and revisit if
+                // the merge() ends up failing because we will need to take action on the
+                // mSession in that case since the termination was not due to the merge
+                // succeeding.
+                if (DBG) {
+                    log("processCallTerminated :: burying termination during ongoing merge.");
+                }
+                mSessionEndDuringMerge = true;
+                mSessionEndDuringMergeReasonInfo = reasonInfo;
+                return;
+            }
+
+            if (mCallGroup != null) {
+                notifyConferenceSessionTerminated(reasonInfo);
+            } else {
+                listener = mListener;
+                clear(reasonInfo);
+            }
+        }
+
+        if (listener != null) {
+            try {
+                listener.onCallTerminated(ImsCall.this, reasonInfo);
+            } catch (Throwable t) {
+                loge("callSessionTerminated :: ", t);
+            }
+        }
+    }
 
     /**
      * This function determines if the ImsCallSession is our actual ImsCallSession or if is
@@ -1639,31 +1714,49 @@
 
     /**
      * We received a callback from ImsCallSession that a merge was complete. Clean up all
-     * internal state to represent this state change.
+     * internal state to represent this state change. This function will be called when
+     * the transient conference session goes active or we get an explicit merge complete
+     * callback on the transient session.
      *
-     * @param session The {@link ImsCallSession} that is the host of this conference.
      */
-    private void processMergeComplete(ImsCallSession session) {
-        // If mTransientConferenceSession is set, this means that this is a newly created
-        // conference. Otherwise, we are adding to an existing conference.
+    private void processMergeComplete() {
+        if (DBG) {
+            String sessionString = mSession != null ? mSession.toString() : "null";
+            String transientSessionString = mTransientConferenceSession != null ?
+                    mTransientConferenceSession.toString() : "null";
+            log("processMergeComplete :: session=" + sessionString + " transientSession=" +
+                    transientSessionString);
+        }
+
         ImsCall.Listener listener;
         synchronized(ImsCall.this) {
             listener = mListener;
-            // TODO: See if we can use the session param to do some further checks here.
             if (mTransientConferenceSession != null) {
-                // We need to replace our ImsCallSession with this new one after adjusting
-                // the listeners on the session.
+                // Swap out the underlying sessions after shutting down the existing session.
                 mSession.setListener(null);
-                mTransientConferenceSession.setListener(createCallSessionListener());
                 mSession = mTransientConferenceSession;
+                // We need to set ourselves as the owner of the call group to indicate that
+                // a conference call is in progress.
+                mCallGroup.setOwner(ImsCall.this);
+                listener = mListener;
+            } else {
+                // This is an interesting state that needs to be logged since we
+                // should only be going through this workflow for new conference calls
+                // and not merges into existing conferences (which a null transient
+                // session would imply)
+                log("processMergeComplete :: ERROR no transient session");
             }
+            // Clear some flags.  If the merge eventually worked, we can safely
+            // ignore the call terminated message for the old session since we closed it already.
+            mSessionEndDuringMerge = false;
+            mSessionEndDuringMergeReasonInfo = null;
             mUpdateRequest = UPDATE_NONE;
         }
         if (listener != null) {
             try {
                 listener.onCallMerged(ImsCall.this);
             } catch (Throwable t) {
-                loge("callSessionMergeStarted :: ", t);
+                loge("processMergeComplete :: ", t);
             }
         }
 
@@ -1674,28 +1767,59 @@
      * We received a callback from ImsCallSession that a merge failed. Clean up all
      * internal state to represent this state change.
      *
-     * @param session The {@link ImsCallSession} that is the host of this conference.
      * @param reasonInfo The {@link ImsReasonInfo} why the merge failed.
      */
-    private void processMergeFailed(ImsCallSession session, ImsReasonInfo reasonInfo) {
-        // If mTransientConferenceSession is set, this means that this is a newly created
-        // conference. Otherwise, we are adding to an existing conference.
+    private void processMergeFailed(ImsReasonInfo reasonInfo) {
+        if (DBG) {
+            String sessionString = mSession != null ? mSession.toString() : "null";
+            String transientSessionString = mTransientConferenceSession != null ?
+                    mTransientConferenceSession.toString() : "null";
+            String reasonString = reasonInfo != null ? reasonInfo.toString() : "null";
+            log("processMergeFailed :: session=" + sessionString + " transientSession=" +
+                    transientSessionString + " reason=" + reasonString);
+        }
+
         ImsCall.Listener listener;
+        boolean notifyFailure = false;
+        ImsReasonInfo notifyFailureReasonInfo = null;
+
         synchronized(ImsCall.this) {
             listener = mListener;
-            // TODO: See if we can use the session param to do some further checks here.
             if (mTransientConferenceSession != null) {
                 // Clean up any work that we performed on the transient session.
                 mTransientConferenceSession.setListener(null);
                 mTransientConferenceSession = null;
+                listener = mListener;
+                if (mSessionEndDuringMerge) {
+                    // Set some local variables that will send out a notification about a
+                    // previously buried termination callback for our primary session now that
+                    // we know that this is not due to the conference call merging succesfully.
+                    if (DBG) {
+                        log("processMergeFailed :: following up on a terminate during the merge");
+                    }
+                    notifyFailure = true;
+                    notifyFailureReasonInfo = mSessionEndDuringMergeReasonInfo;
+                }
+            } else {
+                // This is an interesting state that needs to be logged since we
+                // should only be going through this workflow for new conference calls
+                // and not merges into existing conferences (which a null transient
+                // session would imply)
+                log("processMergeFailed - ERROR no transient session");
             }
+            mSessionEndDuringMerge = false;
+            mSessionEndDuringMergeReasonInfo = null;
             mUpdateRequest = UPDATE_NONE;
         }
         if (listener != null) {
             try {
+                // TODO: are both of these callbacks necessary?
                 listener.onCallMergeFailed(ImsCall.this, reasonInfo);
+                if (notifyFailure) {
+                    processCallTerminated(notifyFailureReasonInfo);
+                }
             } catch (Throwable t) {
-                loge("callSessionMergeFailed :: ", t);
+                loge("processMergeFailed :: ", t);
             }
         }
         return;
@@ -1728,7 +1852,8 @@
         @Override
         public void callSessionProgressing(ImsCallSession session, ImsStreamMediaProfile profile) {
             if (isTransientConferenceSession(session)) {
-                log("callSessionProgressing :: not supported for conference session=" + session);
+                log("callSessionProgressing :: not supported for transient conference session=" +
+                        session);
                 return;
             }
 
@@ -1754,15 +1879,20 @@
 
         @Override
         public void callSessionStarted(ImsCallSession session, ImsCallProfile profile) {
-            if (isTransientConferenceSession(session)) {
-                log("callSessionStarted :: not supported for conference session=" + session);
-                return;
-            }
-
             if (DBG) {
                 log("callSessionStarted :: session=" + session + ", profile=" + profile);
             }
 
+            if (isTransientConferenceSession(session)) {
+                log("callSessionStarted :: transient conference session resumed session=" +
+                        session);
+                // If we get a resume on the transient session, this means that the merge
+                // was completed, let's process it are skip the rest of the processing in
+                // this callback.
+                processMergeComplete();
+                return;
+            }
+
             ImsCall.Listener listener;
 
             synchronized(ImsCall.this) {
@@ -1782,7 +1912,8 @@
         @Override
         public void callSessionStartFailed(ImsCallSession session, ImsReasonInfo reasonInfo) {
             if (isTransientConferenceSession(session)) {
-                log("callSessionStartFailed :: not supported for conference session=" + session);
+                log("callSessionStartFailed :: not supported for transient conference session=" +
+                        session);
                 return;
             }
 
@@ -1810,64 +1941,22 @@
         @Override
         public void callSessionTerminated(ImsCallSession session, ImsReasonInfo reasonInfo) {
             if (isTransientConferenceSession(session)) {
-                log("callSessionTerminated :: not supported for conference session=" + session);
+                log("callSessionTerminated :: not supported for transient conference session=" +
+                        session);
                 return;
             }
 
             if (DBG) {
-                log("callSessionTerminated :: session=" + session +
-                        ", reasonInfo=" + reasonInfo);
+                log("callSessionTerminated :: session=" + session + ", reasonInfo=" + reasonInfo);
             }
 
-            ImsCall.Listener listener = null;
-
-            synchronized(ImsCall.this) {
-                // Let's explicitly handle this situation here...
-                // The ImsCallSession that has just notified that it was terminated could be in the
-                // state where it is waiting for a merge to happen. If this is the case, let's just
-                // assume that the merge is going to succeed but the callbacks are being done in the
-                // wrong order.  We would expect callSessionMergeComplete() to be called on the
-                // transient session first and then this call to the original merge() "host".
-                // If the timing is off, we could be getting this call first.
-                if (mUpdateRequest == UPDATE_MERGE) {
-                    if (mTransientConferenceSession != null) {
-                        log("callSessionTerminated() :: called while waiting for a new conference");
-                        // Let's assume that the merge will complete and the transient session will,
-                        // in fact, be our new session soon enough.
-                        mSession.setListener(null);
-                        mTransientConferenceSession.setListener(createCallSessionListener());
-                        mSession = mTransientConferenceSession;
-                        // At this point, we'll handle this conference call like it was a merge()
-                        // on an existing conference as opposed to creating a new conference. All
-                        // properly callbacks should be made.
-
-                        // TODO: Do we need to handle the case where the new ImsCallSession still
-                        // fails to merge after this?  Should be a very unlikely scenario.
-                        return;
-                    }
-                }
-
-                if (mCallGroup != null) {
-                    notifyConferenceSessionTerminated(reasonInfo);
-                } else {
-                    listener = mListener;
-                    clear(reasonInfo);
-                }
-            }
-
-            if (listener != null) {
-                try {
-                    listener.onCallTerminated(ImsCall.this, reasonInfo);
-                } catch (Throwable t) {
-                    loge("callSessionTerminated :: ", t);
-                }
-            }
+            processCallTerminated(reasonInfo);
         }
 
         @Override
         public void callSessionHeld(ImsCallSession session, ImsCallProfile profile) {
             if (isTransientConferenceSession(session)) {
-                log("callSessionHeld :: not supported for conference session=" + session);
+                log("callSessionHeld :: not supported for transient conference session=" + session);
                 return;
             }
 
@@ -1905,7 +1994,8 @@
         @Override
         public void callSessionHoldFailed(ImsCallSession session, ImsReasonInfo reasonInfo) {
             if (isTransientConferenceSession(session)) {
-                log("callSessionHoldFailed :: not supported for conference session=" + session);
+                log("callSessionHoldFailed :: not supported for transient conference session=" +
+                        session);
                 return;
             }
 
@@ -1947,7 +2037,8 @@
         @Override
         public void callSessionHoldReceived(ImsCallSession session, ImsCallProfile profile) {
             if (isTransientConferenceSession(session)) {
-                log("callSessionHoldReceived :: not supported for conference session=" + session);
+                log("callSessionHoldReceived :: not supported for transient conference session=" +
+                        session);
                 return;
             }
 
@@ -1974,7 +2065,8 @@
         @Override
         public void callSessionResumed(ImsCallSession session, ImsCallProfile profile) {
             if (isTransientConferenceSession(session)) {
-                log("callSessionResumed :: not supported for conference session=" + session);
+                log("callSessionResumed :: not supported for transient conference session=" +
+                        session);
                 return;
             }
 
@@ -2006,7 +2098,8 @@
         @Override
         public void callSessionResumeFailed(ImsCallSession session, ImsReasonInfo reasonInfo) {
             if (isTransientConferenceSession(session)) {
-                log("callSessionResumeFailed :: not supported for conference session=" + session);
+                log("callSessionResumeFailed :: not supported for transient conference session=" +
+                        session);
                 return;
             }
 
@@ -2034,7 +2127,8 @@
         @Override
         public void callSessionResumeReceived(ImsCallSession session, ImsCallProfile profile) {
             if (isTransientConferenceSession(session)) {
-                log("callSessionResumeReceived :: not supported for conference session=" + session);
+                log("callSessionResumeReceived :: not supported for transient conference session=" +
+                        session);
                 return;
             }
 
@@ -2063,7 +2157,7 @@
         public void callSessionMergeStarted(ImsCallSession session,
                 ImsCallSession newSession, ImsCallProfile profile) {
             if (DBG) {
-                String sessionString = newSession == null ? "null" : newSession.toString();
+                String sessionString = session == null ? "null" : session.toString();
                 String newSessionString = newSession == null ? "null" : newSession.toString();
                 log("callSessionMergeStarted :: session=" + sessionString
                         + ", newSession=" + newSessionString + ", profile=" + profile);
@@ -2105,11 +2199,6 @@
                 mTransientConferenceSession.setListener(createCallSessionListener());
             }
 
-            // STOPSHIP: For now, let's force the complete callback since it is not coming
-            // from the vendor layer as of when this code was written.
-            log("callSessionMergeStarted ::  forcing a success callback");
-            callSessionMergeComplete(mTransientConferenceSession);
-
             return;
         }
 
@@ -2128,7 +2217,7 @@
             }
             // Let's let our parent ImsCall now that we received notification that
             // the merge was completed so we can set up our internal state properly
-            processMergeComplete(session);
+            processMergeComplete();
         }
 
         @Override
@@ -2148,13 +2237,14 @@
             }
             // Let's tell our parent ImsCall that the merge has failed and we need to clean
             // up any temporary, transient state.
-            processMergeFailed(session, reasonInfo);
+            processMergeFailed(reasonInfo);
         }
 
         @Override
         public void callSessionUpdated(ImsCallSession session, ImsCallProfile profile) {
             if (isTransientConferenceSession(session)) {
-                log("callSessionUpdated :: not supported for conference session=" + session);
+                log("callSessionUpdated :: not supported for transient conference session=" +
+                        session);
                 return;
             }
 
@@ -2182,7 +2272,8 @@
         @Override
         public void callSessionUpdateFailed(ImsCallSession session, ImsReasonInfo reasonInfo) {
             if (isTransientConferenceSession(session)) {
-                log("callSessionUpdateFailed :: not supported for conference session=" + session);
+                log("callSessionUpdateFailed :: not supported for transient conference session=" +
+                        session);
                 return;
             }
 
@@ -2210,7 +2301,8 @@
         @Override
         public void callSessionUpdateReceived(ImsCallSession session, ImsCallProfile profile) {
             if (isTransientConferenceSession(session)) {
-                log("callSessionUpdateReceived :: not supported for conference session=" + session);
+                log("callSessionUpdateReceived :: not supported for transient conference " +
+                        "session=" + session);
                 return;
             }
 
@@ -2240,8 +2332,8 @@
         public void callSessionConferenceExtended(ImsCallSession session, ImsCallSession newSession,
                 ImsCallProfile profile) {
             if (isTransientConferenceSession(session)) {
-                log("callSessionConferenceExtended :: not supported for conference session=" +
-                        session);
+                log("callSessionConferenceExtended :: not supported for transient conference " +
+                        "session=" + session);
                 return;
             }
 
@@ -2277,8 +2369,8 @@
         public void callSessionConferenceExtendFailed(ImsCallSession session,
                 ImsReasonInfo reasonInfo) {
             if (isTransientConferenceSession(session)) {
-                log("callSessionConferenceExtendFailed :: not supported for conference session=" +
-                        session);
+                log("callSessionConferenceExtendFailed :: not supported for transient " +
+                        "conference session=" + session);
                 return;
             }
 
@@ -2307,8 +2399,8 @@
         public void callSessionConferenceExtendReceived(ImsCallSession session,
                 ImsCallSession newSession, ImsCallProfile profile) {
             if (isTransientConferenceSession(session)) {
-                log("callSessionConferenceExtendReceived :: not supported for conference session=" +
-                        session);
+                log("callSessionConferenceExtendReceived :: not supported for transient " +
+                        "conference session=" + session);
                 return;
             }
 
@@ -2455,8 +2547,8 @@
         public void callSessionConferenceStateUpdated(ImsCallSession session,
                 ImsConferenceState state) {
             if (isTransientConferenceSession(session)) {
-                log("callSessionConferenceStateUpdated :: not supported for conference session=" +
-                        session);
+                log("callSessionConferenceStateUpdated :: not supported for transient " +
+                        "conference session=" + session);
                 return;
             }
 
@@ -2472,8 +2564,8 @@
         public void callSessionUssdMessageReceived(ImsCallSession session, int mode,
                 String ussdMessage) {
             if (isTransientConferenceSession(session)) {
-                log("callSessionUssdMessageReceived :: not supported for conference session=" +
-                        session);
+                log("callSessionUssdMessageReceived :: not supported for transient " +
+                        "conference session=" + session);
                 return;
             }