/** * Copyright (C) 2010-2012 Regis Montoya (aka r3gis - www.r3gis.fr) * Copyright (C) 2010 Chris McCormick (aka mccormix - chris@mccormick.cx) * This file is part of CSipSimple. * * CSipSimple is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * If you own a pjsip commercial license you can also redistribute it * and/or modify it under the terms of the GNU Lesser General Public License * as an android library. * * CSipSimple is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with CSipSimple. If not, see <http://www.gnu.org/licenses/>. */ package com.csipsimple.pjsip; import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; import android.os.Message; import android.os.PowerManager; import android.os.PowerManager.WakeLock; import android.os.SystemClock; import android.provider.CallLog; import android.provider.CallLog.Calls; import android.telephony.TelephonyManager; import android.text.TextUtils; import android.util.SparseArray; import com.csipsimple.R; import com.csipsimple.api.SipCallSession; import com.csipsimple.api.SipCallSession.StatusCode; import com.csipsimple.api.SipConfigManager; import com.csipsimple.api.SipManager; import com.csipsimple.api.SipManager.PresenceStatus; import com.csipsimple.api.SipMessage; import com.csipsimple.api.SipProfile; import com.csipsimple.api.SipUri; import com.csipsimple.api.SipUri.ParsedSipContactInfos; import com.csipsimple.service.MediaManager; import com.csipsimple.service.SipNotifications; import com.csipsimple.service.SipService; import com.csipsimple.service.SipService.SameThreadException; import com.csipsimple.service.SipService.SipRunnable; import com.csipsimple.service.impl.SipCallSessionImpl; import com.csipsimple.utils.CallLogHelper; import com.csipsimple.utils.Compatibility; import com.csipsimple.utils.Log; import com.csipsimple.utils.Threading; import com.csipsimple.utils.TimerWrapper; import org.pjsip.pjsua.Callback; import org.pjsip.pjsua.SWIGTYPE_p_int; import org.pjsip.pjsua.SWIGTYPE_p_pjsip_rx_data; import org.pjsip.pjsua.pj_str_t; import org.pjsip.pjsua.pj_stun_nat_detect_result; import org.pjsip.pjsua.pjsip_event; import org.pjsip.pjsua.pjsip_redirect_op; import org.pjsip.pjsua.pjsip_status_code; import org.pjsip.pjsua.pjsua; import org.pjsip.pjsua.pjsua_buddy_info; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; public class UAStateReceiver extends Callback { private final static String THIS_FILE = "SIP UA Receiver"; private final static String ACTION_PHONE_STATE_CHANGED = "android.intent.action.PHONE_STATE"; private SipNotifications notificationManager; private PjSipService pjService; // private ComponentName remoteControlResponder; // Time in ms during which we should not relaunch call activity again final static long LAUNCH_TRIGGER_DELAY = 2000; private long lastLaunchCallHandler = 0; private int eventLockCount = 0; private boolean mIntegrateWithCallLogs; private boolean mPlayWaittone; private int mPreferedHeadsetAction; private boolean mAutoRecordCalls; private int mMicroSource; private void lockCpu() { if (eventLock != null) { Log.d(THIS_FILE, "< LOCK CPU"); eventLock.acquire(); eventLockCount++; } } private void unlockCpu() { if (eventLock != null && eventLock.isHeld()) { eventLock.release(); eventLockCount--; Log.d(THIS_FILE, "> UNLOCK CPU " + eventLockCount); } } /* * private class IncomingCallInfos { public SipCallSession callInfo; public * Integer accId; } */ @Override public void on_incoming_call(final int accId, final int callId, SWIGTYPE_p_pjsip_rx_data rdata) { lockCpu(); // Check if we have not already an ongoing call boolean hasOngoingSipCall = false; if (pjService != null && pjService.service != null) { SipCallSessionImpl[] calls = getCalls(); if (calls != null) { for (SipCallSessionImpl existingCall : calls) { if (!existingCall.isAfterEnded() && existingCall.getCallId() != callId) { if (!pjService.service.supportMultipleCalls) { Log.e(THIS_FILE, "Settings to not support two call at the same time !!!"); // If there is an ongoing call and we do not support // multiple calls // Send busy here pjsua.call_hangup(callId, StatusCode.BUSY_HERE, null, null); unlockCpu(); return; } else { hasOngoingSipCall = true; } } } } } try { SipCallSessionImpl callInfo = updateCallInfoFromStack(callId, null); Log.d(THIS_FILE, "Incoming call << for account " + accId); // Extra check if set reference counted is false ??? if (!ongoingCallLock.isHeld()) { ongoingCallLock.acquire(); } final String remContact = callInfo.getRemoteContact(); callInfo.setIncoming(true); notificationManager.showNotificationForCall(callInfo); // Auto answer feature SipProfile acc = pjService.getAccountForPjsipId(accId); Bundle extraHdr = new Bundle(); fillRDataHeader("Call-Info", rdata, extraHdr); final int shouldAutoAnswer = pjService.service.shouldAutoAnswer(remContact, acc, extraHdr); Log.d(THIS_FILE, "Should I anto answer ? " + shouldAutoAnswer); if (shouldAutoAnswer >= 200) { // Automatically answer incoming calls with 200 or higher final // code pjService.callAnswer(callId, shouldAutoAnswer); } else { // Ring and inform remote about ringing with 180/RINGING pjService.callAnswer(callId, 180); if (pjService.mediaManager != null) { if (pjService.service.getGSMCallState() == TelephonyManager.CALL_STATE_IDLE && !hasOngoingSipCall) { pjService.mediaManager.startRing(remContact); } else { pjService.mediaManager.playInCallTone(MediaManager.TONE_CALL_WAITING); } } broadCastAndroidCallState("RINGING", remContact); } if (shouldAutoAnswer < 300) { // Or by api launchCallHandler(callInfo); Log.d(THIS_FILE, "Incoming call >>"); } } catch (SameThreadException e) { // That's fine we are in a pjsip thread } finally { unlockCpu(); } } @Override public void on_call_state(final int callId, pjsip_event e) { pjsua.css_on_call_state(callId, e); lockCpu(); Log.d(THIS_FILE, "Call state <<"); try { // Get current infos now on same thread cause fix has been done on // pj final SipCallSession callInfo = updateCallInfoFromStack(callId, e); int callState = callInfo.getCallState(); // If disconnected immediate stop required stuffs if (callState == SipCallSession.InvState.DISCONNECTED) { if (pjService.mediaManager != null) { if(getRingingCall() == null) { pjService.mediaManager.stopRingAndUnfocus(); pjService.mediaManager.resetSettings(); } } if (ongoingCallLock != null && ongoingCallLock.isHeld()) { ongoingCallLock.release(); } // Call is now ended pjService.stopDialtoneGenerator(callId); pjService.stopRecording(callId); pjService.stopPlaying(callId); pjService.stopWaittoneGenerator(callId); } else { if (ongoingCallLock != null && !ongoingCallLock.isHeld()) { ongoingCallLock.acquire(); } } msgHandler.sendMessage(msgHandler.obtainMessage(ON_CALL_STATE, callInfo)); Log.d(THIS_FILE, "Call state >>"); } catch (SameThreadException ex) { // We don't care about that we are at least in a pjsua thread } finally { // Unlock CPU anyway unlockCpu(); } } @Override public void on_call_tsx_state(int call_id, org.pjsip.pjsua.SWIGTYPE_p_pjsip_transaction tsx, pjsip_event e) { lockCpu(); Log.d(THIS_FILE, "Call TSX state <<"); try { updateCallInfoFromStack(call_id, e); Log.d(THIS_FILE, "Call TSX state >>"); } catch (SameThreadException ex) { // We don't care about that we are at least in a pjsua thread } finally { // Unlock CPU anyway unlockCpu(); } } @Override public void on_buddy_state(int buddyId) { lockCpu(); pjsua_buddy_info binfo = new pjsua_buddy_info(); pjsua.buddy_get_info(buddyId, binfo); Log.d(THIS_FILE, "On buddy " + buddyId + " state " + binfo.getMonitor_pres() + " state " + PjSipService.pjStrToString(binfo.getStatus_text())); PresenceStatus presStatus = PresenceStatus.UNKNOWN; // First get info from basic status String presStatusTxt = PjSipService.pjStrToString(binfo.getStatus_text()); boolean isDefaultTxt = presStatusTxt.equalsIgnoreCase("Online") || presStatusTxt.equalsIgnoreCase("Offline"); switch (binfo.getStatus()) { case PJSUA_BUDDY_STATUS_ONLINE: presStatus = PresenceStatus.ONLINE; break; case PJSUA_BUDDY_STATUS_OFFLINE: presStatus = PresenceStatus.OFFLINE; break; case PJSUA_BUDDY_STATUS_UNKNOWN: default: presStatus = PresenceStatus.UNKNOWN; break; } // Now get infos from RPID switch (binfo.getRpid().getActivity()) { case PJRPID_ACTIVITY_AWAY: presStatus = PresenceStatus.AWAY; if (isDefaultTxt) { presStatusTxt = ""; } break; case PJRPID_ACTIVITY_BUSY: presStatus = PresenceStatus.BUSY; if (isDefaultTxt) { presStatusTxt = ""; } break; default: break; } // pjService.service.presenceMgr.changeBuddyState(PjSipService.pjStrToString(binfo.getUri()), // binfo.getMonitor_pres(), presStatus, presStatusTxt); unlockCpu(); } @Override public void on_pager(int callId, pj_str_t from, pj_str_t to, pj_str_t contact, pj_str_t mime_type, pj_str_t body) { lockCpu(); long date = System.currentTimeMillis(); String fromStr = PjSipService.pjStrToString(from); String canonicFromStr = SipUri.getCanonicalSipContact(fromStr); String contactStr = PjSipService.pjStrToString(contact); String toStr = PjSipService.pjStrToString(to); String bodyStr = PjSipService.pjStrToString(body); String mimeStr = PjSipService.pjStrToString(mime_type); // Sanitize from sip uri int slashIndex = fromStr.indexOf("/"); if (slashIndex != -1){ fromStr = fromStr.substring(0, slashIndex); } SipMessage msg = new SipMessage(canonicFromStr, toStr, contactStr, bodyStr, mimeStr, date, SipMessage.MESSAGE_TYPE_INBOX, fromStr); // Insert the message to the DB ContentResolver cr = pjService.service.getContentResolver(); cr.insert(SipMessage.MESSAGE_URI, msg.getContentValues()); // Broadcast the message Intent intent = new Intent(SipManager.ACTION_SIP_MESSAGE_RECEIVED); // TODO : could be parcelable ! intent.putExtra(SipMessage.FIELD_FROM, msg.getFrom()); intent.putExtra(SipMessage.FIELD_BODY, msg.getBody()); pjService.service.sendBroadcast(intent, SipManager.PERMISSION_USE_SIP); // Notify android os of the new message notificationManager.showNotificationForMessage(msg); unlockCpu(); } @Override public void on_pager_status(int callId, pj_str_t to, pj_str_t body, pjsip_status_code status, pj_str_t reason) { lockCpu(); // TODO : treat error / acknowledge of messages int messageType = (status.equals(pjsip_status_code.PJSIP_SC_OK) || status.equals(pjsip_status_code.PJSIP_SC_ACCEPTED)) ? SipMessage.MESSAGE_TYPE_SENT : SipMessage.MESSAGE_TYPE_FAILED; String toStr = SipUri.getCanonicalSipContact(PjSipService.pjStrToString(to)); String reasonStr = PjSipService.pjStrToString(reason); String bodyStr = PjSipService.pjStrToString(body); int statusInt = status.swigValue(); Log.d(THIS_FILE, "SipMessage in on pager status " + status.toString() + " / " + reasonStr); // Update the db ContentResolver cr = pjService.service.getContentResolver(); ContentValues args = new ContentValues(); args.put(SipMessage.FIELD_TYPE, messageType); args.put(SipMessage.FIELD_STATUS, statusInt); if (statusInt != SipCallSession.StatusCode.OK && statusInt != SipCallSession.StatusCode.ACCEPTED) { args.put(SipMessage.FIELD_BODY, bodyStr + " // " + reasonStr); } cr.update(SipMessage.MESSAGE_URI, args, SipMessage.FIELD_TO + "=? AND " + SipMessage.FIELD_BODY + "=? AND " + SipMessage.FIELD_TYPE + "=" + SipMessage.MESSAGE_TYPE_QUEUED, new String[] { toStr, bodyStr }); // Broadcast the information Intent intent = new Intent(SipManager.ACTION_SIP_MESSAGE_RECEIVED); intent.putExtra(SipMessage.FIELD_FROM, toStr); pjService.service.sendBroadcast(intent, SipManager.PERMISSION_USE_SIP); unlockCpu(); } @Override public void on_reg_state(final int accountId) { lockCpu(); pjService.service.getExecutor().execute(new SipRunnable() { @Override public void doRun() throws SameThreadException { // Update java infos pjService.updateProfileStateFromService(accountId); } }); unlockCpu(); } @Override public void on_call_media_state(final int callId) { pjsua.css_on_call_media_state(callId); lockCpu(); if (pjService.mediaManager != null) { // Do not unfocus here since we are probably in call. // Unfocus will be done anyway on call disconnect pjService.mediaManager.stopRing(); } try { final SipCallSession callInfo = updateCallInfoFromStack(callId, null); /* * Connect ports appropriately when media status is ACTIVE or REMOTE * HOLD, otherwise we should NOT connect the ports. */ boolean connectToOtherCalls = false; int callConfSlot = callInfo.getConfPort(); int mediaStatus = callInfo.getMediaStatus(); if (mediaStatus == SipCallSession.MediaState.ACTIVE || mediaStatus == SipCallSession.MediaState.REMOTE_HOLD) { connectToOtherCalls = true; pjsua.conf_connect(callConfSlot, 0); pjsua.conf_connect(0, callConfSlot); // Adjust software volume if (pjService.mediaManager != null) { pjService.mediaManager.setSoftwareVolume(); } // Auto record if (mAutoRecordCalls && pjService.canRecord(callId) && !pjService.isRecording(callId)) { pjService .startRecording(callId, SipManager.BITMASK_IN | SipManager.BITMASK_OUT); } } // Connects/disconnnect to other active calls (for conferencing). boolean hasOtherCall = false; synchronized (callsList) { if (callsList != null) { for (int i = 0; i < callsList.size(); i++) { SipCallSessionImpl otherCallInfo = getCallInfo(i); if (otherCallInfo != null && otherCallInfo != callInfo) { int otherMediaStatus = otherCallInfo.getMediaStatus(); if(otherCallInfo.isActive() && otherMediaStatus != SipCallSession.MediaState.NONE) { hasOtherCall = true; boolean connect = connectToOtherCalls && (otherMediaStatus == SipCallSession.MediaState.ACTIVE || otherMediaStatus == SipCallSession.MediaState.REMOTE_HOLD); int otherCallConfSlot = otherCallInfo.getConfPort(); if(connect) { pjsua.conf_connect(callConfSlot, otherCallConfSlot); pjsua.conf_connect(otherCallConfSlot, callConfSlot); }else { pjsua.conf_disconnect(callConfSlot, otherCallConfSlot); pjsua.conf_disconnect(otherCallConfSlot, callConfSlot); } } } } } } // Play wait tone if(mPlayWaittone) { if(mediaStatus == SipCallSession.MediaState.REMOTE_HOLD && !hasOtherCall) { pjService.startWaittoneGenerator(callId); }else { pjService.stopWaittoneGenerator(callId); } } msgHandler.sendMessage(msgHandler.obtainMessage(ON_MEDIA_STATE, callInfo)); } catch (SameThreadException e) { // Nothing to do we are in a pj thread here } unlockCpu(); } @Override public void on_mwi_info(int acc_id, pj_str_t mime_type, pj_str_t body) { lockCpu(); // Treat incoming voice mail notification. String msg = PjSipService.pjStrToString(body); // Log.d(THIS_FILE, "We have a message :: " + acc_id + " | " + // mime_type.getPtr() + " | " + body.getPtr()); boolean hasMessage = false; boolean hasSomeNumberOfMessage = false; int numberOfMessages = 0; // String accountNbr = ""; String lines[] = msg.split("\\r?\\n"); // Decapsulate the application/simple-message-summary // TODO : should we check mime-type? // rfc3842 Pattern messWaitingPattern = Pattern.compile(".*Messages-Waiting[ \t]?:[ \t]?(yes|no).*", Pattern.CASE_INSENSITIVE); // Pattern messAccountPattern = // Pattern.compile(".*Message-Account[ \t]?:[ \t]?(.*)", // Pattern.CASE_INSENSITIVE); Pattern messVoiceNbrPattern = Pattern.compile( ".*Voice-Message[ \t]?:[ \t]?([0-9]*)/[0-9]*.*", Pattern.CASE_INSENSITIVE); Pattern messFaxNbrPattern = Pattern.compile( ".*Fax-Message[ \t]?:[ \t]?([0-9]*)/[0-9]*.*", Pattern.CASE_INSENSITIVE); for (String line : lines) { Matcher m; m = messWaitingPattern.matcher(line); if (m.matches()) { Log.w(THIS_FILE, "Matches : " + m.group(1)); if ("yes".equalsIgnoreCase(m.group(1))) { Log.d(THIS_FILE, "Hey there is messages !!! "); hasMessage = true; } continue; } /* * m = messAccountPattern.matcher(line); if(m.matches()) { * accountNbr = m.group(1); Log.d(THIS_FILE, "VM acc : " + * accountNbr); continue; } */ m = messVoiceNbrPattern.matcher(line); if (m.matches()) { try { numberOfMessages = Integer.parseInt(m.group(1)); hasSomeNumberOfMessage = true; } catch (NumberFormatException e) { Log.w(THIS_FILE, "Not well formated number " + m.group(1)); } Log.d(THIS_FILE, "Nbr : " + numberOfMessages); continue; } if(messFaxNbrPattern.matcher(line).matches()) { hasSomeNumberOfMessage = true; } } if (hasMessage && (numberOfMessages > 0 || !hasSomeNumberOfMessage)) { SipProfile acc = pjService.getAccountForPjsipId(acc_id); if (acc != null) { Log.d(THIS_FILE, acc_id + " -> Has found account " + acc.getDefaultDomain() + " " + acc.id + " >> " + acc.getProfileName()); } Log.d(THIS_FILE, "We can show the voice messages notification"); notificationManager.showNotificationForVoiceMail(acc, numberOfMessages); } unlockCpu(); } /* (non-Javadoc) * @see org.pjsip.pjsua.Callback#on_call_transfer_status(int, int, org.pjsip.pjsua.pj_str_t, int, org.pjsip.pjsua.SWIGTYPE_p_int) */ @Override public void on_call_transfer_status(int callId, int st_code, pj_str_t st_text, int final_, SWIGTYPE_p_int p_cont) { lockCpu(); if((st_code / 100) == 2) { pjsua.call_hangup(callId, 0, null, null); } unlockCpu(); } // public String sasString = ""; // public boolean zrtpOn = false; public int on_validate_audio_clock_rate(int clockRate) { if (pjService != null) { return pjService.validateAudioClockRate(clockRate); } return -1; } @Override public void on_setup_audio(int beforeInit) { if (pjService != null) { pjService.setAudioInCall(beforeInit); } } @Override public void on_teardown_audio() { if (pjService != null) { pjService.unsetAudioInCall(); } } @Override public pjsip_redirect_op on_call_redirected(int call_id, pj_str_t target) { Log.w(THIS_FILE, "Ask for redirection, not yet implemented, for now allow all " + PjSipService.pjStrToString(target)); return pjsip_redirect_op.PJSIP_REDIRECT_ACCEPT; } @Override public void on_nat_detect(pj_stun_nat_detect_result res) { Log.d(THIS_FILE, "NAT TYPE DETECTED !!!" + res.getNat_type_name()); if (pjService != null) { pjService.setDetectedNatType(res.getNat_type_name(), res.getStatus()); } } @Override public int on_set_micro_source() { return mMicroSource; } @Override public int timer_schedule(int entry, int entryId, int time) { return TimerWrapper.schedule(entry, entryId, time); } @Override public int timer_cancel(int entry, int entryId) { return TimerWrapper.cancel(entry, entryId); } /** * Map callId to known {@link SipCallSession}. This is cache of known * session maintained by the UA state receiver. The UA state receiver is in * charge to maintain calls list integrity for {@link PjSipService}. All * information it gets comes from the stack. Except recording status that * comes from service. */ private SparseArray<SipCallSessionImpl> callsList = new SparseArray<SipCallSessionImpl>(); /** * Update the call information from pjsip stack by calling pjsip primitives. * * @param callId The id to the call to update * @param e the pjsip_even that raised the update request * @return The built sip call session. It's also stored in cache. * @throws SameThreadException if we are calling that from outside the pjsip * thread. It's a virtual exception to make sure not called from * bad place. */ private SipCallSessionImpl updateCallInfoFromStack(Integer callId, pjsip_event e) throws SameThreadException { SipCallSessionImpl callInfo; Log.d(THIS_FILE, "Updating call infos from the stack"); synchronized (callsList) { callInfo = callsList.get(callId); if (callInfo == null) { callInfo = new SipCallSessionImpl(); callInfo.setCallId(callId); } } // We update session infos. callInfo is both in/out and will be updated PjSipCalls.updateSessionFromPj(callInfo, e, pjService.service); // We update from our current recording state callInfo.setIsRecording(pjService.isRecording(callId)); callInfo.setCanRecord(pjService.canRecord(callId)); synchronized (callsList) { // Re-add to list mainly for case newly added session callsList.put(callId, callInfo); } return callInfo; } /** * Get call info for a given call id. * * @param callId the id of the call we want infos for * @return the call session infos. */ public SipCallSessionImpl getCallInfo(Integer callId) { SipCallSessionImpl callInfo; synchronized (callsList) { callInfo = callsList.get(callId, null); } return callInfo; } /** * Get list of calls session available. * * @return List of calls. */ public SipCallSessionImpl[] getCalls() { if (callsList != null) { List<SipCallSessionImpl> calls = new ArrayList<SipCallSessionImpl>(); synchronized (callsList) { for (int i = 0; i < callsList.size(); i++) { SipCallSessionImpl callInfo = getCallInfo(i); if (callInfo != null) { calls.add(callInfo); } } } return calls.toArray(new SipCallSessionImpl[calls.size()]); } return new SipCallSessionImpl[0]; } private WorkerHandler msgHandler; private HandlerThread handlerThread; private WakeLock ongoingCallLock; private WakeLock eventLock; // private static final int ON_INCOMING_CALL = 1; private static final int ON_CALL_STATE = 2; private static final int ON_MEDIA_STATE = 3; // private static final int ON_REGISTRATION_STATE = 4; // private static final int ON_PAGER = 5; private static class WorkerHandler extends Handler { WeakReference<UAStateReceiver> sr; public WorkerHandler(Looper looper, UAStateReceiver stateReceiver) { super(looper); Log.d(THIS_FILE, "Create async worker !!!"); sr = new WeakReference<UAStateReceiver>(stateReceiver); } public void handleMessage(Message msg) { UAStateReceiver stateReceiver = sr.get(); if (stateReceiver == null) { return; } stateReceiver.lockCpu(); switch (msg.what) { case ON_CALL_STATE: { SipCallSessionImpl callInfo = (SipCallSessionImpl) msg.obj; final int callState = callInfo.getCallState(); switch (callState) { case SipCallSession.InvState.INCOMING: case SipCallSession.InvState.CALLING: stateReceiver.notificationManager.showNotificationForCall(callInfo); stateReceiver.launchCallHandler(callInfo); stateReceiver.broadCastAndroidCallState("RINGING", callInfo.getRemoteContact()); break; case SipCallSession.InvState.EARLY: case SipCallSession.InvState.CONNECTING: case SipCallSession.InvState.CONFIRMED: // As per issue #857 we should re-ensure // notification + callHandler at each state // cause we can miss some states due to the fact // treatment of call state is threaded // Anyway if we miss the call early + confirmed we // do not need to show the UI. stateReceiver.notificationManager.showNotificationForCall(callInfo); stateReceiver.launchCallHandler(callInfo); stateReceiver.broadCastAndroidCallState("OFFHOOK", callInfo.getRemoteContact()); if (stateReceiver.pjService.mediaManager != null) { if (callState == SipCallSession.InvState.CONFIRMED) { // Don't unfocus here stateReceiver.pjService.mediaManager.stopRing(); } } // Auto send pending dtmf if (callState == SipCallSession.InvState.CONFIRMED) { stateReceiver.sendPendingDtmf(callInfo.getCallId()); } // If state is confirmed and not already intialized if (callState == SipCallSession.InvState.CONFIRMED && callInfo.getCallStart() == 0) { callInfo.setCallStart(System.currentTimeMillis()); } break; case SipCallSession.InvState.DISCONNECTED: if (stateReceiver.pjService.mediaManager != null && stateReceiver.getRingingCall() == null) { stateReceiver.pjService.mediaManager.stopRing(); } stateReceiver.broadCastAndroidCallState("IDLE", callInfo.getRemoteContact()); // If no remaining calls, cancel the notification if (stateReceiver.getActiveCallInProgress() == null) { stateReceiver.notificationManager.cancelCalls(); // We should now ask parent to stop if needed if (stateReceiver.pjService != null && stateReceiver.pjService.service != null) { stateReceiver.pjService.service .treatDeferUnregistersForOutgoing(); } } // CallLog ContentValues cv = CallLogHelper.logValuesForCall( stateReceiver.pjService.service, callInfo, callInfo.getCallStart()); // Fill our own database stateReceiver.pjService.service.getContentResolver().insert( SipManager.CALLLOG_URI, cv); Integer isNew = cv.getAsInteger(CallLog.Calls.NEW); if (isNew != null && isNew == 1) { stateReceiver.notificationManager.showNotificationForMissedCall(cv); } // If the call goes out in error... if (callInfo.getLastStatusCode() != 200 && callInfo.getLastReasonCode() != 200) { // We notify the user with toaster stateReceiver.pjService.service.notifyUserOfMessage(callInfo .getLastStatusCode() + " / " + callInfo.getLastStatusComment()); } // If needed fill native database if (stateReceiver.mIntegrateWithCallLogs) { // Don't add with new flag cv.put(CallLog.Calls.NEW, false); // Remove csipsimple custom entries cv.remove(SipManager.CALLLOG_PROFILE_ID_FIELD); cv.remove(SipManager.CALLLOG_STATUS_CODE_FIELD); cv.remove(SipManager.CALLLOG_STATUS_TEXT_FIELD); // Reformat number for callogs ParsedSipContactInfos callerInfos = SipUri.parseSipContact(cv .getAsString(Calls.NUMBER)); if (callerInfos != null) { String phoneNumber = SipUri.getPhoneNumber(callerInfos); // Only log numbers that can be called by // GSM too. // TODO : if android 2.3 add sip uri also if (!TextUtils.isEmpty(phoneNumber)) { cv.put(Calls.NUMBER, phoneNumber); // For log in call logs => don't add as // new calls... we manage it ourselves. cv.put(Calls.NEW, false); ContentValues extraCv = new ContentValues(); if (callInfo.getAccId() != SipProfile.INVALID_ID) { SipProfile acc = stateReceiver.pjService.service .getAccount(callInfo.getAccId()); if (acc != null && acc.display_name != null) { extraCv.put(CallLogHelper.EXTRA_SIP_PROVIDER, acc.display_name); } } CallLogHelper.addCallLog(stateReceiver.pjService.service, cv, extraCv); } } } callInfo.applyDisconnect(); break; default: break; } stateReceiver.onBroadcastCallState(callInfo); break; } case ON_MEDIA_STATE: { SipCallSession mediaCallInfo = (SipCallSession) msg.obj; SipCallSessionImpl callInfo = stateReceiver.callsList.get(mediaCallInfo .getCallId()); callInfo.setMediaStatus(mediaCallInfo.getMediaStatus()); stateReceiver.callsList.put(mediaCallInfo.getCallId(), callInfo); stateReceiver.onBroadcastCallState(callInfo); break; } } stateReceiver.unlockCpu(); } }; // ------- // Public configuration for receiver // ------- public void initService(PjSipService srv) { pjService = srv; notificationManager = pjService.service.notificationManager; if (handlerThread == null) { handlerThread = new HandlerThread("UAStateAsyncWorker"); handlerThread.start(); } if (msgHandler == null) { msgHandler = new WorkerHandler(handlerThread.getLooper(), this); } if (eventLock == null) { PowerManager pman = (PowerManager) pjService.service .getSystemService(Context.POWER_SERVICE); eventLock = pman.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "com.csipsimple.inEventLock"); eventLock.setReferenceCounted(true); } if (ongoingCallLock == null) { PowerManager pman = (PowerManager) pjService.service .getSystemService(Context.POWER_SERVICE); ongoingCallLock = pman.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "com.csipsimple.ongoingCallLock"); ongoingCallLock.setReferenceCounted(false); } } public void stopService() { Threading.stopHandlerThread(handlerThread, true); handlerThread = null; msgHandler = null; // Ensure lock is released since this lock is a ref counted one. if (eventLock != null) { while (eventLock.isHeld()) { eventLock.release(); } } if (ongoingCallLock != null) { if (ongoingCallLock.isHeld()) { ongoingCallLock.release(); } } } public void reconfigure(Context ctxt) { mIntegrateWithCallLogs = SipConfigManager.getPreferenceBooleanValue(ctxt, SipConfigManager.INTEGRATE_WITH_CALLLOGS); mPreferedHeadsetAction = SipConfigManager.getPreferenceIntegerValue(ctxt, SipConfigManager.HEADSET_ACTION, SipConfigManager.HEADSET_ACTION_CLEAR_CALL); mAutoRecordCalls = SipConfigManager.getPreferenceBooleanValue(ctxt, SipConfigManager.AUTO_RECORD_CALLS); mMicroSource = SipConfigManager.getPreferenceIntegerValue(ctxt, SipConfigManager.MICRO_SOURCE); mPlayWaittone = SipConfigManager.getPreferenceBooleanValue(ctxt, SipConfigManager.PLAY_WAITTONE_ON_HOLD, false); } // -------- // Private methods // -------- /** * Broadcast csipsimple intent about the fact we are currently have a sip * call state change.<br/> * This may be used by third party applications that wants to track * csipsimple call state * * @param callInfo the new call state infos */ private void onBroadcastCallState(final SipCallSession callInfo) { SipCallSession publicCallInfo = new SipCallSession(callInfo); Intent callStateChangedIntent = new Intent(SipManager.ACTION_SIP_CALL_CHANGED); callStateChangedIntent.putExtra(SipManager.EXTRA_CALL_INFO, publicCallInfo); pjService.service.sendBroadcast(callStateChangedIntent, SipManager.PERMISSION_USE_SIP); } /** * Broadcast to android system that we currently have a phone call. This may * be managed by other sip apps that want to keep track of incoming calls * for example. * * @param state The state of the call * @param number The corresponding remote number */ private void broadCastAndroidCallState(String state, String number) { // Android normalized event if(!Compatibility.isCompatible(19)) { // Not allowed to do that from kitkat Intent intent = new Intent(ACTION_PHONE_STATE_CHANGED); intent.putExtra(TelephonyManager.EXTRA_STATE, state); if (number != null) { intent.putExtra(TelephonyManager.EXTRA_INCOMING_NUMBER, number); } intent.putExtra(pjService.service.getString(R.string.app_name), true); pjService.service.sendBroadcast(intent, android.Manifest.permission.READ_PHONE_STATE); } } /** * Start the call activity for a given Sip Call Session. <br/> * The call activity should take care to get any ongoing calls when started * so the currentCallInfo2 parameter is indication only. <br/> * This method ensure that the start of the activity is not fired too much * in short delay and may just ignore requests if last time someone ask for * a launch is too recent * * @param currentCallInfo2 the call info that raise this request to open the * call handler activity */ private synchronized void launchCallHandler(SipCallSession currentCallInfo2) { long currentElapsedTime = SystemClock.elapsedRealtime(); // Synchronized ensure we do not get this launched several time // We also ensure that a minimum delay has been consumed so that we do // not fire this too much times // Specially for EARLY - CONNECTING states if (lastLaunchCallHandler + LAUNCH_TRIGGER_DELAY < currentElapsedTime) { Context ctxt = pjService.service; // Launch activity to choose what to do with this call Intent callHandlerIntent = SipService.buildCallUiIntent(ctxt, currentCallInfo2); Log.d(THIS_FILE, "Anounce call activity"); ctxt.startActivity(callHandlerIntent); lastLaunchCallHandler = currentElapsedTime; } else { Log.d(THIS_FILE, "Ignore extra launch handler"); } } /** * Check if any of call infos indicate there is an active call in progress. * * @see SipCallSession#isActive() */ public SipCallSession getActiveCallInProgress() { // Go through the whole list of calls and find the first active state. synchronized (callsList) { for (int i = 0; i < callsList.size(); i++) { SipCallSession callInfo = getCallInfo(i); if (callInfo != null && callInfo.isActive()) { return callInfo; } } } return null; } /** * Check if any of call infos indicate there is an active call in progress. * * @see SipCallSession#isActive() */ public SipCallSession getActiveCallOngoing() { // Go through the whole list of calls and find the first active state. synchronized (callsList) { for (int i = 0; i < callsList.size(); i++) { SipCallSession callInfo = getCallInfo(i); if (callInfo != null && callInfo.isActive() && callInfo.isOngoing()) { return callInfo; } } } return null; } public SipCallSession getRingingCall() { // Go through the whole list of calls and find the first ringing state. synchronized (callsList) { for (int i = 0; i < callsList.size(); i++) { SipCallSession callInfo = getCallInfo(i); if (callInfo != null && callInfo.isActive() && callInfo.isBeforeConfirmed() && callInfo.isIncoming()) { return callInfo; } } } return null; } /** * Broadcast the Headset button press event internally if there is any call * in progress. TODO : register and unregister only while in call */ public boolean handleHeadsetButton() { final SipCallSession callInfo = getActiveCallInProgress(); if (callInfo != null) { // Headset button has been pressed by user. If there is an // incoming call ringing the button will be used to answer the // call. If there is an ongoing call in progress the button will // be used to hangup the call or mute the microphone. int state = callInfo.getCallState(); if (callInfo.isIncoming() && (state == SipCallSession.InvState.INCOMING || state == SipCallSession.InvState.EARLY)) { if (pjService != null && pjService.service != null) { pjService.service.getExecutor().execute(new SipRunnable() { @Override protected void doRun() throws SameThreadException { pjService.callAnswer(callInfo.getCallId(), pjsip_status_code.PJSIP_SC_OK.swigValue()); } }); } return true; } else if (state == SipCallSession.InvState.INCOMING || state == SipCallSession.InvState.EARLY || state == SipCallSession.InvState.CALLING || state == SipCallSession.InvState.CONFIRMED || state == SipCallSession.InvState.CONNECTING) { // // In the Android phone app using the media button during // a call mutes the microphone instead of terminating the call. // We check here if this should be the behavior here or if // the call should be cleared. // if (pjService != null && pjService.service != null) { pjService.service.getExecutor().execute(new SipRunnable() { @Override protected void doRun() throws SameThreadException { if (mPreferedHeadsetAction == SipConfigManager.HEADSET_ACTION_CLEAR_CALL) { pjService.callHangup(callInfo.getCallId(), 0); } else if (mPreferedHeadsetAction == SipConfigManager.HEADSET_ACTION_HOLD) { pjService.callHold(callInfo.getCallId()); } else if (mPreferedHeadsetAction == SipConfigManager.HEADSET_ACTION_MUTE) { pjService.mediaManager.toggleMute(); } } }); } return true; } } return false; } /** * Update status of call recording info in call session info * * @param callId The call id to modify * @param canRecord if we can now record the call * @param isRecording if we are currently recording the call */ public void updateRecordingStatus(int callId, boolean canRecord, boolean isRecording) { SipCallSessionImpl callInfo = getCallInfo(callId); callInfo.setCanRecord(canRecord); callInfo.setIsRecording(isRecording); synchronized (callsList) { // Re-add it just to be sure callsList.put(callId, callInfo); } onBroadcastCallState(callInfo); } private void sendPendingDtmf(final int callId) { pjService.service.getExecutor().execute(new SipRunnable() { @Override protected void doRun() throws SameThreadException { pjService.sendPendingDtmf(callId); } }); } private void fillRDataHeader(String hdrName, SWIGTYPE_p_pjsip_rx_data rdata, Bundle out) throws SameThreadException { String valueHdr = PjSipService.pjStrToString(pjsua.get_rx_data_header( pjsua.pj_str_copy(hdrName), rdata)); if (!TextUtils.isEmpty(valueHdr)) { out.putString(hdrName, valueHdr); } } public void updateCallMediaState(int callId) throws SameThreadException { SipCallSession callInfo = updateCallInfoFromStack(callId, null); msgHandler.sendMessage(msgHandler.obtainMessage(ON_MEDIA_STATE, callInfo)); } }