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