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