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