SipPhone.java revision 184ffb58812108624ea0be2732cdae511b99d09d
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 boolean canTake(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("canTake: ret=false, not a SipAudioCall");
100                return false;
101            }
102            if (mRingingCall.getState().isAlive()) {
103                if (DBG) log("canTake: ret=false, ringingCall not alive");
104                return false;
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("canTake: ret=false," +
113                            " foreground and background both alive");
114                }
115                return false;
116            }
117
118            try {
119                SipAudioCall sipAudioCall = (SipAudioCall) incomingCall;
120                if (DBG) log("canTake: 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                    mRingingCall.initIncomingCall(sipAudioCall, makeCallWait);
126                    if (sipAudioCall.getState()
127                            != SipSession.State.INCOMING_CALL) {
128                        // Peer cancelled the call!
129                        if (DBG) log("    canTake: call cancelled !!");
130                        mRingingCall.reset();
131                    }
132                    return true;
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("    canTake: exception e=" + e);
139                mRingingCall.reset();
140            }
141            if (DBG) log("canTake: NOT taking !!");
142            return false;
143        }
144    }
145
146    @Override
147    public void acceptCall() 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) throws CallStateException {
183        synchronized (SipPhone.class) {
184            return dialInternal(dialString);
185        }
186    }
187
188    private Connection dialInternal(String dialString)
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        void 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        }
531
532        void rejectCall() throws CallStateException {
533            if (SC_DBG) log("rejectCall:");
534            hangup();
535        }
536
537        void acceptCall() throws CallStateException {
538            if (SC_DBG) log("acceptCall: accepting");
539            if (this != mRingingCall) {
540                throw new CallStateException("acceptCall() in a non-ringing call");
541            }
542            if (mConnections.size() != 1) {
543                throw new CallStateException("acceptCall() in a conf call");
544            }
545            ((SipConnection) mConnections.get(0)).acceptCall();
546        }
547
548        private boolean isSpeakerOn() {
549            Boolean ret = ((AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE))
550                    .isSpeakerphoneOn();
551            if (SC_VDBG) log("isSpeakerOn: ret=" + ret);
552            return ret;
553        }
554
555        void setAudioGroupMode() {
556            AudioGroup audioGroup = getAudioGroup();
557            if (audioGroup == null) {
558                if (SC_DBG) log("setAudioGroupMode: audioGroup == null ignore");
559                return;
560            }
561            int mode = audioGroup.getMode();
562            if (mState == State.HOLDING) {
563                audioGroup.setMode(AudioGroup.MODE_ON_HOLD);
564            } else if (getMute()) {
565                audioGroup.setMode(AudioGroup.MODE_MUTED);
566            } else if (isSpeakerOn()) {
567                audioGroup.setMode(AudioGroup.MODE_ECHO_SUPPRESSION);
568            } else {
569                audioGroup.setMode(AudioGroup.MODE_NORMAL);
570            }
571            if (SC_DBG) log(String.format(
572                    "setAudioGroupMode change: %d --> %d", mode,
573                    audioGroup.getMode()));
574        }
575
576        void hold() throws CallStateException {
577            if (SC_DBG) log("hold:");
578            setState(State.HOLDING);
579            for (Connection c : mConnections) ((SipConnection) c).hold();
580            setAudioGroupMode();
581        }
582
583        void unhold() throws CallStateException {
584            if (SC_DBG) log("unhold:");
585            setState(State.ACTIVE);
586            AudioGroup audioGroup = new AudioGroup();
587            for (Connection c : mConnections) {
588                ((SipConnection) c).unhold(audioGroup);
589            }
590            setAudioGroupMode();
591        }
592
593        void setMute(boolean muted) {
594            if (SC_DBG) log("setMute: muted=" + muted);
595            for (Connection c : mConnections) {
596                ((SipConnection) c).setMute(muted);
597            }
598        }
599
600        boolean getMute() {
601            boolean ret = mConnections.isEmpty()
602                    ? false
603                    : ((SipConnection) mConnections.get(0)).getMute();
604            if (SC_DBG) log("getMute: ret=" + ret);
605            return ret;
606        }
607
608        void merge(SipCall that) throws CallStateException {
609            if (SC_DBG) log("merge:");
610            AudioGroup audioGroup = getAudioGroup();
611
612            // copy to an array to avoid concurrent modification as connections
613            // in that.connections will be removed in add(SipConnection).
614            Connection[] cc = that.mConnections.toArray(
615                    new Connection[that.mConnections.size()]);
616            for (Connection c : cc) {
617                SipConnection conn = (SipConnection) c;
618                add(conn);
619                if (conn.getState() == Call.State.HOLDING) {
620                    conn.unhold(audioGroup);
621                }
622            }
623            that.setState(Call.State.IDLE);
624        }
625
626        private void add(SipConnection conn) {
627            if (SC_DBG) log("add:");
628            SipCall call = conn.getCall();
629            if (call == this) return;
630            if (call != null) call.mConnections.remove(conn);
631
632            mConnections.add(conn);
633            conn.changeOwner(this);
634        }
635
636        void sendDtmf(char c) {
637            if (SC_DBG) log("sendDtmf: c=" + c);
638            AudioGroup audioGroup = getAudioGroup();
639            if (audioGroup == null) {
640                if (SC_DBG) log("sendDtmf: audioGroup == null, ignore c=" + c);
641                return;
642            }
643            audioGroup.sendDtmf(convertDtmf(c));
644        }
645
646        private int convertDtmf(char c) {
647            int code = c - '0';
648            if ((code < 0) || (code > 9)) {
649                switch (c) {
650                    case '*': return 10;
651                    case '#': return 11;
652                    case 'A': return 12;
653                    case 'B': return 13;
654                    case 'C': return 14;
655                    case 'D': return 15;
656                    default:
657                        throw new IllegalArgumentException(
658                                "invalid DTMF char: " + (int) c);
659                }
660            }
661            return code;
662        }
663
664        @Override
665        protected void setState(State newState) {
666            if (mState != newState) {
667                if (SC_DBG) log("setState: cur state" + mState
668                        + " --> " + newState + ": " + this + ": on phone "
669                        + getPhone() + " " + mConnections.size());
670
671                if (newState == Call.State.ALERTING) {
672                    mState = newState; // need in ALERTING to enable ringback
673                    startRingbackTone();
674                } else if (mState == Call.State.ALERTING) {
675                    stopRingbackTone();
676                }
677                mState = newState;
678                updatePhoneState();
679                notifyPreciseCallStateChanged();
680            }
681        }
682
683        void onConnectionStateChanged(SipConnection conn) {
684            // this can be called back when a conf call is formed
685            if (SC_DBG) log("onConnectionStateChanged: conn=" + conn);
686            if (mState != State.ACTIVE) {
687                setState(conn.getState());
688            }
689        }
690
691        void onConnectionEnded(SipConnection conn) {
692            // set state to DISCONNECTED only when all conns are disconnected
693            if (SC_DBG) log("onConnectionEnded: conn=" + conn);
694            if (mState != State.DISCONNECTED) {
695                boolean allConnectionsDisconnected = true;
696                if (SC_DBG) log("---check connections: "
697                        + mConnections.size());
698                for (Connection c : mConnections) {
699                    if (SC_DBG) log("   state=" + c.getState() + ": "
700                            + c);
701                    if (c.getState() != State.DISCONNECTED) {
702                        allConnectionsDisconnected = false;
703                        break;
704                    }
705                }
706                if (allConnectionsDisconnected) setState(State.DISCONNECTED);
707            }
708            notifyDisconnectP(conn);
709        }
710
711        private AudioGroup getAudioGroup() {
712            if (mConnections.isEmpty()) return null;
713            return ((SipConnection) mConnections.get(0)).getAudioGroup();
714        }
715
716        private void log(String s) {
717            Rlog.d(SC_TAG, s);
718        }
719    }
720
721    private class SipConnection extends SipConnectionBase {
722        private static final String SCN_TAG = "SipConnection";
723        private static final boolean SCN_DBG = true;
724
725        private SipCall mOwner;
726        private SipAudioCall mSipAudioCall;
727        private Call.State mState = Call.State.IDLE;
728        private SipProfile mPeer;
729        private boolean mIncoming = false;
730        private String mOriginalNumber; // may be a PSTN number
731
732        private SipAudioCallAdapter mAdapter = new SipAudioCallAdapter() {
733            @Override
734            protected void onCallEnded(int cause) {
735                if (getDisconnectCause() != DisconnectCause.LOCAL) {
736                    setDisconnectCause(cause);
737                }
738                synchronized (SipPhone.class) {
739                    setState(Call.State.DISCONNECTED);
740                    SipAudioCall sipAudioCall = mSipAudioCall;
741                    // FIXME: This goes null and is synchronized, but many uses aren't sync'd
742                    mSipAudioCall = null;
743                    String sessionState = (sipAudioCall == null)
744                            ? ""
745                            : (sipAudioCall.getState() + ", ");
746                    if (SCN_DBG) log("[SipAudioCallAdapter] onCallEnded: "
747                            + mPeer.getUriString() + ": " + sessionState
748                            + "cause: " + getDisconnectCause() + ", on phone "
749                            + getPhone());
750                    if (sipAudioCall != null) {
751                        sipAudioCall.setListener(null);
752                        sipAudioCall.close();
753                    }
754                    mOwner.onConnectionEnded(SipConnection.this);
755                }
756            }
757
758            @Override
759            public void onCallEstablished(SipAudioCall call) {
760                onChanged(call);
761                // Race onChanged synchronized this isn't
762                if (mState == Call.State.ACTIVE) call.startAudio();
763            }
764
765            @Override
766            public void onCallHeld(SipAudioCall call) {
767                onChanged(call);
768                // Race onChanged synchronized this isn't
769                if (mState == Call.State.HOLDING) call.startAudio();
770            }
771
772            @Override
773            public void onChanged(SipAudioCall call) {
774                synchronized (SipPhone.class) {
775                    Call.State newState = getCallStateFrom(call);
776                    if (mState == newState) return;
777                    if (newState == Call.State.INCOMING) {
778                        setState(mOwner.getState()); // INCOMING or WAITING
779                    } else {
780                        if (mOwner == mRingingCall) {
781                            if (mRingingCall.getState() == Call.State.WAITING) {
782                                try {
783                                    switchHoldingAndActive();
784                                } catch (CallStateException e) {
785                                    // disconnect the call.
786                                    onCallEnded(DisconnectCause.LOCAL);
787                                    return;
788                                }
789                            }
790                            mForegroundCall.switchWith(mRingingCall);
791                        }
792                        setState(newState);
793                    }
794                    mOwner.onConnectionStateChanged(SipConnection.this);
795                    if (SCN_DBG) log("onChanged: "
796                            + mPeer.getUriString() + ": " + mState
797                            + " on phone " + getPhone());
798                }
799            }
800
801            @Override
802            protected void onError(int cause) {
803                if (SCN_DBG) log("onError: " + cause);
804                onCallEnded(cause);
805            }
806        };
807
808        public SipConnection(SipCall owner, SipProfile callee,
809                String originalNumber) {
810            super(originalNumber);
811            mOwner = owner;
812            mPeer = callee;
813            mOriginalNumber = originalNumber;
814        }
815
816        public SipConnection(SipCall owner, SipProfile callee) {
817            this(owner, callee, getUriString(callee));
818        }
819
820        @Override
821        public String getCnapName() {
822            String displayName = mPeer.getDisplayName();
823            return TextUtils.isEmpty(displayName) ? null
824                                                  : displayName;
825        }
826
827        @Override
828        public int getNumberPresentation() {
829            return PhoneConstants.PRESENTATION_ALLOWED;
830        }
831
832        void initIncomingCall(SipAudioCall sipAudioCall, Call.State newState) {
833            setState(newState);
834            mSipAudioCall = sipAudioCall;
835            sipAudioCall.setListener(mAdapter); // call back to set state
836            mIncoming = true;
837        }
838
839        void acceptCall() throws CallStateException {
840            try {
841                mSipAudioCall.answerCall(TIMEOUT_ANSWER_CALL);
842            } catch (SipException e) {
843                throw new CallStateException("acceptCall(): " + e);
844            }
845        }
846
847        void changeOwner(SipCall owner) {
848            mOwner = owner;
849        }
850
851        AudioGroup getAudioGroup() {
852            if (mSipAudioCall == null) return null;
853            return mSipAudioCall.getAudioGroup();
854        }
855
856        void dial() throws SipException {
857            setState(Call.State.DIALING);
858            mSipAudioCall = mSipManager.makeAudioCall(mProfile, mPeer, null,
859                    TIMEOUT_MAKE_CALL);
860            mSipAudioCall.setListener(mAdapter);
861        }
862
863        void hold() throws CallStateException {
864            setState(Call.State.HOLDING);
865            try {
866                mSipAudioCall.holdCall(TIMEOUT_HOLD_CALL);
867            } catch (SipException e) {
868                throw new CallStateException("hold(): " + e);
869            }
870        }
871
872        void unhold(AudioGroup audioGroup) throws CallStateException {
873            mSipAudioCall.setAudioGroup(audioGroup);
874            setState(Call.State.ACTIVE);
875            try {
876                mSipAudioCall.continueCall(TIMEOUT_HOLD_CALL);
877            } catch (SipException e) {
878                throw new CallStateException("unhold(): " + e);
879            }
880        }
881
882        void setMute(boolean muted) {
883            if ((mSipAudioCall != null) && (muted != mSipAudioCall.isMuted())) {
884                if (SCN_DBG) log("setState: prev muted=" + !muted + " new muted=" + muted);
885                mSipAudioCall.toggleMute();
886            }
887        }
888
889        boolean getMute() {
890            return (mSipAudioCall == null) ? false
891                                           : mSipAudioCall.isMuted();
892        }
893
894        @Override
895        protected void setState(Call.State state) {
896            if (state == mState) return;
897            super.setState(state);
898            mState = state;
899        }
900
901        @Override
902        public Call.State getState() {
903            return mState;
904        }
905
906        @Override
907        public boolean isIncoming() {
908            return mIncoming;
909        }
910
911        @Override
912        public String getAddress() {
913            // Phone app uses this to query caller ID. Return the original dial
914            // number (which may be a PSTN number) instead of the peer's SIP
915            // URI.
916            return mOriginalNumber;
917        }
918
919        @Override
920        public SipCall getCall() {
921            return mOwner;
922        }
923
924        @Override
925        protected Phone getPhone() {
926            return mOwner.getPhone();
927        }
928
929        @Override
930        public void hangup() throws CallStateException {
931            synchronized (SipPhone.class) {
932                if (SCN_DBG) log("hangup: conn=" + mPeer.getUriString()
933                        + ": " + mState + ": on phone "
934                        + getPhone().getPhoneName());
935                if (!mState.isAlive()) return;
936                try {
937                    SipAudioCall sipAudioCall = mSipAudioCall;
938                    if (sipAudioCall != null) {
939                        sipAudioCall.setListener(null);
940                        sipAudioCall.endCall();
941                    }
942                } catch (SipException e) {
943                    throw new CallStateException("hangup(): " + e);
944                } finally {
945                    mAdapter.onCallEnded(((mState == Call.State.INCOMING)
946                            || (mState == Call.State.WAITING))
947                            ? DisconnectCause.INCOMING_REJECTED
948                            : DisconnectCause.LOCAL);
949                }
950            }
951        }
952
953        @Override
954        public void separate() throws CallStateException {
955            synchronized (SipPhone.class) {
956                SipCall call = (getPhone() == SipPhone.this)
957                        ? (SipCall) getBackgroundCall()
958                        : (SipCall) getForegroundCall();
959                if (call.getState() != Call.State.IDLE) {
960                    throw new CallStateException(
961                            "cannot put conn back to a call in non-idle state: "
962                            + call.getState());
963                }
964                if (SCN_DBG) log("separate: conn="
965                        + mPeer.getUriString() + " from " + mOwner + " back to "
966                        + call);
967
968                // separate the AudioGroup and connection from the original call
969                Phone originalPhone = getPhone();
970                AudioGroup audioGroup = call.getAudioGroup(); // may be null
971                call.add(this);
972                mSipAudioCall.setAudioGroup(audioGroup);
973
974                // put the original call to bg; and the separated call becomes
975                // fg if it was in bg
976                originalPhone.switchHoldingAndActive();
977
978                // start audio and notify the phone app of the state change
979                call = (SipCall) getForegroundCall();
980                mSipAudioCall.startAudio();
981                call.onConnectionStateChanged(this);
982            }
983        }
984
985        private void log(String s) {
986            Rlog.d(SCN_TAG, s);
987        }
988    }
989
990    private abstract class SipAudioCallAdapter extends SipAudioCall.Listener {
991        private static final String SACA_TAG = "SipAudioCallAdapter";
992        private static final boolean SACA_DBG = true;
993        /** Call ended with cause defined in {@link DisconnectCause}. */
994        protected abstract void onCallEnded(int cause);
995        /** Call failed with cause defined in {@link DisconnectCause}. */
996        protected abstract void onError(int cause);
997
998        @Override
999        public void onCallEnded(SipAudioCall call) {
1000            if (SACA_DBG) log("onCallEnded: call=" + call);
1001            onCallEnded(call.isInCall()
1002                    ? DisconnectCause.NORMAL
1003                    : DisconnectCause.INCOMING_MISSED);
1004        }
1005
1006        @Override
1007        public void onCallBusy(SipAudioCall call) {
1008            if (SACA_DBG) log("onCallBusy: call=" + call);
1009            onCallEnded(DisconnectCause.BUSY);
1010        }
1011
1012        @Override
1013        public void onError(SipAudioCall call, int errorCode,
1014                String errorMessage) {
1015            if (SACA_DBG) {
1016                log("onError: call=" + call + " code="+ SipErrorCode.toString(errorCode)
1017                    + ": " + errorMessage);
1018            }
1019            switch (errorCode) {
1020                case SipErrorCode.SERVER_UNREACHABLE:
1021                    onError(DisconnectCause.SERVER_UNREACHABLE);
1022                    break;
1023                case SipErrorCode.PEER_NOT_REACHABLE:
1024                    onError(DisconnectCause.NUMBER_UNREACHABLE);
1025                    break;
1026                case SipErrorCode.INVALID_REMOTE_URI:
1027                    onError(DisconnectCause.INVALID_NUMBER);
1028                    break;
1029                case SipErrorCode.TIME_OUT:
1030                case SipErrorCode.TRANSACTION_TERMINTED:
1031                    onError(DisconnectCause.TIMED_OUT);
1032                    break;
1033                case SipErrorCode.DATA_CONNECTION_LOST:
1034                    onError(DisconnectCause.LOST_SIGNAL);
1035                    break;
1036                case SipErrorCode.INVALID_CREDENTIALS:
1037                    onError(DisconnectCause.INVALID_CREDENTIALS);
1038                    break;
1039                case SipErrorCode.CROSS_DOMAIN_AUTHENTICATION:
1040                    onError(DisconnectCause.OUT_OF_NETWORK);
1041                    break;
1042                case SipErrorCode.SERVER_ERROR:
1043                    onError(DisconnectCause.SERVER_ERROR);
1044                    break;
1045                case SipErrorCode.SOCKET_ERROR:
1046                case SipErrorCode.CLIENT_ERROR:
1047                default:
1048                    onError(DisconnectCause.ERROR_UNSPECIFIED);
1049            }
1050        }
1051
1052        private void log(String s) {
1053            Rlog.d(SACA_TAG, s);
1054        }
1055    }
1056}
1057