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