SipPhone.java revision 58dd6858dc8013b680ea003d22063fd65ed5fe1c
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 com.android.internal.telephony.sip;
18
19import android.content.Context;
20import android.media.AudioManager;
21import android.net.rtp.AudioGroup;
22import android.net.sip.SipAudioCall;
23import android.net.sip.SipErrorCode;
24import android.net.sip.SipException;
25import android.net.sip.SipManager;
26import android.net.sip.SipProfile;
27import android.net.sip.SipSession;
28import android.os.AsyncResult;
29import android.os.Bundle;
30import android.os.Message;
31import android.telephony.DisconnectCause;
32import android.telephony.PhoneNumberUtils;
33import android.telephony.ServiceState;
34import android.text.TextUtils;
35import android.telephony.Rlog;
36
37import com.android.internal.telephony.Call;
38import com.android.internal.telephony.CallStateException;
39import com.android.internal.telephony.Connection;
40import com.android.internal.telephony.Phone;
41import com.android.internal.telephony.PhoneConstants;
42import com.android.internal.telephony.PhoneNotifier;
43
44import java.text.ParseException;
45import java.util.List;
46import java.util.regex.Pattern;
47
48/**
49 * {@hide}
50 */
51public class SipPhone extends SipPhoneBase {
52    private static final String LOG_TAG = "SipPhone";
53    private static final boolean DBG = true;
54    private static final boolean VDBG = false; // STOPSHIP if true
55    private static final int TIMEOUT_MAKE_CALL = 15; // in seconds
56    private static final int TIMEOUT_ANSWER_CALL = 8; // in seconds
57    private static final int TIMEOUT_HOLD_CALL = 15; // in seconds
58
59    // A call that is ringing or (call) waiting
60    private SipCall mRingingCall = new SipCall();
61    private SipCall mForegroundCall = new SipCall();
62    private SipCall mBackgroundCall = new SipCall();
63
64    private SipManager mSipManager;
65    private SipProfile mProfile;
66
67    SipPhone (Context context, PhoneNotifier notifier, SipProfile profile) {
68        super("SIP:" + profile.getUriString(), context, notifier);
69
70        if (DBG) log("new SipPhone: " + profile.getUriString());
71        mRingingCall = new SipCall();
72        mForegroundCall = new SipCall();
73        mBackgroundCall = new SipCall();
74        mProfile = profile;
75        mSipManager = SipManager.newInstance(context);
76    }
77
78    @Override
79    public boolean equals(Object o) {
80        if (o == this) return true;
81        if (!(o instanceof SipPhone)) return false;
82        SipPhone that = (SipPhone) o;
83        return mProfile.getUriString().equals(that.mProfile.getUriString());
84    }
85
86    public String getSipUri() {
87        return mProfile.getUriString();
88    }
89
90    public boolean equals(SipPhone phone) {
91        return getSipUri().equals(phone.getSipUri());
92    }
93
94    public Connection takeIncomingCall(Object incomingCall) {
95        // FIXME: Is synchronizing on the class necessary, should we use a mLockObj?
96        // Also there are many things not synchronized, of course
97        // this may be true of GsmCdmaPhone too!!!
98        synchronized (SipPhone.class) {
99            if (!(incomingCall instanceof SipAudioCall)) {
100                if (DBG) log("takeIncomingCall: ret=null, not a SipAudioCall");
101                return null;
102            }
103            if (mRingingCall.getState().isAlive()) {
104                if (DBG) log("takeIncomingCall: ret=null, ringingCall not alive");
105                return null;
106            }
107
108            // FIXME: is it true that we cannot take any incoming call if
109            // both foreground and background are active
110            if (mForegroundCall.getState().isAlive()
111                    && mBackgroundCall.getState().isAlive()) {
112                if (DBG) {
113                    log("takeIncomingCall: ret=null," + " foreground and background both alive");
114                }
115                return null;
116            }
117
118            try {
119                SipAudioCall sipAudioCall = (SipAudioCall) incomingCall;
120                if (DBG) log("takeIncomingCall: taking call from: "
121                        + sipAudioCall.getPeerProfile().getUriString());
122                String localUri = sipAudioCall.getLocalProfile().getUriString();
123                if (localUri.equals(mProfile.getUriString())) {
124                    boolean makeCallWait = mForegroundCall.getState().isAlive();
125                    SipConnection connection = mRingingCall.initIncomingCall(sipAudioCall,
126                            makeCallWait);
127                    if (sipAudioCall.getState() != SipSession.State.INCOMING_CALL) {
128                        // Peer cancelled the call!
129                        if (DBG) log("    takeIncomingCall: call cancelled !!");
130                        mRingingCall.reset();
131                        connection = null;
132                    }
133                    return connection;
134                }
135            } catch (Exception e) {
136                // Peer may cancel the call at any time during the time we hook
137                // up ringingCall with sipAudioCall. Clean up ringingCall when
138                // that happens.
139                if (DBG) log("    takeIncomingCall: exception e=" + e);
140                mRingingCall.reset();
141            }
142            if (DBG) log("takeIncomingCall: NOT taking !!");
143            return null;
144        }
145    }
146
147    @Override
148    public void acceptCall(int videoState) throws CallStateException {
149        synchronized (SipPhone.class) {
150            if ((mRingingCall.getState() == Call.State.INCOMING) ||
151                    (mRingingCall.getState() == Call.State.WAITING)) {
152                if (DBG) log("acceptCall: accepting");
153                // Always unmute when answering a new call
154                mRingingCall.setMute(false);
155                mRingingCall.acceptCall();
156            } else {
157                if (DBG) {
158                    log("acceptCall:" +
159                        " throw CallStateException(\"phone not ringing\")");
160                }
161                throw new CallStateException("phone not ringing");
162            }
163        }
164    }
165
166    @Override
167    public void rejectCall() throws CallStateException {
168        synchronized (SipPhone.class) {
169            if (mRingingCall.getState().isRinging()) {
170                if (DBG) log("rejectCall: rejecting");
171                mRingingCall.rejectCall();
172            } else {
173                if (DBG) {
174                    log("rejectCall:" +
175                        " throw CallStateException(\"phone not ringing\")");
176                }
177                throw new CallStateException("phone not ringing");
178            }
179        }
180    }
181
182    @Override
183    public Connection dial(String dialString, int videoState) throws CallStateException {
184        synchronized (SipPhone.class) {
185            return dialInternal(dialString, videoState);
186        }
187    }
188
189    private Connection dialInternal(String dialString, int videoState)
190            throws CallStateException {
191        if (DBG) log("dialInternal: dialString=" + (VDBG ? dialString : "xxxxxx"));
192        clearDisconnected();
193
194        if (!canDial()) {
195            throw new CallStateException("dialInternal: cannot dial in current state");
196        }
197        if (mForegroundCall.getState() == SipCall.State.ACTIVE) {
198            switchHoldingAndActive();
199        }
200        if (mForegroundCall.getState() != SipCall.State.IDLE) {
201            //we should have failed in !canDial() above before we get here
202            throw new CallStateException("cannot dial in current state");
203        }
204
205        mForegroundCall.setMute(false);
206        try {
207            Connection c = mForegroundCall.dial(dialString);
208            return c;
209        } catch (SipException e) {
210            loge("dialInternal: ", e);
211            throw new CallStateException("dial error: " + e);
212        }
213    }
214
215    @Override
216    public void switchHoldingAndActive() throws CallStateException {
217        if (DBG) log("switchHoldingAndActive: switch fg and bg");
218        synchronized (SipPhone.class) {
219            mForegroundCall.switchWith(mBackgroundCall);
220            if (mBackgroundCall.getState().isAlive()) mBackgroundCall.hold();
221            if (mForegroundCall.getState().isAlive()) mForegroundCall.unhold();
222        }
223    }
224
225    @Override
226    public boolean canConference() {
227        if (DBG) log("canConference: ret=true");
228        return true;
229    }
230
231    @Override
232    public void conference() throws CallStateException {
233        synchronized (SipPhone.class) {
234            if ((mForegroundCall.getState() != SipCall.State.ACTIVE)
235                    || (mForegroundCall.getState() != SipCall.State.ACTIVE)) {
236                throw new CallStateException("wrong state to merge calls: fg="
237                        + mForegroundCall.getState() + ", bg="
238                        + mBackgroundCall.getState());
239            }
240            if (DBG) log("conference: merge fg & bg");
241            mForegroundCall.merge(mBackgroundCall);
242        }
243    }
244
245    public void conference(Call that) throws CallStateException {
246        synchronized (SipPhone.class) {
247            if (!(that instanceof SipCall)) {
248                throw new CallStateException("expect " + SipCall.class
249                        + ", cannot merge with " + that.getClass());
250            }
251            mForegroundCall.merge((SipCall) that);
252        }
253    }
254
255    @Override
256    public boolean canTransfer() {
257        return false;
258    }
259
260    @Override
261    public void explicitCallTransfer() {
262        //mCT.explicitCallTransfer();
263    }
264
265    @Override
266    public void clearDisconnected() {
267        synchronized (SipPhone.class) {
268            mRingingCall.clearDisconnected();
269            mForegroundCall.clearDisconnected();
270            mBackgroundCall.clearDisconnected();
271
272            updatePhoneState();
273            notifyPreciseCallStateChanged();
274        }
275    }
276
277    @Override
278    public void sendDtmf(char c) {
279        if (!PhoneNumberUtils.is12Key(c)) {
280            loge("sendDtmf called with invalid character '" + c + "'");
281        } else if (mForegroundCall.getState().isAlive()) {
282            synchronized (SipPhone.class) {
283                mForegroundCall.sendDtmf(c);
284            }
285        }
286    }
287
288    @Override
289    public void startDtmf(char c) {
290        if (!PhoneNumberUtils.is12Key(c)) {
291            loge("startDtmf called with invalid character '" + c + "'");
292        } else {
293            sendDtmf(c);
294        }
295    }
296
297    @Override
298    public void stopDtmf() {
299        // no op
300    }
301
302    public void sendBurstDtmf(String dtmfString) {
303        loge("sendBurstDtmf() is a CDMA method");
304    }
305
306    @Override
307    public void getOutgoingCallerIdDisplay(Message onComplete) {
308        // FIXME: what to reply?
309        AsyncResult.forMessage(onComplete, null, null);
310        onComplete.sendToTarget();
311    }
312
313    @Override
314    public void setOutgoingCallerIdDisplay(int commandInterfaceCLIRMode,
315                                           Message onComplete) {
316        // FIXME: what's this for SIP?
317        AsyncResult.forMessage(onComplete, null, null);
318        onComplete.sendToTarget();
319    }
320
321    @Override
322    public void getCallWaiting(Message onComplete) {
323        // FIXME: what to reply?
324        AsyncResult.forMessage(onComplete, null, null);
325        onComplete.sendToTarget();
326    }
327
328    @Override
329    public void setCallWaiting(boolean enable, Message onComplete) {
330        // FIXME: what to reply?
331        loge("call waiting not supported");
332    }
333
334    @Override
335    public void setEchoSuppressionEnabled() {
336        // Echo suppression may not be available on every device. So, check
337        // whether it is supported
338        synchronized (SipPhone.class) {
339            AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
340            String echoSuppression = audioManager.getParameters("ec_supported");
341            if (echoSuppression.contains("off")) {
342                mForegroundCall.setAudioGroupMode();
343            }
344        }
345    }
346
347    @Override
348    public void setMute(boolean muted) {
349        synchronized (SipPhone.class) {
350            mForegroundCall.setMute(muted);
351        }
352    }
353
354    @Override
355    public boolean getMute() {
356        return (mForegroundCall.getState().isAlive()
357                ? mForegroundCall.getMute()
358                : mBackgroundCall.getMute());
359    }
360
361    @Override
362    public Call getForegroundCall() {
363        return mForegroundCall;
364    }
365
366    @Override
367    public Call getBackgroundCall() {
368        return mBackgroundCall;
369    }
370
371    @Override
372    public Call getRingingCall() {
373        return mRingingCall;
374    }
375
376    @Override
377    public ServiceState getServiceState() {
378        // FIXME: we may need to provide this when data connectivity is lost
379        // or when server is down
380        return super.getServiceState();
381    }
382
383    private String getUriString(SipProfile p) {
384        // SipProfile.getUriString() may contain "SIP:" and port
385        return p.getUserName() + "@" + getSipDomain(p);
386    }
387
388    private String getSipDomain(SipProfile p) {
389        String domain = p.getSipDomain();
390        // TODO: move this to SipProfile
391        if (domain.endsWith(":5060")) {
392            return domain.substring(0, domain.length() - 5);
393        } else {
394            return domain;
395        }
396    }
397
398    private static Call.State getCallStateFrom(SipAudioCall sipAudioCall) {
399        if (sipAudioCall.isOnHold()) return Call.State.HOLDING;
400        int sessionState = sipAudioCall.getState();
401        switch (sessionState) {
402            case SipSession.State.READY_TO_CALL:            return Call.State.IDLE;
403            case SipSession.State.INCOMING_CALL:
404            case SipSession.State.INCOMING_CALL_ANSWERING:  return Call.State.INCOMING;
405            case SipSession.State.OUTGOING_CALL:            return Call.State.DIALING;
406            case SipSession.State.OUTGOING_CALL_RING_BACK:  return Call.State.ALERTING;
407            case SipSession.State.OUTGOING_CALL_CANCELING:  return Call.State.DISCONNECTING;
408            case SipSession.State.IN_CALL:                  return Call.State.ACTIVE;
409            default:
410                slog("illegal connection state: " + sessionState);
411                return Call.State.DISCONNECTED;
412        }
413    }
414
415    private void log(String s) {
416        Rlog.d(LOG_TAG, s);
417    }
418
419    private static void slog(String s) {
420        Rlog.d(LOG_TAG, s);
421    }
422
423    private void loge(String s) {
424        Rlog.e(LOG_TAG, s);
425    }
426
427    private void loge(String s, Exception e) {
428        Rlog.e(LOG_TAG, s, e);
429    }
430
431    private class SipCall extends SipCallBase {
432        private static final String SC_TAG = "SipCall";
433        private static final boolean SC_DBG = true;
434        private static final boolean SC_VDBG = false; // STOPSHIP if true
435
436        void reset() {
437            if (SC_DBG) log("reset");
438            mConnections.clear();
439            setState(Call.State.IDLE);
440        }
441
442        void switchWith(SipCall that) {
443            if (SC_DBG) log("switchWith");
444            synchronized (SipPhone.class) {
445                SipCall tmp = new SipCall();
446                tmp.takeOver(this);
447                this.takeOver(that);
448                that.takeOver(tmp);
449            }
450        }
451
452        private void takeOver(SipCall that) {
453            if (SC_DBG) log("takeOver");
454            mConnections = that.mConnections;
455            mState = that.mState;
456            for (Connection c : mConnections) {
457                ((SipConnection) c).changeOwner(this);
458            }
459        }
460
461        @Override
462        public Phone getPhone() {
463            return SipPhone.this;
464        }
465
466        @Override
467        public List<Connection> getConnections() {
468            if (SC_VDBG) log("getConnections");
469            synchronized (SipPhone.class) {
470                // FIXME should return Collections.unmodifiableList();
471                return mConnections;
472            }
473        }
474
475        Connection dial(String originalNumber) throws SipException {
476            if (SC_DBG) log("dial: num=" + (SC_VDBG ? originalNumber : "xxx"));
477            // TODO: Should this be synchronized?
478            String calleeSipUri = originalNumber;
479            if (!calleeSipUri.contains("@")) {
480                String replaceStr = Pattern.quote(mProfile.getUserName() + "@");
481                calleeSipUri = mProfile.getUriString().replaceFirst(replaceStr,
482                        calleeSipUri + "@");
483            }
484            try {
485                SipProfile callee =
486                        new SipProfile.Builder(calleeSipUri).build();
487                SipConnection c = new SipConnection(this, callee,
488                        originalNumber);
489                c.dial();
490                mConnections.add(c);
491                setState(Call.State.DIALING);
492                return c;
493            } catch (ParseException e) {
494                throw new SipException("dial", e);
495            }
496        }
497
498        @Override
499        public void hangup() throws CallStateException {
500            synchronized (SipPhone.class) {
501                if (mState.isAlive()) {
502                    if (SC_DBG) log("hangup: call " + getState()
503                            + ": " + this + " on phone " + getPhone());
504                    setState(State.DISCONNECTING);
505                    CallStateException excp = null;
506                    for (Connection c : mConnections) {
507                        try {
508                            c.hangup();
509                        } catch (CallStateException e) {
510                            excp = e;
511                        }
512                    }
513                    if (excp != null) throw excp;
514                } else {
515                    if (SC_DBG) log("hangup: dead call " + getState()
516                            + ": " + this + " on phone " + getPhone());
517                }
518            }
519        }
520
521        SipConnection initIncomingCall(SipAudioCall sipAudioCall, boolean makeCallWait) {
522            SipProfile callee = sipAudioCall.getPeerProfile();
523            SipConnection c = new SipConnection(this, callee);
524            mConnections.add(c);
525
526            Call.State newState = makeCallWait ? State.WAITING : State.INCOMING;
527            c.initIncomingCall(sipAudioCall, newState);
528
529            setState(newState);
530            notifyNewRingingConnectionP(c);
531            return c;
532        }
533
534        void rejectCall() throws CallStateException {
535            if (SC_DBG) log("rejectCall:");
536            hangup();
537        }
538
539        void acceptCall() throws CallStateException {
540            if (SC_DBG) log("acceptCall: accepting");
541            if (this != mRingingCall) {
542                throw new CallStateException("acceptCall() in a non-ringing call");
543            }
544            if (mConnections.size() != 1) {
545                throw new CallStateException("acceptCall() in a conf call");
546            }
547            ((SipConnection) mConnections.get(0)).acceptCall();
548        }
549
550        private boolean isSpeakerOn() {
551            Boolean ret = ((AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE))
552                    .isSpeakerphoneOn();
553            if (SC_VDBG) log("isSpeakerOn: ret=" + ret);
554            return ret;
555        }
556
557        void setAudioGroupMode() {
558            AudioGroup audioGroup = getAudioGroup();
559            if (audioGroup == null) {
560                if (SC_DBG) log("setAudioGroupMode: audioGroup == null ignore");
561                return;
562            }
563            int mode = audioGroup.getMode();
564            if (mState == State.HOLDING) {
565                audioGroup.setMode(AudioGroup.MODE_ON_HOLD);
566            } else if (getMute()) {
567                audioGroup.setMode(AudioGroup.MODE_MUTED);
568            } else if (isSpeakerOn()) {
569                audioGroup.setMode(AudioGroup.MODE_ECHO_SUPPRESSION);
570            } else {
571                audioGroup.setMode(AudioGroup.MODE_NORMAL);
572            }
573            if (SC_DBG) log(String.format(
574                    "setAudioGroupMode change: %d --> %d", mode,
575                    audioGroup.getMode()));
576        }
577
578        void hold() throws CallStateException {
579            if (SC_DBG) log("hold:");
580            setState(State.HOLDING);
581            for (Connection c : mConnections) ((SipConnection) c).hold();
582            setAudioGroupMode();
583        }
584
585        void unhold() throws CallStateException {
586            if (SC_DBG) log("unhold:");
587            setState(State.ACTIVE);
588            AudioGroup audioGroup = new AudioGroup();
589            for (Connection c : mConnections) {
590                ((SipConnection) c).unhold(audioGroup);
591            }
592            setAudioGroupMode();
593        }
594
595        void setMute(boolean muted) {
596            if (SC_DBG) log("setMute: muted=" + muted);
597            for (Connection c : mConnections) {
598                ((SipConnection) c).setMute(muted);
599            }
600        }
601
602        boolean getMute() {
603            boolean ret = mConnections.isEmpty()
604                    ? false
605                    : ((SipConnection) mConnections.get(0)).getMute();
606            if (SC_DBG) log("getMute: ret=" + ret);
607            return ret;
608        }
609
610        void merge(SipCall that) throws CallStateException {
611            if (SC_DBG) log("merge:");
612            AudioGroup audioGroup = getAudioGroup();
613
614            // copy to an array to avoid concurrent modification as connections
615            // in that.connections will be removed in add(SipConnection).
616            Connection[] cc = that.mConnections.toArray(
617                    new Connection[that.mConnections.size()]);
618            for (Connection c : cc) {
619                SipConnection conn = (SipConnection) c;
620                add(conn);
621                if (conn.getState() == Call.State.HOLDING) {
622                    conn.unhold(audioGroup);
623                }
624            }
625            that.setState(Call.State.IDLE);
626        }
627
628        private void add(SipConnection conn) {
629            if (SC_DBG) log("add:");
630            SipCall call = conn.getCall();
631            if (call == this) return;
632            if (call != null) call.mConnections.remove(conn);
633
634            mConnections.add(conn);
635            conn.changeOwner(this);
636        }
637
638        void sendDtmf(char c) {
639            if (SC_DBG) log("sendDtmf: c=" + c);
640            AudioGroup audioGroup = getAudioGroup();
641            if (audioGroup == null) {
642                if (SC_DBG) log("sendDtmf: audioGroup == null, ignore c=" + c);
643                return;
644            }
645            audioGroup.sendDtmf(convertDtmf(c));
646        }
647
648        private int convertDtmf(char c) {
649            int code = c - '0';
650            if ((code < 0) || (code > 9)) {
651                switch (c) {
652                    case '*': return 10;
653                    case '#': return 11;
654                    case 'A': return 12;
655                    case 'B': return 13;
656                    case 'C': return 14;
657                    case 'D': return 15;
658                    default:
659                        throw new IllegalArgumentException(
660                                "invalid DTMF char: " + (int) c);
661                }
662            }
663            return code;
664        }
665
666        @Override
667        protected void setState(State newState) {
668            if (mState != newState) {
669                if (SC_DBG) log("setState: cur state" + mState
670                        + " --> " + newState + ": " + this + ": on phone "
671                        + getPhone() + " " + mConnections.size());
672
673                if (newState == Call.State.ALERTING) {
674                    mState = newState; // need in ALERTING to enable ringback
675                    startRingbackTone();
676                } else if (mState == Call.State.ALERTING) {
677                    stopRingbackTone();
678                }
679                mState = newState;
680                updatePhoneState();
681                notifyPreciseCallStateChanged();
682            }
683        }
684
685        void onConnectionStateChanged(SipConnection conn) {
686            // this can be called back when a conf call is formed
687            if (SC_DBG) log("onConnectionStateChanged: conn=" + conn);
688            if (mState != State.ACTIVE) {
689                setState(conn.getState());
690            }
691        }
692
693        void onConnectionEnded(SipConnection conn) {
694            // set state to DISCONNECTED only when all conns are disconnected
695            if (SC_DBG) log("onConnectionEnded: conn=" + conn);
696            if (mState != State.DISCONNECTED) {
697                boolean allConnectionsDisconnected = true;
698                if (SC_DBG) log("---check connections: "
699                        + mConnections.size());
700                for (Connection c : mConnections) {
701                    if (SC_DBG) log("   state=" + c.getState() + ": "
702                            + c);
703                    if (c.getState() != State.DISCONNECTED) {
704                        allConnectionsDisconnected = false;
705                        break;
706                    }
707                }
708                if (allConnectionsDisconnected) setState(State.DISCONNECTED);
709            }
710            notifyDisconnectP(conn);
711        }
712
713        private AudioGroup getAudioGroup() {
714            if (mConnections.isEmpty()) return null;
715            return ((SipConnection) mConnections.get(0)).getAudioGroup();
716        }
717
718        private void log(String s) {
719            Rlog.d(SC_TAG, s);
720        }
721    }
722
723    private class SipConnection extends SipConnectionBase {
724        private static final String SCN_TAG = "SipConnection";
725        private static final boolean SCN_DBG = true;
726
727        private SipCall mOwner;
728        private SipAudioCall mSipAudioCall;
729        private Call.State mState = Call.State.IDLE;
730        private SipProfile mPeer;
731        private boolean mIncoming = false;
732        private String mOriginalNumber; // may be a PSTN number
733
734        private SipAudioCallAdapter mAdapter = new SipAudioCallAdapter() {
735            @Override
736            protected void onCallEnded(int cause) {
737                if (getDisconnectCause() != DisconnectCause.LOCAL) {
738                    setDisconnectCause(cause);
739                }
740                synchronized (SipPhone.class) {
741                    setState(Call.State.DISCONNECTED);
742                    SipAudioCall sipAudioCall = mSipAudioCall;
743                    // FIXME: This goes null and is synchronized, but many uses aren't sync'd
744                    mSipAudioCall = null;
745                    String sessionState = (sipAudioCall == null)
746                            ? ""
747                            : (sipAudioCall.getState() + ", ");
748                    if (SCN_DBG) log("[SipAudioCallAdapter] onCallEnded: "
749                            + mPeer.getUriString() + ": " + sessionState
750                            + "cause: " + getDisconnectCause() + ", on phone "
751                            + getPhone());
752                    if (sipAudioCall != null) {
753                        sipAudioCall.setListener(null);
754                        sipAudioCall.close();
755                    }
756                    mOwner.onConnectionEnded(SipConnection.this);
757                }
758            }
759
760            @Override
761            public void onCallEstablished(SipAudioCall call) {
762                onChanged(call);
763                // Race onChanged synchronized this isn't
764                if (mState == Call.State.ACTIVE) call.startAudio();
765            }
766
767            @Override
768            public void onCallHeld(SipAudioCall call) {
769                onChanged(call);
770                // Race onChanged synchronized this isn't
771                if (mState == Call.State.HOLDING) call.startAudio();
772            }
773
774            @Override
775            public void onChanged(SipAudioCall call) {
776                synchronized (SipPhone.class) {
777                    Call.State newState = getCallStateFrom(call);
778                    if (mState == newState) return;
779                    if (newState == Call.State.INCOMING) {
780                        setState(mOwner.getState()); // INCOMING or WAITING
781                    } else {
782                        if (mOwner == mRingingCall) {
783                            if (mRingingCall.getState() == Call.State.WAITING) {
784                                try {
785                                    switchHoldingAndActive();
786                                } catch (CallStateException e) {
787                                    // disconnect the call.
788                                    onCallEnded(DisconnectCause.LOCAL);
789                                    return;
790                                }
791                            }
792                            mForegroundCall.switchWith(mRingingCall);
793                        }
794                        setState(newState);
795                    }
796                    mOwner.onConnectionStateChanged(SipConnection.this);
797                    if (SCN_DBG) log("onChanged: "
798                            + mPeer.getUriString() + ": " + mState
799                            + " on phone " + getPhone());
800                }
801            }
802
803            @Override
804            protected void onError(int cause) {
805                if (SCN_DBG) log("onError: " + cause);
806                onCallEnded(cause);
807            }
808        };
809
810        public SipConnection(SipCall owner, SipProfile callee,
811                String originalNumber) {
812            super(originalNumber);
813            mOwner = owner;
814            mPeer = callee;
815            mOriginalNumber = originalNumber;
816        }
817
818        public SipConnection(SipCall owner, SipProfile callee) {
819            this(owner, callee, getUriString(callee));
820        }
821
822        @Override
823        public String getCnapName() {
824            String displayName = mPeer.getDisplayName();
825            return TextUtils.isEmpty(displayName) ? null
826                                                  : displayName;
827        }
828
829        @Override
830        public int getNumberPresentation() {
831            return PhoneConstants.PRESENTATION_ALLOWED;
832        }
833
834        void initIncomingCall(SipAudioCall sipAudioCall, Call.State newState) {
835            setState(newState);
836            mSipAudioCall = sipAudioCall;
837            sipAudioCall.setListener(mAdapter); // call back to set state
838            mIncoming = true;
839        }
840
841        void acceptCall() throws CallStateException {
842            try {
843                mSipAudioCall.answerCall(TIMEOUT_ANSWER_CALL);
844            } catch (SipException e) {
845                throw new CallStateException("acceptCall(): " + e);
846            }
847        }
848
849        void changeOwner(SipCall owner) {
850            mOwner = owner;
851        }
852
853        AudioGroup getAudioGroup() {
854            if (mSipAudioCall == null) return null;
855            return mSipAudioCall.getAudioGroup();
856        }
857
858        void dial() throws SipException {
859            setState(Call.State.DIALING);
860            mSipAudioCall = mSipManager.makeAudioCall(mProfile, mPeer, null,
861                    TIMEOUT_MAKE_CALL);
862            mSipAudioCall.setListener(mAdapter);
863        }
864
865        void hold() throws CallStateException {
866            setState(Call.State.HOLDING);
867            try {
868                mSipAudioCall.holdCall(TIMEOUT_HOLD_CALL);
869            } catch (SipException e) {
870                throw new CallStateException("hold(): " + e);
871            }
872        }
873
874        void unhold(AudioGroup audioGroup) throws CallStateException {
875            mSipAudioCall.setAudioGroup(audioGroup);
876            setState(Call.State.ACTIVE);
877            try {
878                mSipAudioCall.continueCall(TIMEOUT_HOLD_CALL);
879            } catch (SipException e) {
880                throw new CallStateException("unhold(): " + e);
881            }
882        }
883
884        void setMute(boolean muted) {
885            if ((mSipAudioCall != null) && (muted != mSipAudioCall.isMuted())) {
886                if (SCN_DBG) log("setState: prev muted=" + !muted + " new muted=" + muted);
887                mSipAudioCall.toggleMute();
888            }
889        }
890
891        boolean getMute() {
892            return (mSipAudioCall == null) ? false
893                                           : mSipAudioCall.isMuted();
894        }
895
896        @Override
897        protected void setState(Call.State state) {
898            if (state == mState) return;
899            super.setState(state);
900            mState = state;
901        }
902
903        @Override
904        public Call.State getState() {
905            return mState;
906        }
907
908        @Override
909        public boolean isIncoming() {
910            return mIncoming;
911        }
912
913        @Override
914        public String getAddress() {
915            // Phone app uses this to query caller ID. Return the original dial
916            // number (which may be a PSTN number) instead of the peer's SIP
917            // URI.
918            return mOriginalNumber;
919        }
920
921        @Override
922        public SipCall getCall() {
923            return mOwner;
924        }
925
926        @Override
927        protected Phone getPhone() {
928            return mOwner.getPhone();
929        }
930
931        @Override
932        public void hangup() throws CallStateException {
933            synchronized (SipPhone.class) {
934                if (SCN_DBG) log("hangup: conn=" + mPeer.getUriString()
935                        + ": " + mState + ": on phone "
936                        + getPhone().getPhoneName());
937                if (!mState.isAlive()) return;
938                try {
939                    SipAudioCall sipAudioCall = mSipAudioCall;
940                    if (sipAudioCall != null) {
941                        sipAudioCall.setListener(null);
942                        sipAudioCall.endCall();
943                    }
944                } catch (SipException e) {
945                    throw new CallStateException("hangup(): " + e);
946                } finally {
947                    mAdapter.onCallEnded(((mState == Call.State.INCOMING)
948                            || (mState == Call.State.WAITING))
949                            ? DisconnectCause.INCOMING_REJECTED
950                            : DisconnectCause.LOCAL);
951                }
952            }
953        }
954
955        @Override
956        public void separate() throws CallStateException {
957            synchronized (SipPhone.class) {
958                SipCall call = (getPhone() == SipPhone.this)
959                        ? (SipCall) getBackgroundCall()
960                        : (SipCall) getForegroundCall();
961                if (call.getState() != Call.State.IDLE) {
962                    throw new CallStateException(
963                            "cannot put conn back to a call in non-idle state: "
964                            + call.getState());
965                }
966                if (SCN_DBG) log("separate: conn="
967                        + mPeer.getUriString() + " from " + mOwner + " back to "
968                        + call);
969
970                // separate the AudioGroup and connection from the original call
971                Phone originalPhone = getPhone();
972                AudioGroup audioGroup = call.getAudioGroup(); // may be null
973                call.add(this);
974                mSipAudioCall.setAudioGroup(audioGroup);
975
976                // put the original call to bg; and the separated call becomes
977                // fg if it was in bg
978                originalPhone.switchHoldingAndActive();
979
980                // start audio and notify the phone app of the state change
981                call = (SipCall) getForegroundCall();
982                mSipAudioCall.startAudio();
983                call.onConnectionStateChanged(this);
984            }
985        }
986
987        private void log(String s) {
988            Rlog.d(SCN_TAG, s);
989        }
990    }
991
992    private abstract class SipAudioCallAdapter extends SipAudioCall.Listener {
993        private static final String SACA_TAG = "SipAudioCallAdapter";
994        private static final boolean SACA_DBG = true;
995        /** Call ended with cause defined in {@link DisconnectCause}. */
996        protected abstract void onCallEnded(int cause);
997        /** Call failed with cause defined in {@link DisconnectCause}. */
998        protected abstract void onError(int cause);
999
1000        @Override
1001        public void onCallEnded(SipAudioCall call) {
1002            if (SACA_DBG) log("onCallEnded: call=" + call);
1003            onCallEnded(call.isInCall()
1004                    ? DisconnectCause.NORMAL
1005                    : DisconnectCause.INCOMING_MISSED);
1006        }
1007
1008        @Override
1009        public void onCallBusy(SipAudioCall call) {
1010            if (SACA_DBG) log("onCallBusy: call=" + call);
1011            onCallEnded(DisconnectCause.BUSY);
1012        }
1013
1014        @Override
1015        public void onError(SipAudioCall call, int errorCode,
1016                String errorMessage) {
1017            if (SACA_DBG) {
1018                log("onError: call=" + call + " code="+ SipErrorCode.toString(errorCode)
1019                    + ": " + errorMessage);
1020            }
1021            switch (errorCode) {
1022                case SipErrorCode.SERVER_UNREACHABLE:
1023                    onError(DisconnectCause.SERVER_UNREACHABLE);
1024                    break;
1025                case SipErrorCode.PEER_NOT_REACHABLE:
1026                    onError(DisconnectCause.NUMBER_UNREACHABLE);
1027                    break;
1028                case SipErrorCode.INVALID_REMOTE_URI:
1029                    onError(DisconnectCause.INVALID_NUMBER);
1030                    break;
1031                case SipErrorCode.TIME_OUT:
1032                case SipErrorCode.TRANSACTION_TERMINTED:
1033                    onError(DisconnectCause.TIMED_OUT);
1034                    break;
1035                case SipErrorCode.DATA_CONNECTION_LOST:
1036                    onError(DisconnectCause.LOST_SIGNAL);
1037                    break;
1038                case SipErrorCode.INVALID_CREDENTIALS:
1039                    onError(DisconnectCause.INVALID_CREDENTIALS);
1040                    break;
1041                case SipErrorCode.CROSS_DOMAIN_AUTHENTICATION:
1042                    onError(DisconnectCause.OUT_OF_NETWORK);
1043                    break;
1044                case SipErrorCode.SERVER_ERROR:
1045                    onError(DisconnectCause.SERVER_ERROR);
1046                    break;
1047                case SipErrorCode.SOCKET_ERROR:
1048                case SipErrorCode.CLIENT_ERROR:
1049                default:
1050                    onError(DisconnectCause.ERROR_UNSPECIFIED);
1051            }
1052        }
1053
1054        private void log(String s) {
1055            Rlog.d(SACA_TAG, s);
1056        }
1057    }
1058}
1059