/*
* Copyright 2010 Arthur Zaczek <arthur@dasz.at>, dasz.at OG; All rights reserved.
* Copyright 2010 David Schmitt <david@dasz.at>, dasz.at OG; All rights reserved.
*
* This file is part of Kolab Sync for Android.
* Kolab Sync for Android 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.
* Kolab Sync for Android 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 Kolab Sync for Android.
* If not, see <http://www.gnu.org/licenses/>.
*/
package at.dasz.KolabDroid.Calendar;
import java.util.Calendar;
import java.util.Date;
import java.util.UUID;
import java.util.regex.Matcher;
import javax.mail.MessagingException;
import javax.xml.parsers.ParserConfigurationException;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import android.content.ContentResolver;
import android.content.Context;
import android.database.Cursor;
import android.text.format.Time;
import android.util.Log;
import at.dasz.KolabDroid.Utils;
import at.dasz.KolabDroid.Provider.LocalCacheProvider;
import at.dasz.KolabDroid.Settings.Settings;
import at.dasz.KolabDroid.Sync.AbstractSyncHandler;
import at.dasz.KolabDroid.Sync.CacheEntry;
import at.dasz.KolabDroid.Sync.SyncContext;
import at.dasz.KolabDroid.Sync.SyncException;
public class SyncCalendarHandler extends AbstractSyncHandler
{
private final String defaultFolderName;
private final LocalCacheProvider cacheProvider;
private final CalendarProvider calendarProvider;
private final ContentResolver cr;
public SyncCalendarHandler(Context context)
{
super(context);
Settings s = new Settings(context);
settings = s;
defaultFolderName = s.getCalendarFolder();
cacheProvider = new LocalCacheProvider.CalendarCacheProvider(context);
calendarProvider = new CalendarProvider(context.getContentResolver());
cr = context.getContentResolver();
status.setTask("Calendar");
}
public String getDefaultFolderName()
{
return defaultFolderName;
}
public boolean shouldProcess()
{
boolean hasFolder = (defaultFolderName != null && !"".equals(defaultFolderName));
return settings.getSyncCalendar() && hasFolder;
}
public LocalCacheProvider getLocalCacheProvider()
{
return cacheProvider;
}
public int getIdColumnIndex(Cursor c)
{
return c.getColumnIndex(CalendarProvider._ID);
}
public Cursor getAllLocalItemsCursor()
{
return cr.query(CalendarProvider.CALENDAR_URI,
new String[] { CalendarProvider._ID }, null, null, null);
}
@Override
public void deleteLocalItem(int localId)
{
calendarProvider.delete(localId);
}
@Override
protected String getMimeType()
{
return "application/x-vnd.kolab.event";
}
public boolean hasLocalItem(SyncContext sync) throws SyncException
{
return getLocalItem(sync) != null;
}
public boolean hasLocalChanges(SyncContext sync) throws SyncException
{
CacheEntry e = sync.getCacheEntry();
CalendarEntry cal = getLocalItem(sync);
String entryHash = e.getLocalHash();
String calHash = cal != null ? cal.getLocalHash() : "";
return !entryHash.equals(calHash);
}
@Override
protected void updateLocalItemFromServer(SyncContext sync, Document xml)
{
CalendarEntry cal = (CalendarEntry) sync.getLocalItem();
if (cal == null)
{
cal = new CalendarEntry();
}
Element root = xml.getDocumentElement();
cal.setUid(Utils.getXmlElementString(root, "uid"));
cal.setDescription(Utils.getXmlElementString(root, "body"));
cal.setTitle(Utils.getXmlElementString(root, "summary"));
cal.setEventLocation(Utils.getXmlElementString(root, "location"));
int reminderTime = Utils.getXmlElementInt(root, "alarm", -1);
if(reminderTime > -1)
{
cal.setHasAlarm(1);
cal.setReminderTime(reminderTime);
}
Time start = Utils.getXmlElementTime(root, "start-date");
Time end = Utils.getXmlElementTime(root, "end-date");
cal.setDtstart(start);
cal.setDtend(end);
cal.setAllDay(start.hour == 0 && end.hour == 0 && start.minute == 0
&& end.minute == 0 && start.second == 0 && end.second == 0);
Element recurrence = Utils.getXmlElement(root, "recurrence");
if (recurrence != null)
{
StringBuilder sb = new StringBuilder();
String cycle = Utils.getXmlAttributeString(recurrence, "cycle")
.toUpperCase();
sb.append("FREQ=");
sb.append(cycle);
sb.append(";WKST=");
int firstDayOfWeek = Calendar.getInstance().getFirstDayOfWeek();
switch (firstDayOfWeek)
{
case Calendar.MONDAY:
sb.append("MO");
break;
case Calendar.TUESDAY:
sb.append("TU");
break;
case Calendar.WEDNESDAY:
sb.append("WE");
break;
case Calendar.THURSDAY:
sb.append("TH");
break;
case Calendar.FRIDAY:
sb.append("FR");
break;
case Calendar.SATURDAY:
sb.append("SA");
break;
case Calendar.SUNDAY:
sb.append("SU");
break;
}
int daynumber = Utils.getXmlElementInt(recurrence, "daynumber", 0);
NodeList days = Utils.getXmlElements(recurrence, "day");
int daysLength = days.getLength();
if (daysLength > 0)
{
sb.append(";BYDAY=");
for (int i = 0; i < daysLength; i++)
{
if (daynumber > 1) sb.append(daynumber);
Element day = (Element) days.item(i);
String d = Utils.getXmlElementString(day);
sb.append(CalendarEntry.kolabWeekDayToWeekDay(d));
if ((i + 1) < daysLength) sb.append(",");
}
if (CalendarEntry.YEARLY.equals(cycle))
{
String month = Utils.getXmlElementString(recurrence,
"month");
if (month != null && !"".equals(month))
{
sb.append(";BYMONTH=");
sb.append(CalendarEntry.kolabMonthToMonth(month));
}
}
}
else if (daynumber != 0 && CalendarEntry.MONTHLY.equals(cycle))
{
sb.append(";BYMONTHDAY=" + daynumber);
}
int interval = Utils.getXmlElementInt(recurrence, "interval", 0);
if (interval > 1)
{
sb.append(";INTERVAL=" + interval);
}
cal.setrRule(sb.toString());
Log.d("sync", "RRule = " + cal.getrRule());
}
sync.setCacheEntry(saveCalender(cal));
}
private CacheEntry saveCalender(CalendarEntry cal)
{
cal.setCalendar_id(1);
calendarProvider.save(cal);
CacheEntry result = new CacheEntry();
result.setLocalId(cal.getId());
result.setLocalHash(cal.getLocalHash());
result.setRemoteId(cal.getUid());
return result;
}
private String getNewUid()
{
// Create Application and Type specific id
// kd == Kolab Droid, ev == event
return "kd-ev-" + UUID.randomUUID().toString();
}
@Override
protected void updateServerItemFromLocal(SyncContext sync, Document xml) throws SyncException
{
CalendarEntry source = getLocalItem(sync);
CacheEntry entry = sync.getCacheEntry();
entry.setLocalHash(source.getLocalHash());
final Date lastChanged = new Date();
entry.setRemoteChangedDate(lastChanged);
writeXml(xml, source, lastChanged);
}
private final static java.util.regex.Pattern regFREQ = java.util.regex.Pattern
.compile("FREQ=(\\w*);.*");
// private final static java.util.regex.Pattern regUntil =
// java.util.regex.Pattern
// .compile(";UNTIL=(\\w*)");
// private final static java.util.regex.Pattern regWKST =
// java.util.regex.Pattern
// .compile(";WKST=(\\w*)");
private final static java.util.regex.Pattern regBYDAY = java.util.regex.Pattern
.compile(".*;BYDAY=([\\+\\-\\,0-9A-Z]*);?.*");
private final static java.util.regex.Pattern regBYDAYSubPattern = java.util.regex.Pattern
.compile("(?:([+-]?)([\\d]*)([A-Z]{2}),?)");
private final static java.util.regex.Pattern regINTERVAL = java.util.regex.Pattern
.compile(".*;INTERVAL=(\\d*);?.*");
private final static java.util.regex.Pattern regBYMONTHDAY = java.util.regex.Pattern
.compile(".*;BYMONTHDAY=(\\d*);?.*");
private final static java.util.regex.Pattern regBYMONTH = java.util.regex.Pattern
.compile(".*;BYMONTH=(\\d*);?.*");
private void writeXml(Document xml, CalendarEntry source,
final Date lastChanged)
{
Element root = xml.getDocumentElement();
Utils.setXmlElementValue(xml, root, "uid", source.getUid());
Utils.setXmlElementValue(xml, root, "body", source.getDescription());
Utils.setXmlElementValue(xml, root, "last-modification-date", Utils
.toUtc(lastChanged));
Utils.setXmlElementValue(xml, root, "summary", source.getTitle());
Utils.setXmlElementValue(xml, root, "location", source
.getEventLocation());
//times have to be in UTC, according to
//http://www.kolab.org/doc/kolabformat-2.0rc7-html/x123.html
Time startTime = source.getDtstart();
startTime.switchTimezone("UTC");
Utils.setXmlElementValue(xml, root, "start-date", startTime
.format3339(source.getAllDay()));
if(source.getHasAlarm() != 0)
{
Utils.setXmlElementValue(xml, root, "alarm", Integer.toString(source.getReminderTime()));
}
Time endTime = source.getDtend();
endTime.switchTimezone("UTC");
Utils.setXmlElementValue(xml, root, "end-date", endTime
.format3339(source.getAllDay()));
String rrule = source.getrRule();
if (rrule != null && !"".equals(rrule))
{
Element recurrence = Utils.getOrCreateXmlElement(xml, root,
"recurrence");
Utils.deleteXmlElements(recurrence, "day");
Matcher result;
// /////////// Frequency /////////////
result = regFREQ.matcher(rrule);
String cycle = "";
if (result.matches())
{
cycle = result.group(1);
Utils.setXmlAttributeValue(xml, recurrence, "cycle", cycle
.toLowerCase());
}
// /////////// Interval /////////////
result = regINTERVAL.matcher(rrule);
if (result.matches())
{
String f = result.group(1);
Utils.setXmlElementValue(xml, recurrence, "interval", f);
}
else
{
//TODO: kmail/kontact need this default value?
Utils.setXmlElementValue(xml, recurrence, "interval", "1");
}
// /////////// weekday recurrence /////////////
String daynumber = "";
result = regBYDAY.matcher(rrule);
if (result.matches())
{
if (CalendarEntry.MONTHLY.equals(cycle)
|| CalendarEntry.YEARLY.equals(cycle))
{
Utils.setXmlAttributeValue(xml, recurrence, "type",
"weekday");
}
final String group = result.group(1);
Matcher grpResult = regBYDAYSubPattern.matcher(group);
while (grpResult.find())
{
String plusMinus = grpResult.group(1);
String d = grpResult.group(2);
if (d != null && !"".equals(d))
{
if ("-".equals(plusMinus))
{
daynumber = "4";
}
else
{
daynumber = d;
}
}
String day = CalendarEntry.weekDayToKolabWeekDay(grpResult
.group(3));
if (!"".equals(day)) Utils.addXmlElementValue(xml,
recurrence, "day", day);
}
}
// Not a weekday recurrence - must be daynumber or monthday
if ("".equals(daynumber))
{
if (CalendarEntry.MONTHLY.equals(cycle))
{
Utils.setXmlAttributeValue(xml, recurrence, "type",
"daynumber");
}
if (CalendarEntry.YEARLY.equals(cycle))
{
Utils.setXmlAttributeValue(xml, recurrence, "type",
"monthday");
}
}
// /////////// Monthday /////////////
result = regBYMONTHDAY.matcher(rrule);
if (result.matches())
{
daynumber = result.group(1);
}
// If daynumber is empty, get the daynumber from startdate for
// MONTHLY and YEARLY recurrences
if ("".equals(daynumber)
&& (CalendarEntry.MONTHLY.equals(cycle) || CalendarEntry.YEARLY
.equals(cycle)))
{
daynumber = Integer.toString(source.getDtstart().monthDay);
}
Utils.setXmlElementValue(xml, recurrence, "daynumber", daynumber);
// /////////// Month /////////////
result = regBYMONTH.matcher(rrule);
String month = "";
if (result.matches())
{
month = CalendarEntry.monthToKolabMonth(result.group(1));
}
// Get month only for YEARLY recurrences from startdate
if (CalendarEntry.YEARLY.equals(cycle) && "".equals(month))
{
month = CalendarEntry.monthToKolabMonth(Integer.toString(source
.getDtstart().month + 1)); // 0-11
}
Utils.setXmlElementValue(xml, recurrence, "month", month);
// TODO: Android does not know until?
// we will use a "never-ending" event for now
Element range = Utils.getOrCreateXmlElement(xml, recurrence, "range");
Utils.setXmlAttributeValue(xml, range, "type", "none");
}
}
@Override
protected String writeXml(SyncContext sync)
throws ParserConfigurationException, SyncException
{
CalendarEntry source = getLocalItem(sync);
CacheEntry entry = sync.getCacheEntry();
entry.setLocalHash(source.getLocalHash());
final Date lastChanged = new Date();
entry.setRemoteChangedDate(lastChanged);
final String newUid = getNewUid();
entry.setRemoteId(newUid);
source.setUid(newUid);
Document xml = Utils.newDocument("event");
writeXml(xml, source, lastChanged);
return Utils.getXml(xml);
}
private CalendarEntry getLocalItem(SyncContext sync) throws SyncException
{
if (sync.getLocalItem() != null) return (CalendarEntry) sync
.getLocalItem();
CalendarEntry c = calendarProvider.loadCalendarEntry(sync
.getCacheEntry().getLocalId(), sync.getCacheEntry().getRemoteId());
sync.setLocalItem(c);
return c;
}
@Override
protected String getMessageBodyText(SyncContext sync) throws SyncException
{
CalendarEntry cal = getLocalItem(sync);
StringBuilder sb = new StringBuilder();
sb.append(cal.getTitle());
sb.append("\n");
sb.append("Location: ");
sb.append(cal.getEventLocation());
sb.append("\n");
sb.append("Start: ");
sb.append(cal.getDtstart().format("%c")); // TODO: Change format for
// allDay events
sb.append("\n");
sb.append("End: ");
sb.append(cal.getDtend().format("%c"));// TODO: Change format for allDay
// events
sb.append("\n");
sb.append("Recurrence: ");
sb.append(cal.getrRule());
sb.append("\n");
sb.append("-----\n");
sb.append(cal.getDescription());
return sb.toString();
}
@Override
public String getItemText(SyncContext sync) throws MessagingException
{
if (sync.getLocalItem() != null)
{
CalendarEntry item = (CalendarEntry) sync.getLocalItem();
return item.getTitle() + ": " + item.getDtstart().toString();
}
else
{
return sync.getMessage().getSubject();
}
}
}