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