Add timer to SIP session creation process.

+ add timer parameter to ISipSession.make/changeCall(),
+ add timer paramter to SipAudioCall.make/answer/hold/continueCall()'s,
+ add timer parameter to SipManager.makeAudioCall(),
+ modify implementation in SipSessionGroup, SipAudioCallImpl accordingly,
+ make SipPhone to use it with 8-second timeout.

http://b/issue?id=2994748

Change-Id: I661a887e5810087ddc5e2318335e2fa427f80ec6
diff --git a/services/java/com/android/server/sip/SipSessionGroup.java b/services/java/com/android/server/sip/SipSessionGroup.java
index 0822350..baf9a8e 100644
--- a/services/java/com/android/server/sip/SipSessionGroup.java
+++ b/services/java/com/android/server/sip/SipSessionGroup.java
@@ -84,7 +84,8 @@
     private static final boolean DEBUG_PING = DEBUG && false;
     private static final String ANONYMOUS = "anonymous";
     private static final String SERVER_ERROR_PREFIX = "Response: ";
-    private static final int EXPIRY_TIME = 3600;
+    private static final int EXPIRY_TIME = 3600; // in seconds
+    private static final int CANCEL_CALL_TIMER = 5; // in seconds
 
     private static final EventObject DEREGISTER = new EventObject("Deregister");
     private static final EventObject END_CALL = new EventObject("End call");
@@ -363,6 +364,40 @@
         String mPeerSessionDescription;
         boolean mInCall;
         boolean mReRegisterFlag = false;
+        SessionTimer mTimer;
+
+        // lightweight timer
+        class SessionTimer {
+            private boolean mRunning = true;
+
+            void start(final int timeout) {
+                new Thread(new Runnable() {
+                    public void run() {
+                        sleep(timeout);
+                        if (mRunning) timeout();
+                    }
+                }).start();
+            }
+
+            synchronized void cancel() {
+                mRunning = false;
+                this.notify();
+            }
+
+            private void timeout() {
+                synchronized (SipSessionGroup.this) {
+                    onError(SipErrorCode.TIME_OUT, "Session timed out!");
+                }
+            }
+
+            private synchronized void sleep(int timeout) {
+                try {
+                    this.wait(timeout * 1000);
+                } catch (InterruptedException e) {
+                    Log.e(TAG, "session timer interrupted!");
+                }
+            }
+        }
 
         public SipSessionImpl(ISipSessionListener listener) {
             setListener(listener);
@@ -382,6 +417,8 @@
             mServerTransaction = null;
             mClientTransaction = null;
             mPeerSessionDescription = null;
+
+            cancelSessionTimer();
         }
 
         public boolean isInCall() {
@@ -434,16 +471,16 @@
             }).start();
         }
 
-        public void makeCall(SipProfile peerProfile,
-                String sessionDescription) {
-            doCommandAsync(
-                    new MakeCallCommand(peerProfile, sessionDescription));
+        public void makeCall(SipProfile peerProfile, String sessionDescription,
+                int timeout) {
+            doCommandAsync(new MakeCallCommand(peerProfile, sessionDescription,
+                    timeout));
         }
 
-        public void answerCall(String sessionDescription) {
+        public void answerCall(String sessionDescription, int timeout) {
             try {
-                processCommand(
-                        new MakeCallCommand(mPeerProfile, sessionDescription));
+                processCommand(new MakeCallCommand(mPeerProfile,
+                        sessionDescription, timeout));
             } catch (SipException e) {
                 onError(e);
             }
@@ -453,9 +490,15 @@
             doCommandAsync(END_CALL);
         }
 
-        public void changeCall(String sessionDescription) {
-            doCommandAsync(
-                    new MakeCallCommand(mPeerProfile, sessionDescription));
+        public void changeCall(String sessionDescription, int timeout) {
+            doCommandAsync(new MakeCallCommand(mPeerProfile, sessionDescription,
+                    timeout));
+        }
+
+        public void changeCallWithTimeout(
+                String sessionDescription, int timeout) {
+            doCommandAsync(new MakeCallCommand(mPeerProfile, sessionDescription,
+                    timeout));
         }
 
         public void register(int duration) {
@@ -800,6 +843,7 @@
                 addSipSession(this);
                 mState = SipSessionState.OUTGOING_CALL;
                 mProxy.onCalling(this);
+                startSessionTimer(cmd.getTimeout());
                 return true;
             } else if (evt instanceof RegisterCommand) {
                 int duration = ((RegisterCommand) evt).getDuration();
@@ -831,6 +875,7 @@
                         ((MakeCallCommand) evt).getSessionDescription(),
                         mServerTransaction);
                 mState = SipSessionState.INCOMING_CALL_ANSWERING;
+                startSessionTimer(((MakeCallCommand) evt).getTimeout());
                 return true;
             } else if (END_CALL == evt) {
                 mSipHelper.sendInviteBusyHere(mInviteReceived,
@@ -873,6 +918,7 @@
                     if (mState == SipSessionState.OUTGOING_CALL) {
                         mState = SipSessionState.OUTGOING_CALL_RING_BACK;
                         mProxy.onRingingBack(this);
+                        cancelSessionTimer();
                     }
                     return true;
                 case Response.OK:
@@ -885,10 +931,10 @@
                     if (handleAuthentication(event)) {
                         addSipSession(this);
                     } else if (mLastNonce == null) {
-                        endCallOnError(SipErrorCode.SERVER_ERROR,
+                        onError(SipErrorCode.SERVER_ERROR,
                                 "server does not provide challenge");
                     } else {
-                        endCallOnError(SipErrorCode.INVALID_CREDENTIALS,
+                        onError(SipErrorCode.INVALID_CREDENTIALS,
                                 "incorrect username or password");
                     }
                     return true;
@@ -914,6 +960,7 @@
                 // response.
                 mSipHelper.sendCancel(mClientTransaction);
                 mState = SipSessionState.OUTGOING_CALL_CANCELING;
+                startSessionTimer(CANCEL_CALL_TIMER);
                 return true;
             }
             return false;
@@ -926,9 +973,13 @@
                 Response response = event.getResponse();
                 int statusCode = response.getStatusCode();
                 if (expectResponse(Request.CANCEL, evt)) {
-                    if (statusCode == Response.OK) {
-                        // do nothing; wait for REQUEST_TERMINATED
-                        return true;
+                    switch (statusCode) {
+                        case Response.OK:
+                            // do nothing; wait for REQUEST_TERMINATED
+                            return true;
+                        case Response.REQUEST_TERMINATED:
+                            endCallNormally();
+                            return true;
                     }
                 } else if (expectResponse(Request.INVITE, evt)) {
                     if (statusCode == Response.OK) {
@@ -978,11 +1029,27 @@
                 mClientTransaction = mSipHelper.sendReinvite(mDialog,
                         ((MakeCallCommand) evt).getSessionDescription());
                 mState = SipSessionState.OUTGOING_CALL;
+                startSessionTimer(((MakeCallCommand) evt).getTimeout());
                 return true;
             }
             return false;
         }
 
+        // timeout in seconds
+        private void startSessionTimer(int timeout) {
+            if (timeout > 0) {
+                mTimer = new SessionTimer();
+                mTimer.start(timeout);
+            }
+        }
+
+        private void cancelSessionTimer() {
+            if (mTimer != null) {
+                mTimer.cancel();
+                mTimer = null;
+            }
+        }
+
         private String createErrorMessage(Response response) {
             return String.format(SERVER_ERROR_PREFIX + "%s (%d)",
                     response.getReasonPhrase(), response.getStatusCode());
@@ -991,15 +1058,10 @@
         private void establishCall() {
             mState = SipSessionState.IN_CALL;
             mInCall = true;
+            cancelSessionTimer();
             mProxy.onCallEstablished(this, mPeerSessionDescription);
         }
 
-        private void fallbackToPreviousInCall(Throwable exception) {
-            exception = getRootCause(exception);
-            fallbackToPreviousInCall(getErrorCode(exception),
-                    exception.toString());
-        }
-
         private void fallbackToPreviousInCall(SipErrorCode errorCode,
                 String message) {
             mState = SipSessionState.IN_CALL;
@@ -1022,6 +1084,7 @@
         }
 
         private void onError(SipErrorCode errorCode, String message) {
+            cancelSessionTimer();
             switch (mState) {
                 case REGISTERING:
                 case DEREGISTERING:
@@ -1255,11 +1318,18 @@
 
     private class MakeCallCommand extends EventObject {
         private String mSessionDescription;
+        private int mTimeout; // in seconds
 
         public MakeCallCommand(SipProfile peerProfile,
                 String sessionDescription) {
+            this(peerProfile, sessionDescription, -1);
+        }
+
+        public MakeCallCommand(SipProfile peerProfile,
+                String sessionDescription, int timeout) {
             super(peerProfile);
             mSessionDescription = sessionDescription;
+            mTimeout = timeout;
         }
 
         public SipProfile getPeerProfile() {
@@ -1269,6 +1339,9 @@
         public String getSessionDescription() {
             return mSessionDescription;
         }
-    }
 
+        public int getTimeout() {
+            return mTimeout;
+        }
+    }
 }
diff --git a/telephony/java/com/android/internal/telephony/sip/SipPhone.java b/telephony/java/com/android/internal/telephony/sip/SipPhone.java
index 07dd35d..d7dc4ab 100755
--- a/telephony/java/com/android/internal/telephony/sip/SipPhone.java
+++ b/telephony/java/com/android/internal/telephony/sip/SipPhone.java
@@ -74,6 +74,7 @@
 public class SipPhone extends SipPhoneBase {
     private static final String LOG_TAG = "SipPhone";
     private static final boolean LOCAL_DEBUG = true;
+    private static final int SESSION_TIMEOUT = 8; // in seconds
 
     // A call that is ringing or (call) waiting
     private SipCall ringingCall = new SipCall();
@@ -675,7 +676,7 @@
 
         void acceptCall() throws CallStateException {
             try {
-                mSipAudioCall.answerCall();
+                mSipAudioCall.answerCall(SESSION_TIMEOUT);
             } catch (SipException e) {
                 throw new CallStateException("acceptCall(): " + e);
             }
@@ -693,7 +694,7 @@
         void dial() throws SipException {
             setState(Call.State.DIALING);
             mSipAudioCall = mSipManager.makeAudioCall(mContext, mProfile,
-                    mPeer, null);
+                    mPeer, null, SESSION_TIMEOUT);
             mSipAudioCall.setRingbackToneEnabled(false);
             mSipAudioCall.setListener(mAdapter);
         }
@@ -701,7 +702,7 @@
         void hold() throws CallStateException {
             setState(Call.State.HOLDING);
             try {
-                mSipAudioCall.holdCall();
+                mSipAudioCall.holdCall(SESSION_TIMEOUT);
             } catch (SipException e) {
                 throw new CallStateException("hold(): " + e);
             }
@@ -711,7 +712,7 @@
             mSipAudioCall.setAudioGroup(audioGroup);
             setState(Call.State.ACTIVE);
             try {
-                mSipAudioCall.continueCall();
+                mSipAudioCall.continueCall(SESSION_TIMEOUT);
             } catch (SipException e) {
                 throw new CallStateException("unhold(): " + e);
             }
diff --git a/voip/java/android/net/sip/ISipSession.aidl b/voip/java/android/net/sip/ISipSession.aidl
index cd8bd2c..5661b8f 100644
--- a/voip/java/android/net/sip/ISipSession.aidl
+++ b/voip/java/android/net/sip/ISipSession.aidl
@@ -112,9 +112,11 @@
      *
      * @param callee the SIP profile to make the call to
      * @param sessionDescription the session description of this call
+     * @param timeout the session will be timed out if the call is not
+     *        established within {@code timeout} seconds
      * @see ISipSessionListener
      */
-    void makeCall(in SipProfile callee, String sessionDescription);
+    void makeCall(in SipProfile callee, String sessionDescription, int timeout);
 
     /**
      * Answers an incoming call with the specified session description. The
@@ -122,8 +124,10 @@
      * {@link SipSessionState#INCOMING_CALL}.
      *
      * @param sessionDescription the session description to answer this call
+     * @param timeout the session will be timed out if the call is not
+     *        established within {@code timeout} seconds
      */
-    void answerCall(String sessionDescription);
+    void answerCall(String sessionDescription, int timeout);
 
     /**
      * Ends an established call, terminates an outgoing call or rejects an
@@ -140,6 +144,8 @@
      * to call when the session state is in {@link SipSessionState#IN_CALL}.
      *
      * @param sessionDescription the new session description
+     * @param timeout the session will be timed out if the call is not
+     *        established within {@code timeout} seconds
      */
-    void changeCall(String sessionDescription);
+    void changeCall(String sessionDescription, int timeout);
 }
diff --git a/voip/java/android/net/sip/SipAudioCall.java b/voip/java/android/net/sip/SipAudioCall.java
index 573760e..2a9a65b 100644
--- a/voip/java/android/net/sip/SipAudioCall.java
+++ b/voip/java/android/net/sip/SipAudioCall.java
@@ -158,12 +158,18 @@
     void close();
 
     /**
-     * Initiates an audio call to the specified profile.
+     * Initiates an audio call to the specified profile. The attempt will be
+     * timed out if the call is not established within {@code timeout} seconds
+     * and {@code Listener.onError(SipAudioCall, SipErrorCode.TIME_OUT, String)}
+     * will be called.
      *
      * @param callee the SIP profile to make the call to
      * @param sipManager the {@link SipManager} object to help make call with
+     * @param timeout the timeout value in seconds
+     * @see Listener.onError
      */
-    void makeCall(SipProfile callee, SipManager sipManager) throws SipException;
+    void makeCall(SipProfile callee, SipManager sipManager, int timeout)
+            throws SipException;
 
     /**
      * Attaches an incoming call to this call object.
@@ -179,18 +185,38 @@
 
     /**
      * Puts a call on hold.  When succeeds, {@link Listener#onCallHeld} is
-     * called.
+     * called. The attempt will be timed out if the call is not established
+     * within {@code timeout} seconds and
+     * {@code Listener.onError(SipAudioCall, SipErrorCode.TIME_OUT, String)}
+     * will be called.
+     *
+     * @param timeout the timeout value in seconds
+     * @see Listener.onError
      */
-    void holdCall() throws SipException;
+    void holdCall(int timeout) throws SipException;
 
-    /** Answers a call. */
-    void answerCall() throws SipException;
+    /**
+     * Answers a call. The attempt will be timed out if the call is not
+     * established within {@code timeout} seconds and
+     * {@code Listener.onError(SipAudioCall, SipErrorCode.TIME_OUT, String)}
+     * will be called.
+     *
+     * @param timeout the timeout value in seconds
+     * @see Listener.onError
+     */
+    void answerCall(int timeout) throws SipException;
 
     /**
      * Continues a call that's on hold. When succeeds,
-     * {@link Listener#onCallEstablished} is called.
+     * {@link Listener#onCallEstablished} is called. The attempt will be timed
+     * out if the call is not established within {@code timeout} seconds and
+     * {@code Listener.onError(SipAudioCall, SipErrorCode.TIME_OUT, String)}
+     * will be called.
+     *
+     * @param timeout the timeout value in seconds
+     * @see Listener.onError
      */
-    void continueCall() throws SipException;
+    void continueCall(int timeout) throws SipException;
 
     /** Puts the device to speaker mode. */
     void setSpeakerMode(boolean speakerMode);
diff --git a/voip/java/android/net/sip/SipAudioCallImpl.java b/voip/java/android/net/sip/SipAudioCallImpl.java
index 8bf486a..fcabcc4 100644
--- a/voip/java/android/net/sip/SipAudioCallImpl.java
+++ b/voip/java/android/net/sip/SipAudioCallImpl.java
@@ -55,6 +55,7 @@
     private static final boolean DONT_RELEASE_SOCKET = false;
     private static final String AUDIO = "audio";
     private static final int DTMF = 101;
+    private static final int SESSION_TIMEOUT = 5; // in seconds
 
     private Context mContext;
     private SipProfile mLocalProfile;
@@ -144,12 +145,21 @@
         if (closeRtp) stopCall(RELEASE_SOCKET);
         stopRingbackTone();
         stopRinging();
-        mSipSession = null;
+
         mInCall = false;
         mHold = false;
         mSessionId = -1L;
         mErrorCode = null;
         mErrorMessage = null;
+
+        if (mSipSession != null) {
+            try {
+                mSipSession.setListener(null);
+            } catch (RemoteException e) {
+                // don't care
+            }
+            mSipSession = null;
+        }
     }
 
     public synchronized SipProfile getLocalProfile() {
@@ -219,7 +229,7 @@
             // session changing request
             try {
                 mPeerSd = new SdpSessionDescription(sessionDescription);
-                answerCall();
+                answerCall(SESSION_TIMEOUT);
             } catch (Throwable e) {
                 Log.e(TAG, "onRinging()", e);
                 session.endCall();
@@ -346,14 +356,15 @@
     }
 
     public synchronized void makeCall(SipProfile peerProfile,
-            SipManager sipManager) throws SipException {
+            SipManager sipManager, int timeout) throws SipException {
         try {
             mSipSession = sipManager.createSipSession(mLocalProfile, this);
             if (mSipSession == null) {
                 throw new SipException(
                         "Failed to create SipSession; network available?");
             }
-            mSipSession.makeCall(peerProfile, createOfferSessionDescription());
+            mSipSession.makeCall(peerProfile, createOfferSessionDescription(),
+                    timeout);
         } catch (Throwable e) {
             if (e instanceof SipException) {
                 throw (SipException) e;
@@ -376,10 +387,10 @@
         }
     }
 
-    public synchronized void holdCall() throws SipException {
+    public synchronized void holdCall(int timeout) throws SipException {
         if (mHold) return;
         try {
-            mSipSession.changeCall(createHoldSessionDescription());
+            mSipSession.changeCall(createHoldSessionDescription(), timeout);
             mHold = true;
         } catch (Throwable e) {
             throwSipException(e);
@@ -389,21 +400,21 @@
         if (audioGroup != null) audioGroup.setMode(AudioGroup.MODE_ON_HOLD);
     }
 
-    public synchronized void answerCall() throws SipException {
+    public synchronized void answerCall(int timeout) throws SipException {
         try {
             stopRinging();
-            mSipSession.answerCall(createAnswerSessionDescription());
+            mSipSession.answerCall(createAnswerSessionDescription(), timeout);
         } catch (Throwable e) {
             Log.e(TAG, "answerCall()", e);
             throwSipException(e);
         }
     }
 
-    public synchronized void continueCall() throws SipException {
+    public synchronized void continueCall(int timeout) throws SipException {
         if (!mHold) return;
         try {
             mHold = false;
-            mSipSession.changeCall(createContinueSessionDescription());
+            mSipSession.changeCall(createContinueSessionDescription(), timeout);
         } catch (Throwable e) {
             throwSipException(e);
         }
diff --git a/voip/java/android/net/sip/SipManager.java b/voip/java/android/net/sip/SipManager.java
index 5b1767d..36895cd 100644
--- a/voip/java/android/net/sip/SipManager.java
+++ b/voip/java/android/net/sip/SipManager.java
@@ -218,44 +218,55 @@
     }
 
     /**
-     * Creates a {@link SipAudioCall} to make a call.
+     * Creates a {@link SipAudioCall} to make a call. The attempt will be timed
+     * out if the call is not established within {@code timeout} seconds and
+     * {@code SipAudioCall.Listener.onError(SipAudioCall, SipErrorCode.TIME_OUT, String)}
+     * will be called.
      *
      * @param context context to create a {@link SipAudioCall} object
      * @param localProfile the SIP profile to make the call from
      * @param peerProfile the SIP profile to make the call to
      * @param listener to listen to the call events from {@link SipAudioCall};
      *      can be null
+     * @param timeout the timeout value in seconds
      * @return a {@link SipAudioCall} object
      * @throws SipException if calling the SIP service results in an error
+     * @see SipAudioCall.Listener.onError
      */
     public SipAudioCall makeAudioCall(Context context, SipProfile localProfile,
-            SipProfile peerProfile, SipAudioCall.Listener listener)
+            SipProfile peerProfile, SipAudioCall.Listener listener, int timeout)
             throws SipException {
         SipAudioCall call = new SipAudioCallImpl(context, localProfile);
         call.setListener(listener);
-        call.makeCall(peerProfile, this);
+        call.makeCall(peerProfile, this, timeout);
         return call;
     }
 
     /**
      * Creates a {@link SipAudioCall} to make a call. To use this method, one
-     * must call {@link #open(SipProfile)} first.
+     * must call {@link #open(SipProfile)} first. The attempt will be timed out
+     * if the call is not established within {@code timeout} seconds and
+     * {@code SipAudioCall.Listener.onError(SipAudioCall, SipErrorCode.TIME_OUT, String)}
+     * will be called.
      *
      * @param context context to create a {@link SipAudioCall} object
      * @param localProfileUri URI of the SIP profile to make the call from
      * @param peerProfileUri URI of the SIP profile to make the call to
      * @param listener to listen to the call events from {@link SipAudioCall};
      *      can be null
+     * @param timeout the timeout value in seconds
      * @return a {@link SipAudioCall} object
      * @throws SipException if calling the SIP service results in an error
+     * @see SipAudioCall.Listener.onError
      */
     public SipAudioCall makeAudioCall(Context context, String localProfileUri,
-            String peerProfileUri, SipAudioCall.Listener listener)
+            String peerProfileUri, SipAudioCall.Listener listener, int timeout)
             throws SipException {
         try {
             return makeAudioCall(context,
                     new SipProfile.Builder(localProfileUri).build(),
-                    new SipProfile.Builder(peerProfileUri).build(), listener);
+                    new SipProfile.Builder(peerProfileUri).build(), listener,
+                    timeout);
         } catch (ParseException e) {
             throw new SipException("build SipProfile", e);
         }