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