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 android.net.sip;
  
  import android.content.Context;
  import android.media.AudioManager;
  import android.net.rtp.AudioCodec;
  import android.net.rtp.AudioGroup;
  import android.net.rtp.AudioStream;
  import android.net.rtp.RtpStream;
  import android.net.sip.SimpleSessionDescription.Media;
  import android.net.wifi.WifiManager;
  import android.os.Message;
  import android.telephony.Rlog;
  import android.text.TextUtils;
  import java.io.IOException;
  import java.net.InetAddress;
  import java.net.UnknownHostException;
  
  /**
   * Handles an Internet audio call over SIP. You can instantiate this class with {@link SipManager},
   * using {@link SipManager#makeAudioCall makeAudioCall()} and  {@link SipManager#takeAudioCall
   * takeAudioCall()}.
   *
   * <p class="note"><strong>Note:</strong> Using this class require the
   *   {@link android.Manifest.permission#INTERNET} and
   *   {@link android.Manifest.permission#USE_SIP} permissions. In addition, {@link
   *   #startAudio} requires the
   *   {@link android.Manifest.permission#RECORD_AUDIO},
   *   {@link android.Manifest.permission#ACCESS_WIFI_STATE}, and
   *   {@link android.Manifest.permission#WAKE_LOCK} permissions; and {@link #setSpeakerMode
   *   setSpeakerMode()} requires the
   *   {@link android.Manifest.permission#MODIFY_AUDIO_SETTINGS} permission.</p>
   *
   * <div class="special reference">
   * <h3>Developer Guides</h3>
   * <p>For more information about using SIP, read the
   * <a href="{@docRoot}guide/topics/network/sip.html">Session Initiation Protocol</a>
   * developer guide.</p>
   * </div>
   */
  public class SipAudioCall {
      private static final String LOG_TAG = SipAudioCall.class.getSimpleName();
      private static final boolean DBG = true;
      private static final boolean RELEASE_SOCKET = true;
      private static final boolean DONT_RELEASE_SOCKET = false;
      private static final int SESSION_TIMEOUT = 5; // in seconds
      private static final int TRANSFER_TIMEOUT = 15; // in seconds
  
      /** Listener for events relating to a SIP call, such as when a call is being
       * recieved ("on ringing") or a call is outgoing ("on calling").
       * <p>Many of these events are also received by {@link SipSession.Listener}.</p>
       */
      public static class Listener {
          /**
           * Called when the call object is ready to make another call.
           * The default implementation calls {@link #onChanged}.
           *
           * @param call the call object that is ready to make another call
           */
          public void onReadyToCall(SipAudioCall call) {
              onChanged(call);
          }
  
          /**
           * Called when a request is sent out to initiate a new call.
           * The default implementation calls {@link #onChanged}.
           *
           * @param call the call object that carries out the audio call
           */
          public void onCalling(SipAudioCall call) {
              onChanged(call);
          }
  
          /**
           * Called when a new call comes in.
           * The default implementation calls {@link #onChanged}.
           *
           * @param call the call object that carries out the audio call
           * @param caller the SIP profile of the caller
           */
          public void onRinging(SipAudioCall call, SipProfile caller) {
              onChanged(call);
          }
  
          /**
           * Called when a RINGING response is received for the INVITE request
           * sent. The default implementation calls {@link #onChanged}.
           *
           * @param call the call object that carries out the audio call
           */
          public void onRingingBack(SipAudioCall call) {
              onChanged(call);
          }
  
          /**
           * Called when the session is established.
           * The default implementation calls {@link #onChanged}.
           *
           * @param call the call object that carries out the audio call
           */
          public void onCallEstablished(SipAudioCall call) {
              onChanged(call);
          }
  
          /**
           * Called when the session is terminated.
           * The default implementation calls {@link #onChanged}.
           *
           * @param call the call object that carries out the audio call
           */
          public void onCallEnded(SipAudioCall call) {
              onChanged(call);
          }
  
          /**
           * Called when the peer is busy during session initialization.
           * The default implementation calls {@link #onChanged}.
           *
           * @param call the call object that carries out the audio call
           */
          public void onCallBusy(SipAudioCall call) {
              onChanged(call);
          }
  
          /**
           * Called when the call is on hold.
           * The default implementation calls {@link #onChanged}.
           *
           * @param call the call object that carries out the audio call
           */
          public void onCallHeld(SipAudioCall call) {
              onChanged(call);
          }
  
          /**
           * Called when an error occurs. The default implementation is no op.
           *
           * @param call the call object that carries out the audio call
           * @param errorCode error code of this error
           * @param errorMessage error message
           * @see SipErrorCode
           */
          public void onError(SipAudioCall call, int errorCode,
                  String errorMessage) {
              // no-op
          }
  
          /**
           * Called when an event occurs and the corresponding callback is not
           * overridden. The default implementation is no op. Error events are
           * not re-directed to this callback and are handled in {@link #onError}.
           */
          public void onChanged(SipAudioCall call) {
              // no-op
          }
      }
  
      private Context mContext;
      private SipProfile mLocalProfile;
      private SipAudioCall.Listener mListener;
      private SipSession mSipSession;
      private SipSession mTransferringSession;
  
      private long mSessionId = System.currentTimeMillis();
      private String mPeerSd;
  
      private AudioStream mAudioStream;
      private AudioGroup mAudioGroup;
  
      private boolean mInCall = false;
      private boolean mMuted = false;
      private boolean mHold = false;
  
      private WifiManager mWm;
      private WifiManager.WifiLock mWifiHighPerfLock;
  
      private int mErrorCode = SipErrorCode.NO_ERROR;
      private String mErrorMessage;
  
      /**
       * Creates a call object with the local SIP profile.
       * @param context the context for accessing system services such as
       *        ringtone, audio, WIFI etc
       */
      public SipAudioCall(Context context, SipProfile localProfile) {
          mContext = context;
          mLocalProfile = localProfile;
          mWm = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
      }
  
      /**
       * Sets the listener to listen to the audio call events. The method calls
       * {@link #setListener setListener(listener, false)}.
       *
       * @param listener to listen to the audio call events of this object
       * @see #setListener(Listener, boolean)
       */
      public void setListener(SipAudioCall.Listener listener) {
          setListener(listener, false);
      }
  
      /**
       * Sets the listener to listen to the audio call events. A
       * {@link SipAudioCall} can only hold one listener at a time. Subsequent
       * calls to this method override the previous listener.
       *
       * @param listener to listen to the audio call events of this object
       * @param callbackImmediately set to true if the caller wants to be called
       *      back immediately on the current state
       */
      public void setListener(SipAudioCall.Listener listener,
              boolean callbackImmediately) {
          mListener = listener;
          try {
              if ((listener == null) || !callbackImmediately) {
                  // do nothing
              } else if (mErrorCode != SipErrorCode.NO_ERROR) {
                  listener.onError(this, mErrorCode, mErrorMessage);
              } else if (mInCall) {
                  if (mHold) {
                      listener.onCallHeld(this);
                  } else {
                      listener.onCallEstablished(this);
                  }
              } else {
                  int state = getState();
                  switch (state) {
                      case SipSession.State.READY_TO_CALL:
                          listener.onReadyToCall(this);
                          break;
                      case SipSession.State.INCOMING_CALL:
                          listener.onRinging(this, getPeerProfile());
                          break;
                      case SipSession.State.OUTGOING_CALL:
                          listener.onCalling(this);
                          break;
                      case SipSession.State.OUTGOING_CALL_RING_BACK:
                          listener.onRingingBack(this);
                          break;
                  }
              }
          } catch (Throwable t) {
              loge("setListener()", t);
          }
      }
  
      /**
       * Checks if the call is established.
       *
       * @return true if the call is established
       */
      public boolean isInCall() {
          synchronized (this) {
              return mInCall;
          }
      }
  
      /**
       * Checks if the call is on hold.
       *
       * @return true if the call is on hold
       */
      public boolean isOnHold() {
          synchronized (this) {
              return mHold;
          }
      }
  
      /**
       * Closes this object. This object is not usable after being closed.
       */
      public void close() {
          close(true);
      }
  
      private synchronized void close(boolean closeRtp) {
          if (closeRtp) stopCall(RELEASE_SOCKET);
  
          mInCall = false;
          mHold = false;
          mSessionId = System.currentTimeMillis();
          mErrorCode = SipErrorCode.NO_ERROR;
          mErrorMessage = null;
  
          if (mSipSession != null) {
              mSipSession.setListener(null);
              mSipSession = null;
          }
      }
  
      /**
       * Gets the local SIP profile.
       *
       * @return the local SIP profile
       */
      public SipProfile getLocalProfile() {
          synchronized (this) {
              return mLocalProfile;
          }
      }
  
      /**
       * Gets the peer's SIP profile.
       *
       * @return the peer's SIP profile
       */
      public SipProfile getPeerProfile() {
          synchronized (this) {
              return (mSipSession == null) ? null : mSipSession.getPeerProfile();
          }
      }
  
      /**
       * Gets the state of the {@link SipSession} that carries this call.
       * The value returned must be one of the states in {@link SipSession.State}.
       *
       * @return the session state
       */
      public int getState() {
          synchronized (this) {
              if (mSipSession == null) return SipSession.State.READY_TO_CALL;
              return mSipSession.getState();
          }
      }
  
  
      /**
       * Gets the {@link SipSession} that carries this call.
       *
       * @return the session object that carries this call
       * @hide
       */
      public SipSession getSipSession() {
          synchronized (this) {
              return mSipSession;
          }
      }
  
      private synchronized void transferToNewSession() {
          if (mTransferringSession == null) return;
          SipSession origin = mSipSession;
          mSipSession = mTransferringSession;
          mTransferringSession = null;
  
          // stop the replaced call.
          if (mAudioStream != null) {
              mAudioStream.join(null);
          } else {
              try {
                  mAudioStream = new AudioStream(InetAddress.getByName(
                          getLocalIp()));
              } catch (Throwable t) {
                  loge("transferToNewSession():", t);
              }
          }
          if (origin != null) origin.endCall();
          startAudio();
      }
  
      private SipSession.Listener createListener() {
          return new SipSession.Listener() {
              @Override
              public void onCalling(SipSession session) {
                  if (DBG) log("onCalling: session=" + session);
                  Listener listener = mListener;
                  if (listener != null) {
                      try {
                          listener.onCalling(SipAudioCall.this);
                      } catch (Throwable t) {
                          loge("onCalling():", t);
                      }
                  }
              }
  
              @Override
              public void onRingingBack(SipSession session) {
                  if (DBG) log("onRingingBackk: " + session);
                  Listener listener = mListener;
                  if (listener != null) {
                      try {
                          listener.onRingingBack(SipAudioCall.this);
                      } catch (Throwable t) {
                          loge("onRingingBack():", t);
                      }
                  }
              }
  
              @Override
              public void onRinging(SipSession session,
                      SipProfile peerProfile, String sessionDescription) {
                  // this callback is triggered only for reinvite.
                  synchronized (SipAudioCall.this) {
                      if ((mSipSession == null) || !mInCall
                              || !session.getCallId().equals(
                                      mSipSession.getCallId())) {
                          // should not happen
                          session.endCall();
                          return;
                      }
  
                      // session changing request
                      try {
                          String answer = createAnswer(sessionDescription).encode();
                          mSipSession.answerCall(answer, SESSION_TIMEOUT);
                      } catch (Throwable e) {
                          loge("onRinging():", e);
                          session.endCall();
                      }
                  }
              }
  
              @Override
              public void onCallEstablished(SipSession session,
                      String sessionDescription) {
                  mPeerSd = sessionDescription;
                  if (DBG) log("onCallEstablished(): " + mPeerSd);
  
                  // TODO: how to notify the UI that the remote party is changed
                  if ((mTransferringSession != null)
                          && (session == mTransferringSession)) {
                      transferToNewSession();
                      return;
                  }
  
                  Listener listener = mListener;
                  if (listener != null) {
                      try {
                          if (mHold) {
                              listener.onCallHeld(SipAudioCall.this);
                          } else {
                              listener.onCallEstablished(SipAudioCall.this);
                          }
                      } catch (Throwable t) {
                          loge("onCallEstablished(): ", t);
                      }
                  }
              }
  
              @Override
              public void onCallEnded(SipSession session) {
                  if (DBG) log("onCallEnded: " + session + " mSipSession:" + mSipSession);
                  // reset the trasnferring session if it is the one.
                  if (session == mTransferringSession) {
                      mTransferringSession = null;
                      return;
                  }
                  // or ignore the event if the original session is being
                  // transferred to the new one.
                  if ((mTransferringSession != null) ||
                      (session != mSipSession)) return;
  
                  Listener listener = mListener;
                  if (listener != null) {
                      try {
                          listener.onCallEnded(SipAudioCall.this);
                      } catch (Throwable t) {
                          loge("onCallEnded(): ", t);
                      }
                  }
                  close();
              }
  
              @Override
              public void onCallBusy(SipSession session) {
                  if (DBG) log("onCallBusy: " + session);
                  Listener listener = mListener;
                  if (listener != null) {
                      try {
                          listener.onCallBusy(SipAudioCall.this);
                      } catch (Throwable t) {
                          loge("onCallBusy(): ", t);
                      }
                  }
                  close(false);
              }
  
              @Override
              public void onCallChangeFailed(SipSession session, int errorCode,
                      String message) {
                  if (DBG) log("onCallChangedFailed: " + message);
                  mErrorCode = errorCode;
                  mErrorMessage = message;
                  Listener listener = mListener;
                  if (listener != null) {
                      try {
                          listener.onError(SipAudioCall.this, mErrorCode,
                                  message);
                      } catch (Throwable t) {
                          loge("onCallBusy():", t);
                      }
                  }
              }
  
              @Override
              public void onError(SipSession session, int errorCode,
                      String message) {
                  SipAudioCall.this.onError(errorCode, message);
              }
  
              @Override
              public void onRegistering(SipSession session) {
                  // irrelevant
              }
  
              @Override
              public void onRegistrationTimeout(SipSession session) {
                  // irrelevant
              }
  
              @Override
              public void onRegistrationFailed(SipSession session, int errorCode,
                      String message) {
                  // irrelevant
              }
  
              @Override
              public void onRegistrationDone(SipSession session, int duration) {
                  // irrelevant
              }
  
              @Override
              public void onCallTransferring(SipSession newSession,
                      String sessionDescription) {
                  if (DBG) log("onCallTransferring: mSipSession="
                          + mSipSession + " newSession=" + newSession);
                  mTransferringSession = newSession;
                  try {
                      if (sessionDescription == null) {
                          newSession.makeCall(newSession.getPeerProfile(),
                                  createOffer().encode(), TRANSFER_TIMEOUT);
                      } else {
                          String answer = createAnswer(sessionDescription).encode();
                          newSession.answerCall(answer, SESSION_TIMEOUT);
                      }
                  } catch (Throwable e) {
                      loge("onCallTransferring()", e);
                      newSession.endCall();
                  }
              }
          };
      }
  
      private void onError(int errorCode, String message) {
          if (DBG) log("onError: "
                  + SipErrorCode.toString(errorCode) + ": " + message);
          mErrorCode = errorCode;
          mErrorMessage = message;
          Listener listener = mListener;
          if (listener != null) {
              try {
                  listener.onError(this, errorCode, message);
              } catch (Throwable t) {
                  loge("onError():", t);
              }
          }
          synchronized (this) {
              if ((errorCode == SipErrorCode.DATA_CONNECTION_LOST)
                      || !isInCall()) {
                  close(true);
              }
          }
      }
  
      /**
       * Attaches an incoming call to this call object.
       *
       * @param session the session that receives the incoming call
       * @param sessionDescription the session description of the incoming call
       * @throws SipException if the SIP service fails to attach this object to
       *        the session or VOIP API is not supported by the device
       * @see SipManager#isVoipSupported
       */
      public void attachCall(SipSession session, String sessionDescription)
              throws SipException {
          if (!SipManager.isVoipSupported(mContext)) {
              throw new SipException("VOIP API is not supported");
          }
  
          synchronized (this) {
              mSipSession = session;
              mPeerSd = sessionDescription;
              if (DBG) log("attachCall(): " + mPeerSd);
              try {
                  session.setListener(createListener());
              } catch (Throwable e) {
                  loge("attachCall()", e);
                  throwSipException(e);
              }
          }
      }
  
      /**
       * 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 {@link Listener#onError onError(SipAudioCall, SipErrorCode.TIME_OUT, String)}
       * will be called.
       *
       * @param peerProfile the SIP profile to make the call to
       * @param sipSession the {@link SipSession} for carrying out the call
       * @param timeout the timeout value in seconds. Default value (defined by
       *        SIP protocol) is used if {@code timeout} is zero or negative.
       * @see Listener#onError
       * @throws SipException if the SIP service fails to create a session for the
       *        call or VOIP API is not supported by the device
       * @see SipManager#isVoipSupported
       */
      public void makeCall(SipProfile peerProfile, SipSession sipSession,
              int timeout) throws SipException {
          if (DBG) log("makeCall: " + peerProfile + " session=" + sipSession + " timeout=" + timeout);
          if (!SipManager.isVoipSupported(mContext)) {
              throw new SipException("VOIP API is not supported");
          }
  
          synchronized (this) {
              mSipSession = sipSession;
              try {
                  mAudioStream = new AudioStream(InetAddress.getByName(
                          getLocalIp()));
                  sipSession.setListener(createListener());
                  sipSession.makeCall(peerProfile, createOffer().encode(),
                          timeout);
              } catch (IOException e) {
                  loge("makeCall:", e);
                  throw new SipException("makeCall()", e);
              }
          }
      }
  
      /**
       * Ends a call.
       * @throws SipException if the SIP service fails to end the call
       */
      public void endCall() throws SipException {
          if (DBG) log("endCall: mSipSession" + mSipSession);
          synchronized (this) {
              stopCall(RELEASE_SOCKET);
              mInCall = false;
  
              // perform the above local ops first and then network op
              if (mSipSession != null) mSipSession.endCall();
          }
      }
  
      /**
       * Puts a call on hold.  When succeeds, {@link Listener#onCallHeld} is
       * called. The attempt will be timed out if the call is not established
       * within {@code timeout} seconds and
       * {@link Listener#onError onError(SipAudioCall, SipErrorCode.TIME_OUT, String)}
       * will be called.
       *
       * @param timeout the timeout value in seconds. Default value (defined by
       *        SIP protocol) is used if {@code timeout} is zero or negative.
       * @see Listener#onError
       * @throws SipException if the SIP service fails to hold the call
       */
      public void holdCall(int timeout) throws SipException {
          if (DBG) log("holdCall: mSipSession" + mSipSession + " timeout=" + timeout);
          synchronized (this) {
              if (mHold) return;
              if (mSipSession == null) {
                  loge("holdCall:");
                  throw new SipException("Not in a call to hold call");
              }
              mSipSession.changeCall(createHoldOffer().encode(), timeout);
              mHold = true;
              setAudioGroupMode();
          }
      }
  
      /**
       * Answers a call. The attempt will be timed out if the call is not
       * established within {@code timeout} seconds and
       * {@link Listener#onError onError(SipAudioCall, SipErrorCode.TIME_OUT, String)}
       * will be called.
       *
       * @param timeout the timeout value in seconds. Default value (defined by
       *        SIP protocol) is used if {@code timeout} is zero or negative.
       * @see Listener#onError
       * @throws SipException if the SIP service fails to answer the call
       */
      public void answerCall(int timeout) throws SipException {
          if (DBG) log("answerCall: mSipSession" + mSipSession + " timeout=" + timeout);
          synchronized (this) {
              if (mSipSession == null) {
                  throw new SipException("No call to answer");
              }
              try {
                  mAudioStream = new AudioStream(InetAddress.getByName(
                          getLocalIp()));
                  mSipSession.answerCall(createAnswer(mPeerSd).encode(), timeout);
              } catch (IOException e) {
                  loge("answerCall:", e);
                  throw new SipException("answerCall()", e);
              }
          }
      }
  
      /**
       * Continues a call that's on hold. When succeeds,
       * {@link Listener#onCallEstablished} is called. The attempt will be timed
       * out if the call is not established within {@code timeout} seconds and
       * {@link Listener#onError onError(SipAudioCall, SipErrorCode.TIME_OUT, String)}
       * will be called.
       *
       * @param timeout the timeout value in seconds. Default value (defined by
       *        SIP protocol) is used if {@code timeout} is zero or negative.
       * @see Listener#onError
       * @throws SipException if the SIP service fails to unhold the call
       */
      public void continueCall(int timeout) throws SipException {
          if (DBG) log("continueCall: mSipSession" + mSipSession + " timeout=" + timeout);
          synchronized (this) {
              if (!mHold) return;
              mSipSession.changeCall(createContinueOffer().encode(), timeout);
              mHold = false;
              setAudioGroupMode();
          }
      }
  
      private SimpleSessionDescription createOffer() {
          SimpleSessionDescription offer =
                  new SimpleSessionDescription(mSessionId, getLocalIp());
          AudioCodec[] codecs = AudioCodec.getCodecs();
          Media media = offer.newMedia(
                  "audio", mAudioStream.getLocalPort(), 1, "RTP/AVP");
          for (AudioCodec codec : AudioCodec.getCodecs()) {
              media.setRtpPayload(codec.type, codec.rtpmap, codec.fmtp);
          }
          media.setRtpPayload(127, "telephone-event/8000", "0-15");
          if (DBG) log("createOffer: offer=" + offer);
          return offer;
      }
  
      private SimpleSessionDescription createAnswer(String offerSd) {
          if (TextUtils.isEmpty(offerSd)) return createOffer();
          SimpleSessionDescription offer =
                  new SimpleSessionDescription(offerSd);
          SimpleSessionDescription answer =
                  new SimpleSessionDescription(mSessionId, getLocalIp());
          AudioCodec codec = null;
          for (Media media : offer.getMedia()) {
              if ((codec == null) && (media.getPort() > 0)
                      && "audio".equals(media.getType())
                      && "RTP/AVP".equals(media.getProtocol())) {
                  // Find the first audio codec we supported.
                  for (int type : media.getRtpPayloadTypes()) {
                      codec = AudioCodec.getCodec(type, media.getRtpmap(type),
                              media.getFmtp(type));
                      if (codec != null) {
                          break;
                      }
                  }
                  if (codec != null) {
                      Media reply = answer.newMedia(
                              "audio", mAudioStream.getLocalPort(), 1, "RTP/AVP");
                      reply.setRtpPayload(codec.type, codec.rtpmap, codec.fmtp);
  
                      // Check if DTMF is supported in the same media.
                      for (int type : media.getRtpPayloadTypes()) {
                          String rtpmap = media.getRtpmap(type);
                          if ((type != codec.type) && (rtpmap != null)
                                  && rtpmap.startsWith("telephone-event")) {
                              reply.setRtpPayload(
                                      type, rtpmap, media.getFmtp(type));
                          }
                      }
  
                      // Handle recvonly and sendonly.
                      if (media.getAttribute("recvonly") != null) {
                          answer.setAttribute("sendonly", "");
                      } else if(media.getAttribute("sendonly") != null) {
                          answer.setAttribute("recvonly", "");
                      } else if(offer.getAttribute("recvonly") != null) {
                          answer.setAttribute("sendonly", "");
                      } else if(offer.getAttribute("sendonly") != null) {
                          answer.setAttribute("recvonly", "");
                      }
                      continue;
                  }
              }
              // Reject the media.
              Media reply = answer.newMedia(
                      media.getType(), 0, 1, media.getProtocol());
              for (String format : media.getFormats()) {
                  reply.setFormat(format, null);
              }
          }
          if (codec == null) {
              loge("createAnswer: no suitable codes");
              throw new IllegalStateException("Reject SDP: no suitable codecs");
          }
          if (DBG) log("createAnswer: answer=" + answer);
          return answer;
      }
  
      private SimpleSessionDescription createHoldOffer() {
          SimpleSessionDescription offer = createContinueOffer();
          offer.setAttribute("sendonly", "");
          if (DBG) log("createHoldOffer: offer=" + offer);
          return offer;
      }
  
      private SimpleSessionDescription createContinueOffer() {
          if (DBG) log("createContinueOffer");
          SimpleSessionDescription offer =
                  new SimpleSessionDescription(mSessionId, getLocalIp());
          Media media = offer.newMedia(
                  "audio", mAudioStream.getLocalPort(), 1, "RTP/AVP");
          AudioCodec codec = mAudioStream.getCodec();
          media.setRtpPayload(codec.type, codec.rtpmap, codec.fmtp);
          int dtmfType = mAudioStream.getDtmfType();
          if (dtmfType != -1) {
              media.setRtpPayload(dtmfType, "telephone-event/8000", "0-15");
          }
          return offer;
      }
  
      private void grabWifiHighPerfLock() {
          if (mWifiHighPerfLock == null) {
              if (DBG) log("grabWifiHighPerfLock:");
              mWifiHighPerfLock = ((WifiManager)
                      mContext.getSystemService(Context.WIFI_SERVICE))
                      .createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, LOG_TAG);
              mWifiHighPerfLock.acquire();
          }
      }
  
      private void releaseWifiHighPerfLock() {
          if (mWifiHighPerfLock != null) {
              if (DBG) log("releaseWifiHighPerfLock:");
              mWifiHighPerfLock.release();
              mWifiHighPerfLock = null;
          }
      }
  
      private boolean isWifiOn() {
          return (mWm.getConnectionInfo().getBSSID() == null) ? false : true;
      }
  
      /** Toggles mute. */
      public void toggleMute() {
          synchronized (this) {
              mMuted = !mMuted;
              setAudioGroupMode();
          }
      }
  
      /**
       * Checks if the call is muted.
       *
       * @return true if the call is muted
       */
      public boolean isMuted() {
          synchronized (this) {
              return mMuted;
          }
      }
  
      /**
       * Puts the device to speaker mode.
       * <p class="note"><strong>Note:</strong> Requires the
       *   {@link android.Manifest.permission#MODIFY_AUDIO_SETTINGS} permission.</p>
       *
       * @param speakerMode set true to enable speaker mode; false to disable
       */
      public void setSpeakerMode(boolean speakerMode) {
          synchronized (this) {
              ((AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE))
                      .setSpeakerphoneOn(speakerMode);
              setAudioGroupMode();
          }
      }
  
      private boolean isSpeakerOn() {
          return ((AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE))
                  .isSpeakerphoneOn();
      }
  
      /**
       * Sends a DTMF code. According to <a href="http://tools.ietf.org/html/rfc2833">RFC 2883</a>,
       * event 0--9 maps to decimal
       * value 0--9, '*' to 10, '#' to 11, event 'A'--'D' to 12--15, and event
       * flash to 16. Currently, event flash is not supported.
       *
       * @param code the DTMF code to send. Value 0 to 15 (inclusive) are valid
       *        inputs.
       */
      public void sendDtmf(int code) {
          sendDtmf(code, null);
      }
  
      /**
       * Sends a DTMF code. According to <a href="http://tools.ietf.org/html/rfc2833">RFC 2883</a>,
       * event 0--9 maps to decimal
       * value 0--9, '*' to 10, '#' to 11, event 'A'--'D' to 12--15, and event
       * flash to 16. Currently, event flash is not supported.
       *
       * @param code the DTMF code to send. Value 0 to 15 (inclusive) are valid
       *        inputs.
       * @param result the result message to send when done
       */
      public void sendDtmf(int code, Message result) {
          synchronized (this) {
              AudioGroup audioGroup = getAudioGroup();
              if ((audioGroup != null) && (mSipSession != null)
                      && (SipSession.State.IN_CALL == getState())) {
                  if (DBG) log("sendDtmf: code=" + code + " result=" + result);
                  audioGroup.sendDtmf(code);
              }
              if (result != null) result.sendToTarget();
          }
      }
  
      /**
       * Gets the {@link AudioStream} object used in this call. The object
       * represents the RTP stream that carries the audio data to and from the
       * peer. The object may not be created before the call is established. And
       * it is undefined after the call ends or the {@link #close} method is
       * called.
       *
       * @return the {@link AudioStream} object or null if the RTP stream has not
       *      yet been set up
       * @hide
       */
      public AudioStream getAudioStream() {
          synchronized (this) {
              return mAudioStream;
          }
      }
  
      /**
       * Gets the {@link AudioGroup} object which the {@link AudioStream} object
       * joins. The group object may not exist before the call is established.
       * Also, the {@code AudioStream} may change its group during a call (e.g.,
       * after the call is held/un-held). Finally, the {@code AudioGroup} object
       * returned by this method is undefined after the call ends or the
       * {@link #close} method is called. If a group object is set by
       * {@link #setAudioGroup(AudioGroup)}, then this method returns that object.
       *
       * @return the {@link AudioGroup} object or null if the RTP stream has not
       *      yet been set up
       * @see #getAudioStream
       * @hide
       */
      public AudioGroup getAudioGroup() {
          synchronized (this) {
              if (mAudioGroup != null) return mAudioGroup;
              return ((mAudioStream == null) ? null : mAudioStream.getGroup());
          }
      }
  
      /**
       * Sets the {@link AudioGroup} object which the {@link AudioStream} object
       * joins. If {@code audioGroup} is null, then the {@code AudioGroup} object
       * will be dynamically created when needed. Note that the mode of the
       * {@code AudioGroup} is not changed according to the audio settings (i.e.,
       * hold, mute, speaker phone) of this object. This is mainly used to merge
       * multiple {@code SipAudioCall} objects to form a conference call. The
       * settings of the first object (that merges others) override others'.
       *
       * @see #getAudioStream
       * @hide
       */
      public void setAudioGroup(AudioGroup group) {
          synchronized (this) {
              if (DBG) log("setAudioGroup: group=" + group);
              if ((mAudioStream != null) && (mAudioStream.getGroup() != null)) {
                  mAudioStream.join(group);
              }
              mAudioGroup = group;
          }
      }
  
      /**
       * Starts the audio for the established call. This method should be called
       * after {@link Listener#onCallEstablished} is called.
       * <p class="note"><strong>Note:</strong> Requires the
       *   {@link android.Manifest.permission#RECORD_AUDIO},
       *   {@link android.Manifest.permission#ACCESS_WIFI_STATE} and
       *   {@link android.Manifest.permission#WAKE_LOCK} permissions.</p>
       */
      public void startAudio() {
          try {
              startAudioInternal();
          } catch (UnknownHostException e) {
              onError(SipErrorCode.PEER_NOT_REACHABLE, e.getMessage());
          } catch (Throwable e) {
              onError(SipErrorCode.CLIENT_ERROR, e.getMessage());
          }
      }
  
      private synchronized void startAudioInternal() throws UnknownHostException {
          if (DBG) loge("startAudioInternal: mPeerSd=" + mPeerSd);
          if (mPeerSd == null) {
              throw new IllegalStateException("mPeerSd = null");
          }
  
          stopCall(DONT_RELEASE_SOCKET);
          mInCall = true;
  
          // Run exact the same logic in createAnswer() to setup mAudioStream.
          SimpleSessionDescription offer =
                  new SimpleSessionDescription(mPeerSd);
          AudioStream stream = mAudioStream;
          AudioCodec codec = null;
          for (Media media : offer.getMedia()) {
              if ((codec == null) && (media.getPort() > 0)
                      && "audio".equals(media.getType())
                      && "RTP/AVP".equals(media.getProtocol())) {
                  // Find the first audio codec we supported.
                  for (int type : media.getRtpPayloadTypes()) {
                      codec = AudioCodec.getCodec(
                              type, media.getRtpmap(type), media.getFmtp(type));
                      if (codec != null) {
                          break;
                      }
                  }
  
                  if (codec != null) {
                      // Associate with the remote host.
                      String address = media.getAddress();
                      if (address == null) {
                          address = offer.getAddress();
                      }
                      stream.associate(InetAddress.getByName(address),
                              media.getPort());
  
                      stream.setDtmfType(-1);
                      stream.setCodec(codec);
                      // Check if DTMF is supported in the same media.
                      for (int type : media.getRtpPayloadTypes()) {
                          String rtpmap = media.getRtpmap(type);
                          if ((type != codec.type) && (rtpmap != null)
                                  && rtpmap.startsWith("telephone-event")) {
                              stream.setDtmfType(type);
                          }
                      }
  
                      // Handle recvonly and sendonly.
                      if (mHold) {
                          stream.setMode(RtpStream.MODE_NORMAL);
                      } else if (media.getAttribute("recvonly") != null) {
                          stream.setMode(RtpStream.MODE_SEND_ONLY);
                      } else if(media.getAttribute("sendonly") != null) {
                          stream.setMode(RtpStream.MODE_RECEIVE_ONLY);
                      } else if(offer.getAttribute("recvonly") != null) {
                          stream.setMode(RtpStream.MODE_SEND_ONLY);
                      } else if(offer.getAttribute("sendonly") != null) {
                          stream.setMode(RtpStream.MODE_RECEIVE_ONLY);
                      } else {
                          stream.setMode(RtpStream.MODE_NORMAL);
                      }
                      break;
                  }
              }
          }
          if (codec == null) {
              throw new IllegalStateException("Reject SDP: no suitable codecs");
          }
  
          if (isWifiOn()) grabWifiHighPerfLock();
  
          // AudioGroup logic:
          AudioGroup audioGroup = getAudioGroup();
          if (mHold) {
              // don't create an AudioGroup here; doing so will fail if
              // there's another AudioGroup out there that's active
          } else {
              if (audioGroup == null) audioGroup = new AudioGroup();
              stream.join(audioGroup);
          }
          setAudioGroupMode();
      }
  
      // set audio group mode based on current audio configuration
      private void setAudioGroupMode() {
          AudioGroup audioGroup = getAudioGroup();
          if (DBG) log("setAudioGroupMode: audioGroup=" + audioGroup);
          if (audioGroup != null) {
              if (mHold) {
                  audioGroup.setMode(AudioGroup.MODE_ON_HOLD);
              } else if (mMuted) {
                  audioGroup.setMode(AudioGroup.MODE_MUTED);
              } else if (isSpeakerOn()) {
                  audioGroup.setMode(AudioGroup.MODE_ECHO_SUPPRESSION);
              } else {
                  audioGroup.setMode(AudioGroup.MODE_NORMAL);
              }
          }
      }
  
      private void stopCall(boolean releaseSocket) {
          if (DBG) log("stopCall: releaseSocket=" + releaseSocket);
          releaseWifiHighPerfLock();
          if (mAudioStream != null) {
              mAudioStream.join(null);
  
              if (releaseSocket) {
                  mAudioStream.release();
                  mAudioStream = null;
              }
          }
      }
  
      private String getLocalIp() {
          return mSipSession.getLocalIp();
      }
  
      private void throwSipException(Throwable throwable) throws SipException {
          if (throwable instanceof SipException) {
              throw (SipException) throwable;
          } else {
              throw new SipException("", throwable);
          }
      }
  
      private void log(String s) {
          Rlog.d(LOG_TAG, s);
      }
  
      private void loge(String s) {
          Rlog.e(LOG_TAG, s);
      }
  
      private void loge(String s, Throwable t) {
          Rlog.e(LOG_TAG, s, t);
      }
  }
  