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