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