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