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