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