/* * 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.server.sip; import gov.nist.javax.sip.clientauthutils.AccountManager; import gov.nist.javax.sip.clientauthutils.UserCredentials; import gov.nist.javax.sip.header.ProxyAuthenticate; import gov.nist.javax.sip.header.ReferTo; import gov.nist.javax.sip.header.SIPHeaderNames; import gov.nist.javax.sip.header.StatusLine; import gov.nist.javax.sip.header.WWWAuthenticate; import gov.nist.javax.sip.header.extensions.ReferredByHeader; import gov.nist.javax.sip.header.extensions.ReplacesHeader; import gov.nist.javax.sip.message.SIPMessage; import gov.nist.javax.sip.message.SIPResponse; import android.net.sip.ISipSession; import android.net.sip.ISipSessionListener; import android.net.sip.SipErrorCode; import android.net.sip.SipProfile; import android.net.sip.SipSession; import android.net.sip.SipSessionAdapter; import android.text.TextUtils; import android.telephony.Rlog; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.DatagramSocket; import java.net.InetAddress; import java.net.UnknownHostException; import java.text.ParseException; import java.util.EventObject; import java.util.HashMap; import java.util.Map; import java.util.Properties; import javax.sip.ClientTransaction; import javax.sip.Dialog; import javax.sip.DialogTerminatedEvent; import javax.sip.IOExceptionEvent; import javax.sip.ObjectInUseException; import javax.sip.RequestEvent; import javax.sip.ResponseEvent; import javax.sip.ServerTransaction; import javax.sip.SipException; import javax.sip.SipFactory; import javax.sip.SipListener; import javax.sip.SipProvider; import javax.sip.SipStack; import javax.sip.TimeoutEvent; import javax.sip.Transaction; import javax.sip.TransactionTerminatedEvent; import javax.sip.address.Address; import javax.sip.address.SipURI; import javax.sip.header.CSeqHeader; import javax.sip.header.ContactHeader; import javax.sip.header.ExpiresHeader; import javax.sip.header.FromHeader; import javax.sip.header.HeaderAddress; import javax.sip.header.MinExpiresHeader; import javax.sip.header.ReferToHeader; import javax.sip.header.ViaHeader; import javax.sip.message.Message; import javax.sip.message.Request; import javax.sip.message.Response; /** * Manages {@link ISipSession}'s for a SIP account. */ class SipSessionGroup implements SipListener { private static final String TAG = "SipSession"; private static final boolean DBG = false; private static final boolean DBG_PING = false; private static final String ANONYMOUS = "anonymous"; // Limit the size of thread pool to 1 for the order issue when the phone is // waken up from sleep and there are many packets to be processed in the SIP // stack. Note: The default thread pool size in NIST SIP stack is -1 which is // unlimited. private static final String THREAD_POOL_SIZE = "1"; private static final int EXPIRY_TIME = 3600; // in seconds private static final int CANCEL_CALL_TIMER = 3; // in seconds private static final int END_CALL_TIMER = 3; // in seconds private static final int KEEPALIVE_TIMEOUT = 5; // in seconds private static final int INCALL_KEEPALIVE_INTERVAL = 10; // in seconds private static final long WAKE_LOCK_HOLDING_TIME = 500; // in milliseconds private static final EventObject DEREGISTER = new EventObject("Deregister"); private static final EventObject END_CALL = new EventObject("End call"); private final SipProfile mLocalProfile; private final String mPassword; private SipStack mSipStack; private SipHelper mSipHelper; // session that processes INVITE requests private SipSessionImpl mCallReceiverSession; private String mLocalIp; private SipWakeupTimer mWakeupTimer; private SipWakeLock mWakeLock; // call-id-to-SipSession map private Map mSessionMap = new HashMap(); // external address observed from any response private String mExternalIp; private int mExternalPort; /** * @param profile the local profile with password crossed out * @param password the password of the profile * @throws SipException if cannot assign requested address */ public SipSessionGroup(SipProfile profile, String password, SipWakeupTimer timer, SipWakeLock wakeLock) throws SipException { mLocalProfile = profile; mPassword = password; mWakeupTimer = timer; mWakeLock = wakeLock; reset(); } // TODO: remove this method once SipWakeupTimer can better handle variety // of timeout values void setWakeupTimer(SipWakeupTimer timer) { mWakeupTimer = timer; } synchronized void reset() throws SipException { Properties properties = new Properties(); String protocol = mLocalProfile.getProtocol(); int port = mLocalProfile.getPort(); String server = mLocalProfile.getProxyAddress(); if (!TextUtils.isEmpty(server)) { properties.setProperty("javax.sip.OUTBOUND_PROXY", server + ':' + port + '/' + protocol); } else { server = mLocalProfile.getSipDomain(); } if (server.startsWith("[") && server.endsWith("]")) { server = server.substring(1, server.length() - 1); } String local = null; try { for (InetAddress remote : InetAddress.getAllByName(server)) { DatagramSocket socket = new DatagramSocket(); socket.connect(remote, port); if (socket.isConnected()) { local = socket.getLocalAddress().getHostAddress(); port = socket.getLocalPort(); socket.close(); break; } socket.close(); } } catch (Exception e) { // ignore. } if (local == null) { // We are unable to reach the server. Just bail out. return; } close(); mLocalIp = local; properties.setProperty("javax.sip.STACK_NAME", getStackName()); properties.setProperty( "gov.nist.javax.sip.THREAD_POOL_SIZE", THREAD_POOL_SIZE); mSipStack = SipFactory.getInstance().createSipStack(properties); try { SipProvider provider = mSipStack.createSipProvider( mSipStack.createListeningPoint(local, port, protocol)); provider.addSipListener(this); mSipHelper = new SipHelper(mSipStack, provider); } catch (SipException e) { throw e; } catch (Exception e) { throw new SipException("failed to initialize SIP stack", e); } if (DBG) log("reset: start stack for " + mLocalProfile.getUriString()); mSipStack.start(); } synchronized void onConnectivityChanged() { SipSessionImpl[] ss = mSessionMap.values().toArray( new SipSessionImpl[mSessionMap.size()]); // Iterate on the copied array instead of directly on mSessionMap to // avoid ConcurrentModificationException being thrown when // SipSessionImpl removes itself from mSessionMap in onError() in the // following loop. for (SipSessionImpl s : ss) { s.onError(SipErrorCode.DATA_CONNECTION_LOST, "data connection lost"); } } synchronized void resetExternalAddress() { if (DBG) { log("resetExternalAddress: " + mSipStack); } mExternalIp = null; mExternalPort = 0; } public SipProfile getLocalProfile() { return mLocalProfile; } public String getLocalProfileUri() { return mLocalProfile.getUriString(); } private String getStackName() { return "stack" + System.currentTimeMillis(); } public synchronized void close() { if (DBG) log("close: " + SipService.obfuscateSipUri(mLocalProfile.getUriString())); onConnectivityChanged(); mSessionMap.clear(); closeToNotReceiveCalls(); if (mSipStack != null) { mSipStack.stop(); mSipStack = null; mSipHelper = null; } resetExternalAddress(); } public synchronized boolean isClosed() { return (mSipStack == null); } // For internal use, require listener not to block in callbacks. public synchronized void openToReceiveCalls(ISipSessionListener listener) { if (mCallReceiverSession == null) { mCallReceiverSession = new SipSessionCallReceiverImpl(listener); } else { mCallReceiverSession.setListener(listener); } } public synchronized void closeToNotReceiveCalls() { mCallReceiverSession = null; } public ISipSession createSession(ISipSessionListener listener) { return (isClosed() ? null : new SipSessionImpl(listener)); } synchronized boolean containsSession(String callId) { return mSessionMap.containsKey(callId); } private synchronized SipSessionImpl getSipSession(EventObject event) { String key = SipHelper.getCallId(event); SipSessionImpl session = mSessionMap.get(key); if ((session != null) && isLoggable(session)) { if (DBG) log("getSipSession: event=" + key); if (DBG) log("getSipSession: active sessions:"); for (String k : mSessionMap.keySet()) { if (DBG) log("getSipSession: ..." + k + ": " + mSessionMap.get(k)); } } return ((session != null) ? session : mCallReceiverSession); } private synchronized void addSipSession(SipSessionImpl newSession) { removeSipSession(newSession); String key = newSession.getCallId(); mSessionMap.put(key, newSession); if (isLoggable(newSession)) { if (DBG) log("addSipSession: key='" + key + "'"); for (String k : mSessionMap.keySet()) { if (DBG) log("addSipSession: " + k + ": " + mSessionMap.get(k)); } } } private synchronized void removeSipSession(SipSessionImpl session) { if (session == mCallReceiverSession) return; String key = session.getCallId(); SipSessionImpl s = mSessionMap.remove(key); // sanity check if ((s != null) && (s != session)) { if (DBG) log("removeSession: " + session + " is not associated with key '" + key + "'"); mSessionMap.put(key, s); for (Map.Entry entry : mSessionMap.entrySet()) { if (entry.getValue() == s) { key = entry.getKey(); mSessionMap.remove(key); } } } if ((s != null) && isLoggable(s)) { if (DBG) log("removeSession: " + session + " @key '" + key + "'"); for (String k : mSessionMap.keySet()) { if (DBG) log("removeSession: " + k + ": " + mSessionMap.get(k)); } } } @Override public void processRequest(final RequestEvent event) { if (isRequestEvent(Request.INVITE, event)) { if (DBG) log("processRequest: mWakeLock.acquire got INVITE, thread:" + Thread.currentThread()); // Acquire a wake lock and keep it for WAKE_LOCK_HOLDING_TIME; // should be large enough to bring up the app. mWakeLock.acquire(WAKE_LOCK_HOLDING_TIME); } process(event); } @Override public void processResponse(ResponseEvent event) { process(event); } @Override public void processIOException(IOExceptionEvent event) { process(event); } @Override public void processTimeout(TimeoutEvent event) { process(event); } @Override public void processTransactionTerminated(TransactionTerminatedEvent event) { process(event); } @Override public void processDialogTerminated(DialogTerminatedEvent event) { process(event); } private synchronized void process(EventObject event) { SipSessionImpl session = getSipSession(event); try { boolean isLoggable = isLoggable(session, event); boolean processed = (session != null) && session.process(event); if (isLoggable && processed) { log("process: event new state after: " + SipSession.State.toString(session.mState)); } } catch (Throwable e) { loge("process: error event=" + event, getRootCause(e)); session.onError(e); } } private String extractContent(Message message) { // Currently we do not support secure MIME bodies. byte[] bytes = message.getRawContent(); if (bytes != null) { try { if (message instanceof SIPMessage) { return ((SIPMessage) message).getMessageContent(); } else { return new String(bytes, "UTF-8"); } } catch (UnsupportedEncodingException e) { } } return null; } private void extractExternalAddress(ResponseEvent evt) { Response response = evt.getResponse(); ViaHeader viaHeader = (ViaHeader)(response.getHeader( SIPHeaderNames.VIA)); if (viaHeader == null) return; int rport = viaHeader.getRPort(); String externalIp = viaHeader.getReceived(); if ((rport > 0) && (externalIp != null)) { mExternalIp = externalIp; mExternalPort = rport; if (DBG) { log("extractExternalAddress: external addr " + externalIp + ":" + rport + " on " + mSipStack); } } } private Throwable getRootCause(Throwable exception) { Throwable cause = exception.getCause(); while (cause != null) { exception = cause; cause = exception.getCause(); } return exception; } private SipSessionImpl createNewSession(RequestEvent event, ISipSessionListener listener, ServerTransaction transaction, int newState) throws SipException { SipSessionImpl newSession = new SipSessionImpl(listener); newSession.mServerTransaction = transaction; newSession.mState = newState; newSession.mDialog = newSession.mServerTransaction.getDialog(); newSession.mInviteReceived = event; newSession.mPeerProfile = createPeerProfile((HeaderAddress) event.getRequest().getHeader(FromHeader.NAME)); newSession.mPeerSessionDescription = extractContent(event.getRequest()); return newSession; } private class SipSessionCallReceiverImpl extends SipSessionImpl { private static final String SSCRI_TAG = "SipSessionCallReceiverImpl"; private static final boolean SSCRI_DBG = true; public SipSessionCallReceiverImpl(ISipSessionListener listener) { super(listener); } private int processInviteWithReplaces(RequestEvent event, ReplacesHeader replaces) { String callId = replaces.getCallId(); SipSessionImpl session = mSessionMap.get(callId); if (session == null) { return Response.CALL_OR_TRANSACTION_DOES_NOT_EXIST; } Dialog dialog = session.mDialog; if (dialog == null) return Response.DECLINE; if (!dialog.getLocalTag().equals(replaces.getToTag()) || !dialog.getRemoteTag().equals(replaces.getFromTag())) { // No match is found, returns 481. return Response.CALL_OR_TRANSACTION_DOES_NOT_EXIST; } ReferredByHeader referredBy = (ReferredByHeader) event.getRequest() .getHeader(ReferredByHeader.NAME); if ((referredBy == null) || !dialog.getRemoteParty().equals(referredBy.getAddress())) { return Response.CALL_OR_TRANSACTION_DOES_NOT_EXIST; } return Response.OK; } private void processNewInviteRequest(RequestEvent event) throws SipException { ReplacesHeader replaces = (ReplacesHeader) event.getRequest() .getHeader(ReplacesHeader.NAME); SipSessionImpl newSession = null; if (replaces != null) { int response = processInviteWithReplaces(event, replaces); if (SSCRI_DBG) { log("processNewInviteRequest: " + replaces + " response=" + response); } if (response == Response.OK) { SipSessionImpl replacedSession = mSessionMap.get(replaces.getCallId()); // got INVITE w/ replaces request. newSession = createNewSession(event, replacedSession.mProxy.getListener(), mSipHelper.getServerTransaction(event), SipSession.State.INCOMING_CALL); newSession.mProxy.onCallTransferring(newSession, newSession.mPeerSessionDescription); } else { mSipHelper.sendResponse(event, response); } } else { // New Incoming call. newSession = createNewSession(event, mProxy, mSipHelper.sendRinging(event, generateTag()), SipSession.State.INCOMING_CALL); mProxy.onRinging(newSession, newSession.mPeerProfile, newSession.mPeerSessionDescription); } if (newSession != null) addSipSession(newSession); } @Override public boolean process(EventObject evt) throws SipException { if (isLoggable(this, evt)) log("process: " + this + ": " + SipSession.State.toString(mState) + ": processing " + logEvt(evt)); if (isRequestEvent(Request.INVITE, evt)) { processNewInviteRequest((RequestEvent) evt); return true; } else if (isRequestEvent(Request.OPTIONS, evt)) { mSipHelper.sendResponse((RequestEvent) evt, Response.OK); return true; } else { return false; } } private void log(String s) { Rlog.d(SSCRI_TAG, s); } } static interface KeepAliveProcessCallback { /** Invoked when the response of keeping alive comes back. */ void onResponse(boolean portChanged); void onError(int errorCode, String description); } class SipSessionImpl extends ISipSession.Stub { private static final String SSI_TAG = "SipSessionImpl"; private static final boolean SSI_DBG = true; SipProfile mPeerProfile; SipSessionListenerProxy mProxy = new SipSessionListenerProxy(); int mState = SipSession.State.READY_TO_CALL; RequestEvent mInviteReceived; Dialog mDialog; ServerTransaction mServerTransaction; ClientTransaction mClientTransaction; String mPeerSessionDescription; boolean mInCall; SessionTimer mSessionTimer; int mAuthenticationRetryCount; private SipKeepAlive mSipKeepAlive; private SipSessionImpl mSipSessionImpl; // the following three members are used for handling refer request. SipSessionImpl mReferSession; ReferredByHeader mReferredBy; String mReplaces; // lightweight timer class SessionTimer { private boolean mRunning = true; void start(final int timeout) { new Thread(new Runnable() { @Override public void run() { sleep(timeout); if (mRunning) timeout(); } }, "SipSessionTimerThread").start(); } synchronized void cancel() { mRunning = false; this.notify(); } private void timeout() { synchronized (SipSessionGroup.this) { onError(SipErrorCode.TIME_OUT, "Session timed out!"); } } private synchronized void sleep(int timeout) { try { this.wait(timeout * 1000); } catch (InterruptedException e) { loge("session timer interrupted!", e); } } } public SipSessionImpl(ISipSessionListener listener) { setListener(listener); } SipSessionImpl duplicate() { return new SipSessionImpl(mProxy.getListener()); } private void reset() { mInCall = false; removeSipSession(this); mPeerProfile = null; mState = SipSession.State.READY_TO_CALL; mInviteReceived = null; mPeerSessionDescription = null; mAuthenticationRetryCount = 0; mReferSession = null; mReferredBy = null; mReplaces = null; if (mDialog != null) mDialog.delete(); mDialog = null; try { if (mServerTransaction != null) mServerTransaction.terminate(); } catch (ObjectInUseException e) { // ignored } mServerTransaction = null; try { if (mClientTransaction != null) mClientTransaction.terminate(); } catch (ObjectInUseException e) { // ignored } mClientTransaction = null; cancelSessionTimer(); if (mSipSessionImpl != null) { mSipSessionImpl.stopKeepAliveProcess(); mSipSessionImpl = null; } } @Override public boolean isInCall() { return mInCall; } @Override public String getLocalIp() { return mLocalIp; } @Override public SipProfile getLocalProfile() { return mLocalProfile; } @Override public SipProfile getPeerProfile() { return mPeerProfile; } @Override public String getCallId() { return SipHelper.getCallId(getTransaction()); } private Transaction getTransaction() { if (mClientTransaction != null) return mClientTransaction; if (mServerTransaction != null) return mServerTransaction; return null; } @Override public int getState() { return mState; } @Override public void setListener(ISipSessionListener listener) { mProxy.setListener((listener instanceof SipSessionListenerProxy) ? ((SipSessionListenerProxy) listener).getListener() : listener); } // process the command in a new thread private void doCommandAsync(final EventObject command) { new Thread(new Runnable() { @Override public void run() { try { processCommand(command); } catch (Throwable e) { loge("command error: " + command + ": " + mLocalProfile.getUriString(), getRootCause(e)); onError(e); } } }, "SipSessionAsyncCmdThread").start(); } @Override public void makeCall(SipProfile peerProfile, String sessionDescription, int timeout) { doCommandAsync(new MakeCallCommand(peerProfile, sessionDescription, timeout)); } @Override public void answerCall(String sessionDescription, int timeout) { synchronized (SipSessionGroup.this) { if (mPeerProfile == null) return; doCommandAsync(new MakeCallCommand(mPeerProfile, sessionDescription, timeout)); } } @Override public void endCall() { doCommandAsync(END_CALL); } @Override public void changeCall(String sessionDescription, int timeout) { synchronized (SipSessionGroup.this) { if (mPeerProfile == null) return; doCommandAsync(new MakeCallCommand(mPeerProfile, sessionDescription, timeout)); } } @Override public void register(int duration) { doCommandAsync(new RegisterCommand(duration)); } @Override public void unregister() { doCommandAsync(DEREGISTER); } private void processCommand(EventObject command) throws SipException { if (isLoggable(command)) log("process cmd: " + command); if (!process(command)) { onError(SipErrorCode.IN_PROGRESS, "cannot initiate a new transaction to execute: " + command); } } protected String generateTag() { // 32-bit randomness return String.valueOf((long) (Math.random() * 0x100000000L)); } @Override public String toString() { try { String s = super.toString(); return s.substring(s.indexOf("@")) + ":" + SipSession.State.toString(mState); } catch (Throwable e) { return super.toString(); } } public boolean process(EventObject evt) throws SipException { if (isLoggable(this, evt)) log(" ~~~~~ " + this + ": " + SipSession.State.toString(mState) + ": processing " + logEvt(evt)); synchronized (SipSessionGroup.this) { if (isClosed()) return false; if (mSipKeepAlive != null) { // event consumed by keepalive process if (mSipKeepAlive.process(evt)) return true; } Dialog dialog = null; if (evt instanceof RequestEvent) { dialog = ((RequestEvent) evt).getDialog(); } else if (evt instanceof ResponseEvent) { dialog = ((ResponseEvent) evt).getDialog(); extractExternalAddress((ResponseEvent) evt); } if (dialog != null) mDialog = dialog; boolean processed; switch (mState) { case SipSession.State.REGISTERING: case SipSession.State.DEREGISTERING: processed = registeringToReady(evt); break; case SipSession.State.READY_TO_CALL: processed = readyForCall(evt); break; case SipSession.State.INCOMING_CALL: processed = incomingCall(evt); break; case SipSession.State.INCOMING_CALL_ANSWERING: processed = incomingCallToInCall(evt); break; case SipSession.State.OUTGOING_CALL: case SipSession.State.OUTGOING_CALL_RING_BACK: processed = outgoingCall(evt); break; case SipSession.State.OUTGOING_CALL_CANCELING: processed = outgoingCallToReady(evt); break; case SipSession.State.IN_CALL: processed = inCall(evt); break; case SipSession.State.ENDING_CALL: processed = endingCall(evt); break; default: processed = false; } return (processed || processExceptions(evt)); } } private boolean processExceptions(EventObject evt) throws SipException { if (isRequestEvent(Request.BYE, evt)) { // terminate the call whenever a BYE is received mSipHelper.sendResponse((RequestEvent) evt, Response.OK); endCallNormally(); return true; } else if (isRequestEvent(Request.CANCEL, evt)) { mSipHelper.sendResponse((RequestEvent) evt, Response.CALL_OR_TRANSACTION_DOES_NOT_EXIST); return true; } else if (evt instanceof TransactionTerminatedEvent) { if (isCurrentTransaction((TransactionTerminatedEvent) evt)) { if (evt instanceof TimeoutEvent) { processTimeout((TimeoutEvent) evt); } else { processTransactionTerminated( (TransactionTerminatedEvent) evt); } return true; } } else if (isRequestEvent(Request.OPTIONS, evt)) { mSipHelper.sendResponse((RequestEvent) evt, Response.OK); return true; } else if (evt instanceof DialogTerminatedEvent) { processDialogTerminated((DialogTerminatedEvent) evt); return true; } return false; } private void processDialogTerminated(DialogTerminatedEvent event) { if (mDialog == event.getDialog()) { onError(new SipException("dialog terminated")); } else { if (SSI_DBG) log("not the current dialog; current=" + mDialog + ", terminated=" + event.getDialog()); } } private boolean isCurrentTransaction(TransactionTerminatedEvent event) { Transaction current = event.isServerTransaction() ? mServerTransaction : mClientTransaction; Transaction target = event.isServerTransaction() ? event.getServerTransaction() : event.getClientTransaction(); if ((current != target) && (mState != SipSession.State.PINGING)) { if (SSI_DBG) log("not the current transaction; current=" + toString(current) + ", target=" + toString(target)); return false; } else if (current != null) { if (SSI_DBG) log("transaction terminated: " + toString(current)); return true; } else { // no transaction; shouldn't be here; ignored return true; } } private String toString(Transaction transaction) { if (transaction == null) return "null"; Request request = transaction.getRequest(); Dialog dialog = transaction.getDialog(); CSeqHeader cseq = (CSeqHeader) request.getHeader(CSeqHeader.NAME); return String.format("req=%s,%s,s=%s,ds=%s,", request.getMethod(), cseq.getSeqNumber(), transaction.getState(), ((dialog == null) ? "-" : dialog.getState())); } private void processTransactionTerminated( TransactionTerminatedEvent event) { switch (mState) { case SipSession.State.IN_CALL: case SipSession.State.READY_TO_CALL: if (SSI_DBG) log("Transaction terminated; do nothing"); break; default: if (SSI_DBG) log("Transaction terminated early: " + this); onError(SipErrorCode.TRANSACTION_TERMINTED, "transaction terminated"); } } private void processTimeout(TimeoutEvent event) { if (SSI_DBG) log("processing Timeout..."); switch (mState) { case SipSession.State.REGISTERING: case SipSession.State.DEREGISTERING: reset(); mProxy.onRegistrationTimeout(this); break; case SipSession.State.INCOMING_CALL: case SipSession.State.INCOMING_CALL_ANSWERING: case SipSession.State.OUTGOING_CALL: case SipSession.State.OUTGOING_CALL_CANCELING: onError(SipErrorCode.TIME_OUT, event.toString()); break; default: if (SSI_DBG) log(" do nothing"); break; } } private int getExpiryTime(Response response) { int time = -1; ContactHeader contact = (ContactHeader) response.getHeader(ContactHeader.NAME); if (contact != null) { time = contact.getExpires(); } ExpiresHeader expires = (ExpiresHeader) response.getHeader(ExpiresHeader.NAME); if (expires != null && (time < 0 || time > expires.getExpires())) { time = expires.getExpires(); } if (time <= 0) { time = EXPIRY_TIME; } expires = (ExpiresHeader) response.getHeader(MinExpiresHeader.NAME); if (expires != null && time < expires.getExpires()) { time = expires.getExpires(); } if (SSI_DBG) { log("Expiry time = " + time); } return time; } private boolean registeringToReady(EventObject evt) throws SipException { if (expectResponse(Request.REGISTER, evt)) { ResponseEvent event = (ResponseEvent) evt; Response response = event.getResponse(); int statusCode = response.getStatusCode(); switch (statusCode) { case Response.OK: int state = mState; onRegistrationDone((state == SipSession.State.REGISTERING) ? getExpiryTime(((ResponseEvent) evt).getResponse()) : -1); return true; case Response.UNAUTHORIZED: case Response.PROXY_AUTHENTICATION_REQUIRED: handleAuthentication(event); return true; default: if (statusCode >= 500) { onRegistrationFailed(response); return true; } } } return false; } private boolean handleAuthentication(ResponseEvent event) throws SipException { Response response = event.getResponse(); String nonce = getNonceFromResponse(response); if (nonce == null) { onError(SipErrorCode.SERVER_ERROR, "server does not provide challenge"); return false; } else if (mAuthenticationRetryCount < 2) { mClientTransaction = mSipHelper.handleChallenge( event, getAccountManager()); mDialog = mClientTransaction.getDialog(); mAuthenticationRetryCount++; if (isLoggable(this, event)) { if (SSI_DBG) log(" authentication retry count=" + mAuthenticationRetryCount); } return true; } else { if (crossDomainAuthenticationRequired(response)) { onError(SipErrorCode.CROSS_DOMAIN_AUTHENTICATION, getRealmFromResponse(response)); } else { onError(SipErrorCode.INVALID_CREDENTIALS, "incorrect username or password"); } return false; } } private boolean crossDomainAuthenticationRequired(Response response) { String realm = getRealmFromResponse(response); if (realm == null) realm = ""; return !mLocalProfile.getSipDomain().trim().equals(realm.trim()); } private AccountManager getAccountManager() { return new AccountManager() { @Override public UserCredentials getCredentials(ClientTransaction challengedTransaction, String realm) { return new UserCredentials() { @Override public String getUserName() { String username = mLocalProfile.getAuthUserName(); return (!TextUtils.isEmpty(username) ? username : mLocalProfile.getUserName()); } @Override public String getPassword() { return mPassword; } @Override public String getSipDomain() { return mLocalProfile.getSipDomain(); } }; } }; } private String getRealmFromResponse(Response response) { WWWAuthenticate wwwAuth = (WWWAuthenticate)response.getHeader( SIPHeaderNames.WWW_AUTHENTICATE); if (wwwAuth != null) return wwwAuth.getRealm(); ProxyAuthenticate proxyAuth = (ProxyAuthenticate)response.getHeader( SIPHeaderNames.PROXY_AUTHENTICATE); return (proxyAuth == null) ? null : proxyAuth.getRealm(); } private String getNonceFromResponse(Response response) { WWWAuthenticate wwwAuth = (WWWAuthenticate)response.getHeader( SIPHeaderNames.WWW_AUTHENTICATE); if (wwwAuth != null) return wwwAuth.getNonce(); ProxyAuthenticate proxyAuth = (ProxyAuthenticate)response.getHeader( SIPHeaderNames.PROXY_AUTHENTICATE); return (proxyAuth == null) ? null : proxyAuth.getNonce(); } private String getResponseString(int statusCode) { StatusLine statusLine = new StatusLine(); statusLine.setStatusCode(statusCode); statusLine.setReasonPhrase(SIPResponse.getReasonPhrase(statusCode)); return statusLine.encode(); } private boolean readyForCall(EventObject evt) throws SipException { // expect MakeCallCommand, RegisterCommand, DEREGISTER if (evt instanceof MakeCallCommand) { mState = SipSession.State.OUTGOING_CALL; MakeCallCommand cmd = (MakeCallCommand) evt; mPeerProfile = cmd.getPeerProfile(); if (mReferSession != null) { mSipHelper.sendReferNotify(mReferSession.mDialog, getResponseString(Response.TRYING)); } mClientTransaction = mSipHelper.sendInvite( mLocalProfile, mPeerProfile, cmd.getSessionDescription(), generateTag(), mReferredBy, mReplaces); mDialog = mClientTransaction.getDialog(); addSipSession(this); startSessionTimer(cmd.getTimeout()); mProxy.onCalling(this); return true; } else if (evt instanceof RegisterCommand) { mState = SipSession.State.REGISTERING; int duration = ((RegisterCommand) evt).getDuration(); mClientTransaction = mSipHelper.sendRegister(mLocalProfile, generateTag(), duration); mDialog = mClientTransaction.getDialog(); addSipSession(this); mProxy.onRegistering(this); return true; } else if (DEREGISTER == evt) { mState = SipSession.State.DEREGISTERING; mClientTransaction = mSipHelper.sendRegister(mLocalProfile, generateTag(), 0); mDialog = mClientTransaction.getDialog(); addSipSession(this); mProxy.onRegistering(this); return true; } return false; } private boolean incomingCall(EventObject evt) throws SipException { // expect MakeCallCommand(answering) , END_CALL cmd , Cancel if (evt instanceof MakeCallCommand) { // answer call mState = SipSession.State.INCOMING_CALL_ANSWERING; mServerTransaction = mSipHelper.sendInviteOk(mInviteReceived, mLocalProfile, ((MakeCallCommand) evt).getSessionDescription(), mServerTransaction, mExternalIp, mExternalPort); startSessionTimer(((MakeCallCommand) evt).getTimeout()); return true; } else if (END_CALL == evt) { mSipHelper.sendInviteBusyHere(mInviteReceived, mServerTransaction); endCallNormally(); return true; } else if (isRequestEvent(Request.CANCEL, evt)) { RequestEvent event = (RequestEvent) evt; mSipHelper.sendResponse(event, Response.OK); mSipHelper.sendInviteRequestTerminated( mInviteReceived.getRequest(), mServerTransaction); endCallNormally(); return true; } return false; } private boolean incomingCallToInCall(EventObject evt) { // expect ACK, CANCEL request if (isRequestEvent(Request.ACK, evt)) { String sdp = extractContent(((RequestEvent) evt).getRequest()); if (sdp != null) mPeerSessionDescription = sdp; if (mPeerSessionDescription == null) { onError(SipErrorCode.CLIENT_ERROR, "peer sdp is empty"); } else { establishCall(false); } return true; } else if (isRequestEvent(Request.CANCEL, evt)) { // http://tools.ietf.org/html/rfc3261#section-9.2 // Final response has been sent; do nothing here. return true; } return false; } private boolean outgoingCall(EventObject evt) throws SipException { if (expectResponse(Request.INVITE, evt)) { ResponseEvent event = (ResponseEvent) evt; Response response = event.getResponse(); int statusCode = response.getStatusCode(); switch (statusCode) { case Response.RINGING: case Response.CALL_IS_BEING_FORWARDED: case Response.QUEUED: case Response.SESSION_PROGRESS: // feedback any provisional responses (except TRYING) as // ring back for better UX if (mState == SipSession.State.OUTGOING_CALL) { mState = SipSession.State.OUTGOING_CALL_RING_BACK; cancelSessionTimer(); mProxy.onRingingBack(this); } return true; case Response.OK: if (mReferSession != null) { mSipHelper.sendReferNotify(mReferSession.mDialog, getResponseString(Response.OK)); // since we don't need to remember the session anymore. mReferSession = null; } mSipHelper.sendInviteAck(event, mDialog); mPeerSessionDescription = extractContent(response); establishCall(true); return true; case Response.UNAUTHORIZED: case Response.PROXY_AUTHENTICATION_REQUIRED: if (handleAuthentication(event)) { addSipSession(this); } return true; case Response.REQUEST_PENDING: // TODO: rfc3261#section-14.1; re-schedule invite return true; default: if (mReferSession != null) { mSipHelper.sendReferNotify(mReferSession.mDialog, getResponseString(Response.SERVICE_UNAVAILABLE)); } if (statusCode >= 400) { // error: an ack is sent automatically by the stack onError(response); return true; } else if (statusCode >= 300) { // TODO: handle 3xx (redirect) } else { return true; } } return false; } else if (END_CALL == evt) { // RFC says that UA should not send out cancel when no // response comes back yet. We are cheating for not checking // response. mState = SipSession.State.OUTGOING_CALL_CANCELING; mSipHelper.sendCancel(mClientTransaction); startSessionTimer(CANCEL_CALL_TIMER); return true; } else if (isRequestEvent(Request.INVITE, evt)) { // Call self? Send BUSY HERE so server may redirect the call to // voice mailbox. RequestEvent event = (RequestEvent) evt; mSipHelper.sendInviteBusyHere(event, event.getServerTransaction()); return true; } return false; } private boolean outgoingCallToReady(EventObject evt) throws SipException { if (evt instanceof ResponseEvent) { ResponseEvent event = (ResponseEvent) evt; Response response = event.getResponse(); int statusCode = response.getStatusCode(); if (expectResponse(Request.CANCEL, evt)) { if (statusCode == Response.OK) { // do nothing; wait for REQUEST_TERMINATED return true; } } else if (expectResponse(Request.INVITE, evt)) { switch (statusCode) { case Response.OK: outgoingCall(evt); // abort Cancel return true; case Response.REQUEST_TERMINATED: endCallNormally(); return true; } } else { return false; } if (statusCode >= 400) { onError(response); return true; } } else if (evt instanceof TransactionTerminatedEvent) { // rfc3261#section-14.1: // if re-invite gets timed out, terminate the dialog; but // re-invite is not reliable, just let it go and pretend // nothing happened. onError(new SipException("timed out")); } return false; } private boolean processReferRequest(RequestEvent event) throws SipException { try { ReferToHeader referto = (ReferToHeader) event.getRequest() .getHeader(ReferTo.NAME); Address address = referto.getAddress(); SipURI uri = (SipURI) address.getURI(); String replacesHeader = uri.getHeader(ReplacesHeader.NAME); String username = uri.getUser(); if (username == null) { mSipHelper.sendResponse(event, Response.BAD_REQUEST); return false; } // send notify accepted mSipHelper.sendResponse(event, Response.ACCEPTED); SipSessionImpl newSession = createNewSession(event, this.mProxy.getListener(), mSipHelper.getServerTransaction(event), SipSession.State.READY_TO_CALL); newSession.mReferSession = this; newSession.mReferredBy = (ReferredByHeader) event.getRequest() .getHeader(ReferredByHeader.NAME); newSession.mReplaces = replacesHeader; newSession.mPeerProfile = createPeerProfile(referto); newSession.mProxy.onCallTransferring(newSession, null); return true; } catch (IllegalArgumentException e) { throw new SipException("createPeerProfile()", e); } } private boolean inCall(EventObject evt) throws SipException { // expect END_CALL cmd, BYE request, hold call (MakeCallCommand) // OK retransmission is handled in SipStack if (END_CALL == evt) { // rfc3261#section-15.1.1 mState = SipSession.State.ENDING_CALL; mSipHelper.sendBye(mDialog); mProxy.onCallEnded(this); startSessionTimer(END_CALL_TIMER); return true; } else if (isRequestEvent(Request.INVITE, evt)) { // got Re-INVITE mState = SipSession.State.INCOMING_CALL; RequestEvent event = mInviteReceived = (RequestEvent) evt; mPeerSessionDescription = extractContent(event.getRequest()); mServerTransaction = null; mProxy.onRinging(this, mPeerProfile, mPeerSessionDescription); return true; } else if (isRequestEvent(Request.BYE, evt)) { mSipHelper.sendResponse((RequestEvent) evt, Response.OK); endCallNormally(); return true; } else if (isRequestEvent(Request.REFER, evt)) { return processReferRequest((RequestEvent) evt); } else if (evt instanceof MakeCallCommand) { // to change call mState = SipSession.State.OUTGOING_CALL; mClientTransaction = mSipHelper.sendReinvite(mDialog, ((MakeCallCommand) evt).getSessionDescription()); startSessionTimer(((MakeCallCommand) evt).getTimeout()); return true; } else if (evt instanceof ResponseEvent) { if (expectResponse(Request.NOTIFY, evt)) return true; } return false; } private boolean endingCall(EventObject evt) throws SipException { if (expectResponse(Request.BYE, evt)) { ResponseEvent event = (ResponseEvent) evt; Response response = event.getResponse(); int statusCode = response.getStatusCode(); switch (statusCode) { case Response.UNAUTHORIZED: case Response.PROXY_AUTHENTICATION_REQUIRED: if (handleAuthentication(event)) { return true; } else { // can't authenticate; pass through to end session } } cancelSessionTimer(); reset(); return true; } return false; } // timeout in seconds private void startSessionTimer(int timeout) { if (timeout > 0) { mSessionTimer = new SessionTimer(); mSessionTimer.start(timeout); } } private void cancelSessionTimer() { if (mSessionTimer != null) { mSessionTimer.cancel(); mSessionTimer = null; } } private String createErrorMessage(Response response) { return String.format("%s (%d)", response.getReasonPhrase(), response.getStatusCode()); } private void enableKeepAlive() { if (mSipSessionImpl != null) { mSipSessionImpl.stopKeepAliveProcess(); } else { mSipSessionImpl = duplicate(); } try { mSipSessionImpl.startKeepAliveProcess( INCALL_KEEPALIVE_INTERVAL, mPeerProfile, null); } catch (SipException e) { loge("keepalive cannot be enabled; ignored", e); mSipSessionImpl.stopKeepAliveProcess(); } } private void establishCall(boolean enableKeepAlive) { mState = SipSession.State.IN_CALL; cancelSessionTimer(); if (!mInCall && enableKeepAlive) enableKeepAlive(); mInCall = true; mProxy.onCallEstablished(this, mPeerSessionDescription); } private void endCallNormally() { reset(); mProxy.onCallEnded(this); } private void endCallOnError(int errorCode, String message) { reset(); mProxy.onError(this, errorCode, message); } private void endCallOnBusy() { reset(); mProxy.onCallBusy(this); } private void onError(int errorCode, String message) { cancelSessionTimer(); switch (mState) { case SipSession.State.REGISTERING: case SipSession.State.DEREGISTERING: onRegistrationFailed(errorCode, message); break; default: endCallOnError(errorCode, message); } } private void onError(Throwable exception) { exception = getRootCause(exception); onError(getErrorCode(exception), exception.toString()); } private void onError(Response response) { int statusCode = response.getStatusCode(); if (!mInCall && (statusCode == Response.BUSY_HERE)) { endCallOnBusy(); } else { onError(getErrorCode(statusCode), createErrorMessage(response)); } } private int getErrorCode(int responseStatusCode) { switch (responseStatusCode) { case Response.TEMPORARILY_UNAVAILABLE: case Response.FORBIDDEN: case Response.GONE: case Response.NOT_FOUND: case Response.NOT_ACCEPTABLE: case Response.NOT_ACCEPTABLE_HERE: return SipErrorCode.PEER_NOT_REACHABLE; case Response.REQUEST_URI_TOO_LONG: case Response.ADDRESS_INCOMPLETE: case Response.AMBIGUOUS: return SipErrorCode.INVALID_REMOTE_URI; case Response.REQUEST_TIMEOUT: return SipErrorCode.TIME_OUT; default: if (responseStatusCode < 500) { return SipErrorCode.CLIENT_ERROR; } else { return SipErrorCode.SERVER_ERROR; } } } private int getErrorCode(Throwable exception) { String message = exception.getMessage(); if (exception instanceof UnknownHostException) { return SipErrorCode.SERVER_UNREACHABLE; } else if (exception instanceof IOException) { return SipErrorCode.SOCKET_ERROR; } else { return SipErrorCode.CLIENT_ERROR; } } private void onRegistrationDone(int duration) { reset(); mProxy.onRegistrationDone(this, duration); } private void onRegistrationFailed(int errorCode, String message) { reset(); mProxy.onRegistrationFailed(this, errorCode, message); } private void onRegistrationFailed(Response response) { int statusCode = response.getStatusCode(); onRegistrationFailed(getErrorCode(statusCode), createErrorMessage(response)); } // Notes: SipSessionListener will be replaced by the keepalive process // @param interval in seconds public void startKeepAliveProcess(int interval, KeepAliveProcessCallback callback) throws SipException { synchronized (SipSessionGroup.this) { startKeepAliveProcess(interval, mLocalProfile, callback); } } // Notes: SipSessionListener will be replaced by the keepalive process // @param interval in seconds public void startKeepAliveProcess(int interval, SipProfile peerProfile, KeepAliveProcessCallback callback) throws SipException { synchronized (SipSessionGroup.this) { if (mSipKeepAlive != null) { throw new SipException("Cannot create more than one " + "keepalive process in a SipSession"); } mPeerProfile = peerProfile; mSipKeepAlive = new SipKeepAlive(); mProxy.setListener(mSipKeepAlive); mSipKeepAlive.start(interval, callback); } } public void stopKeepAliveProcess() { synchronized (SipSessionGroup.this) { if (mSipKeepAlive != null) { mSipKeepAlive.stop(); mSipKeepAlive = null; } } } class SipKeepAlive extends SipSessionAdapter implements Runnable { private static final String SKA_TAG = "SipKeepAlive"; private static final boolean SKA_DBG = true; private boolean mRunning = false; private KeepAliveProcessCallback mCallback; private boolean mPortChanged = false; private int mRPort = 0; private int mInterval; // just for debugging // @param interval in seconds void start(int interval, KeepAliveProcessCallback callback) { if (mRunning) return; mRunning = true; mInterval = interval; mCallback = new KeepAliveProcessCallbackProxy(callback); mWakeupTimer.set(interval * 1000, this); if (SKA_DBG) { log("start keepalive:" + mLocalProfile.getUriString()); } // No need to run the first time in a separate thread for now run(); } // return true if the event is consumed boolean process(EventObject evt) { if (mRunning && (mState == SipSession.State.PINGING)) { if (evt instanceof ResponseEvent) { if (parseOptionsResult(evt)) { if (mPortChanged) { resetExternalAddress(); stop(); } else { cancelSessionTimer(); removeSipSession(SipSessionImpl.this); } mCallback.onResponse(mPortChanged); return true; } } } return false; } // SipSessionAdapter // To react to the session timeout event and network error. @Override public void onError(ISipSession session, int errorCode, String message) { stop(); mCallback.onError(errorCode, message); } // SipWakeupTimer timeout handler // To send out keepalive message. @Override public void run() { synchronized (SipSessionGroup.this) { if (!mRunning) return; if (DBG_PING) { String peerUri = (mPeerProfile == null) ? "null" : mPeerProfile.getUriString(); log("keepalive: " + mLocalProfile.getUriString() + " --> " + peerUri + ", interval=" + mInterval); } try { sendKeepAlive(); } catch (Throwable t) { if (SKA_DBG) { loge("keepalive error: " + mLocalProfile.getUriString(), getRootCause(t)); } // It's possible that the keepalive process is being stopped // during session.sendKeepAlive() so need to check mRunning // again here. if (mRunning) SipSessionImpl.this.onError(t); } } } void stop() { synchronized (SipSessionGroup.this) { if (SKA_DBG) { log("stop keepalive:" + mLocalProfile.getUriString() + ",RPort=" + mRPort); } mRunning = false; mWakeupTimer.cancel(this); reset(); } } private void sendKeepAlive() throws SipException { synchronized (SipSessionGroup.this) { mState = SipSession.State.PINGING; mClientTransaction = mSipHelper.sendOptions( mLocalProfile, mPeerProfile, generateTag()); mDialog = mClientTransaction.getDialog(); addSipSession(SipSessionImpl.this); startSessionTimer(KEEPALIVE_TIMEOUT); // when timed out, onError() will be called with SipErrorCode.TIME_OUT } } private boolean parseOptionsResult(EventObject evt) { if (expectResponse(Request.OPTIONS, evt)) { ResponseEvent event = (ResponseEvent) evt; int rPort = getRPortFromResponse(event.getResponse()); if (rPort != -1) { if (mRPort == 0) mRPort = rPort; if (mRPort != rPort) { mPortChanged = true; if (SKA_DBG) log(String.format( "rport is changed: %d <> %d", mRPort, rPort)); mRPort = rPort; } else { if (SKA_DBG) log("rport is the same: " + rPort); } } else { if (SKA_DBG) log("peer did not respond rport"); } return true; } return false; } private int getRPortFromResponse(Response response) { ViaHeader viaHeader = (ViaHeader)(response.getHeader( SIPHeaderNames.VIA)); return (viaHeader == null) ? -1 : viaHeader.getRPort(); } private void log(String s) { Rlog.d(SKA_TAG, s); } } private void log(String s) { Rlog.d(SSI_TAG, s); } } /** * @return true if the event is a request event matching the specified * method; false otherwise */ private static boolean isRequestEvent(String method, EventObject event) { try { if (event instanceof RequestEvent) { RequestEvent requestEvent = (RequestEvent) event; return method.equals(requestEvent.getRequest().getMethod()); } } catch (Throwable e) { } return false; } private static String getCseqMethod(Message message) { return ((CSeqHeader) message.getHeader(CSeqHeader.NAME)).getMethod(); } /** * @return true if the event is a response event and the CSeqHeader method * match the given arguments; false otherwise */ private static boolean expectResponse( String expectedMethod, EventObject evt) { if (evt instanceof ResponseEvent) { ResponseEvent event = (ResponseEvent) evt; Response response = event.getResponse(); return expectedMethod.equalsIgnoreCase(getCseqMethod(response)); } return false; } private static SipProfile createPeerProfile(HeaderAddress header) throws SipException { try { Address address = header.getAddress(); SipURI uri = (SipURI) address.getURI(); String username = uri.getUser(); if (username == null) username = ANONYMOUS; int port = uri.getPort(); SipProfile.Builder builder = new SipProfile.Builder(username, uri.getHost()) .setDisplayName(address.getDisplayName()); if (port > 0) builder.setPort(port); return builder.build(); } catch (IllegalArgumentException e) { throw new SipException("createPeerProfile()", e); } catch (ParseException e) { throw new SipException("createPeerProfile()", e); } } private static boolean isLoggable(SipSessionImpl s) { if (s != null) { switch (s.mState) { case SipSession.State.PINGING: return DBG_PING; } } return DBG; } private static boolean isLoggable(EventObject evt) { return isLoggable(null, evt); } private static boolean isLoggable(SipSessionImpl s, EventObject evt) { if (!isLoggable(s)) return false; if (evt == null) return false; if (evt instanceof ResponseEvent) { Response response = ((ResponseEvent) evt).getResponse(); if (Request.OPTIONS.equals(response.getHeader(CSeqHeader.NAME))) { return DBG_PING; } return DBG; } else if (evt instanceof RequestEvent) { if (isRequestEvent(Request.OPTIONS, evt)) { return DBG_PING; } return DBG; } return false; } private static String logEvt(EventObject evt) { if (evt instanceof RequestEvent) { return ((RequestEvent) evt).getRequest().toString(); } else if (evt instanceof ResponseEvent) { return ((ResponseEvent) evt).getResponse().toString(); } else { return evt.toString(); } } private class RegisterCommand extends EventObject { private int mDuration; public RegisterCommand(int duration) { super(SipSessionGroup.this); mDuration = duration; } public int getDuration() { return mDuration; } } private class MakeCallCommand extends EventObject { private String mSessionDescription; private int mTimeout; // in seconds public MakeCallCommand(SipProfile peerProfile, String sessionDescription, int timeout) { super(peerProfile); mSessionDescription = sessionDescription; mTimeout = timeout; } public SipProfile getPeerProfile() { return (SipProfile) getSource(); } public String getSessionDescription() { return mSessionDescription; } public int getTimeout() { return mTimeout; } } /** Class to help safely run KeepAliveProcessCallback in a different thread. */ static class KeepAliveProcessCallbackProxy implements KeepAliveProcessCallback { private static final String KAPCP_TAG = "KeepAliveProcessCallbackProxy"; private KeepAliveProcessCallback mCallback; KeepAliveProcessCallbackProxy(KeepAliveProcessCallback callback) { mCallback = callback; } private void proxy(Runnable runnable) { // One thread for each calling back. // Note: Guarantee ordering if the issue becomes important. Currently, // the chance of handling two callback events at a time is none. new Thread(runnable, "SIP-KeepAliveProcessCallbackThread").start(); } @Override public void onResponse(final boolean portChanged) { if (mCallback == null) return; proxy(new Runnable() { @Override public void run() { try { mCallback.onResponse(portChanged); } catch (Throwable t) { loge("onResponse", t); } } }); } @Override public void onError(final int errorCode, final String description) { if (mCallback == null) return; proxy(new Runnable() { @Override public void run() { try { mCallback.onError(errorCode, description); } catch (Throwable t) { loge("onError", t); } } }); } private void loge(String s, Throwable t) { Rlog.e(KAPCP_TAG, s, t); } } private void log(String s) { Rlog.d(TAG, s); } private void loge(String s, Throwable t) { Rlog.e(TAG, s, t); } }