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