/* * 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.plugin.addrbook.msoutlook.calendar; import java.beans.*; import java.text.*; import java.util.*; import java.util.regex.*; import net.java.sip.communicator.plugin.addrbook.*; import net.java.sip.communicator.plugin.addrbook.msoutlook.*; import net.java.sip.communicator.service.calendar.*; import net.java.sip.communicator.service.protocol.*; import net.java.sip.communicator.service.protocol.event.*; import net.java.sip.communicator.util.*; /** * A implementation of <tt>CalendarService</tt> for MS Outlook calendar. * The class resolves the free busy status and also changes the presence status * according to free busy status. * * @author Hristo Terezov */ public class CalendarServiceImpl implements CalendarService { /** * Types for the MAPI properties values. */ public enum MAPIType { PT_SYSTIME, PT_LONG, PT_BOOL, PT_BINARY }; /** * Response statuses of the calendar events (meeting objects). */ public static enum ResponseStatus { /** * No response is required for this object. */ respNone(0x00000000), /** * This meeting belongs to the organizer. */ respOrganized(0x00000001), /** * This value on the attendee's meeting indicates that the attendee has * tentatively accepted the meeting request. */ respTentative(0x00000002), /** * This value on the attendee's meeting t indicates that the attendee * has accepted the meeting request. */ respAccepted(0x00000003), /** * This value on the attendee's meeting indicates that the attendee has * declined the meeting request. */ respDeclined(0x00000004), /** * This value on the attendee's meeting indicates the attendee has not * yet responded. */ respNotResponded(0x00000005); /** * The ID of the property */ private final long id; private ResponseStatus(int id) { this.id = id; } /** * Finds <tt>ResponseStatuse</tt> instance by given value of the status. * @param value the value of the status we are searching for. * @return the status or <tt>FREE</tt> if no status is found. */ public static ResponseStatus getFromLong(long value) { for(ResponseStatus state : values()) { if(state.getID() == value) { return state; } } return respNone; } /** * Returns the ID of the status. * @return the ID of the status. */ private long getID() { return id; } }; /** * MAPI properties that we use to get information about the calendar items. */ public static enum MAPICalendarProperties { /** * A property for the start date of the calendar item. */ PidLidAppointmentStartWhole(0x0000820D, MAPIType.PT_SYSTIME), /** * A property for the end date of the calendar item. */ PidLidAppointmentEndWhole(0x0000820E, MAPIType.PT_SYSTIME), /** * A property for the free busy status of the calendar item. */ PidLidBusyStatus(0x00008205, MAPIType.PT_LONG), /** * A property that indicates if the calendar item is recurring or not. */ PidLidRecurring(0x00008223, MAPIType.PT_BOOL), /** * A property with information about the recurrent pattern of the event. */ PidLidAppointmentRecur(0x00008216, MAPIType.PT_BINARY), /** * A property with information about the accepted state of the event. */ PidLidResponseStatus(0x00008218, MAPIType.PT_LONG); /** * The id of the property */ private final long id; /** * The <tt>MAPIType</tt> of the property. */ private final MAPIType type; /** * Constructs new property. * @param id the id * @param type the type */ MAPICalendarProperties(long id, MAPIType type) { this.id = id; this.type = type; } /** * Returns array of IDs of created properties. * @return array of IDs of created properties. */ public static long[] getALLPropertyIDs() { MAPICalendarProperties properties[] = values(); long[] result = new long[properties.length]; for(int i = 0; i < properties.length; i++) { result[i] = properties[i].getID(); } return result; } /** * Returns the ID of the property. * @return the ID of the property. */ public long getID() { return id; } /** * Returns the type of the property * @return the type of the property */ public MAPIType getType() { return type; } /** * Returns <tt>MAPICalendarProperties</tt> instance by given order ID * @param i the order ID * @return <tt>MAPICalendarProperties</tt> instance */ public static MAPICalendarProperties getByOrderId(int i) { return values()[i]; } } /** * The <tt>Logger</tt> used by the <tt>CalendarServiceImpl</tt> * class and its instances for logging output. */ private static final Logger logger = Logger.getLogger(CalendarServiceImpl.class); /** * A list with currently active <tt>CalendarItemTimerTask</tt>s */ private List<CalendarItemTimerTask> currentCalendarItems = new LinkedList<CalendarItemTimerTask>(); /** * A map with the calendar items IDs and <tt>CalendarItemTimerTask</tt>s. * The map contains the current and future calendar items. */ private Map<String, CalendarItemTimerTask> taskMap = new HashMap<String, CalendarItemTimerTask>(); /** * The current free busy status. */ private BusyStatusEnum currentState = BusyStatusEnum.FREE; /** * Instance of <tt>InMeetingStatusPolicy</tt> class which is used to update * the presence status according the current free busy status. */ private InMeetingStatusPolicy inMeetingStatusPolicy = new InMeetingStatusPolicy(); public ProviderPresenceStatusListener presenceStatusListener = new ProviderPresenceStatusListener() { @Override public void providerStatusMessageChanged(PropertyChangeEvent evt) { } @Override public void providerStatusChanged(ProviderPresenceStatusChangeEvent evt) { if(evt.getNewStatus().isOnline()) { inMeetingStatusPolicy.handleProtocolProvider( evt.getProvider(), null, false, true); } } }; /** * The flag which signals that MAPI strings should be returned in the * unicode character set. */ public static final long MAPI_UNICODE = 0x80000000; static { System.loadLibrary("jmsoutlookaddrbook"); } /** * Adds <tt>CalendarItemTimerTask</tt> to the map of tasks. * @param id the id of the calendar item to be added. * @param task the <tt>CalendarItemTimerTask</tt> instance to be added. */ public void addToTaskMap(String id, CalendarItemTimerTask task) { synchronized(taskMap) { taskMap.put(id, task); } } /** * Removes <tt>CalendarItemTimerTask</tt> from the map of tasks. * @param id the id of the calendar item to be removed. */ public void removeFromTaskMap(String id) { synchronized(taskMap) { taskMap.remove(id); } } /** * Adds <tt>CalendarItemTimerTask</tt> to the list of current tasks. * @param task the <tt>CalendarItemTimerTask</tt> instance to be added. */ public void addToCurrentItems(CalendarItemTimerTask task) { synchronized(currentCalendarItems) { currentCalendarItems.add(task); } } /** * Removes <tt>CalendarItemTimerTask</tt> from the list of current tasks. * @param task the task of the calendar item to be removed. */ public void removeFromCurrentItems(CalendarItemTimerTask task) { synchronized(currentCalendarItems) { currentCalendarItems.remove(task); } } /** * Retrieves, parses and stores all the calendar items from the outlook. */ public void start() { getAllCalendarItems(new NotificationsDelegate()); } /** * Retrieves, parses and stores all the calendar items from the outlook. * @param callback the callback object that receives the results. */ private static native void getAllCalendarItems( NotificationsDelegate callback); /** * Returns array of property values for the given calendar item. * @param entryId the entry id of the calendar item. * @param propIds the IDs of the properties that we are interested for. * @param flags the flags. * @return array of property values for the given calendar item. * @throws MsOutlookMAPIHResultException */ public static native Object[] IMAPIProp_GetProps(String entryId, long[] propIds, long flags) throws MsOutlookMAPIHResultException; /** * Gets the property values of given calendar item and creates * <tt>CalendarItemTimerTask</tt> instance for it. * @param id The outlook calendar item identifier. * * @throws MsOutlookMAPIHResultException if anything goes wrong while * getting the properties of the calendar item. */ private synchronized void insert(String id) throws MsOutlookMAPIHResultException { Object[] props = null; props = IMAPIProp_GetProps(id, MAPICalendarProperties.getALLPropertyIDs(), MAPI_UNICODE); addCalendarItem(props, id); } /** * Parses the property values of calendar item and creates * <tt>CalendarItemTimerTask</tt> instance for the calendar item. * @param props the property values. * @param id the ID of the calendar item. */ private void addCalendarItem(Object[] props, String id) { Date startTime = null, endTime = null; BusyStatusEnum status = BusyStatusEnum.FREE; ResponseStatus responseStatus = ResponseStatus.respNone; boolean isRecurring = false; byte[] recurringData = null; for(int i = 0; i < props.length; i++) { if(props[i] == null) continue; MAPICalendarProperties propertyName = MAPICalendarProperties.getByOrderId(i); switch(propertyName) { case PidLidAppointmentStartWhole: try { long time = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z") .parse((String)props[i] + " UTC").getTime(); startTime = new Date(time); } catch (ParseException e) { logger.error("Cannot parse date string: " + props[i]); return; } break; case PidLidAppointmentEndWhole: try { long time = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z") .parse((String)props[i] + " UTC").getTime(); endTime = new Date(time); } catch (ParseException e) { logger.error("Cannot parse date string: " + props[i]); return; } break; case PidLidBusyStatus: status = BusyStatusEnum.getFromLong((Long)props[i]); break; case PidLidRecurring: isRecurring = (Boolean)props[i]; break; case PidLidAppointmentRecur: recurringData = ((byte[])props[i]); break; case PidLidResponseStatus: responseStatus = ResponseStatus.getFromLong((Long) props[i]); break; } } if(responseStatus != ResponseStatus.respNone && responseStatus != ResponseStatus.respAccepted && responseStatus != ResponseStatus.respOrganized) return; if(status == BusyStatusEnum.FREE || startTime == null || endTime == null) return; Date currentTime = new Date(); boolean executeNow = false; if(startTime.before(currentTime) || startTime.equals(currentTime)) executeNow = true; CalendarItemTimerTask task = null; if(recurringData != null) { task = new CalendarItemTimerTask(status, startTime, endTime, id, executeNow, null); try { RecurringPattern pattern = new RecurringPattern(recurringData, task); task.setPattern(pattern); } catch(IndexOutOfBoundsException e) { logger.error( "Error parsing reccuring pattern." + e.getMessage(),e); logger.error("Reccuring data:\n" + bytesToHex(recurringData)); return; } } if(endTime.before(currentTime) || endTime.equals(currentTime)) { if(isRecurring) { task = task.getPattern().next(startTime, endTime); } else return; } if(task == null) task = new CalendarItemTimerTask(status, startTime, endTime, id, executeNow, null); task.scheduleTasks(); } private static String bytesToHex(byte[] bytes) { StringBuilder sb = new StringBuilder(); for(byte b: bytes) sb.append(String.format("%02x", b & 0xff)); return sb.toString(); } /** * Changes the value of the current status * @param state the new value. */ protected void setCurrentState(BusyStatusEnum state) { if(currentState == state) return; BusyStatusEnum oldState = currentState; this.currentState = state; if((oldState == BusyStatusEnum.FREE && state != BusyStatusEnum.FREE) || (oldState != BusyStatusEnum.FREE && state == BusyStatusEnum.FREE)) { inMeetingStatusPolicy.freeBusyStateChanged(); } } /** * Handles presence status changed from "On the Phone" * * @param presenceStatuses the remembered presence statuses * @return <tt>true</tt> if the status is changed. */ public boolean onThePhoneStatusChanged( Map<ProtocolProviderService,PresenceStatus> presenceStatuses) { if(currentState != BusyStatusEnum.FREE) { inMeetingStatusPolicy.onThePhoneStatusChanged(presenceStatuses); return true; } return false; } /** * Calculates and changes the value of current status using the current * active calendar items and their statuses. */ public void updateStateFromCurrentItems() { BusyStatusEnum tmpState = BusyStatusEnum.FREE; synchronized(currentCalendarItems) { for(CalendarItemTimerTask task : currentCalendarItems) { if(tmpState.getPriority() < task.getStatus().getPriority()) { tmpState = task.getStatus(); } } } setCurrentState(tmpState); } @Override public BusyStatusEnum getStatus() { return currentState; } /** * The method is not implemented yet. */ @Override public void addFreeBusySateListener(FreeBusySateListener listener) { } /** * The method is not implemented yet. */ @Override public void removeFreeBusySateListener(FreeBusySateListener listener) { } /** * Implements the policy to have the presence statuses of online accounts * (i.e. registered <tt>ProtocolProviderService</tt>s) set to * "In meeting" according the free busy status. * */ private class InMeetingStatusPolicy { /** * The regular expression which removes whitespace from the * <tt>statusName</tt> property value of <tt>PresenceStatus</tt> * instances in order to recognize the <tt>PresenceStatus</tt> which * represents "In meeting". */ private final Pattern presenceStatusNameWhitespace = Pattern.compile("\\p{Space}"); /** * The <tt>PresenceStatus</tt>es of <tt>ProtocolProviderService</tt>s * before they were changed to "In meeting" remembered so * that they can be restored. */ private final Map<ProtocolProviderService,PresenceStatus> presenceStatuses = Collections.synchronizedMap( new WeakHashMap<ProtocolProviderService,PresenceStatus>()); /** * Notifies this instance that the free busy status has changed. */ public void freeBusyStateChanged() { run(false); } /** * Handles presence status changed from "On the Phone" * @param presenceStatuses the remembered presence statuses */ public void onThePhoneStatusChanged( Map<ProtocolProviderService,PresenceStatus> presenceStatuses) { run(true); for(ProtocolProviderService pps : presenceStatuses.keySet()) rememberPresenceStatus(pps, presenceStatuses.get(pps)); } /** * Returns the remembered presence statuses * @return the remembered presence statuses */ public Map<ProtocolProviderService,PresenceStatus> getRememberedStatuses() { return presenceStatuses; } /** * Finds the first <tt>PresenceStatus</tt> among the set of * <tt>PresenceStatus</tt>es supported by a specific * <tt>OperationSetPresence</tt> which represents * "In meeting". * * @param presence the <tt>OperationSetPresence</tt> which represents * the set of supported <tt>PresenceStatus</tt>es * @return the first <tt>PresenceStatus</tt> among the set of * <tt>PresenceStatus</tt>es supported by <tt>presence</tt> which * represents "In meeting" if such a <tt>PresenceStatus</tt> * was found; otherwise, <tt>null</tt> */ private PresenceStatus findInMeetingPresenceStatus( OperationSetPresence presence) { for (Iterator<PresenceStatus> i = presence.getSupportedStatusSet(); i.hasNext();) { PresenceStatus presenceStatus = i.next(); if (presenceStatusNameWhitespace .matcher(presenceStatus.getStatusName()) .replaceAll("") .equalsIgnoreCase("InAMeeting")) { return presenceStatus; } } return null; } /** * Finds the first <tt>PresenceStatus</tt> among the set of * <tt>PresenceStatus</tt>es supported by a specific * <tt>OperationSetPresence</tt> which represents * "On the phone". * * @param presence the <tt>OperationSetPresence</tt> which represents * the set of supported <tt>PresenceStatus</tt>es * @return the first <tt>PresenceStatus</tt> among the set of * <tt>PresenceStatus</tt>es supported by <tt>presence</tt> which * represents "On the phone" if such a <tt>PresenceStatus</tt> * was found; otherwise, <tt>null</tt> */ private PresenceStatus findOnThePhonePresenceStatus( OperationSetPresence presence) { for (Iterator<PresenceStatus> i = presence.getSupportedStatusSet(); i.hasNext();) { PresenceStatus presenceStatus = i.next(); if (presenceStatusNameWhitespace .matcher(presenceStatus.getStatusName()) .replaceAll("") .equalsIgnoreCase("OnThePhone")) { return presenceStatus; } } return null; } /** * Removes the remembered presence status for given provider * @param pps the provider * @return the removed value */ private PresenceStatus forgetPresenceStatus(ProtocolProviderService pps) { return presenceStatuses.remove(pps); } /** * Removes all remembered presence statuses. */ private void forgetPresenceStatuses() { presenceStatuses.clear(); } /** * Determines whether the free busy status is busy or not * * @return <tt>true</tt> if the status is busy and <tt>false</tt> if the * status is free */ private boolean isInMeeting() { return currentState != BusyStatusEnum.FREE; } /** * Invokes * {@link OperationSetPresence#publishPresenceStatus(PresenceStatus, * String)} on a specific <tt>OperationSetPresence</tt> with a specific * <tt>PresenceStatus</tt> and catches any exceptions. * * @param presence the <tt>OperationSetPresence</tt> on which the method * is to be invoked * @param presenceStatus the <tt>PresenceStatus</tt> to provide as the * respective method argument value */ private void publishPresenceStatus( OperationSetPresence presence, PresenceStatus presenceStatus) { try { presence.publishPresenceStatus(presenceStatus, null); } catch (Throwable t) { if (t instanceof InterruptedException) Thread.currentThread().interrupt(); else if (t instanceof ThreadDeath) throw (ThreadDeath) t; } } private PresenceStatus rememberPresenceStatus( ProtocolProviderService pps, PresenceStatus presenceStatus) { return presenceStatuses.put(pps, presenceStatus); } /** * Applies this policy to the current state of the application. */ private void run(boolean onThePhoneStatusChanged) { List<ProtocolProviderService> providers = AddrBookActivator.getProtocolProviders(); if ((providers == null) || (providers.size() == 0)) { forgetPresenceStatuses(); } else { boolean isInMeeting = isInMeeting(); for (ProtocolProviderService pps : providers) { if (pps == null) continue; handleProtocolProvider(pps, isInMeeting, onThePhoneStatusChanged, false); } } } public void handleProtocolProvider(ProtocolProviderService pps, Boolean isInMeeting, boolean onThePhoneStatusChanged, boolean dontAddListeners) { if(isInMeeting == null) isInMeeting = isInMeeting(); OperationSetPresence presence = pps.getOperationSet(OperationSetPresence.class); if (presence == null) { /* * "In meeting" is a PresenceStatus so it is available * only to accounts which support presence in the first * place. */ forgetPresenceStatus(pps); } else if (pps.isRegistered()) { PresenceStatus inMeetingPresenceStatus = findInMeetingPresenceStatus(presence); PresenceStatus onThePhone = findOnThePhonePresenceStatus(presence); if (inMeetingPresenceStatus == null) { /* * If do not know how to define "On the phone" for * an OperationSetPresence, then we'd better not * mess with it in the first place. */ forgetPresenceStatus(pps); } else if (isInMeeting) { if(!dontAddListeners) { presence.addProviderPresenceStatusListener( presenceStatusListener); } PresenceStatus presenceStatus = presence.getPresenceStatus(); if (presenceStatus == null) { logger.info("HANDLE provider 55"); /* * It is strange that an OperationSetPresence * does not have a PresenceStatus so it may be * safer to not mess with it. */ forgetPresenceStatus(pps); presence.removeProviderPresenceStatusListener( presenceStatusListener); } else if (!inMeetingPresenceStatus.equals( presenceStatus) && (!presenceStatus.equals(onThePhone) || onThePhoneStatusChanged)) { if(!dontAddListeners) { if(!presenceStatus.isOnline()) { return; } presence.removeProviderPresenceStatusListener( presenceStatusListener); } publishPresenceStatus( presence, inMeetingPresenceStatus); if (inMeetingPresenceStatus.equals( presence.getPresenceStatus())) { rememberPresenceStatus(pps, presenceStatus); } else { forgetPresenceStatus(pps); } } else { presence.removeProviderPresenceStatusListener( presenceStatusListener); } } else { PresenceStatus presenceStatus = forgetPresenceStatus(pps); if ((presenceStatus != null) && inMeetingPresenceStatus.equals( presence.getPresenceStatus())) { publishPresenceStatus(presence, presenceStatus); } } } else { forgetPresenceStatus(pps); } } } /** * Delegate class to be notified for calendar changes. */ public class NotificationsDelegate { /** * Callback method when receiving notifications for inserted items. */ public void inserted(String id) { try { insert(id); } catch (MsOutlookMAPIHResultException e) { e.printStackTrace(); } } /** * Callback method when receiving notifications for updated items. */ public void updated(String id) { try { synchronized(taskMap) { CalendarItemTimerTask task = taskMap.get(id); //Expired tasks can be removed earlier from the taskMap. if(task != null) { task.remove(); } } insert(id); } catch (MsOutlookMAPIHResultException e) { e.printStackTrace(); } } /** * Callback method when receiving notifications for deleted items. */ public void deleted(String id) { synchronized(taskMap) { CalendarItemTimerTask task = taskMap.get(id); if(task != null) { task.remove(); } } } /** * Callback method when receiving notifications for deleted items. */ public boolean callback(String id) { try { insert(id); } catch (MsOutlookMAPIHResultException e) { e.printStackTrace(); } return true; } } public void handleProviderAdded(ProtocolProviderService pps) { inMeetingStatusPolicy.handleProtocolProvider(pps, null, false, false); } /** * Returns the remembered presence statuses * @return the remembered presence statuses */ public Map<ProtocolProviderService,PresenceStatus> getRememberedStatuses() { return inMeetingStatusPolicy.getRememberedStatuses(); } }