• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2010 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.net.sip;
18 
19 import android.content.Context;
20 import android.media.AudioManager;
21 import android.net.rtp.AudioCodec;
22 import android.net.rtp.AudioGroup;
23 import android.net.rtp.AudioStream;
24 import android.net.rtp.RtpStream;
25 import android.net.sip.SimpleSessionDescription.Media;
26 import android.net.wifi.WifiManager;
27 import android.os.Message;
28 import android.os.RemoteException;
29 import android.text.TextUtils;
30 import android.util.Log;
31 
32 import java.io.IOException;
33 import java.net.InetAddress;
34 import java.net.UnknownHostException;
35 import java.util.ArrayList;
36 import java.util.HashMap;
37 import java.util.List;
38 import java.util.Map;
39 
40 /**
41  * Handles an Internet audio call over SIP. You can instantiate this class with {@link SipManager},
42  * using {@link SipManager#makeAudioCall makeAudioCall()} and  {@link SipManager#takeAudioCall
43  * takeAudioCall()}.
44  *
45  * <p class="note"><strong>Note:</strong> Using this class require the
46  *   {@link android.Manifest.permission#INTERNET} and
47  *   {@link android.Manifest.permission#USE_SIP} permissions. In addition, {@link
48  *   #startAudio} requires the
49  *   {@link android.Manifest.permission#RECORD_AUDIO},
50  *   {@link android.Manifest.permission#ACCESS_WIFI_STATE}, and
51  *   {@link android.Manifest.permission#WAKE_LOCK} permissions; and {@link #setSpeakerMode
52  *   setSpeakerMode()} requires the
53  *   {@link android.Manifest.permission#MODIFY_AUDIO_SETTINGS} permission.</p>
54  *
55  * <div class="special reference">
56  * <h3>Developer Guides</h3>
57  * <p>For more information about using SIP, read the
58  * <a href="{@docRoot}guide/topics/network/sip.html">Session Initiation Protocol</a>
59  * developer guide.</p>
60  * </div>
61  */
62 public class SipAudioCall {
63     private static final String TAG = SipAudioCall.class.getSimpleName();
64     private static final boolean RELEASE_SOCKET = true;
65     private static final boolean DONT_RELEASE_SOCKET = false;
66     private static final int SESSION_TIMEOUT = 5; // in seconds
67     private static final int TRANSFER_TIMEOUT = 15; // in seconds
68 
69     /** Listener for events relating to a SIP call, such as when a call is being
70      * recieved ("on ringing") or a call is outgoing ("on calling").
71      * <p>Many of these events are also received by {@link SipSession.Listener}.</p>
72      */
73     public static class Listener {
74         /**
75          * Called when the call object is ready to make another call.
76          * The default implementation calls {@link #onChanged}.
77          *
78          * @param call the call object that is ready to make another call
79          */
onReadyToCall(SipAudioCall call)80         public void onReadyToCall(SipAudioCall call) {
81             onChanged(call);
82         }
83 
84         /**
85          * Called when a request is sent out to initiate a new call.
86          * The default implementation calls {@link #onChanged}.
87          *
88          * @param call the call object that carries out the audio call
89          */
onCalling(SipAudioCall call)90         public void onCalling(SipAudioCall call) {
91             onChanged(call);
92         }
93 
94         /**
95          * Called when a new call comes in.
96          * The default implementation calls {@link #onChanged}.
97          *
98          * @param call the call object that carries out the audio call
99          * @param caller the SIP profile of the caller
100          */
onRinging(SipAudioCall call, SipProfile caller)101         public void onRinging(SipAudioCall call, SipProfile caller) {
102             onChanged(call);
103         }
104 
105         /**
106          * Called when a RINGING response is received for the INVITE request
107          * sent. The default implementation calls {@link #onChanged}.
108          *
109          * @param call the call object that carries out the audio call
110          */
onRingingBack(SipAudioCall call)111         public void onRingingBack(SipAudioCall call) {
112             onChanged(call);
113         }
114 
115         /**
116          * Called when the session is established.
117          * The default implementation calls {@link #onChanged}.
118          *
119          * @param call the call object that carries out the audio call
120          */
onCallEstablished(SipAudioCall call)121         public void onCallEstablished(SipAudioCall call) {
122             onChanged(call);
123         }
124 
125         /**
126          * Called when the session is terminated.
127          * The default implementation calls {@link #onChanged}.
128          *
129          * @param call the call object that carries out the audio call
130          */
onCallEnded(SipAudioCall call)131         public void onCallEnded(SipAudioCall call) {
132             onChanged(call);
133         }
134 
135         /**
136          * Called when the peer is busy during session initialization.
137          * The default implementation calls {@link #onChanged}.
138          *
139          * @param call the call object that carries out the audio call
140          */
onCallBusy(SipAudioCall call)141         public void onCallBusy(SipAudioCall call) {
142             onChanged(call);
143         }
144 
145         /**
146          * Called when the call is on hold.
147          * The default implementation calls {@link #onChanged}.
148          *
149          * @param call the call object that carries out the audio call
150          */
onCallHeld(SipAudioCall call)151         public void onCallHeld(SipAudioCall call) {
152             onChanged(call);
153         }
154 
155         /**
156          * Called when an error occurs. The default implementation is no op.
157          *
158          * @param call the call object that carries out the audio call
159          * @param errorCode error code of this error
160          * @param errorMessage error message
161          * @see SipErrorCode
162          */
onError(SipAudioCall call, int errorCode, String errorMessage)163         public void onError(SipAudioCall call, int errorCode,
164                 String errorMessage) {
165             // no-op
166         }
167 
168         /**
169          * Called when an event occurs and the corresponding callback is not
170          * overridden. The default implementation is no op. Error events are
171          * not re-directed to this callback and are handled in {@link #onError}.
172          */
onChanged(SipAudioCall call)173         public void onChanged(SipAudioCall call) {
174             // no-op
175         }
176     }
177 
178     private Context mContext;
179     private SipProfile mLocalProfile;
180     private SipAudioCall.Listener mListener;
181     private SipSession mSipSession;
182     private SipSession mTransferringSession;
183 
184     private long mSessionId = System.currentTimeMillis();
185     private String mPeerSd;
186 
187     private AudioStream mAudioStream;
188     private AudioGroup mAudioGroup;
189 
190     private boolean mInCall = false;
191     private boolean mMuted = false;
192     private boolean mHold = false;
193 
194     private SipProfile mPendingCallRequest;
195     private WifiManager mWm;
196     private WifiManager.WifiLock mWifiHighPerfLock;
197 
198     private int mErrorCode = SipErrorCode.NO_ERROR;
199     private String mErrorMessage;
200 
201     /**
202      * Creates a call object with the local SIP profile.
203      * @param context the context for accessing system services such as
204      *        ringtone, audio, WIFI etc
205      */
SipAudioCall(Context context, SipProfile localProfile)206     public SipAudioCall(Context context, SipProfile localProfile) {
207         mContext = context;
208         mLocalProfile = localProfile;
209         mWm = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
210     }
211 
212     /**
213      * Sets the listener to listen to the audio call events. The method calls
214      * {@link #setListener setListener(listener, false)}.
215      *
216      * @param listener to listen to the audio call events of this object
217      * @see #setListener(Listener, boolean)
218      */
setListener(SipAudioCall.Listener listener)219     public void setListener(SipAudioCall.Listener listener) {
220         setListener(listener, false);
221     }
222 
223     /**
224      * Sets the listener to listen to the audio call events. A
225      * {@link SipAudioCall} can only hold one listener at a time. Subsequent
226      * calls to this method override the previous listener.
227      *
228      * @param listener to listen to the audio call events of this object
229      * @param callbackImmediately set to true if the caller wants to be called
230      *      back immediately on the current state
231      */
setListener(SipAudioCall.Listener listener, boolean callbackImmediately)232     public void setListener(SipAudioCall.Listener listener,
233             boolean callbackImmediately) {
234         mListener = listener;
235         try {
236             if ((listener == null) || !callbackImmediately) {
237                 // do nothing
238             } else if (mErrorCode != SipErrorCode.NO_ERROR) {
239                 listener.onError(this, mErrorCode, mErrorMessage);
240             } else if (mInCall) {
241                 if (mHold) {
242                     listener.onCallHeld(this);
243                 } else {
244                     listener.onCallEstablished(this);
245                 }
246             } else {
247                 int state = getState();
248                 switch (state) {
249                     case SipSession.State.READY_TO_CALL:
250                         listener.onReadyToCall(this);
251                         break;
252                     case SipSession.State.INCOMING_CALL:
253                         listener.onRinging(this, getPeerProfile());
254                         break;
255                     case SipSession.State.OUTGOING_CALL:
256                         listener.onCalling(this);
257                         break;
258                     case SipSession.State.OUTGOING_CALL_RING_BACK:
259                         listener.onRingingBack(this);
260                         break;
261                 }
262             }
263         } catch (Throwable t) {
264             Log.e(TAG, "setListener()", t);
265         }
266     }
267 
268     /**
269      * Checks if the call is established.
270      *
271      * @return true if the call is established
272      */
isInCall()273     public boolean isInCall() {
274         synchronized (this) {
275             return mInCall;
276         }
277     }
278 
279     /**
280      * Checks if the call is on hold.
281      *
282      * @return true if the call is on hold
283      */
isOnHold()284     public boolean isOnHold() {
285         synchronized (this) {
286             return mHold;
287         }
288     }
289 
290     /**
291      * Closes this object. This object is not usable after being closed.
292      */
close()293     public void close() {
294         close(true);
295     }
296 
close(boolean closeRtp)297     private synchronized void close(boolean closeRtp) {
298         if (closeRtp) stopCall(RELEASE_SOCKET);
299 
300         mInCall = false;
301         mHold = false;
302         mSessionId = System.currentTimeMillis();
303         mErrorCode = SipErrorCode.NO_ERROR;
304         mErrorMessage = null;
305 
306         if (mSipSession != null) {
307             mSipSession.setListener(null);
308             mSipSession = null;
309         }
310     }
311 
312     /**
313      * Gets the local SIP profile.
314      *
315      * @return the local SIP profile
316      */
getLocalProfile()317     public SipProfile getLocalProfile() {
318         synchronized (this) {
319             return mLocalProfile;
320         }
321     }
322 
323     /**
324      * Gets the peer's SIP profile.
325      *
326      * @return the peer's SIP profile
327      */
getPeerProfile()328     public SipProfile getPeerProfile() {
329         synchronized (this) {
330             return (mSipSession == null) ? null : mSipSession.getPeerProfile();
331         }
332     }
333 
334     /**
335      * Gets the state of the {@link SipSession} that carries this call.
336      * The value returned must be one of the states in {@link SipSession.State}.
337      *
338      * @return the session state
339      */
getState()340     public int getState() {
341         synchronized (this) {
342             if (mSipSession == null) return SipSession.State.READY_TO_CALL;
343             return mSipSession.getState();
344         }
345     }
346 
347 
348     /**
349      * Gets the {@link SipSession} that carries this call.
350      *
351      * @return the session object that carries this call
352      * @hide
353      */
getSipSession()354     public SipSession getSipSession() {
355         synchronized (this) {
356             return mSipSession;
357         }
358     }
359 
transferToNewSession()360     private synchronized void transferToNewSession() {
361         if (mTransferringSession == null) return;
362         SipSession origin = mSipSession;
363         mSipSession = mTransferringSession;
364         mTransferringSession = null;
365 
366         // stop the replaced call.
367         if (mAudioStream != null) {
368             mAudioStream.join(null);
369         } else {
370             try {
371                 mAudioStream = new AudioStream(InetAddress.getByName(
372                         getLocalIp()));
373             } catch (Throwable t) {
374                 Log.i(TAG, "transferToNewSession(): " + t);
375             }
376         }
377         if (origin != null) origin.endCall();
378         startAudio();
379     }
380 
createListener()381     private SipSession.Listener createListener() {
382         return new SipSession.Listener() {
383             @Override
384             public void onCalling(SipSession session) {
385                 Log.d(TAG, "calling... " + session);
386                 Listener listener = mListener;
387                 if (listener != null) {
388                     try {
389                         listener.onCalling(SipAudioCall.this);
390                     } catch (Throwable t) {
391                         Log.i(TAG, "onCalling(): " + t);
392                     }
393                 }
394             }
395 
396             @Override
397             public void onRingingBack(SipSession session) {
398                 Log.d(TAG, "sip call ringing back: " + session);
399                 Listener listener = mListener;
400                 if (listener != null) {
401                     try {
402                         listener.onRingingBack(SipAudioCall.this);
403                     } catch (Throwable t) {
404                         Log.i(TAG, "onRingingBack(): " + t);
405                     }
406                 }
407             }
408 
409             @Override
410             public void onRinging(SipSession session,
411                     SipProfile peerProfile, String sessionDescription) {
412                 // this callback is triggered only for reinvite.
413                 synchronized (SipAudioCall.this) {
414                     if ((mSipSession == null) || !mInCall
415                             || !session.getCallId().equals(
416                                     mSipSession.getCallId())) {
417                         // should not happen
418                         session.endCall();
419                         return;
420                     }
421 
422                     // session changing request
423                     try {
424                         String answer = createAnswer(sessionDescription).encode();
425                         mSipSession.answerCall(answer, SESSION_TIMEOUT);
426                     } catch (Throwable e) {
427                         Log.e(TAG, "onRinging()", e);
428                         session.endCall();
429                     }
430                 }
431             }
432 
433             @Override
434             public void onCallEstablished(SipSession session,
435                     String sessionDescription) {
436                 mPeerSd = sessionDescription;
437                 Log.v(TAG, "onCallEstablished()" + mPeerSd);
438 
439                 // TODO: how to notify the UI that the remote party is changed
440                 if ((mTransferringSession != null)
441                         && (session == mTransferringSession)) {
442                     transferToNewSession();
443                     return;
444                 }
445 
446                 Listener listener = mListener;
447                 if (listener != null) {
448                     try {
449                         if (mHold) {
450                             listener.onCallHeld(SipAudioCall.this);
451                         } else {
452                             listener.onCallEstablished(SipAudioCall.this);
453                         }
454                     } catch (Throwable t) {
455                         Log.i(TAG, "onCallEstablished(): " + t);
456                     }
457                 }
458             }
459 
460             @Override
461             public void onCallEnded(SipSession session) {
462                 Log.d(TAG, "sip call ended: " + session + " mSipSession:" + mSipSession);
463                 // reset the trasnferring session if it is the one.
464                 if (session == mTransferringSession) {
465                     mTransferringSession = null;
466                     return;
467                 }
468                 // or ignore the event if the original session is being
469                 // transferred to the new one.
470                 if ((mTransferringSession != null) ||
471                     (session != mSipSession)) return;
472 
473                 Listener listener = mListener;
474                 if (listener != null) {
475                     try {
476                         listener.onCallEnded(SipAudioCall.this);
477                     } catch (Throwable t) {
478                         Log.i(TAG, "onCallEnded(): " + t);
479                     }
480                 }
481                 close();
482             }
483 
484             @Override
485             public void onCallBusy(SipSession session) {
486                 Log.d(TAG, "sip call busy: " + session);
487                 Listener listener = mListener;
488                 if (listener != null) {
489                     try {
490                         listener.onCallBusy(SipAudioCall.this);
491                     } catch (Throwable t) {
492                         Log.i(TAG, "onCallBusy(): " + t);
493                     }
494                 }
495                 close(false);
496             }
497 
498             @Override
499             public void onCallChangeFailed(SipSession session, int errorCode,
500                     String message) {
501                 Log.d(TAG, "sip call change failed: " + message);
502                 mErrorCode = errorCode;
503                 mErrorMessage = message;
504                 Listener listener = mListener;
505                 if (listener != null) {
506                     try {
507                         listener.onError(SipAudioCall.this, mErrorCode,
508                                 message);
509                     } catch (Throwable t) {
510                         Log.i(TAG, "onCallBusy(): " + t);
511                     }
512                 }
513             }
514 
515             @Override
516             public void onError(SipSession session, int errorCode,
517                     String message) {
518                 SipAudioCall.this.onError(errorCode, message);
519             }
520 
521             @Override
522             public void onRegistering(SipSession session) {
523                 // irrelevant
524             }
525 
526             @Override
527             public void onRegistrationTimeout(SipSession session) {
528                 // irrelevant
529             }
530 
531             @Override
532             public void onRegistrationFailed(SipSession session, int errorCode,
533                     String message) {
534                 // irrelevant
535             }
536 
537             @Override
538             public void onRegistrationDone(SipSession session, int duration) {
539                 // irrelevant
540             }
541 
542             @Override
543             public void onCallTransferring(SipSession newSession,
544                     String sessionDescription) {
545                 Log.v(TAG, "onCallTransferring mSipSession:"
546                         + mSipSession + " newSession:" + newSession);
547                 mTransferringSession = newSession;
548                 try {
549                     if (sessionDescription == null) {
550                         newSession.makeCall(newSession.getPeerProfile(),
551                                 createOffer().encode(), TRANSFER_TIMEOUT);
552                     } else {
553                         String answer = createAnswer(sessionDescription).encode();
554                         newSession.answerCall(answer, SESSION_TIMEOUT);
555                     }
556                 } catch (Throwable e) {
557                     Log.e(TAG, "onCallTransferring()", e);
558                     newSession.endCall();
559                 }
560             }
561         };
562     }
563 
564     private void onError(int errorCode, String message) {
565         Log.d(TAG, "sip session error: "
566                 + SipErrorCode.toString(errorCode) + ": " + message);
567         mErrorCode = errorCode;
568         mErrorMessage = message;
569         Listener listener = mListener;
570         if (listener != null) {
571             try {
572                 listener.onError(this, errorCode, message);
573             } catch (Throwable t) {
574                 Log.i(TAG, "onError(): " + t);
575             }
576         }
577         synchronized (this) {
578             if ((errorCode == SipErrorCode.DATA_CONNECTION_LOST)
579                     || !isInCall()) {
580                 close(true);
581             }
582         }
583     }
584 
585     /**
586      * Attaches an incoming call to this call object.
587      *
588      * @param session the session that receives the incoming call
589      * @param sessionDescription the session description of the incoming call
590      * @throws SipException if the SIP service fails to attach this object to
591      *        the session or VOIP API is not supported by the device
592      * @see SipManager#isVoipSupported
593      */
594     public void attachCall(SipSession session, String sessionDescription)
595             throws SipException {
596         if (!SipManager.isVoipSupported(mContext)) {
597             throw new SipException("VOIP API is not supported");
598         }
599 
600         synchronized (this) {
601             mSipSession = session;
602             mPeerSd = sessionDescription;
603             Log.v(TAG, "attachCall()" + mPeerSd);
604             try {
605                 session.setListener(createListener());
606             } catch (Throwable e) {
607                 Log.e(TAG, "attachCall()", e);
608                 throwSipException(e);
609             }
610         }
611     }
612 
613     /**
614      * Initiates an audio call to the specified profile. The attempt will be
615      * timed out if the call is not established within {@code timeout} seconds
616      * and {@link Listener#onError onError(SipAudioCall, SipErrorCode.TIME_OUT, String)}
617      * will be called.
618      *
619      * @param peerProfile the SIP profile to make the call to
620      * @param sipSession the {@link SipSession} for carrying out the call
621      * @param timeout the timeout value in seconds. Default value (defined by
622      *        SIP protocol) is used if {@code timeout} is zero or negative.
623      * @see Listener#onError
624      * @throws SipException if the SIP service fails to create a session for the
625      *        call or VOIP API is not supported by the device
626      * @see SipManager#isVoipSupported
627      */
628     public void makeCall(SipProfile peerProfile, SipSession sipSession,
629             int timeout) throws SipException {
630         if (!SipManager.isVoipSupported(mContext)) {
631             throw new SipException("VOIP API is not supported");
632         }
633 
634         synchronized (this) {
635             mSipSession = sipSession;
636             try {
637                 mAudioStream = new AudioStream(InetAddress.getByName(
638                         getLocalIp()));
639                 sipSession.setListener(createListener());
640                 sipSession.makeCall(peerProfile, createOffer().encode(),
641                         timeout);
642             } catch (IOException e) {
643                 throw new SipException("makeCall()", e);
644             }
645         }
646     }
647 
648     /**
649      * Ends a call.
650      * @throws SipException if the SIP service fails to end the call
651      */
652     public void endCall() throws SipException {
653         synchronized (this) {
654             stopCall(RELEASE_SOCKET);
655             mInCall = false;
656 
657             // perform the above local ops first and then network op
658             if (mSipSession != null) mSipSession.endCall();
659         }
660     }
661 
662     /**
663      * Puts a call on hold.  When succeeds, {@link Listener#onCallHeld} is
664      * called. The attempt will be timed out if the call is not established
665      * within {@code timeout} seconds and
666      * {@link Listener#onError onError(SipAudioCall, SipErrorCode.TIME_OUT, String)}
667      * will be called.
668      *
669      * @param timeout the timeout value in seconds. Default value (defined by
670      *        SIP protocol) is used if {@code timeout} is zero or negative.
671      * @see Listener#onError
672      * @throws SipException if the SIP service fails to hold the call
673      */
674     public void holdCall(int timeout) throws SipException {
675         synchronized (this) {
676             if (mHold) return;
677             if (mSipSession == null) {
678                 throw new SipException("Not in a call to hold call");
679             }
680             mSipSession.changeCall(createHoldOffer().encode(), timeout);
681             mHold = true;
682             setAudioGroupMode();
683         }
684     }
685 
686     /**
687      * Answers a call. The attempt will be timed out if the call is not
688      * established within {@code timeout} seconds and
689      * {@link Listener#onError onError(SipAudioCall, SipErrorCode.TIME_OUT, String)}
690      * will be called.
691      *
692      * @param timeout the timeout value in seconds. Default value (defined by
693      *        SIP protocol) is used if {@code timeout} is zero or negative.
694      * @see Listener#onError
695      * @throws SipException if the SIP service fails to answer the call
696      */
697     public void answerCall(int timeout) throws SipException {
698         synchronized (this) {
699             if (mSipSession == null) {
700                 throw new SipException("No call to answer");
701             }
702             try {
703                 mAudioStream = new AudioStream(InetAddress.getByName(
704                         getLocalIp()));
705                 mSipSession.answerCall(createAnswer(mPeerSd).encode(), timeout);
706             } catch (IOException e) {
707                 throw new SipException("answerCall()", e);
708             }
709         }
710     }
711 
712     /**
713      * Continues a call that's on hold. When succeeds,
714      * {@link Listener#onCallEstablished} is called. The attempt will be timed
715      * out if the call is not established within {@code timeout} seconds and
716      * {@link Listener#onError onError(SipAudioCall, SipErrorCode.TIME_OUT, String)}
717      * will be called.
718      *
719      * @param timeout the timeout value in seconds. Default value (defined by
720      *        SIP protocol) is used if {@code timeout} is zero or negative.
721      * @see Listener#onError
722      * @throws SipException if the SIP service fails to unhold the call
723      */
724     public void continueCall(int timeout) throws SipException {
725         synchronized (this) {
726             if (!mHold) return;
727             mSipSession.changeCall(createContinueOffer().encode(), timeout);
728             mHold = false;
729             setAudioGroupMode();
730         }
731     }
732 
733     private SimpleSessionDescription createOffer() {
734         SimpleSessionDescription offer =
735                 new SimpleSessionDescription(mSessionId, getLocalIp());
736         AudioCodec[] codecs = AudioCodec.getCodecs();
737         Media media = offer.newMedia(
738                 "audio", mAudioStream.getLocalPort(), 1, "RTP/AVP");
739         for (AudioCodec codec : AudioCodec.getCodecs()) {
740             media.setRtpPayload(codec.type, codec.rtpmap, codec.fmtp);
741         }
742         media.setRtpPayload(127, "telephone-event/8000", "0-15");
743         return offer;
744     }
745 
746     private SimpleSessionDescription createAnswer(String offerSd) {
747         if (TextUtils.isEmpty(offerSd)) return createOffer();
748         SimpleSessionDescription offer =
749                 new SimpleSessionDescription(offerSd);
750         SimpleSessionDescription answer =
751                 new SimpleSessionDescription(mSessionId, getLocalIp());
752         AudioCodec codec = null;
753         for (Media media : offer.getMedia()) {
754             if ((codec == null) && (media.getPort() > 0)
755                     && "audio".equals(media.getType())
756                     && "RTP/AVP".equals(media.getProtocol())) {
757                 // Find the first audio codec we supported.
758                 for (int type : media.getRtpPayloadTypes()) {
759                     codec = AudioCodec.getCodec(type, media.getRtpmap(type),
760                             media.getFmtp(type));
761                     if (codec != null) {
762                         break;
763                     }
764                 }
765                 if (codec != null) {
766                     Media reply = answer.newMedia(
767                             "audio", mAudioStream.getLocalPort(), 1, "RTP/AVP");
768                     reply.setRtpPayload(codec.type, codec.rtpmap, codec.fmtp);
769 
770                     // Check if DTMF is supported in the same media.
771                     for (int type : media.getRtpPayloadTypes()) {
772                         String rtpmap = media.getRtpmap(type);
773                         if ((type != codec.type) && (rtpmap != null)
774                                 && rtpmap.startsWith("telephone-event")) {
775                             reply.setRtpPayload(
776                                     type, rtpmap, media.getFmtp(type));
777                         }
778                     }
779 
780                     // Handle recvonly and sendonly.
781                     if (media.getAttribute("recvonly") != null) {
782                         answer.setAttribute("sendonly", "");
783                     } else if(media.getAttribute("sendonly") != null) {
784                         answer.setAttribute("recvonly", "");
785                     } else if(offer.getAttribute("recvonly") != null) {
786                         answer.setAttribute("sendonly", "");
787                     } else if(offer.getAttribute("sendonly") != null) {
788                         answer.setAttribute("recvonly", "");
789                     }
790                     continue;
791                 }
792             }
793             // Reject the media.
794             Media reply = answer.newMedia(
795                     media.getType(), 0, 1, media.getProtocol());
796             for (String format : media.getFormats()) {
797                 reply.setFormat(format, null);
798             }
799         }
800         if (codec == null) {
801             throw new IllegalStateException("Reject SDP: no suitable codecs");
802         }
803         return answer;
804     }
805 
806     private SimpleSessionDescription createHoldOffer() {
807         SimpleSessionDescription offer = createContinueOffer();
808         offer.setAttribute("sendonly", "");
809         return offer;
810     }
811 
812     private SimpleSessionDescription createContinueOffer() {
813         SimpleSessionDescription offer =
814                 new SimpleSessionDescription(mSessionId, getLocalIp());
815         Media media = offer.newMedia(
816                 "audio", mAudioStream.getLocalPort(), 1, "RTP/AVP");
817         AudioCodec codec = mAudioStream.getCodec();
818         media.setRtpPayload(codec.type, codec.rtpmap, codec.fmtp);
819         int dtmfType = mAudioStream.getDtmfType();
820         if (dtmfType != -1) {
821             media.setRtpPayload(dtmfType, "telephone-event/8000", "0-15");
822         }
823         return offer;
824     }
825 
826     private void grabWifiHighPerfLock() {
827         if (mWifiHighPerfLock == null) {
828             Log.v(TAG, "acquire wifi high perf lock");
829             mWifiHighPerfLock = ((WifiManager)
830                     mContext.getSystemService(Context.WIFI_SERVICE))
831                     .createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, TAG);
832             mWifiHighPerfLock.acquire();
833         }
834     }
835 
836     private void releaseWifiHighPerfLock() {
837         if (mWifiHighPerfLock != null) {
838             Log.v(TAG, "release wifi high perf lock");
839             mWifiHighPerfLock.release();
840             mWifiHighPerfLock = null;
841         }
842     }
843 
844     private boolean isWifiOn() {
845         return (mWm.getConnectionInfo().getBSSID() == null) ? false : true;
846     }
847 
848     /** Toggles mute. */
849     public void toggleMute() {
850         synchronized (this) {
851             mMuted = !mMuted;
852             setAudioGroupMode();
853         }
854     }
855 
856     /**
857      * Checks if the call is muted.
858      *
859      * @return true if the call is muted
860      */
861     public boolean isMuted() {
862         synchronized (this) {
863             return mMuted;
864         }
865     }
866 
867     /**
868      * Puts the device to speaker mode.
869      * <p class="note"><strong>Note:</strong> Requires the
870      *   {@link android.Manifest.permission#MODIFY_AUDIO_SETTINGS} permission.</p>
871      *
872      * @param speakerMode set true to enable speaker mode; false to disable
873      */
874     public void setSpeakerMode(boolean speakerMode) {
875         synchronized (this) {
876             ((AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE))
877                     .setSpeakerphoneOn(speakerMode);
878             setAudioGroupMode();
879         }
880     }
881 
882     private boolean isSpeakerOn() {
883         return ((AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE))
884                 .isSpeakerphoneOn();
885     }
886 
887     /**
888      * Sends a DTMF code. According to <a href="http://tools.ietf.org/html/rfc2833">RFC 2883</a>,
889      * event 0--9 maps to decimal
890      * value 0--9, '*' to 10, '#' to 11, event 'A'--'D' to 12--15, and event
891      * flash to 16. Currently, event flash is not supported.
892      *
893      * @param code the DTMF code to send. Value 0 to 15 (inclusive) are valid
894      *        inputs.
895      */
896     public void sendDtmf(int code) {
897         sendDtmf(code, null);
898     }
899 
900     /**
901      * Sends a DTMF code. According to <a href="http://tools.ietf.org/html/rfc2833">RFC 2883</a>,
902      * event 0--9 maps to decimal
903      * value 0--9, '*' to 10, '#' to 11, event 'A'--'D' to 12--15, and event
904      * flash to 16. Currently, event flash is not supported.
905      *
906      * @param code the DTMF code to send. Value 0 to 15 (inclusive) are valid
907      *        inputs.
908      * @param result the result message to send when done
909      */
910     public void sendDtmf(int code, Message result) {
911         synchronized (this) {
912             AudioGroup audioGroup = getAudioGroup();
913             if ((audioGroup != null) && (mSipSession != null)
914                     && (SipSession.State.IN_CALL == getState())) {
915                 Log.v(TAG, "send DTMF: " + code);
916                 audioGroup.sendDtmf(code);
917             }
918             if (result != null) result.sendToTarget();
919         }
920     }
921 
922     /**
923      * Gets the {@link AudioStream} object used in this call. The object
924      * represents the RTP stream that carries the audio data to and from the
925      * peer. The object may not be created before the call is established. And
926      * it is undefined after the call ends or the {@link #close} method is
927      * called.
928      *
929      * @return the {@link AudioStream} object or null if the RTP stream has not
930      *      yet been set up
931      * @hide
932      */
933     public AudioStream getAudioStream() {
934         synchronized (this) {
935             return mAudioStream;
936         }
937     }
938 
939     /**
940      * Gets the {@link AudioGroup} object which the {@link AudioStream} object
941      * joins. The group object may not exist before the call is established.
942      * Also, the {@code AudioStream} may change its group during a call (e.g.,
943      * after the call is held/un-held). Finally, the {@code AudioGroup} object
944      * returned by this method is undefined after the call ends or the
945      * {@link #close} method is called. If a group object is set by
946      * {@link #setAudioGroup(AudioGroup)}, then this method returns that object.
947      *
948      * @return the {@link AudioGroup} object or null if the RTP stream has not
949      *      yet been set up
950      * @see #getAudioStream
951      * @hide
952      */
953     public AudioGroup getAudioGroup() {
954         synchronized (this) {
955             if (mAudioGroup != null) return mAudioGroup;
956             return ((mAudioStream == null) ? null : mAudioStream.getGroup());
957         }
958     }
959 
960     /**
961      * Sets the {@link AudioGroup} object which the {@link AudioStream} object
962      * joins. If {@code audioGroup} is null, then the {@code AudioGroup} object
963      * will be dynamically created when needed. Note that the mode of the
964      * {@code AudioGroup} is not changed according to the audio settings (i.e.,
965      * hold, mute, speaker phone) of this object. This is mainly used to merge
966      * multiple {@code SipAudioCall} objects to form a conference call. The
967      * settings of the first object (that merges others) override others'.
968      *
969      * @see #getAudioStream
970      * @hide
971      */
972     public void setAudioGroup(AudioGroup group) {
973         synchronized (this) {
974             if ((mAudioStream != null) && (mAudioStream.getGroup() != null)) {
975                 mAudioStream.join(group);
976             }
977             mAudioGroup = group;
978         }
979     }
980 
981     /**
982      * Starts the audio for the established call. This method should be called
983      * after {@link Listener#onCallEstablished} is called.
984      * <p class="note"><strong>Note:</strong> Requires the
985      *   {@link android.Manifest.permission#RECORD_AUDIO},
986      *   {@link android.Manifest.permission#ACCESS_WIFI_STATE} and
987      *   {@link android.Manifest.permission#WAKE_LOCK} permissions.</p>
988      */
989     public void startAudio() {
990         try {
991             startAudioInternal();
992         } catch (UnknownHostException e) {
993             onError(SipErrorCode.PEER_NOT_REACHABLE, e.getMessage());
994         } catch (Throwable e) {
995             onError(SipErrorCode.CLIENT_ERROR, e.getMessage());
996         }
997     }
998 
999     private synchronized void startAudioInternal() throws UnknownHostException {
1000         if (mPeerSd == null) {
1001             Log.v(TAG, "startAudioInternal() mPeerSd = null");
1002             throw new IllegalStateException("mPeerSd = null");
1003         }
1004 
1005         stopCall(DONT_RELEASE_SOCKET);
1006         mInCall = true;
1007 
1008         // Run exact the same logic in createAnswer() to setup mAudioStream.
1009         SimpleSessionDescription offer =
1010                 new SimpleSessionDescription(mPeerSd);
1011         AudioStream stream = mAudioStream;
1012         AudioCodec codec = null;
1013         for (Media media : offer.getMedia()) {
1014             if ((codec == null) && (media.getPort() > 0)
1015                     && "audio".equals(media.getType())
1016                     && "RTP/AVP".equals(media.getProtocol())) {
1017                 // Find the first audio codec we supported.
1018                 for (int type : media.getRtpPayloadTypes()) {
1019                     codec = AudioCodec.getCodec(
1020                             type, media.getRtpmap(type), media.getFmtp(type));
1021                     if (codec != null) {
1022                         break;
1023                     }
1024                 }
1025 
1026                 if (codec != null) {
1027                     // Associate with the remote host.
1028                     String address = media.getAddress();
1029                     if (address == null) {
1030                         address = offer.getAddress();
1031                     }
1032                     stream.associate(InetAddress.getByName(address),
1033                             media.getPort());
1034 
1035                     stream.setDtmfType(-1);
1036                     stream.setCodec(codec);
1037                     // Check if DTMF is supported in the same media.
1038                     for (int type : media.getRtpPayloadTypes()) {
1039                         String rtpmap = media.getRtpmap(type);
1040                         if ((type != codec.type) && (rtpmap != null)
1041                                 && rtpmap.startsWith("telephone-event")) {
1042                             stream.setDtmfType(type);
1043                         }
1044                     }
1045 
1046                     // Handle recvonly and sendonly.
1047                     if (mHold) {
1048                         stream.setMode(RtpStream.MODE_NORMAL);
1049                     } else if (media.getAttribute("recvonly") != null) {
1050                         stream.setMode(RtpStream.MODE_SEND_ONLY);
1051                     } else if(media.getAttribute("sendonly") != null) {
1052                         stream.setMode(RtpStream.MODE_RECEIVE_ONLY);
1053                     } else if(offer.getAttribute("recvonly") != null) {
1054                         stream.setMode(RtpStream.MODE_SEND_ONLY);
1055                     } else if(offer.getAttribute("sendonly") != null) {
1056                         stream.setMode(RtpStream.MODE_RECEIVE_ONLY);
1057                     } else {
1058                         stream.setMode(RtpStream.MODE_NORMAL);
1059                     }
1060                     break;
1061                 }
1062             }
1063         }
1064         if (codec == null) {
1065             throw new IllegalStateException("Reject SDP: no suitable codecs");
1066         }
1067 
1068         if (isWifiOn()) grabWifiHighPerfLock();
1069 
1070         // AudioGroup logic:
1071         AudioGroup audioGroup = getAudioGroup();
1072         if (mHold) {
1073             // don't create an AudioGroup here; doing so will fail if
1074             // there's another AudioGroup out there that's active
1075         } else {
1076             if (audioGroup == null) audioGroup = new AudioGroup();
1077             stream.join(audioGroup);
1078         }
1079         setAudioGroupMode();
1080     }
1081 
1082     // set audio group mode based on current audio configuration
1083     private void setAudioGroupMode() {
1084         AudioGroup audioGroup = getAudioGroup();
1085         if (audioGroup != null) {
1086             if (mHold) {
1087                 audioGroup.setMode(AudioGroup.MODE_ON_HOLD);
1088             } else if (mMuted) {
1089                 audioGroup.setMode(AudioGroup.MODE_MUTED);
1090             } else if (isSpeakerOn()) {
1091                 audioGroup.setMode(AudioGroup.MODE_ECHO_SUPPRESSION);
1092             } else {
1093                 audioGroup.setMode(AudioGroup.MODE_NORMAL);
1094             }
1095         }
1096     }
1097 
1098     private void stopCall(boolean releaseSocket) {
1099         Log.d(TAG, "stop audiocall");
1100         releaseWifiHighPerfLock();
1101         if (mAudioStream != null) {
1102             mAudioStream.join(null);
1103 
1104             if (releaseSocket) {
1105                 mAudioStream.release();
1106                 mAudioStream = null;
1107             }
1108         }
1109     }
1110 
1111     private String getLocalIp() {
1112         return mSipSession.getLocalIp();
1113     }
1114 
1115     private void throwSipException(Throwable throwable) throws SipException {
1116         if (throwable instanceof SipException) {
1117             throw (SipException) throwable;
1118         } else {
1119             throw new SipException("", throwable);
1120         }
1121     }
1122 
1123     private SipProfile getPeerProfile(SipSession session) {
1124         return session.getPeerProfile();
1125     }
1126 }
1127