content: /*
   * Copyright (C) 2010 The Android Open Source Project
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *      http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  
  package com.android.internal.telephony.sip;
  
  import android.content.Context;
  import android.media.AudioManager;
  import android.net.rtp.AudioGroup;
  import android.net.sip.SipAudioCall;
  import android.net.sip.SipErrorCode;
  import android.net.sip.SipException;
  import android.net.sip.SipManager;
  import android.net.sip.SipProfile;
  import android.net.sip.SipSession;
  import android.os.AsyncResult;
  import android.os.Message;
  import android.telephony.DisconnectCause;
  import android.telephony.PhoneNumberUtils;
  import android.telephony.ServiceState;
  import android.text.TextUtils;
  import android.telephony.Rlog;
  
  import com.android.internal.telephony.Call;
  import com.android.internal.telephony.CallStateException;
  import com.android.internal.telephony.Connection;
  import com.android.internal.telephony.Phone;
  import com.android.internal.telephony.PhoneConstants;
  import com.android.internal.telephony.PhoneNotifier;
  
  import java.text.ParseException;
  import java.util.List;
  import java.util.regex.Pattern;
  
  /**
   * {@hide}
   */
  public class SipPhone extends SipPhoneBase {
      private static final String LOG_TAG = "SipPhone";
      private static final boolean DBG = true;
      private static final boolean VDBG = false; // STOPSHIP if true
      private static final int TIMEOUT_MAKE_CALL = 15; // in seconds
      private static final int TIMEOUT_ANSWER_CALL = 8; // in seconds
      private static final int TIMEOUT_HOLD_CALL = 15; // in seconds
      // Minimum time needed between hold/unhold requests.
      private static final long TIMEOUT_HOLD_PROCESSING = 1000; // ms
  
      // A call that is ringing or (call) waiting
      private SipCall mRingingCall = new SipCall();
      private SipCall mForegroundCall = new SipCall();
      private SipCall mBackgroundCall = new SipCall();
  
      private SipManager mSipManager;
      private SipProfile mProfile;
  
      private long mTimeOfLastValidHoldRequest = System.currentTimeMillis();
  
      SipPhone (Context context, PhoneNotifier notifier, SipProfile profile) {
          super("SIP:" + profile.getUriString(), context, notifier);
  
          if (DBG) log("new SipPhone: " + hidePii(profile.getUriString()));
          mRingingCall = new SipCall();
          mForegroundCall = new SipCall();
          mBackgroundCall = new SipCall();
          mProfile = profile;
          mSipManager = SipManager.newInstance(context);
      }
  
      @Override
      public boolean equals(Object o) {
          if (o == this) return true;
          if (!(o instanceof SipPhone)) return false;
          SipPhone that = (SipPhone) o;
          return mProfile.getUriString().equals(that.mProfile.getUriString());
      }
  
      public String getSipUri() {
          return mProfile.getUriString();
      }
  
      public boolean equals(SipPhone phone) {
          return getSipUri().equals(phone.getSipUri());
      }
  
      public Connection takeIncomingCall(Object incomingCall) {
          // FIXME: Is synchronizing on the class necessary, should we use a mLockObj?
          // Also there are many things not synchronized, of course
          // this may be true of GsmCdmaPhone too!!!
          synchronized (SipPhone.class) {
              if (!(incomingCall instanceof SipAudioCall)) {
                  if (DBG) log("takeIncomingCall: ret=null, not a SipAudioCall");
                  return null;
              }
              if (mRingingCall.getState().isAlive()) {
                  if (DBG) log("takeIncomingCall: ret=null, ringingCall not alive");
                  return null;
              }
  
              // FIXME: is it true that we cannot take any incoming call if
              // both foreground and background are active
              if (mForegroundCall.getState().isAlive()
                      && mBackgroundCall.getState().isAlive()) {
                  if (DBG) {
                      log("takeIncomingCall: ret=null," + " foreground and background both alive");
                  }
                  return null;
              }
  
              try {
                  SipAudioCall sipAudioCall = (SipAudioCall) incomingCall;
                  if (DBG) log("takeIncomingCall: taking call from: "
                          + hidePii(sipAudioCall.getPeerProfile().getUriString()));
                  String localUri = sipAudioCall.getLocalProfile().getUriString();
                  if (localUri.equals(mProfile.getUriString())) {
                      boolean makeCallWait = mForegroundCall.getState().isAlive();
                      SipConnection connection = mRingingCall.initIncomingCall(sipAudioCall,
                              makeCallWait);
                      if (sipAudioCall.getState() != SipSession.State.INCOMING_CALL) {
                          // Peer cancelled the call!
                          if (DBG) log("    takeIncomingCall: call cancelled !!");
                          mRingingCall.reset();
                          connection = null;
                      }
                      return connection;
                  }
              } catch (Exception e) {
                  // Peer may cancel the call at any time during the time we hook
                  // up ringingCall with sipAudioCall. Clean up ringingCall when
                  // that happens.
                  if (DBG) log("    takeIncomingCall: exception e=" + e);
                  mRingingCall.reset();
              }
              if (DBG) log("takeIncomingCall: NOT taking !!");
              return null;
          }
      }
  
      @Override
      public void acceptCall(int videoState) throws CallStateException {
          synchronized (SipPhone.class) {
              if ((mRingingCall.getState() == Call.State.INCOMING) ||
                      (mRingingCall.getState() == Call.State.WAITING)) {
                  if (DBG) log("acceptCall: accepting");
                  // Always unmute when answering a new call
                  mRingingCall.setMute(false);
                  mRingingCall.acceptCall();
              } else {
                  if (DBG) {
                      log("acceptCall:" +
                          " throw CallStateException(\"phone not ringing\")");
                  }
                  throw new CallStateException("phone not ringing");
              }
          }
      }
  
      @Override
      public void rejectCall() throws CallStateException {
          synchronized (SipPhone.class) {
              if (mRingingCall.getState().isRinging()) {
                  if (DBG) log("rejectCall: rejecting");
                  mRingingCall.rejectCall();
              } else {
                  if (DBG) {
                      log("rejectCall:" +
                          " throw CallStateException(\"phone not ringing\")");
                  }
                  throw new CallStateException("phone not ringing");
              }
          }
      }
  
      @Override
      public Connection dial(String dialString, DialArgs dialArgs) throws CallStateException {
          synchronized (SipPhone.class) {
              return dialInternal(dialString, dialArgs.videoState);
          }
      }
  
      private Connection dialInternal(String dialString, int videoState)
              throws CallStateException {
          if (DBG) log("dialInternal: dialString=" + hidePii(dialString));
          clearDisconnected();
  
          if (!canDial()) {
              throw new CallStateException("dialInternal: cannot dial in current state");
          }
          if (mForegroundCall.getState() == SipCall.State.ACTIVE) {
              switchHoldingAndActive();
          }
          if (mForegroundCall.getState() != SipCall.State.IDLE) {
              //we should have failed in !canDial() above before we get here
              throw new CallStateException("cannot dial in current state");
          }
  
          mForegroundCall.setMute(false);
          try {
              Connection c = mForegroundCall.dial(dialString);
              return c;
          } catch (SipException e) {
              loge("dialInternal: ", e);
              throw new CallStateException("dial error: " + e);
          }
      }
  
      @Override
      public void switchHoldingAndActive() throws CallStateException {
          // Wait for at least TIMEOUT_HOLD_PROCESSING ms to occur before sending hold/unhold requests
          // to prevent spamming the SipAudioCall state machine and putting it into an invalid state.
          if (!isHoldTimeoutExpired()) {
              if (DBG) log("switchHoldingAndActive: Disregarded! Under " + TIMEOUT_HOLD_PROCESSING +
                      " ms...");
              return;
          }
          if (DBG) log("switchHoldingAndActive: switch fg and bg");
          synchronized (SipPhone.class) {
              mForegroundCall.switchWith(mBackgroundCall);
              if (mBackgroundCall.getState().isAlive()) mBackgroundCall.hold();
              if (mForegroundCall.getState().isAlive()) mForegroundCall.unhold();
          }
      }
  
      @Override
      public boolean canConference() {
          if (DBG) log("canConference: ret=true");
          return true;
      }
  
      @Override
      public void conference() throws CallStateException {
          synchronized (SipPhone.class) {
              if ((mForegroundCall.getState() != SipCall.State.ACTIVE)
                      || (mForegroundCall.getState() != SipCall.State.ACTIVE)) {
                  throw new CallStateException("wrong state to merge calls: fg="
                          + mForegroundCall.getState() + ", bg="
                          + mBackgroundCall.getState());
              }
              if (DBG) log("conference: merge fg & bg");
              mForegroundCall.merge(mBackgroundCall);
          }
      }
  
      public void conference(Call that) throws CallStateException {
          synchronized (SipPhone.class) {
              if (!(that instanceof SipCall)) {
                  throw new CallStateException("expect " + SipCall.class
                          + ", cannot merge with " + that.getClass());
              }
              mForegroundCall.merge((SipCall) that);
          }
      }
  
      @Override
      public boolean canTransfer() {
          return false;
      }
  
      @Override
      public void explicitCallTransfer() {
          //mCT.explicitCallTransfer();
      }
  
      @Override
      public void clearDisconnected() {
          synchronized (SipPhone.class) {
              mRingingCall.clearDisconnected();
              mForegroundCall.clearDisconnected();
              mBackgroundCall.clearDisconnected();
  
              updatePhoneState();
              notifyPreciseCallStateChanged();
          }
      }
  
      @Override
      public void sendDtmf(char c) {
          if (!PhoneNumberUtils.is12Key(c)) {
              loge("sendDtmf called with invalid character '" + c + "'");
          } else if (mForegroundCall.getState().isAlive()) {
              synchronized (SipPhone.class) {
                  mForegroundCall.sendDtmf(c);
              }
          }
      }
  
      @Override
      public void startDtmf(char c) {
          if (!PhoneNumberUtils.is12Key(c)) {
              loge("startDtmf called with invalid character '" + c + "'");
          } else {
              sendDtmf(c);
          }
      }
  
      @Override
      public void stopDtmf() {
          // no op
      }
  
      public void sendBurstDtmf(String dtmfString) {
          loge("sendBurstDtmf() is a CDMA method");
      }
  
      @Override
      public void getOutgoingCallerIdDisplay(Message onComplete) {
          // FIXME: what to reply?
          AsyncResult.forMessage(onComplete, null, null);
          onComplete.sendToTarget();
      }
  
      @Override
      public void setOutgoingCallerIdDisplay(int commandInterfaceCLIRMode,
                                             Message onComplete) {
          // FIXME: what's this for SIP?
          AsyncResult.forMessage(onComplete, null, null);
          onComplete.sendToTarget();
      }
  
      @Override
      public void getCallWaiting(Message onComplete) {
          // FIXME: what to reply?
          AsyncResult.forMessage(onComplete, null, null);
          onComplete.sendToTarget();
      }
  
      @Override
      public void setCallWaiting(boolean enable, Message onComplete) {
          // FIXME: what to reply?
          loge("call waiting not supported");
      }
  
      @Override
      public void setEchoSuppressionEnabled() {
          // Echo suppression may not be available on every device. So, check
          // whether it is supported
          synchronized (SipPhone.class) {
              AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
              String echoSuppression = audioManager.getParameters("ec_supported");
              if (echoSuppression.contains("off")) {
                  mForegroundCall.setAudioGroupMode();
              }
          }
      }
  
      @Override
      public void setMute(boolean muted) {
          synchronized (SipPhone.class) {
              mForegroundCall.setMute(muted);
          }
      }
  
      @Override
      public boolean getMute() {
          return (mForegroundCall.getState().isAlive()
                  ? mForegroundCall.getMute()
                  : mBackgroundCall.getMute());
      }
  
      @Override
      public Call getForegroundCall() {
          return mForegroundCall;
      }
  
      @Override
      public Call getBackgroundCall() {
          return mBackgroundCall;
      }
  
      @Override
      public Call getRingingCall() {
          return mRingingCall;
      }
  
      @Override
      public ServiceState getServiceState() {
          // FIXME: we may need to provide this when data connectivity is lost
          // or when server is down
          return super.getServiceState();
      }
  
      private String getUriString(SipProfile p) {
          // SipProfile.getUriString() may contain "SIP:" and port
          return p.getUserName() + "@" + getSipDomain(p);
      }
  
      private String getSipDomain(SipProfile p) {
          String domain = p.getSipDomain();
          // TODO: move this to SipProfile
          if (domain.endsWith(":5060")) {
              return domain.substring(0, domain.length() - 5);
          } else {
              return domain;
          }
      }
  
      private static Call.State getCallStateFrom(SipAudioCall sipAudioCall) {
          if (sipAudioCall.isOnHold()) return Call.State.HOLDING;
          int sessionState = sipAudioCall.getState();
          switch (sessionState) {
              case SipSession.State.READY_TO_CALL:            return Call.State.IDLE;
              case SipSession.State.INCOMING_CALL:
              case SipSession.State.INCOMING_CALL_ANSWERING:  return Call.State.INCOMING;
              case SipSession.State.OUTGOING_CALL:            return Call.State.DIALING;
              case SipSession.State.OUTGOING_CALL_RING_BACK:  return Call.State.ALERTING;
              case SipSession.State.OUTGOING_CALL_CANCELING:  return Call.State.DISCONNECTING;
              case SipSession.State.IN_CALL:                  return Call.State.ACTIVE;
              default:
                  slog("illegal connection state: " + sessionState);
                  return Call.State.DISCONNECTED;
          }
      }
  
      private synchronized boolean isHoldTimeoutExpired() {
          long currTime = System.currentTimeMillis();
          if ((currTime - mTimeOfLastValidHoldRequest) > TIMEOUT_HOLD_PROCESSING) {
              mTimeOfLastValidHoldRequest = currTime;
              return true;
          }
          return false;
      }
  
      private void log(String s) {
          Rlog.d(LOG_TAG, s);
      }
  
      private static void slog(String s) {
          Rlog.d(LOG_TAG, s);
      }
  
      private void loge(String s) {
          Rlog.e(LOG_TAG, s);
      }
  
      private void loge(String s, Exception e) {
          Rlog.e(LOG_TAG, s, e);
      }
  
      private class SipCall extends SipCallBase {
          private static final String SC_TAG = "SipCall";
          private static final boolean SC_DBG = true;
          private static final boolean SC_VDBG = false; // STOPSHIP if true
  
          void reset() {
              if (SC_DBG) log("reset");
              mConnections.clear();
              setState(Call.State.IDLE);
          }
  
          void switchWith(SipCall that) {
              if (SC_DBG) log("switchWith");
              synchronized (SipPhone.class) {
                  SipCall tmp = new SipCall();
                  tmp.takeOver(this);
                  this.takeOver(that);
                  that.takeOver(tmp);
              }
          }
  
          private void takeOver(SipCall that) {
              if (SC_DBG) log("takeOver");
              mConnections = that.mConnections;
              mState = that.mState;
              for (Connection c : mConnections) {
                  ((SipConnection) c).changeOwner(this);
              }
          }
  
          @Override
          public Phone getPhone() {
              return SipPhone.this;
          }
  
          @Override
          public List<Connection> getConnections() {
              if (SC_VDBG) log("getConnections");
              synchronized (SipPhone.class) {
                  // FIXME should return Collections.unmodifiableList();
                  return mConnections;
              }
          }
  
          Connection dial(String originalNumber) throws SipException {
              if (SC_DBG) log("dial: num=" + (SC_VDBG ? originalNumber : "xxx"));
              // TODO: Should this be synchronized?
              String calleeSipUri = originalNumber;
              if (!calleeSipUri.contains("@")) {
                  String replaceStr = Pattern.quote(mProfile.getUserName() + "@");
                  calleeSipUri = mProfile.getUriString().replaceFirst(replaceStr,
                          calleeSipUri + "@");
              }
              try {
                  SipProfile callee =
                          new SipProfile.Builder(calleeSipUri).build();
                  SipConnection c = new SipConnection(this, callee,
                          originalNumber);
                  c.dial();
                  mConnections.add(c);
                  setState(Call.State.DIALING);
                  return c;
              } catch (ParseException e) {
                  throw new SipException("dial", e);
              }
          }
  
          @Override
          public void hangup() throws CallStateException {
              synchronized (SipPhone.class) {
                  if (mState.isAlive()) {
                      if (SC_DBG) log("hangup: call " + getState()
                              + ": " + this + " on phone " + getPhone());
                      setState(State.DISCONNECTING);
                      CallStateException excp = null;
                      for (Connection c : mConnections) {
                          try {
                              c.hangup();
                          } catch (CallStateException e) {
                              excp = e;
                          }
                      }
                      if (excp != null) throw excp;
                  } else {
                      if (SC_DBG) log("hangup: dead call " + getState()
                              + ": " + this + " on phone " + getPhone());
                  }
              }
          }
  
          SipConnection initIncomingCall(SipAudioCall sipAudioCall, boolean makeCallWait) {
              SipProfile callee = sipAudioCall.getPeerProfile();
              SipConnection c = new SipConnection(this, callee);
              mConnections.add(c);
  
              Call.State newState = makeCallWait ? State.WAITING : State.INCOMING;
              c.initIncomingCall(sipAudioCall, newState);
  
              setState(newState);
              notifyNewRingingConnectionP(c);
              return c;
          }
  
          void rejectCall() throws CallStateException {
              if (SC_DBG) log("rejectCall:");
              hangup();
          }
  
          void acceptCall() throws CallStateException {
              if (SC_DBG) log("acceptCall: accepting");
              if (this != mRingingCall) {
                  throw new CallStateException("acceptCall() in a non-ringing call");
              }
              if (mConnections.size() != 1) {
                  throw new CallStateException("acceptCall() in a conf call");
              }
              ((SipConnection) mConnections.get(0)).acceptCall();
          }
  
          private boolean isSpeakerOn() {
              Boolean ret = ((AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE))
                      .isSpeakerphoneOn();
              if (SC_VDBG) log("isSpeakerOn: ret=" + ret);
              return ret;
          }
  
          void setAudioGroupMode() {
              AudioGroup audioGroup = getAudioGroup();
              if (audioGroup == null) {
                  if (SC_DBG) log("setAudioGroupMode: audioGroup == null ignore");
                  return;
              }
              int mode = audioGroup.getMode();
              if (mState == State.HOLDING) {
                  audioGroup.setMode(AudioGroup.MODE_ON_HOLD);
              } else if (getMute()) {
                  audioGroup.setMode(AudioGroup.MODE_MUTED);
              } else if (isSpeakerOn()) {
                  audioGroup.setMode(AudioGroup.MODE_ECHO_SUPPRESSION);
              } else {
                  audioGroup.setMode(AudioGroup.MODE_NORMAL);
              }
              if (SC_DBG) log(String.format(
                      "setAudioGroupMode change: %d --> %d", mode,
                      audioGroup.getMode()));
          }
  
          void hold() throws CallStateException {
              if (SC_DBG) log("hold:");
              setState(State.HOLDING);
              for (Connection c : mConnections) ((SipConnection) c).hold();
              setAudioGroupMode();
          }
  
          void unhold() throws CallStateException {
              if (SC_DBG) log("unhold:");
              setState(State.ACTIVE);
              AudioGroup audioGroup = new AudioGroup();
              for (Connection c : mConnections) {
                  ((SipConnection) c).unhold(audioGroup);
              }
              setAudioGroupMode();
          }
  
          void setMute(boolean muted) {
              if (SC_DBG) log("setMute: muted=" + muted);
              for (Connection c : mConnections) {
                  ((SipConnection) c).setMute(muted);
              }
          }
  
          boolean getMute() {
              boolean ret = mConnections.isEmpty()
                      ? false
                      : ((SipConnection) mConnections.get(0)).getMute();
              if (SC_DBG) log("getMute: ret=" + ret);
              return ret;
          }
  
          void merge(SipCall that) throws CallStateException {
              if (SC_DBG) log("merge:");
              AudioGroup audioGroup = getAudioGroup();
  
              // copy to an array to avoid concurrent modification as connections
              // in that.connections will be removed in add(SipConnection).
              Connection[] cc = that.mConnections.toArray(
                      new Connection[that.mConnections.size()]);
              for (Connection c : cc) {
                  SipConnection conn = (SipConnection) c;
                  add(conn);
                  if (conn.getState() == Call.State.HOLDING) {
                      conn.unhold(audioGroup);
                  }
              }
              that.setState(Call.State.IDLE);
          }
  
          private void add(SipConnection conn) {
              if (SC_DBG) log("add:");
              SipCall call = conn.getCall();
              if (call == this) return;
              if (call != null) call.mConnections.remove(conn);
  
              mConnections.add(conn);
              conn.changeOwner(this);
          }
  
          void sendDtmf(char c) {
              if (SC_DBG) log("sendDtmf: c=" + c);
              AudioGroup audioGroup = getAudioGroup();
              if (audioGroup == null) {
                  if (SC_DBG) log("sendDtmf: audioGroup == null, ignore c=" + c);
                  return;
              }
              audioGroup.sendDtmf(convertDtmf(c));
          }
  
          private int convertDtmf(char c) {
              int code = c - '0';
              if ((code < 0) || (code > 9)) {
                  switch (c) {
                      case '*': return 10;
                      case '#': return 11;
                      case 'A': return 12;
                      case 'B': return 13;
                      case 'C': return 14;
                      case 'D': return 15;
                      default:
                          throw new IllegalArgumentException(
                                  "invalid DTMF char: " + (int) c);
                  }
              }
              return code;
          }
  
          @Override
          protected void setState(State newState) {
              if (mState != newState) {
                  if (SC_DBG) log("setState: cur state" + mState
                          + " --> " + newState + ": " + this + ": on phone "
                          + getPhone() + " " + mConnections.size());
  
                  if (newState == Call.State.ALERTING) {
                      mState = newState; // need in ALERTING to enable ringback
                      startRingbackTone();
                  } else if (mState == Call.State.ALERTING) {
                      stopRingbackTone();
                  }
                  mState = newState;
                  updatePhoneState();
                  notifyPreciseCallStateChanged();
              }
          }
  
          void onConnectionStateChanged(SipConnection conn) {
              // this can be called back when a conf call is formed
              if (SC_DBG) log("onConnectionStateChanged: conn=" + conn);
              if (mState != State.ACTIVE) {
                  setState(conn.getState());
              }
          }
  
          void onConnectionEnded(SipConnection conn) {
              // set state to DISCONNECTED only when all conns are disconnected
              if (SC_DBG) log("onConnectionEnded: conn=" + conn);
              if (mState != State.DISCONNECTED) {
                  boolean allConnectionsDisconnected = true;
                  if (SC_DBG) log("---check connections: "
                          + mConnections.size());
                  for (Connection c : mConnections) {
                      if (SC_DBG) log("   state=" + c.getState() + ": "
                              + c);
                      if (c.getState() != State.DISCONNECTED) {
                          allConnectionsDisconnected = false;
                          break;
                      }
                  }
                  if (allConnectionsDisconnected) setState(State.DISCONNECTED);
              }
              notifyDisconnectP(conn);
          }
  
          private AudioGroup getAudioGroup() {
              if (mConnections.isEmpty()) return null;
              return ((SipConnection) mConnections.get(0)).getAudioGroup();
          }
  
          private void log(String s) {
              Rlog.d(SC_TAG, s);
          }
      }
  
      private class SipConnection extends SipConnectionBase {
          private static final String SCN_TAG = "SipConnection";
          private static final boolean SCN_DBG = true;
  
          private SipCall mOwner;
          private SipAudioCall mSipAudioCall;
          private Call.State mState = Call.State.IDLE;
          private SipProfile mPeer;
          private boolean mIncoming = false;
          private String mOriginalNumber; // may be a PSTN number
  
          private SipAudioCallAdapter mAdapter = new SipAudioCallAdapter() {
              @Override
              protected void onCallEnded(int cause) {
                  if (getDisconnectCause() != DisconnectCause.LOCAL) {
                      setDisconnectCause(cause);
                  }
                  synchronized (SipPhone.class) {
                      setState(Call.State.DISCONNECTED);
                      SipAudioCall sipAudioCall = mSipAudioCall;
                      // FIXME: This goes null and is synchronized, but many uses aren't sync'd
                      mSipAudioCall = null;
                      String sessionState = (sipAudioCall == null)
                              ? ""
                              : (sipAudioCall.getState() + ", ");
                      if (SCN_DBG) log("[SipAudioCallAdapter] onCallEnded: "
                              + hidePii(mPeer.getUriString()) + ": " + sessionState
                              + "cause: " + getDisconnectCause() + ", on phone "
                              + getPhone());
                      if (sipAudioCall != null) {
                          sipAudioCall.setListener(null);
                          sipAudioCall.close();
                      }
                      mOwner.onConnectionEnded(SipConnection.this);
                  }
              }
  
              @Override
              public void onCallEstablished(SipAudioCall call) {
                  onChanged(call);
                  // Race onChanged synchronized this isn't
                  if (mState == Call.State.ACTIVE) call.startAudio();
              }
  
              @Override
              public void onCallHeld(SipAudioCall call) {
                  onChanged(call);
                  // Race onChanged synchronized this isn't
                  if (mState == Call.State.HOLDING) call.startAudio();
              }
  
              @Override
              public void onChanged(SipAudioCall call) {
                  synchronized (SipPhone.class) {
                      Call.State newState = getCallStateFrom(call);
                      if (mState == newState) return;
                      if (newState == Call.State.INCOMING) {
                          setState(mOwner.getState()); // INCOMING or WAITING
                      } else {
                          if (mOwner == mRingingCall) {
                              if (mRingingCall.getState() == Call.State.WAITING) {
                                  try {
                                      switchHoldingAndActive();
                                  } catch (CallStateException e) {
                                      // disconnect the call.
                                      onCallEnded(DisconnectCause.LOCAL);
                                      return;
                                  }
                              }
                              mForegroundCall.switchWith(mRingingCall);
                          }
                          setState(newState);
                      }
                      mOwner.onConnectionStateChanged(SipConnection.this);
                      if (SCN_DBG) {
                          log("onChanged: " + hidePii(mPeer.getUriString()) + ": " + mState
                                  + " on phone " + getPhone());
                      }
                  }
              }
  
              @Override
              protected void onError(int cause) {
                  if (SCN_DBG) log("onError: " + cause);
                  onCallEnded(cause);
              }
          };
  
          public SipConnection(SipCall owner, SipProfile callee,
                  String originalNumber) {
              super(originalNumber);
              mOwner = owner;
              mPeer = callee;
              mOriginalNumber = originalNumber;
          }
  
          public SipConnection(SipCall owner, SipProfile callee) {
              this(owner, callee, getUriString(callee));
          }
  
          @Override
          public String getCnapName() {
              String displayName = mPeer.getDisplayName();
              return TextUtils.isEmpty(displayName) ? null
                                                    : displayName;
          }
  
          @Override
          public int getNumberPresentation() {
              return PhoneConstants.PRESENTATION_ALLOWED;
          }
  
          void initIncomingCall(SipAudioCall sipAudioCall, Call.State newState) {
              setState(newState);
              mSipAudioCall = sipAudioCall;
              sipAudioCall.setListener(mAdapter); // call back to set state
              mIncoming = true;
          }
  
          void acceptCall() throws CallStateException {
              try {
                  mSipAudioCall.answerCall(TIMEOUT_ANSWER_CALL);
              } catch (SipException e) {
                  throw new CallStateException("acceptCall(): " + e);
              }
          }
  
          void changeOwner(SipCall owner) {
              mOwner = owner;
          }
  
          AudioGroup getAudioGroup() {
              if (mSipAudioCall == null) return null;
              return mSipAudioCall.getAudioGroup();
          }
  
          void dial() throws SipException {
              setState(Call.State.DIALING);
              mSipAudioCall = mSipManager.makeAudioCall(mProfile, mPeer, null,
                      TIMEOUT_MAKE_CALL);
              mSipAudioCall.setListener(mAdapter);
          }
  
          void hold() throws CallStateException {
              setState(Call.State.HOLDING);
              try {
                  mSipAudioCall.holdCall(TIMEOUT_HOLD_CALL);
              } catch (SipException e) {
                  throw new CallStateException("hold(): " + e);
              }
          }
  
          void unhold(AudioGroup audioGroup) throws CallStateException {
              mSipAudioCall.setAudioGroup(audioGroup);
              setState(Call.State.ACTIVE);
              try {
                  mSipAudioCall.continueCall(TIMEOUT_HOLD_CALL);
              } catch (SipException e) {
                  throw new CallStateException("unhold(): " + e);
              }
          }
  
          void setMute(boolean muted) {
              if ((mSipAudioCall != null) && (muted != mSipAudioCall.isMuted())) {
                  if (SCN_DBG) log("setState: prev muted=" + !muted + " new muted=" + muted);
                  mSipAudioCall.toggleMute();
              }
          }
  
          boolean getMute() {
              return (mSipAudioCall == null) ? false
                                             : mSipAudioCall.isMuted();
          }
  
          @Override
          protected void setState(Call.State state) {
              if (state == mState) return;
              super.setState(state);
              mState = state;
          }
  
          @Override
          public Call.State getState() {
              return mState;
          }
  
          @Override
          public boolean isIncoming() {
              return mIncoming;
          }
  
          @Override
          public String getAddress() {
              // Phone app uses this to query caller ID. Return the original dial
              // number (which may be a PSTN number) instead of the peer's SIP
              // URI.
              return mOriginalNumber;
          }
  
          @Override
          public SipCall getCall() {
              return mOwner;
          }
  
          @Override
          protected Phone getPhone() {
              return mOwner.getPhone();
          }
  
          @Override
          public void hangup() throws CallStateException {
              synchronized (SipPhone.class) {
                  if (SCN_DBG) {
                      log("hangup: conn=" + hidePii(mPeer.getUriString())
                              + ": " + mState + ": on phone "
                              + getPhone().getPhoneName());
                  }
                  if (!mState.isAlive()) return;
                  try {
                      SipAudioCall sipAudioCall = mSipAudioCall;
                      if (sipAudioCall != null) {
                          sipAudioCall.setListener(null);
                          sipAudioCall.endCall();
                      }
                  } catch (SipException e) {
                      throw new CallStateException("hangup(): " + e);
                  } finally {
                      mAdapter.onCallEnded(((mState == Call.State.INCOMING)
                              || (mState == Call.State.WAITING))
                              ? DisconnectCause.INCOMING_REJECTED
                              : DisconnectCause.LOCAL);
                  }
              }
          }
  
          @Override
          public void separate() throws CallStateException {
              synchronized (SipPhone.class) {
                  SipCall call = (getPhone() == SipPhone.this)
                          ? (SipCall) getBackgroundCall()
                          : (SipCall) getForegroundCall();
                  if (call.getState() != Call.State.IDLE) {
                      throw new CallStateException(
                              "cannot put conn back to a call in non-idle state: "
                              + call.getState());
                  }
                  if (SCN_DBG) log("separate: conn="
                          + mPeer.getUriString() + " from " + mOwner + " back to "
                          + call);
  
                  // separate the AudioGroup and connection from the original call
                  Phone originalPhone = getPhone();
                  AudioGroup audioGroup = call.getAudioGroup(); // may be null
                  call.add(this);
                  mSipAudioCall.setAudioGroup(audioGroup);
  
                  // put the original call to bg; and the separated call becomes
                  // fg if it was in bg
                  originalPhone.switchHoldingAndActive();
  
                  // start audio and notify the phone app of the state change
                  call = (SipCall) getForegroundCall();
                  mSipAudioCall.startAudio();
                  call.onConnectionStateChanged(this);
              }
          }
  
          @Override
          public void deflect(String number) throws CallStateException {
              //Deflect is not supported.
              throw new CallStateException ("deflect is not supported for SipPhone");
          }
  
          private void log(String s) {
              Rlog.d(SCN_TAG, s);
          }
      }
  
      private abstract class SipAudioCallAdapter extends SipAudioCall.Listener {
          private static final String SACA_TAG = "SipAudioCallAdapter";
          private static final boolean SACA_DBG = true;
          /** Call ended with cause defined in {@link DisconnectCause}. */
          protected abstract void onCallEnded(int cause);
          /** Call failed with cause defined in {@link DisconnectCause}. */
          protected abstract void onError(int cause);
  
          @Override
          public void onCallEnded(SipAudioCall call) {
              if (SACA_DBG) log("onCallEnded: call=" + call);
              onCallEnded(call.isInCall()
                      ? DisconnectCause.NORMAL
                      : DisconnectCause.INCOMING_MISSED);
          }
  
          @Override
          public void onCallBusy(SipAudioCall call) {
              if (SACA_DBG) log("onCallBusy: call=" + call);
              onCallEnded(DisconnectCause.BUSY);
          }
  
          @Override
          public void onError(SipAudioCall call, int errorCode,
                  String errorMessage) {
              if (SACA_DBG) {
                  log("onError: call=" + call + " code="+ SipErrorCode.toString(errorCode)
                      + ": " + errorMessage);
              }
              switch (errorCode) {
                  case SipErrorCode.SERVER_UNREACHABLE:
                      onError(DisconnectCause.SERVER_UNREACHABLE);
                      break;
                  case SipErrorCode.PEER_NOT_REACHABLE:
                      onError(DisconnectCause.NUMBER_UNREACHABLE);
                      break;
                  case SipErrorCode.INVALID_REMOTE_URI:
                      onError(DisconnectCause.INVALID_NUMBER);
                      break;
                  case SipErrorCode.TIME_OUT:
                  case SipErrorCode.TRANSACTION_TERMINTED:
                      onError(DisconnectCause.TIMED_OUT);
                      break;
                  case SipErrorCode.DATA_CONNECTION_LOST:
                      onError(DisconnectCause.LOST_SIGNAL);
                      break;
                  case SipErrorCode.INVALID_CREDENTIALS:
                      onError(DisconnectCause.INVALID_CREDENTIALS);
                      break;
                  case SipErrorCode.CROSS_DOMAIN_AUTHENTICATION:
                      onError(DisconnectCause.OUT_OF_NETWORK);
                      break;
                  case SipErrorCode.SERVER_ERROR:
                      onError(DisconnectCause.SERVER_ERROR);
                      break;
                  case SipErrorCode.SOCKET_ERROR:
                  case SipErrorCode.CLIENT_ERROR:
                  default:
                      onError(DisconnectCause.ERROR_UNSPECIFIED);
              }
          }
  
          private void log(String s) {
              Rlog.d(SACA_TAG, s);
          }
      }
  
      public static String hidePii(String s) {
          return VDBG ? Rlog.pii(LOG_TAG, s) : "xxxxx";
      }
  }
  