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