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