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