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