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