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