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