/* * Jitsi, the OpenSource Java VoIP and Instant Messaging client. * * Copyright @ 2015 Atlassian Pty Ltd * * 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 net.java.sip.communicator.impl.callhistory; import java.io.*; import java.text.*; import java.util.*; import net.java.sip.communicator.service.callhistory.*; import net.java.sip.communicator.service.callhistory.event.*; import net.java.sip.communicator.service.contactlist.*; import net.java.sip.communicator.service.history.*; import net.java.sip.communicator.service.history.event.*; import net.java.sip.communicator.service.history.event.ProgressEvent; import net.java.sip.communicator.service.history.records.*; import net.java.sip.communicator.service.protocol.*; import net.java.sip.communicator.service.protocol.event.*; import net.java.sip.communicator.util.*; import org.osgi.framework.*; /** * The Call History Service stores info about the calls made. * Logs calls info for all protocol providers that support basic telephony * (i.e. those that implement OperationSetBasicTelephony). * * @author Damian Minkov * @author Lubomir Marinov */ public class CallHistoryServiceImpl implements CallHistoryService, CallListener, ServiceListener { /** * The logger for this class. */ private static final Logger logger = Logger.getLogger(CallHistoryServiceImpl.class); private static String[] STRUCTURE_NAMES = new String[] { "accountUID", "callStart", "callEnd", "dir", "callParticipantIDs", "callParticipantStart", "callParticipantEnd", "callParticipantStates", "callEndReason", "callParticipantNames", "secondaryCallParticipantIDs"}; private static HistoryRecordStructure recordStructure = new HistoryRecordStructure(STRUCTURE_NAMES); private static final char DELIM = ','; /** * The BundleContext that we got from the OSGI bus. */ private BundleContext bundleContext = null; private HistoryService historyService = null; private Object syncRoot_HistoryService = new Object(); private final Map<CallHistorySearchProgressListener, SearchProgressWrapper> progressListeners = new Hashtable<CallHistorySearchProgressListener, SearchProgressWrapper>(); private final List<CallRecordImpl> currentCallRecords = new Vector<CallRecordImpl>(); private final CallChangeListener historyCallChangeListener = new HistoryCallChangeListener(); private HistoryReader historyReader; private List<CallHistoryPeerRecordListener> callHistoryRecordlisteners = new LinkedList<CallHistoryPeerRecordListener>(); /** * Returns the underlying history service. * @return the underlying history service */ public HistoryService getHistoryService() { return historyService; } /** * Returns all the calls made by all the contacts in the supplied * <tt>contact</tt> after the given date. * * @param contact MetaContact which contacts participate in * the returned calls * @param startDate Date the start date of the calls * @return the <tt>CallHistoryQuery</tt>, corresponding to this find * @throws RuntimeException */ public Collection<CallRecord> findByStartDate( MetaContact contact, Date startDate) throws RuntimeException { throw new UnsupportedOperationException("Not implemented yet!"); } /** * Returns all the calls made after the given date * * @param startDate Date the start date of the calls * @return the <tt>CallHistoryQuery</tt>, corresponding to this find * @throws RuntimeException */ public Collection<CallRecord> findByStartDate(Date startDate) { TreeSet<CallRecord> result = new TreeSet<CallRecord>(new CallRecordComparator()); try { // the default ones History history = this.getHistory(null, null); historyReader = history.getReader(); addHistorySearchProgressListeners(historyReader, 1); QueryResultSet<HistoryRecord> rs = historyReader.findByStartDate(startDate); while (rs.hasNext()) { HistoryRecord hr = rs.next(); result.add(convertHistoryRecordToCallRecord(hr)); } removeHistorySearchProgressListeners(historyReader); } catch (IOException ex) { logger.error("Could not read history", ex); } return result; } /** * Returns all the calls made by all the contacts * in the supplied metacontact before the given date * * @param contact MetaContact which contacts participate in * the returned calls * @param endDate Date the end date of the calls * @return Collection of CallRecords with CallPeerRecord * @throws RuntimeException */ public Collection<CallRecord> findByEndDate(MetaContact contact, Date endDate) throws RuntimeException { throw new UnsupportedOperationException("Not implemented yet!"); } /** * Returns all the calls made before the given date * * @param endDate Date the end date of the calls * @return Collection of CallRecords with CallPeerRecord * @throws RuntimeException */ public Collection<CallRecord> findByEndDate(Date endDate) throws RuntimeException { TreeSet<CallRecord> result = new TreeSet<CallRecord>(new CallRecordComparator()); try { // the default ones History history = this.getHistory(null, null); historyReader = history.getReader(); addHistorySearchProgressListeners(historyReader, 1); QueryResultSet<HistoryRecord> rs = historyReader.findByEndDate(endDate); while (rs.hasNext()) { HistoryRecord hr = rs.next(); result.add(convertHistoryRecordToCallRecord(hr)); } removeHistorySearchProgressListeners(historyReader); } catch (IOException ex) { logger.error("Could not read history", ex); } return result; } /** * Returns all the calls made by all the contacts * in the supplied metacontact between the given dates * * @param contact MetaContact * @param startDate Date the start date of the calls * @param endDate Date the end date of the conversations * @return Collection of CallRecords with CallPeerRecord * @throws RuntimeException */ public Collection<CallRecord> findByPeriod(MetaContact contact, Date startDate, Date endDate) throws RuntimeException { throw new UnsupportedOperationException("Not implemented yet!"); } /** * Returns all the calls made between the given dates * * @param startDate Date the start date of the calls * @param endDate Date the end date of the conversations * @return Collection of CallRecords with CallPeerRecord * @throws RuntimeException */ public Collection<CallRecord> findByPeriod(Date startDate, Date endDate) throws RuntimeException { TreeSet<CallRecord> result = new TreeSet<CallRecord>(new CallRecordComparator()); try { // the default ones History history = this.getHistory(null, null); historyReader = history.getReader(); addHistorySearchProgressListeners(historyReader, 1); QueryResultSet<HistoryRecord> rs = historyReader.findByPeriod(startDate, endDate); while (rs.hasNext()) { HistoryRecord hr = rs.next(); result.add(convertHistoryRecordToCallRecord(hr)); } removeHistorySearchProgressListeners(historyReader); } catch (IOException ex) { logger.error("Could not read history", ex); } return result; } /** * Returns the supplied number of calls by all the contacts * in the supplied metacontact * * @param contact MetaContact which contacts participate in * the returned calls * @param count calls count * @return Collection of CallRecords with CallPeerRecord * @throws RuntimeException */ public Collection<CallRecord> findLast(MetaContact contact, int count) throws RuntimeException { throw new UnsupportedOperationException("Not implemented yet!"); } /** * Returns the supplied number of calls made * * @param count calls count * @return Collection of CallRecords with CallPeerRecord * @throws RuntimeException */ public Collection<CallRecord> findLast(int count) throws RuntimeException { TreeSet<CallRecord> result = new TreeSet<CallRecord>(new CallRecordComparator()); try { // the default ones History history = this.getHistory(null, null); historyReader = history.getReader(); QueryResultSet<HistoryRecord> rs = historyReader.findLast(count); while (rs.hasNext()) { HistoryRecord hr = rs.next(); result.add(convertHistoryRecordToCallRecord(hr)); } } catch (IOException ex) { logger.error("Could not read history", ex); } return result; } /** * Find the calls made by the supplied peer address * @param address String the address of the peer * @param recordCount the number of records to return * @return Collection of CallRecords with CallPeerRecord * @throws RuntimeException */ public CallHistoryQuery findByPeer(String address, int recordCount) throws RuntimeException { CallHistoryQueryImpl callQuery = null; try { // the default ones History history = this.getHistory(null, null); InteractiveHistoryReader historyReader = history.getInteractiveReader(); HistoryQuery historyQuery = historyReader.findByKeyword( address, "callParticipantIDs", recordCount); callQuery = new CallHistoryQueryImpl(historyQuery); } catch (IOException ex) { logger.error("Could not read history", ex); } return callQuery; } /** * Returns the history by specified local and remote contact * if one of them is null the default is used * * @param localContact Contact * @param remoteContact Contact * @return History * @throws IOException */ private History getHistory(Contact localContact, Contact remoteContact) throws IOException { String localId = localContact == null ? "default" : localContact .getAddress(); String remoteId = remoteContact == null ? "default" : remoteContact .getAddress(); HistoryID historyId = HistoryID.createFromRawID( new String[] { "callhistory", localId, remoteId }); return this.historyService.createHistory(historyId, recordStructure); } /** * Used to convert HistoryRecord in CallReord and CallPeerRecord * which are returned by the finder methods * * @param hr HistoryRecord * @return Object CallRecord */ static CallRecord convertHistoryRecordToCallRecord(HistoryRecord hr) { CallRecordImpl result = new CallRecordImpl(); List<String> callPeerIDs = null; List<String> callPeerNames = null; List<String> callPeerStart = null; List<String> callPeerEnd = null; List<CallPeerState> callPeerStates = null; List<String> callPeerSecondaryIDs = null; // History structure // 0 - callStart // 1 - callEnd // 2 - dir // 3 - callParticipantIDs // 4 - callParticipantStart // 5 - callParticipantEnd SimpleDateFormat sdf = new SimpleDateFormat(HistoryService.DATE_FORMAT); for (int i = 0; i < hr.getPropertyNames().length; i++) { String propName = hr.getPropertyNames()[i]; String value = hr.getPropertyValues()[i]; if (propName.equals(STRUCTURE_NAMES[0])) result.setProtocolProvider(getProtocolProvider(value)); else if(propName.equals(STRUCTURE_NAMES[1])) try { result.setStartTime(sdf.parse(value)); } catch (ParseException e) { result.setStartTime(new Date(Long.parseLong(value))); } else if(propName.equals(STRUCTURE_NAMES[2])) try { result.setEndTime(sdf.parse(value)); } catch (ParseException e) { result.setEndTime(new Date(Long.parseLong(value))); } else if(propName.equals(STRUCTURE_NAMES[3])) result.setDirection(value); else if(propName.equals(STRUCTURE_NAMES[4])) callPeerIDs = getCSVs(value); else if(propName.equals(STRUCTURE_NAMES[5])) callPeerStart = getCSVs(value); else if(propName.equals(STRUCTURE_NAMES[6])) callPeerEnd = getCSVs(value); else if(propName.equals(STRUCTURE_NAMES[7])) callPeerStates = getStates(value); else if(propName.equals(STRUCTURE_NAMES[8])) result.setEndReason(Integer.parseInt(value)); else if(propName.equals(STRUCTURE_NAMES[9])) callPeerNames = getCSVs(value); else if(propName.equals(STRUCTURE_NAMES[10])) callPeerSecondaryIDs = getCSVs(value); } final int callPeerCount = callPeerIDs == null ? 0 : callPeerIDs.size(); for (int i = 0; i < callPeerCount; i++) { // As we iterate over the CallPeer IDs we could not be sure that // for some reason the start or end call list could result in // different size lists, so we check this first. Date callPeerStartValue = null; Date callPeerEndValue = null; if (i < callPeerStart.size()) { try { callPeerStartValue = sdf.parse(callPeerStart.get(i)); } catch (ParseException e) { callPeerStartValue = new Date(Long.parseLong(callPeerStart.get(i))); } } else { callPeerStartValue = result.getStartTime(); if (logger.isInfoEnabled()) logger.info( "Call history start time list different from ids list: " + hr.toString()); } if (i < callPeerEnd.size()) { try { callPeerEndValue = sdf.parse(callPeerEnd.get(i)); } catch (ParseException e) { callPeerEndValue = new Date(Long.parseLong(callPeerEnd.get(i))); } } else { callPeerEndValue = result.getEndTime(); if (logger.isInfoEnabled()) logger.info( "Call history end time list different from ids list: " + hr.toString()); } CallPeerRecordImpl cpr = new CallPeerRecordImpl( callPeerIDs.get(i), callPeerStartValue, callPeerEndValue); String callPeerSecondaryID = null; if(callPeerSecondaryIDs != null && !callPeerSecondaryIDs.isEmpty()) callPeerSecondaryID = callPeerSecondaryIDs.get(i); if(callPeerSecondaryID != null && !callPeerSecondaryID.equals("")) { cpr.setPeerSecondaryAddress(callPeerSecondaryID); } // if there is no record about the states (backward compatibility) if (callPeerStates != null && i < callPeerStates.size()) cpr.setState(callPeerStates.get(i)); else if (logger.isInfoEnabled()) logger.info( "Call history state list different from ids list: " + hr.toString()); result.getPeerRecords().add(cpr); if (callPeerNames != null && i < callPeerNames.size()) cpr.setDisplayName(callPeerNames.get(i)); } return result; } /** * Returns list of String items contained in the supplied string * separated by DELIM * @param str String * @return LinkedList */ private static List<String> getCSVs(String str) { List<String> result = new LinkedList<String>(); if(str == null) return result; StreamTokenizer stt = new StreamTokenizer(new StringReader(str)); stt.resetSyntax(); stt.wordChars('\u0000','\uFFFF'); stt.eolIsSignificant(false); stt.quoteChar('"'); stt.whitespaceChars(DELIM, DELIM); try { while(stt.nextToken() != StreamTokenizer.TT_EOF) { if (stt.sval != null) { result.add(stt.sval.trim()); } } } catch (IOException e) { logger.error("failed to parse " + str, e); } return result; } /** * Get the delimited strings and converts them to CallPeerState * * @param str String delimited string states * @return LinkedList the converted values list */ private static List<CallPeerState> getStates(String str) { List<CallPeerState> result = new LinkedList<CallPeerState>(); Collection<String> stateStrs = getCSVs(str); for (String item : stateStrs) { result.add(convertStateStringToState(item)); } return result; } /** * Converts the state string to state * @param state String the string * @return CallPeerState the state */ private static CallPeerState convertStateStringToState(String state) { if(state.equals(CallPeerState._CONNECTED)) return CallPeerState.CONNECTED; else if(state.equals(CallPeerState._BUSY)) return CallPeerState.BUSY; else if(state.equals(CallPeerState._FAILED)) return CallPeerState.FAILED; else if(state.equals(CallPeerState._DISCONNECTED)) return CallPeerState.DISCONNECTED; else if(state.equals(CallPeerState._ALERTING_REMOTE_SIDE)) return CallPeerState.ALERTING_REMOTE_SIDE; else if(state.equals(CallPeerState._CONNECTING)) return CallPeerState.CONNECTING; else if(state.equals(CallPeerState._ON_HOLD_LOCALLY)) return CallPeerState.ON_HOLD_LOCALLY; else if(state.equals(CallPeerState._ON_HOLD_MUTUALLY)) return CallPeerState.ON_HOLD_MUTUALLY; else if(state.equals(CallPeerState._ON_HOLD_REMOTELY)) return CallPeerState.ON_HOLD_REMOTELY; else if(state.equals(CallPeerState._INITIATING_CALL)) return CallPeerState.INITIATING_CALL; else if(state.equals(CallPeerState._INCOMING_CALL)) return CallPeerState.INCOMING_CALL; else return CallPeerState.UNKNOWN; } /** * starts the service. Check the current registerd protocol providers * which supports BasicTelephony and adds calls listener to them * * @param bc BundleContext */ public void start(BundleContext bc) { if (logger.isDebugEnabled()) logger.debug("Starting the call history implementation."); this.bundleContext = bc; // start listening for newly register or removed protocol providers bc.addServiceListener(this); Collection<ServiceReference<ProtocolProviderService>> ppsRefs = ServiceUtils.getServiceReferences( bc, ProtocolProviderService.class); // in case we found any if (!ppsRefs.isEmpty()) { if (logger.isDebugEnabled()) { logger.debug( "Found " + ppsRefs.size() + " already installed providers."); } for (ServiceReference<ProtocolProviderService> ppsRef : ppsRefs) { ProtocolProviderService pps = bc.getService(ppsRef); handleProviderAdded(pps); } } } /** * stops the service. * * @param bc BundleContext */ public void stop(BundleContext bc) { bc.removeServiceListener(this); Collection<ServiceReference<ProtocolProviderService>> ppsRefs = ServiceUtils.getServiceReferences( bc, ProtocolProviderService.class); // in case we found any if (!ppsRefs.isEmpty()) { for (ServiceReference<ProtocolProviderService> ppsRef : ppsRefs) { ProtocolProviderService pps = bc.getService(ppsRef); handleProviderRemoved(pps); } } } /** * Writes the given record to the history service * @param callRecord CallRecord * @param source Contact * @param destination Contact */ private void writeCall( CallRecordImpl callRecord, Contact source, Contact destination) { try { SimpleDateFormat sdf = new SimpleDateFormat(HistoryService.DATE_FORMAT); History history = this.getHistory(source, destination); HistoryWriter historyWriter = history.getWriter(); StringBuffer callPeerIDs = new StringBuffer(); StringBuffer callPeerNames = new StringBuffer(); StringBuffer callPeerStartTime = new StringBuffer(); StringBuffer callPeerEndTime = new StringBuffer(); StringBuffer callPeerStates = new StringBuffer(); StringBuffer callPeerSecondaryIDs = new StringBuffer(); for (CallPeerRecord item : callRecord .getPeerRecords()) { if (callPeerIDs.length() > 0) { callPeerIDs.append(DELIM); callPeerNames.append(DELIM); callPeerStartTime.append(DELIM); callPeerEndTime.append(DELIM); callPeerStates.append(DELIM); callPeerSecondaryIDs.append(DELIM); } callPeerIDs.append(item.getPeerAddress()); String dn = item.getDisplayName(); if (dn != null) { //escape quotes dn = dn.replace("\"", "\\\""); //then insert the quoted string callPeerNames.append('"'); callPeerNames.append(dn); callPeerNames.append('"'); } callPeerStartTime.append(sdf.format(item.getStartTime())); callPeerEndTime.append(sdf.format(item.getEndTime())); callPeerStates.append(item.getState().getStateString()); callPeerSecondaryIDs.append( item.getPeerSecondaryAddress() == null? "" : item.getPeerSecondaryAddress()); } historyWriter.addRecord(new String[] { callRecord.getSourceCall().getProtocolProvider() .getAccountID().getAccountUniqueID(), sdf.format(callRecord.getStartTime()), sdf.format(callRecord.getEndTime()), callRecord.getDirection(), callPeerIDs.toString(), callPeerStartTime.toString(), callPeerEndTime.toString(), callPeerStates.toString(), String.valueOf(callRecord.getEndReason()), callPeerNames.toString(), callPeerSecondaryIDs.toString()}, new Date()); // this date is when the history // record is written } catch (IOException e) { logger.error("Could not add call to history", e); } } /** * Set the configuration service. * * @param historyService HistoryService * @throws IOException * @throws IllegalArgumentException */ public void setHistoryService(HistoryService historyService) throws IllegalArgumentException, IOException { synchronized (this.syncRoot_HistoryService) { this.historyService = historyService; if (logger.isDebugEnabled()) logger.debug("New history service registered."); } } /** * Remove a configuration service. * * @param hService HistoryService */ public void unsetHistoryService(HistoryService hService) { synchronized (this.syncRoot_HistoryService) { if (this.historyService == hService) { this.historyService = null; if (logger.isDebugEnabled()) logger.debug("History service unregistered."); } } } /** * Permanently removes all locally stored call history. * * @throws java.io.IOException * Thrown if the history could not be removed due to a IO error. */ public void eraseLocallyStoredHistory() throws IOException { HistoryID historyId = HistoryID.createFromRawID( new String[] { "callhistory" }); historyService.purgeLocallyStoredHistory(historyId); } /** * When new protocol provider is registered we check * does it supports BasicTelephony and if so add a listener to it * * @param serviceEvent ServiceEvent */ public void serviceChanged(ServiceEvent serviceEvent) { Object sService = bundleContext.getService(serviceEvent.getServiceReference()); if (logger.isTraceEnabled()) logger.trace("Received a service event for: " + sService.getClass().getName()); // we don't care if the source service is not a protocol provider if (! (sService instanceof ProtocolProviderService)) { return; } if (logger.isDebugEnabled()) logger.debug("Service is a protocol provider."); if (serviceEvent.getType() == ServiceEvent.REGISTERED) { if (logger.isDebugEnabled()) logger.debug("Handling registration of a new Protocol Provider."); this.handleProviderAdded((ProtocolProviderService)sService); } else if (serviceEvent.getType() == ServiceEvent.UNREGISTERING) { this.handleProviderRemoved( (ProtocolProviderService) sService); } } /** * Used to attach the Call History Service to existing or * just registered protocol provider. Checks if the provider has * implementation of OperationSetBasicTelephony * * @param provider ProtocolProviderService */ private void handleProviderAdded(ProtocolProviderService provider) { if (logger.isDebugEnabled()) logger.debug("Adding protocol provider " + provider.getProtocolName()); // check whether the provider has a basic telephony operation set OperationSetBasicTelephony<?> opSetTelephony = provider.getOperationSet(OperationSetBasicTelephony.class); if (opSetTelephony != null) { opSetTelephony.addCallListener(this); } else { if (logger.isTraceEnabled()) logger.trace("Service did not have a basic telephony op. set."); } } /** * Removes the specified provider from the list of currently known providers * and ignores all the calls made by it * * @param provider the ProtocolProviderService that has been unregistered. */ private void handleProviderRemoved(ProtocolProviderService provider) { OperationSetBasicTelephony<?> opSetTelephony = provider.getOperationSet(OperationSetBasicTelephony.class); if (opSetTelephony != null) { opSetTelephony.removeCallListener(this); } } /** * Adding progress listener for monitoring progress of search process * * @param listener HistorySearchProgressListener */ public void addSearchProgressListener(CallHistorySearchProgressListener listener) { synchronized (progressListeners) { progressListeners .put(listener, new SearchProgressWrapper(listener)); } } /** * Removing progress listener * * @param listener HistorySearchProgressListener */ public void removeSearchProgressListener( CallHistorySearchProgressListener listener) { synchronized(progressListeners){ progressListeners.remove(listener); } } /** * Adding <tt>CallHistoryRecordListener</tt> listener to the list. * * @param listener CallHistoryRecordListener */ public void addCallHistoryRecordListener(CallHistoryPeerRecordListener listener) { synchronized (callHistoryRecordlisteners) { callHistoryRecordlisteners.add(listener); } } /** * Removing <tt>CallHistoryRecordListener</tt> listener * * @param listener CallHistoryRecordListener */ public void removeCallHistoryRecordListener( CallHistoryPeerRecordListener listener) { synchronized(callHistoryRecordlisteners){ callHistoryRecordlisteners.remove(listener); } } /** * Fires the given event to all <tt>CallHistoryRecordListener</tt> listeners * @param event the <tt>CallHistoryRecordReceivedEvent</tt> event to be * fired */ private void fireCallHistoryRecordReceivedEvent( CallHistoryPeerRecordEvent event) { List<CallHistoryPeerRecordListener> tmpListeners; synchronized (callHistoryRecordlisteners) { tmpListeners = new LinkedList<CallHistoryPeerRecordListener>( callHistoryRecordlisteners); } for(CallHistoryPeerRecordListener listener : tmpListeners) { listener.callPeerRecordReceived(event); } } /** * Add the registered CallHistorySearchProgressListeners to the given * HistoryReader * * @param reader HistoryReader * @param countContacts number of contacts will search */ private void addHistorySearchProgressListeners(HistoryReader reader, int countContacts) { synchronized (progressListeners) { for (SearchProgressWrapper l : progressListeners.values()) { l.contactCount = countContacts; reader.addSearchProgressListener(l); } } } /** * Removes the registered CallHistorySearchProgressListeners from the given * HistoryReader * * @param reader HistoryReader */ private void removeHistorySearchProgressListeners(HistoryReader reader) { synchronized (progressListeners) { for (SearchProgressWrapper l : progressListeners.values()) { l.clear(); reader.removeSearchProgressListener(l); } } } /** * CallListener implementation for incoming calls * @param event CallEvent */ public void incomingCallReceived(CallEvent event) { handleNewCall(event.getSourceCall(), CallRecord.IN); } /** * CallListener implementation for outgoing calls * @param event CallEvent */ public void outgoingCallCreated(CallEvent event) { handleNewCall(event.getSourceCall(), CallRecord.OUT); } /** * CallListener implementation for call endings * @param event CallEvent */ public void callEnded(CallEvent event) { // We store the call in the callStateChangeEvent where we // have more information on the previous state of the call. } /** * Adding a record for joining peer * @param callPeer CallPeer */ private void handlePeerAdded(CallPeer callPeer) { CallRecord callRecord = findCallRecord(callPeer.getCall()); // no such call if(callRecord == null) return; callPeer.addCallPeerListener(new CallPeerAdapter() { @Override public void peerStateChanged(CallPeerChangeEvent evt) { if(evt.getNewValue().equals(CallPeerState.DISCONNECTED)) return; else { CallPeerRecordImpl peerRecord = findPeerRecord(evt.getSourceCallPeer()); if(peerRecord == null) return; CallPeerState newState = (CallPeerState) evt.getNewValue(); if (newState.equals(CallPeerState.CONNECTED) && !CallPeerState.isOnHold((CallPeerState) evt.getOldValue())) peerRecord.setStartTime(new Date()); peerRecord.setState(newState); //Disconnected / Busy //Disconnected / Connecting - fail //Disconnected / Connected } } }); Date startDate = new Date(); CallPeerRecordImpl newRec = new CallPeerRecordImpl( callPeer.getAddress(), startDate, startDate); newRec.setDisplayName(callPeer.getDisplayName()); callRecord.getPeerRecords().add(newRec); fireCallHistoryRecordReceivedEvent(new CallHistoryPeerRecordEvent( callPeer.getAddress(), startDate, callPeer.getProtocolProvider())); } /** * Adding a record for removing peer from call * @param callPeer CallPeer * @param srcCall Call */ private void handlePeerRemoved( CallPeer callPeer, Call srcCall) { CallRecord callRecord = findCallRecord(srcCall); String pAddress = callPeer.getAddress(); if (callRecord == null) return; CallPeerRecordImpl cpRecord = (CallPeerRecordImpl)callRecord.findPeerRecord(pAddress); // no such peer if(cpRecord == null) return; if(!callPeer.getState().equals(CallPeerState.DISCONNECTED)) cpRecord.setState(callPeer.getState()); CallPeerState cpRecordState = cpRecord.getState(); if (cpRecordState.equals(CallPeerState.CONNECTED) || CallPeerState.isOnHold(cpRecordState)) { cpRecord.setEndTime(new Date()); } } /** * Updates the secondary address field of call record. * @param date the start date of the record which will be updated. * @param peerAddress the address of the peer of the record which will be * updated. * @param address the value of the secondary address . */ public void updateCallRecordPeerSecondaryAddress(final Date date, final String peerAddress, final String address) { boolean callRecordFound = false; synchronized (currentCallRecords) { for(CallRecord record : currentCallRecords) for(CallPeerRecord peerRecord : record.getPeerRecords()) { if(peerRecord.getPeerAddress().equals(peerAddress) && peerRecord.getStartTime().equals(date)) { callRecordFound = true; peerRecord.setPeerSecondaryAddress(address); } } } if(callRecordFound) return; History history; try { history = this.getHistory(null, null); } catch (IOException e) { logger.warn("Failed to get the history object."); return; } HistoryWriter historyWriter = history.getWriter(); HistoryWriter.HistoryRecordUpdater updater = new HistoryWriter.HistoryRecordUpdater() { private HistoryRecord record; private int dateIndex; private int peerIDIndex; private int peerSecondaryIDIndex; @Override public void setHistoryRecord(HistoryRecord historyRecord) { record = historyRecord; String propertyNames[] = record.getPropertyNames(); for(int i = 0; i < propertyNames.length; i++) { if(propertyNames[i].equals(STRUCTURE_NAMES[5])) { dateIndex = i; } if(propertyNames[i].equals(STRUCTURE_NAMES[4])) { peerIDIndex = i; } if(propertyNames[i].equals(STRUCTURE_NAMES[10])) { peerSecondaryIDIndex = i; } } } @Override public boolean isMatching() { String[] propertyVlaues = record.getPropertyValues(); List<String> peerIDs = getCSVs(propertyVlaues[peerIDIndex]); int i = peerIDs.indexOf(peerAddress); if(i == -1) return false; String dateString = getCSVs(propertyVlaues[dateIndex]).get(i); SimpleDateFormat sdf = new SimpleDateFormat(HistoryService.DATE_FORMAT); try { if(!sdf.parse(dateString).equals(date)) return false; } catch (ParseException e) { logger.warn("Failed to parse the date."); return false; } String secondaryID = getCSVs(propertyVlaues[peerSecondaryIDIndex]).get(i); if(secondaryID != null) return false; return true; } @Override public Map<String, String> getUpdateChanges() { String[] propertyVlaues = record.getPropertyValues(); List<String> peerIDs = getCSVs(propertyVlaues[peerIDIndex]); int i = peerIDs.indexOf(peerAddress); if(i == -1) return null; List<String> secondaryID = getCSVs(record.getPropertyValues()[peerSecondaryIDIndex]); secondaryID.set(i, peerAddress); String res = ""; int j = 0; for(String id : secondaryID) { if(j++ != 0) res += DELIM; res += id; } Map<String, String> changesMap = new HashMap<String, String>(); changesMap.put(STRUCTURE_NAMES[10], res); return changesMap; } }; try { historyWriter.updateRecord(updater); } catch (IOException e) { logger.warn("Failed to update the record."); } } /** * Finding a CallRecord for the given call * * @param call Call * @return CallRecord */ private CallRecordImpl findCallRecord(Call call) { synchronized (currentCallRecords) { for (CallRecordImpl item : currentCallRecords) { if (item.getSourceCall().equals(call)) return item; } } return null; } /** * Returns the peer record for the given peer * @param callPeer CallPeer peer * @return CallPeerRecordImpl the corresponding record */ private CallPeerRecordImpl findPeerRecord( CallPeer callPeer) { CallRecord record = findCallRecord(callPeer.getCall()); if (record == null) return null; return (CallPeerRecordImpl) record.findPeerRecord( callPeer.getAddress()); } /** * Adding a record for a new call * @param sourceCall Call * @param direction String */ private void handleNewCall(Call sourceCall, String direction) { // if call exist. its not new synchronized (currentCallRecords) { for (CallRecordImpl currentCallRecord : currentCallRecords) { if (currentCallRecord.getSourceCall().equals(sourceCall)) return; } } CallRecordImpl newRecord = new CallRecordImpl( direction, new Date(), null); newRecord.setSourceCall(sourceCall); sourceCall.addCallChangeListener(historyCallChangeListener); synchronized (currentCallRecords) { currentCallRecords.add(newRecord); } // if has already perticipants Dispatch them Iterator<? extends CallPeer> iter = sourceCall.getCallPeers(); while (iter.hasNext()) { handlePeerAdded(iter.next()); } } /** * A wrapper around HistorySearchProgressListener * that fires events for CallHistorySearchProgressListener */ private class SearchProgressWrapper implements HistorySearchProgressListener { private CallHistorySearchProgressListener listener = null; int contactCount = 0; int currentContactCount = 0; int currentProgress = 0; int lastHistoryProgress = 0; SearchProgressWrapper(CallHistorySearchProgressListener listener) { this.listener = listener; } public void progressChanged(ProgressEvent evt) { int progress = getProgressMapping(evt.getProgress()); listener.progressChanged( new net.java.sip.communicator.service.callhistory.event. ProgressEvent(CallHistoryServiceImpl.this, evt, progress)); } /** * Calculates the progress according the count of the contacts * we will search * @param historyProgress int * @return int */ private int getProgressMapping(int historyProgress) { currentProgress += (historyProgress - lastHistoryProgress)/contactCount; if(historyProgress == HistorySearchProgressListener.PROGRESS_MAXIMUM_VALUE) { currentContactCount++; lastHistoryProgress = 0; // this is the last one and the last event fire the max // there will be looses in currentProgress due to the devision if(currentContactCount == contactCount) currentProgress = CallHistorySearchProgressListener.PROGRESS_MAXIMUM_VALUE; } else lastHistoryProgress = historyProgress; return currentProgress; } /** * clear the values */ void clear() { contactCount = 0; currentProgress = 0; lastHistoryProgress = 0; currentContactCount = 0; } } /** * Used to compare CallRecords and to be ordered in TreeSet according their * timestamp */ private static class CallRecordComparator implements Comparator<CallRecord> { public int compare(CallRecord o1, CallRecord o2) { return o2.getStartTime().compareTo(o1.getStartTime()); } } /** * Receive events for adding or removing peers from a call */ private class HistoryCallChangeListener implements CallChangeListener { /** * Indicates that a new call peer has joined the source call. * * @param evt the <tt>CallPeerEvent</tt> containing the source call * and call peer. */ public void callPeerAdded(CallPeerEvent evt) { handlePeerAdded(evt.getSourceCallPeer()); } /** * Indicates that a call peer has left the source call. * * @param evt the <tt>CallPeerEvent</tt> containing the source call * and call peer. */ public void callPeerRemoved(CallPeerEvent evt) { handlePeerRemoved(evt.getSourceCallPeer(), evt.getSourceCall()); } /** * A dummy implementation of this listener's callStateChanged() method. * * @param evt the <tt>CallChangeEvent</tt> instance containing the source * calls and its old and new state. */ public void callStateChanged(CallChangeEvent evt) { CallRecordImpl callRecord = findCallRecord(evt.getSourceCall()); // no such call if (callRecord == null) return; if (!CallChangeEvent.CALL_STATE_CHANGE .equals(evt.getPropertyName())) return; if (evt.getNewValue().equals(CallState.CALL_ENDED)) { boolean writeRecord = true; if(evt.getOldValue().equals(CallState.CALL_INITIALIZATION)) { callRecord.setEndTime(callRecord.getStartTime()); // if call was answered elsewhere, add its reason // so we can distinguish it from missed if(evt.getCause() != null && evt.getCause().getReasonCode() == CallPeerChangeEvent.NORMAL_CALL_CLEARING) { callRecord.setEndReason(evt.getCause().getReasonCode()); if ("Call completed elsewhere".equals( evt.getCause().getReasonString())) { writeRecord = false; } } } else callRecord.setEndTime(new Date()); if (writeRecord) { writeCall(callRecord, null, null); } synchronized (currentCallRecords) { currentCallRecords.remove(callRecord); } } } } /** * Returns the <tt>ProtocolProviderService</tt> corresponding to the given * account identifier. * @param accountUID the identifier of the account. * @return the <tt>ProtocolProviderService</tt> corresponding to the given * account identifier */ private static ProtocolProviderService getProtocolProvider( String accountUID) { for (ProtocolProviderFactory providerFactory : CallHistoryActivator.getProtocolProviderFactories().values()) { for (AccountID accountID : providerFactory.getRegisteredAccounts()) { if (accountID.getAccountUniqueID().equals(accountUID)) { ServiceReference<ProtocolProviderService> serRef = providerFactory.getProviderForAccount(accountID); return CallHistoryActivator.bundleContext.getService(serRef); } } } return null; } }