/* * Copyright (C) 2010 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.internal.telephony.sip; import android.content.Context; import android.media.AudioManager; import android.net.rtp.AudioGroup; import android.net.sip.SipAudioCall; import android.net.sip.SipErrorCode; import android.net.sip.SipException; import android.net.sip.SipManager; import android.net.sip.SipProfile; import android.net.sip.SipSession; import android.os.AsyncResult; import android.os.Message; import android.telephony.DisconnectCause; import android.telephony.PhoneNumberUtils; import android.telephony.ServiceState; import android.text.TextUtils; import android.telephony.Rlog; import com.android.internal.telephony.Call; import com.android.internal.telephony.CallStateException; import com.android.internal.telephony.Connection; import com.android.internal.telephony.Phone; import com.android.internal.telephony.PhoneConstants; import com.android.internal.telephony.PhoneNotifier; import java.text.ParseException; import java.util.List; import java.util.regex.Pattern; /** * {@hide} */ public class SipPhone extends SipPhoneBase { private static final String LOG_TAG = "SipPhone"; private static final boolean DBG = true; private static final boolean VDBG = false; // STOPSHIP if true private static final int TIMEOUT_MAKE_CALL = 15; // in seconds private static final int TIMEOUT_ANSWER_CALL = 8; // in seconds private static final int TIMEOUT_HOLD_CALL = 15; // in seconds // Minimum time needed between hold/unhold requests. private static final long TIMEOUT_HOLD_PROCESSING = 1000; // ms // A call that is ringing or (call) waiting private SipCall mRingingCall = new SipCall(); private SipCall mForegroundCall = new SipCall(); private SipCall mBackgroundCall = new SipCall(); private SipManager mSipManager; private SipProfile mProfile; private long mTimeOfLastValidHoldRequest = System.currentTimeMillis(); SipPhone (Context context, PhoneNotifier notifier, SipProfile profile) { super("SIP:" + profile.getUriString(), context, notifier); if (DBG) log("new SipPhone: " + hidePii(profile.getUriString())); mRingingCall = new SipCall(); mForegroundCall = new SipCall(); mBackgroundCall = new SipCall(); mProfile = profile; mSipManager = SipManager.newInstance(context); } @Override public boolean equals(Object o) { if (o == this) return true; if (!(o instanceof SipPhone)) return false; SipPhone that = (SipPhone) o; return mProfile.getUriString().equals(that.mProfile.getUriString()); } public String getSipUri() { return mProfile.getUriString(); } public boolean equals(SipPhone phone) { return getSipUri().equals(phone.getSipUri()); } public Connection takeIncomingCall(Object incomingCall) { // FIXME: Is synchronizing on the class necessary, should we use a mLockObj? // Also there are many things not synchronized, of course // this may be true of GsmCdmaPhone too!!! synchronized (SipPhone.class) { if (!(incomingCall instanceof SipAudioCall)) { if (DBG) log("takeIncomingCall: ret=null, not a SipAudioCall"); return null; } if (mRingingCall.getState().isAlive()) { if (DBG) log("takeIncomingCall: ret=null, ringingCall not alive"); return null; } // FIXME: is it true that we cannot take any incoming call if // both foreground and background are active if (mForegroundCall.getState().isAlive() && mBackgroundCall.getState().isAlive()) { if (DBG) { log("takeIncomingCall: ret=null," + " foreground and background both alive"); } return null; } try { SipAudioCall sipAudioCall = (SipAudioCall) incomingCall; if (DBG) log("takeIncomingCall: taking call from: " + hidePii(sipAudioCall.getPeerProfile().getUriString())); String localUri = sipAudioCall.getLocalProfile().getUriString(); if (localUri.equals(mProfile.getUriString())) { boolean makeCallWait = mForegroundCall.getState().isAlive(); SipConnection connection = mRingingCall.initIncomingCall(sipAudioCall, makeCallWait); if (sipAudioCall.getState() != SipSession.State.INCOMING_CALL) { // Peer cancelled the call! if (DBG) log(" takeIncomingCall: call cancelled !!"); mRingingCall.reset(); connection = null; } return connection; } } catch (Exception e) { // Peer may cancel the call at any time during the time we hook // up ringingCall with sipAudioCall. Clean up ringingCall when // that happens. if (DBG) log(" takeIncomingCall: exception e=" + e); mRingingCall.reset(); } if (DBG) log("takeIncomingCall: NOT taking !!"); return null; } } @Override public void acceptCall(int videoState) throws CallStateException { synchronized (SipPhone.class) { if ((mRingingCall.getState() == Call.State.INCOMING) || (mRingingCall.getState() == Call.State.WAITING)) { if (DBG) log("acceptCall: accepting"); // Always unmute when answering a new call mRingingCall.setMute(false); mRingingCall.acceptCall(); } else { if (DBG) { log("acceptCall:" + " throw CallStateException(\"phone not ringing\")"); } throw new CallStateException("phone not ringing"); } } } @Override public void rejectCall() throws CallStateException { synchronized (SipPhone.class) { if (mRingingCall.getState().isRinging()) { if (DBG) log("rejectCall: rejecting"); mRingingCall.rejectCall(); } else { if (DBG) { log("rejectCall:" + " throw CallStateException(\"phone not ringing\")"); } throw new CallStateException("phone not ringing"); } } } @Override public Connection dial(String dialString, int videoState) throws CallStateException { synchronized (SipPhone.class) { return dialInternal(dialString, videoState); } } private Connection dialInternal(String dialString, int videoState) throws CallStateException { if (DBG) log("dialInternal: dialString=" + hidePii(dialString)); clearDisconnected(); if (!canDial()) { throw new CallStateException("dialInternal: cannot dial in current state"); } if (mForegroundCall.getState() == SipCall.State.ACTIVE) { switchHoldingAndActive(); } if (mForegroundCall.getState() != SipCall.State.IDLE) { //we should have failed in !canDial() above before we get here throw new CallStateException("cannot dial in current state"); } mForegroundCall.setMute(false); try { Connection c = mForegroundCall.dial(dialString); return c; } catch (SipException e) { loge("dialInternal: ", e); throw new CallStateException("dial error: " + e); } } @Override public void switchHoldingAndActive() throws CallStateException { // Wait for at least TIMEOUT_HOLD_PROCESSING ms to occur before sending hold/unhold requests // to prevent spamming the SipAudioCall state machine and putting it into an invalid state. if (!isHoldTimeoutExpired()) { if (DBG) log("switchHoldingAndActive: Disregarded! Under " + TIMEOUT_HOLD_PROCESSING + " ms..."); return; } if (DBG) log("switchHoldingAndActive: switch fg and bg"); synchronized (SipPhone.class) { mForegroundCall.switchWith(mBackgroundCall); if (mBackgroundCall.getState().isAlive()) mBackgroundCall.hold(); if (mForegroundCall.getState().isAlive()) mForegroundCall.unhold(); } } @Override public boolean canConference() { if (DBG) log("canConference: ret=true"); return true; } @Override public void conference() throws CallStateException { synchronized (SipPhone.class) { if ((mForegroundCall.getState() != SipCall.State.ACTIVE) || (mForegroundCall.getState() != SipCall.State.ACTIVE)) { throw new CallStateException("wrong state to merge calls: fg=" + mForegroundCall.getState() + ", bg=" + mBackgroundCall.getState()); } if (DBG) log("conference: merge fg & bg"); mForegroundCall.merge(mBackgroundCall); } } public void conference(Call that) throws CallStateException { synchronized (SipPhone.class) { if (!(that instanceof SipCall)) { throw new CallStateException("expect " + SipCall.class + ", cannot merge with " + that.getClass()); } mForegroundCall.merge((SipCall) that); } } @Override public boolean canTransfer() { return false; } @Override public void explicitCallTransfer() { //mCT.explicitCallTransfer(); } @Override public void clearDisconnected() { synchronized (SipPhone.class) { mRingingCall.clearDisconnected(); mForegroundCall.clearDisconnected(); mBackgroundCall.clearDisconnected(); updatePhoneState(); notifyPreciseCallStateChanged(); } } @Override public void sendDtmf(char c) { if (!PhoneNumberUtils.is12Key(c)) { loge("sendDtmf called with invalid character '" + c + "'"); } else if (mForegroundCall.getState().isAlive()) { synchronized (SipPhone.class) { mForegroundCall.sendDtmf(c); } } } @Override public void startDtmf(char c) { if (!PhoneNumberUtils.is12Key(c)) { loge("startDtmf called with invalid character '" + c + "'"); } else { sendDtmf(c); } } @Override public void stopDtmf() { // no op } public void sendBurstDtmf(String dtmfString) { loge("sendBurstDtmf() is a CDMA method"); } @Override public void getOutgoingCallerIdDisplay(Message onComplete) { // FIXME: what to reply? AsyncResult.forMessage(onComplete, null, null); onComplete.sendToTarget(); } @Override public void setOutgoingCallerIdDisplay(int commandInterfaceCLIRMode, Message onComplete) { // FIXME: what's this for SIP? AsyncResult.forMessage(onComplete, null, null); onComplete.sendToTarget(); } @Override public void getCallWaiting(Message onComplete) { // FIXME: what to reply? AsyncResult.forMessage(onComplete, null, null); onComplete.sendToTarget(); } @Override public void setCallWaiting(boolean enable, Message onComplete) { // FIXME: what to reply? loge("call waiting not supported"); } @Override public void setEchoSuppressionEnabled() { // Echo suppression may not be available on every device. So, check // whether it is supported synchronized (SipPhone.class) { AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE); String echoSuppression = audioManager.getParameters("ec_supported"); if (echoSuppression.contains("off")) { mForegroundCall.setAudioGroupMode(); } } } @Override public void setMute(boolean muted) { synchronized (SipPhone.class) { mForegroundCall.setMute(muted); } } @Override public boolean getMute() { return (mForegroundCall.getState().isAlive() ? mForegroundCall.getMute() : mBackgroundCall.getMute()); } @Override public Call getForegroundCall() { return mForegroundCall; } @Override public Call getBackgroundCall() { return mBackgroundCall; } @Override public Call getRingingCall() { return mRingingCall; } @Override public ServiceState getServiceState() { // FIXME: we may need to provide this when data connectivity is lost // or when server is down return super.getServiceState(); } private String getUriString(SipProfile p) { // SipProfile.getUriString() may contain "SIP:" and port return p.getUserName() + "@" + getSipDomain(p); } private String getSipDomain(SipProfile p) { String domain = p.getSipDomain(); // TODO: move this to SipProfile if (domain.endsWith(":5060")) { return domain.substring(0, domain.length() - 5); } else { return domain; } } private static Call.State getCallStateFrom(SipAudioCall sipAudioCall) { if (sipAudioCall.isOnHold()) return Call.State.HOLDING; int sessionState = sipAudioCall.getState(); switch (sessionState) { case SipSession.State.READY_TO_CALL: return Call.State.IDLE; case SipSession.State.INCOMING_CALL: case SipSession.State.INCOMING_CALL_ANSWERING: return Call.State.INCOMING; case SipSession.State.OUTGOING_CALL: return Call.State.DIALING; case SipSession.State.OUTGOING_CALL_RING_BACK: return Call.State.ALERTING; case SipSession.State.OUTGOING_CALL_CANCELING: return Call.State.DISCONNECTING; case SipSession.State.IN_CALL: return Call.State.ACTIVE; default: slog("illegal connection state: " + sessionState); return Call.State.DISCONNECTED; } } private synchronized boolean isHoldTimeoutExpired() { long currTime = System.currentTimeMillis(); if ((currTime - mTimeOfLastValidHoldRequest) > TIMEOUT_HOLD_PROCESSING) { mTimeOfLastValidHoldRequest = currTime; return true; } return false; } private void log(String s) { Rlog.d(LOG_TAG, s); } private static void slog(String s) { Rlog.d(LOG_TAG, s); } private void loge(String s) { Rlog.e(LOG_TAG, s); } private void loge(String s, Exception e) { Rlog.e(LOG_TAG, s, e); } private class SipCall extends SipCallBase { private static final String SC_TAG = "SipCall"; private static final boolean SC_DBG = true; private static final boolean SC_VDBG = false; // STOPSHIP if true void reset() { if (SC_DBG) log("reset"); mConnections.clear(); setState(Call.State.IDLE); } void switchWith(SipCall that) { if (SC_DBG) log("switchWith"); synchronized (SipPhone.class) { SipCall tmp = new SipCall(); tmp.takeOver(this); this.takeOver(that); that.takeOver(tmp); } } private void takeOver(SipCall that) { if (SC_DBG) log("takeOver"); mConnections = that.mConnections; mState = that.mState; for (Connection c : mConnections) { ((SipConnection) c).changeOwner(this); } } @Override public Phone getPhone() { return SipPhone.this; } @Override public List getConnections() { if (SC_VDBG) log("getConnections"); synchronized (SipPhone.class) { // FIXME should return Collections.unmodifiableList(); return mConnections; } } Connection dial(String originalNumber) throws SipException { if (SC_DBG) log("dial: num=" + (SC_VDBG ? originalNumber : "xxx")); // TODO: Should this be synchronized? String calleeSipUri = originalNumber; if (!calleeSipUri.contains("@")) { String replaceStr = Pattern.quote(mProfile.getUserName() + "@"); calleeSipUri = mProfile.getUriString().replaceFirst(replaceStr, calleeSipUri + "@"); } try { SipProfile callee = new SipProfile.Builder(calleeSipUri).build(); SipConnection c = new SipConnection(this, callee, originalNumber); c.dial(); mConnections.add(c); setState(Call.State.DIALING); return c; } catch (ParseException e) { throw new SipException("dial", e); } } @Override public void hangup() throws CallStateException { synchronized (SipPhone.class) { if (mState.isAlive()) { if (SC_DBG) log("hangup: call " + getState() + ": " + this + " on phone " + getPhone()); setState(State.DISCONNECTING); CallStateException excp = null; for (Connection c : mConnections) { try { c.hangup(); } catch (CallStateException e) { excp = e; } } if (excp != null) throw excp; } else { if (SC_DBG) log("hangup: dead call " + getState() + ": " + this + " on phone " + getPhone()); } } } SipConnection initIncomingCall(SipAudioCall sipAudioCall, boolean makeCallWait) { SipProfile callee = sipAudioCall.getPeerProfile(); SipConnection c = new SipConnection(this, callee); mConnections.add(c); Call.State newState = makeCallWait ? State.WAITING : State.INCOMING; c.initIncomingCall(sipAudioCall, newState); setState(newState); notifyNewRingingConnectionP(c); return c; } void rejectCall() throws CallStateException { if (SC_DBG) log("rejectCall:"); hangup(); } void acceptCall() throws CallStateException { if (SC_DBG) log("acceptCall: accepting"); if (this != mRingingCall) { throw new CallStateException("acceptCall() in a non-ringing call"); } if (mConnections.size() != 1) { throw new CallStateException("acceptCall() in a conf call"); } ((SipConnection) mConnections.get(0)).acceptCall(); } private boolean isSpeakerOn() { Boolean ret = ((AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE)) .isSpeakerphoneOn(); if (SC_VDBG) log("isSpeakerOn: ret=" + ret); return ret; } void setAudioGroupMode() { AudioGroup audioGroup = getAudioGroup(); if (audioGroup == null) { if (SC_DBG) log("setAudioGroupMode: audioGroup == null ignore"); return; } int mode = audioGroup.getMode(); if (mState == State.HOLDING) { audioGroup.setMode(AudioGroup.MODE_ON_HOLD); } else if (getMute()) { audioGroup.setMode(AudioGroup.MODE_MUTED); } else if (isSpeakerOn()) { audioGroup.setMode(AudioGroup.MODE_ECHO_SUPPRESSION); } else { audioGroup.setMode(AudioGroup.MODE_NORMAL); } if (SC_DBG) log(String.format( "setAudioGroupMode change: %d --> %d", mode, audioGroup.getMode())); } void hold() throws CallStateException { if (SC_DBG) log("hold:"); setState(State.HOLDING); for (Connection c : mConnections) ((SipConnection) c).hold(); setAudioGroupMode(); } void unhold() throws CallStateException { if (SC_DBG) log("unhold:"); setState(State.ACTIVE); AudioGroup audioGroup = new AudioGroup(); for (Connection c : mConnections) { ((SipConnection) c).unhold(audioGroup); } setAudioGroupMode(); } void setMute(boolean muted) { if (SC_DBG) log("setMute: muted=" + muted); for (Connection c : mConnections) { ((SipConnection) c).setMute(muted); } } boolean getMute() { boolean ret = mConnections.isEmpty() ? false : ((SipConnection) mConnections.get(0)).getMute(); if (SC_DBG) log("getMute: ret=" + ret); return ret; } void merge(SipCall that) throws CallStateException { if (SC_DBG) log("merge:"); AudioGroup audioGroup = getAudioGroup(); // copy to an array to avoid concurrent modification as connections // in that.connections will be removed in add(SipConnection). Connection[] cc = that.mConnections.toArray( new Connection[that.mConnections.size()]); for (Connection c : cc) { SipConnection conn = (SipConnection) c; add(conn); if (conn.getState() == Call.State.HOLDING) { conn.unhold(audioGroup); } } that.setState(Call.State.IDLE); } private void add(SipConnection conn) { if (SC_DBG) log("add:"); SipCall call = conn.getCall(); if (call == this) return; if (call != null) call.mConnections.remove(conn); mConnections.add(conn); conn.changeOwner(this); } void sendDtmf(char c) { if (SC_DBG) log("sendDtmf: c=" + c); AudioGroup audioGroup = getAudioGroup(); if (audioGroup == null) { if (SC_DBG) log("sendDtmf: audioGroup == null, ignore c=" + c); return; } audioGroup.sendDtmf(convertDtmf(c)); } private int convertDtmf(char c) { int code = c - '0'; if ((code < 0) || (code > 9)) { switch (c) { case '*': return 10; case '#': return 11; case 'A': return 12; case 'B': return 13; case 'C': return 14; case 'D': return 15; default: throw new IllegalArgumentException( "invalid DTMF char: " + (int) c); } } return code; } @Override protected void setState(State newState) { if (mState != newState) { if (SC_DBG) log("setState: cur state" + mState + " --> " + newState + ": " + this + ": on phone " + getPhone() + " " + mConnections.size()); if (newState == Call.State.ALERTING) { mState = newState; // need in ALERTING to enable ringback startRingbackTone(); } else if (mState == Call.State.ALERTING) { stopRingbackTone(); } mState = newState; updatePhoneState(); notifyPreciseCallStateChanged(); } } void onConnectionStateChanged(SipConnection conn) { // this can be called back when a conf call is formed if (SC_DBG) log("onConnectionStateChanged: conn=" + conn); if (mState != State.ACTIVE) { setState(conn.getState()); } } void onConnectionEnded(SipConnection conn) { // set state to DISCONNECTED only when all conns are disconnected if (SC_DBG) log("onConnectionEnded: conn=" + conn); if (mState != State.DISCONNECTED) { boolean allConnectionsDisconnected = true; if (SC_DBG) log("---check connections: " + mConnections.size()); for (Connection c : mConnections) { if (SC_DBG) log(" state=" + c.getState() + ": " + c); if (c.getState() != State.DISCONNECTED) { allConnectionsDisconnected = false; break; } } if (allConnectionsDisconnected) setState(State.DISCONNECTED); } notifyDisconnectP(conn); } private AudioGroup getAudioGroup() { if (mConnections.isEmpty()) return null; return ((SipConnection) mConnections.get(0)).getAudioGroup(); } private void log(String s) { Rlog.d(SC_TAG, s); } } private class SipConnection extends SipConnectionBase { private static final String SCN_TAG = "SipConnection"; private static final boolean SCN_DBG = true; private SipCall mOwner; private SipAudioCall mSipAudioCall; private Call.State mState = Call.State.IDLE; private SipProfile mPeer; private boolean mIncoming = false; private String mOriginalNumber; // may be a PSTN number private SipAudioCallAdapter mAdapter = new SipAudioCallAdapter() { @Override protected void onCallEnded(int cause) { if (getDisconnectCause() != DisconnectCause.LOCAL) { setDisconnectCause(cause); } synchronized (SipPhone.class) { setState(Call.State.DISCONNECTED); SipAudioCall sipAudioCall = mSipAudioCall; // FIXME: This goes null and is synchronized, but many uses aren't sync'd mSipAudioCall = null; String sessionState = (sipAudioCall == null) ? "" : (sipAudioCall.getState() + ", "); if (SCN_DBG) log("[SipAudioCallAdapter] onCallEnded: " + hidePii(mPeer.getUriString()) + ": " + sessionState + "cause: " + getDisconnectCause() + ", on phone " + getPhone()); if (sipAudioCall != null) { sipAudioCall.setListener(null); sipAudioCall.close(); } mOwner.onConnectionEnded(SipConnection.this); } } @Override public void onCallEstablished(SipAudioCall call) { onChanged(call); // Race onChanged synchronized this isn't if (mState == Call.State.ACTIVE) call.startAudio(); } @Override public void onCallHeld(SipAudioCall call) { onChanged(call); // Race onChanged synchronized this isn't if (mState == Call.State.HOLDING) call.startAudio(); } @Override public void onChanged(SipAudioCall call) { synchronized (SipPhone.class) { Call.State newState = getCallStateFrom(call); if (mState == newState) return; if (newState == Call.State.INCOMING) { setState(mOwner.getState()); // INCOMING or WAITING } else { if (mOwner == mRingingCall) { if (mRingingCall.getState() == Call.State.WAITING) { try { switchHoldingAndActive(); } catch (CallStateException e) { // disconnect the call. onCallEnded(DisconnectCause.LOCAL); return; } } mForegroundCall.switchWith(mRingingCall); } setState(newState); } mOwner.onConnectionStateChanged(SipConnection.this); if (SCN_DBG) { log("onChanged: " + hidePii(mPeer.getUriString()) + ": " + mState + " on phone " + getPhone()); } } } @Override protected void onError(int cause) { if (SCN_DBG) log("onError: " + cause); onCallEnded(cause); } }; public SipConnection(SipCall owner, SipProfile callee, String originalNumber) { super(originalNumber); mOwner = owner; mPeer = callee; mOriginalNumber = originalNumber; } public SipConnection(SipCall owner, SipProfile callee) { this(owner, callee, getUriString(callee)); } @Override public String getCnapName() { String displayName = mPeer.getDisplayName(); return TextUtils.isEmpty(displayName) ? null : displayName; } @Override public int getNumberPresentation() { return PhoneConstants.PRESENTATION_ALLOWED; } void initIncomingCall(SipAudioCall sipAudioCall, Call.State newState) { setState(newState); mSipAudioCall = sipAudioCall; sipAudioCall.setListener(mAdapter); // call back to set state mIncoming = true; } void acceptCall() throws CallStateException { try { mSipAudioCall.answerCall(TIMEOUT_ANSWER_CALL); } catch (SipException e) { throw new CallStateException("acceptCall(): " + e); } } void changeOwner(SipCall owner) { mOwner = owner; } AudioGroup getAudioGroup() { if (mSipAudioCall == null) return null; return mSipAudioCall.getAudioGroup(); } void dial() throws SipException { setState(Call.State.DIALING); mSipAudioCall = mSipManager.makeAudioCall(mProfile, mPeer, null, TIMEOUT_MAKE_CALL); mSipAudioCall.setListener(mAdapter); } void hold() throws CallStateException { setState(Call.State.HOLDING); try { mSipAudioCall.holdCall(TIMEOUT_HOLD_CALL); } catch (SipException e) { throw new CallStateException("hold(): " + e); } } void unhold(AudioGroup audioGroup) throws CallStateException { mSipAudioCall.setAudioGroup(audioGroup); setState(Call.State.ACTIVE); try { mSipAudioCall.continueCall(TIMEOUT_HOLD_CALL); } catch (SipException e) { throw new CallStateException("unhold(): " + e); } } void setMute(boolean muted) { if ((mSipAudioCall != null) && (muted != mSipAudioCall.isMuted())) { if (SCN_DBG) log("setState: prev muted=" + !muted + " new muted=" + muted); mSipAudioCall.toggleMute(); } } boolean getMute() { return (mSipAudioCall == null) ? false : mSipAudioCall.isMuted(); } @Override protected void setState(Call.State state) { if (state == mState) return; super.setState(state); mState = state; } @Override public Call.State getState() { return mState; } @Override public boolean isIncoming() { return mIncoming; } @Override public String getAddress() { // Phone app uses this to query caller ID. Return the original dial // number (which may be a PSTN number) instead of the peer's SIP // URI. return mOriginalNumber; } @Override public SipCall getCall() { return mOwner; } @Override protected Phone getPhone() { return mOwner.getPhone(); } @Override public void hangup() throws CallStateException { synchronized (SipPhone.class) { if (SCN_DBG) { log("hangup: conn=" + hidePii(mPeer.getUriString()) + ": " + mState + ": on phone " + getPhone().getPhoneName()); } if (!mState.isAlive()) return; try { SipAudioCall sipAudioCall = mSipAudioCall; if (sipAudioCall != null) { sipAudioCall.setListener(null); sipAudioCall.endCall(); } } catch (SipException e) { throw new CallStateException("hangup(): " + e); } finally { mAdapter.onCallEnded(((mState == Call.State.INCOMING) || (mState == Call.State.WAITING)) ? DisconnectCause.INCOMING_REJECTED : DisconnectCause.LOCAL); } } } @Override public void separate() throws CallStateException { synchronized (SipPhone.class) { SipCall call = (getPhone() == SipPhone.this) ? (SipCall) getBackgroundCall() : (SipCall) getForegroundCall(); if (call.getState() != Call.State.IDLE) { throw new CallStateException( "cannot put conn back to a call in non-idle state: " + call.getState()); } if (SCN_DBG) log("separate: conn=" + mPeer.getUriString() + " from " + mOwner + " back to " + call); // separate the AudioGroup and connection from the original call Phone originalPhone = getPhone(); AudioGroup audioGroup = call.getAudioGroup(); // may be null call.add(this); mSipAudioCall.setAudioGroup(audioGroup); // put the original call to bg; and the separated call becomes // fg if it was in bg originalPhone.switchHoldingAndActive(); // start audio and notify the phone app of the state change call = (SipCall) getForegroundCall(); mSipAudioCall.startAudio(); call.onConnectionStateChanged(this); } } private void log(String s) { Rlog.d(SCN_TAG, s); } } private abstract class SipAudioCallAdapter extends SipAudioCall.Listener { private static final String SACA_TAG = "SipAudioCallAdapter"; private static final boolean SACA_DBG = true; /** Call ended with cause defined in {@link DisconnectCause}. */ protected abstract void onCallEnded(int cause); /** Call failed with cause defined in {@link DisconnectCause}. */ protected abstract void onError(int cause); @Override public void onCallEnded(SipAudioCall call) { if (SACA_DBG) log("onCallEnded: call=" + call); onCallEnded(call.isInCall() ? DisconnectCause.NORMAL : DisconnectCause.INCOMING_MISSED); } @Override public void onCallBusy(SipAudioCall call) { if (SACA_DBG) log("onCallBusy: call=" + call); onCallEnded(DisconnectCause.BUSY); } @Override public void onError(SipAudioCall call, int errorCode, String errorMessage) { if (SACA_DBG) { log("onError: call=" + call + " code="+ SipErrorCode.toString(errorCode) + ": " + errorMessage); } switch (errorCode) { case SipErrorCode.SERVER_UNREACHABLE: onError(DisconnectCause.SERVER_UNREACHABLE); break; case SipErrorCode.PEER_NOT_REACHABLE: onError(DisconnectCause.NUMBER_UNREACHABLE); break; case SipErrorCode.INVALID_REMOTE_URI: onError(DisconnectCause.INVALID_NUMBER); break; case SipErrorCode.TIME_OUT: case SipErrorCode.TRANSACTION_TERMINTED: onError(DisconnectCause.TIMED_OUT); break; case SipErrorCode.DATA_CONNECTION_LOST: onError(DisconnectCause.LOST_SIGNAL); break; case SipErrorCode.INVALID_CREDENTIALS: onError(DisconnectCause.INVALID_CREDENTIALS); break; case SipErrorCode.CROSS_DOMAIN_AUTHENTICATION: onError(DisconnectCause.OUT_OF_NETWORK); break; case SipErrorCode.SERVER_ERROR: onError(DisconnectCause.SERVER_ERROR); break; case SipErrorCode.SOCKET_ERROR: case SipErrorCode.CLIENT_ERROR: default: onError(DisconnectCause.ERROR_UNSPECIFIED); } } private void log(String s) { Rlog.d(SACA_TAG, s); } } public static String hidePii(String s) { return VDBG ? Rlog.pii(LOG_TAG, s) : "xxxxx"; } }