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