/* * Funambol is a mobile platform developed by Funambol, Inc. * Copyright (C) 2010 Funambol, Inc. * * This program is free software; you can redistribute it and/or modify it under * the terms of the GNU Affero General Public License version 3 as published by * the Free Software Foundation with the addition of the following permission * added to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED * WORK IN WHICH THE COPYRIGHT IS OWNED BY FUNAMBOL, FUNAMBOL DISCLAIMS THE * WARRANTY OF NON INFRINGEMENT OF THIRD PARTY RIGHTS. * * This program 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 Affero General Public License * along with this program; if not, see http://www.gnu.org/licenses or write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * MA 02110-1301 USA. * * You can contact Funambol, Inc. headquarters at 643 Bair Island Road, Suite * 305, Redwood City, CA 94063, USA, or at email address info@funambol.com. * * The interactive user interfaces in modified source and object code versions * of this program must display Appropriate Legal Notices, as required under * Section 5 of the GNU Affero General Public License version 3. * * In accordance with Section 7(b) of the GNU Affero General Public License * version 3, these Appropriate Legal Notices must retain the display of the * "Powered by Funambol" logo. If the display of the logo is not reasonably * feasible for technical reasons, the Appropriate Legal Notices must display * the words "Powered by Funambol". */ package de.chbosync.android.syncmlclient.source.pim.task; import java.io.ByteArrayInputStream; import java.io.IOException; import java.util.Enumeration; import java.util.TimeZone; import java.util.Vector; import android.content.ContentUris; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.net.Uri; import com.funambol.client.source.AppSyncSource; import com.funambol.common.pim.icalendar.ICalendarSyntaxParser; import com.funambol.common.pim.model.calendar.Event; import com.funambol.common.pim.model.calendar.RecurrencePattern; import com.funambol.common.pim.model.calendar.Task; import com.funambol.common.pim.model.common.Property; import com.funambol.common.pim.model.common.PropertyWithTimeZone; import com.funambol.common.pim.model.converter.ConverterException; import com.funambol.common.pim.model.converter.VCalendarConverter; import com.funambol.common.pim.model.icalendar.ICalendarSyntaxParserListenerImpl; import com.funambol.common.pim.model.model.VCalendar; import com.funambol.common.pim.vcalendar.CalendarUtils; import com.funambol.util.DateUtil; import com.funambol.util.Log; import com.funambol.util.StringUtil; import de.chbosync.android.syncmlclient.source.AbstractDataManager; import de.chbosync.android.syncmlclient.source.pim.calendar.Calendar; public class AstridTaskManager extends AbstractDataManager<Calendar> { /** Log entries tag */ private static final String TAG_LOG = "AstridTaskManager"; /** Native calendar authority. Value calendar[String] */ //public static final String AUTHORITY = "com.todoroo.astrid"; public static final String AUTHORITY = "com.eztransition.tasquid"; private AppSyncSource appSource = null; /** * Final representation of calendar properties for calendar provider. */ public static final class Tasks { //public static final Uri CONTENT_URI = Uri.parse("content://com.todoroo.astrid/tasks"); public static final Uri CONTENT_URI = Uri.parse("content://com.eztransition.tasquid/tasks"); public static final String _ID = "_id"; public static final String TITLE = "title"; public static final String IMPORTANCE = "importance"; public static final String DUE_DATE = "dueDate"; // unix time public static final String COMPLETED = "completed"; // unix time public static final String NOTES = "notes"; public static final String RECURRENCE = "recurrence"; public static final String FLAGS = "flags"; public static final int IMPORTANCE_DO_OR_DIE = 0; public static final int IMPORTANCE_MUST_DO = 1; public static final int IMPORTANCE_SHOULD_DO = 2; public static final int IMPORTANCE_NONE = 3; public static final int FLAG_REPEAT_AFTER_COMPLETION = 1; public static final String[] PROJECTION = { _ID, TITLE, IMPORTANCE, DUE_DATE, COMPLETED, NOTES, FLAGS, RECURRENCE }; } /** * Default constructor. * @param context the Context object * @param appSource the AppSyncSource object to be related to this manager */ public AstridTaskManager(Context context, AppSyncSource appSource) { super(context); this.appSource = appSource; } /** * Accessor method: get the calendar authority that manages calendars in the * system * @return String the String formatted representation of the authority */ protected String getAuthority() { return AUTHORITY; } /** * Load a particular calendar entry * @param key the long formatted entry key to load * @return Calendar the Calendar object related to that entry * @throws IOException if anything went wrong accessing the calendar db */ public Calendar load(String key) throws IOException { if (Log.isLoggable(Log.TRACE)) { Log.trace(TAG_LOG, "Loading Task: " + key); } long id; try { id = Long.parseLong(key); } catch (Exception e) { Log.error(TAG_LOG, "Invalid key: " + key, e); throw new IOException("Invalid key: " + key); } Calendar cal = new Calendar(); cal.setId(id); Uri uri = ContentUris.withAppendedId(Tasks.CONTENT_URI, id); Cursor cursor = resolver.query(uri, null, null, null, null); try { if(cursor != null && cursor.moveToFirst()) { loadTaskFields(cursor, cal, id); } else { // Item not found throw new IOException("Cannot find event " + key); } } finally { cursor.close(); } return cal; } /** * Add a Calendar item to the db * @param item the Calendar object to be added * @return long the key given to the added calendar * @throws IOException if anything went wrong accessing the calendar db */ @Override public String add(Calendar item) throws IOException { if (Log.isLoggable(Log.TRACE)) { Log.trace(TAG_LOG, "Adding Task"); } Task task = item.getTask(); ContentValues cv = createTaskContentValues(task); Uri taskUri = resolver.insert(Tasks.CONTENT_URI, cv); long id = Long.parseLong(taskUri.getLastPathSegment()); if (Log.isLoggable(Log.TRACE)) { Log.trace(TAG_LOG, "The new task has id: " + id); } //addReminders(item, id); return "" + id; } /** * Update a Calendar item to the db * @param id the calendar key that represents the calendar to be updated * @param newItem the Calendar object taht must replace the existing one * @throws IOException if anything went wrong accessing the calendar db */ @Override public void update(String key, Calendar newItem) throws IOException { if (Log.isLoggable(Log.TRACE)) { Log.trace(TAG_LOG, "Updating task: " + key); } long id; try { id = Long.parseLong(key); } catch(Exception e) { Log.error(TAG_LOG, "Invalid item key " + key, e); throw new IOException("Invalid item key"); } // If the contact does not exist, then we perform an add if (!exists(key)) { if (Log.isLoggable(Log.INFO)) { Log.info(TAG_LOG, "Tried to update a non existing event. Creating a new one "); } add(newItem); return; } Task task = newItem.getTask(); ContentValues cv = createTaskContentValues(task); Uri uri = ContentUris.withAppendedId(Tasks.CONTENT_URI, id); resolver.update(uri, cv, null, null); // TODO: update reminders } /** * Delete a Calendar item to the db * @param id the calendar key that must be deleted * @throws IOException if anything went wrong accessing the calendar db */ public void delete(String key) throws IOException { if (Log.isLoggable(Log.TRACE)) { Log.trace(TAG_LOG, "Deleting task with id: " + key); } long itemId; try { itemId = Long.parseLong(key); } catch (Exception e) { Log.error(TAG_LOG, "Invalid item key " + key, e); throw new IOException("Invalid item key"); } Uri uri = ContentUris.withAppendedId(Tasks.CONTENT_URI, itemId); int count = resolver.delete(uri, null, null); if (Log.isLoggable(Log.DEBUG)) { Log.debug(TAG_LOG, "Deleted task count: " + count); } if (count < 0) { Log.error(TAG_LOG, "Cannot delete task"); throw new IOException("Cannot delete task"); } } /** * Delete all calendars from the calendar db * @throws IOException if anything went wrong accessing the calendar db */ public void deleteAll() throws IOException { if (Log.isLoggable(Log.TRACE)) { Log.trace(TAG_LOG, "Deleting all tasks"); } Enumeration keys = getAllKeys(); while(keys.hasMoreElements()) { String key = (String)keys.nextElement(); delete(key); } } /** * Check if a calendar with the given id exists in the calendar db * @param id the id which existence is to be checked * @return true if the given id exists in the db false otherwise */ public boolean exists(String key) { long id; try { id = Long.parseLong(key); } catch (Exception e) { Log.error(TAG_LOG, "Invalid item key " + key, e); return false; } Uri uri = ContentUris.withAppendedId(Tasks.CONTENT_URI, id); Cursor cur = resolver.query(uri, null, null, null, null); if(cur == null) { return false; } boolean found = cur.getCount() > 0; cur.close(); return found; } /** * Get all of the calendar keys that exist into the DB * @return Enumeration the enumeration object that contains alll of the * calendar keys * @throws IOException if anything went wrong accessing the calendar db */ public Enumeration getAllKeys() throws IOException { String cols[] = {Tasks._ID}; Cursor cursor = resolver.query(Tasks.CONTENT_URI, cols, null, null, null); try { int size = cursor.getCount(); Vector<String> itemKeys = new Vector<String>(size); if (!cursor.moveToFirst()) { return itemKeys.elements(); } for (int i = 0; i < size; i++) { String key = cursor.getString(0); if (Log.isLoggable(Log.TRACE)) { Log.trace(TAG_LOG, "Found item with key: " + key); } itemKeys.addElement(key); cursor.moveToNext(); } return itemKeys.elements(); } catch (Exception e) { Log.error(TAG_LOG, "Cannot get all items keys: ", e); throw new IOException("Cannot get all items keys"); } finally { cursor.close(); } } public Vector<com.funambol.syncml.protocol.Property> getSupportedProperties() { // TODO: FIXME return null; } private void loadTaskFields(Cursor cursor, Calendar cal, long key) { Task task = new Task(); // Load TITLE String name = cursor.getString(cursor.getColumnIndex(Tasks.TITLE)); if(name != null) { task.setSummary(new Property(name)); } // Load COMPLETED long completed = cursor.getLong(cursor.getColumnIndex(Tasks.COMPLETED)); if (completed > 0) { Property completedProp = new Property(); completedProp.setPropertyValue(Boolean.TRUE); task.setComplete(completedProp); // Set also the completed date String d = DateUtil.formatDateTimeUTC(completed); Property completedDate = new Property(d); task.setDateCompleted(completedDate); } // Load NOTES (mapped to description) String notes = cursor.getString(cursor.getColumnIndex(Tasks.NOTES)); if(notes != null) { task.setDescription(new Property(notes)); } // Load DUE DATE (mapped to DtEnd) long due = cursor.getLong(cursor.getColumnIndex(Tasks.DUE_DATE)); String dtEnd = null; if (due > 0) { dtEnd = DateUtil.formatDateTimeUTC(due); task.setDtEnd(new PropertyWithTimeZone(dtEnd, "GMT")); } // Load importance long importance = cursor.getLong(cursor.getColumnIndex(Tasks.IMPORTANCE)); // Load recurrence String recurrence = cursor.getString(cursor.getColumnIndex(Tasks.RECURRENCE)); if (!StringUtil.isNullOrEmpty(recurrence)) { try { if (recurrence.startsWith("RRULE:")) { recurrence = recurrence.substring(6); recurrence = recurrence.trim(); } // We need to understand if the task repeats after the due date // or the completion one int flags = cursor.getInt(cursor.getColumnIndexOrThrow(Tasks.FLAGS)); if ((flags & Tasks.FLAG_REPEAT_AFTER_COMPLETION) != 0) { // How do we represent this in VCalendar??? Not sure we // can... Log.error(TAG_LOG, "Unsupported repeat rule based on completion date which is unknown"); } else if (dtEnd == null) { Log.error(TAG_LOG, "Unsupported repeat rule based on unknown due date"); } else { RecurrencePattern rp = createRecurrencePattern(recurrence, dtEnd); if (rp == null) { Log.error(TAG_LOG, "Cannot load recurrence"); } else { task.setRecurrencePattern(rp); } } } catch (Exception e) { Log.error(TAG_LOG, "Cannot load recurrence", e); } } /* // Load REMINDER int hasRem = cursor.getInt(cursor.getColumnIndexOrThrow(Events.HAS_ALARM)); if (hasRem == 1) { if (Log.isLoggable(Log.TRACE)) { Log.trace(TAG_LOG, "This event has an alarm associated"); } String fields[] = { Reminders.MINUTES }; String whereClause = Reminders.EVENT_ID + " = " + key; Cursor rems = resolver.query(Reminders.CONTENT_URI, fields, whereClause, null, null); if (rems != null && rems.moveToFirst()) { int mins = rems.getInt(rems.getColumnIndexOrThrow(Reminders.MINUTES)); Reminder rem = new Reminder(); rem.setMinutes(mins); rem.setActive(true); event.setReminder(rem); } else { Log.error(TAG_LOG, "Internal error: cannot find reminder for: " + key); } if (rems != null) { if(rems.moveToNext()) { Log.error(TAG_LOG, "Only one reminder is currently supported, ignoring the others"); } rems.close(); } } */ cal.setTask(task); } public Vector commit() { return null; } /** * Fills a new ContentValues objects with all the given Event's properties * @param event the event to be used to fill the ContentValue object * @return ContentValues the filled ContenValues object. * @throws IOException if anything went wrong accessing the calendar db */ private ContentValues createTaskContentValues(Task task) throws IOException { ContentValues cv = new ContentValues(); // Put title property putStringProperty(Tasks.TITLE, task.getSummary(), cv); // We must set the complete date PropertyWithTimeZone completedDate = task.getDateCompleted(); if (completedDate != null) { putDateTimeProperty(Tasks.COMPLETED, completedDate, false, cv); } // Notes putStringProperty(Tasks.NOTES, task.getDescription(), cv); // DUE DATE (mapped to DtEnd) PropertyWithTimeZone dueDate = task.getDtEnd(); if (dueDate != null) { putDateTimeProperty(Tasks.DUE_DATE, dueDate, false, cv); } // Recurrence try { putRecurrence(task, cv); } catch (Exception e) { Log.error(TAG_LOG, "Cannot convert recurrence rule", e); throw new IOException("Cannot write recurrence rule"); } /* putStringProperty(Events.DESCRIPTION, event.getDescription(), cv); putStringProperty(Events.LOCATION, event.getLocation(), cv); // Put date properties PropertyWithTimeZone start = event.getDtStart(); PropertyWithTimeZone end = event.getDtEnd(); boolean allDay = false; if(putAllDay(event, cv)) { start.setTimeZone("UTC"); putDateTimeProperty(Events.DTSTART, start, false, cv); end.setTimeZone("UTC"); allDay = true; } else { putDateTimeProperty(Events.DTSTART, start, false, cv); } // Android requires that we set DURATION or DTEND for all events Property duration = event.getDuration(); if (!Property.isEmptyProperty(duration)) { putStringProperty(Events.DURATION, duration, cv); } else if (!Property.isEmptyProperty(end)) { if (allDay) { // Android dislike events all day with a date whose hour/min/sec // are not zero (the calendar app crashes). For this reason we // save all day events with their duration // TODO FIXME: handle multi days all day events putStringProperty(Events.DURATION, new Property("P1D"), cv); } else { putDateTimeProperty(Events.DTEND, end, false, cv); } } else { // Use a default DURATION of 1 in this case putStringProperty(Events.DURATION, new Property("1"), cv); } // Put Timezone putTimeZone(event.getDtStart(), cv); // Put visibility class property putVisibilityClass(event.getAccessClass(), cv); // Put constant values cv.put(Events.HAS_ATTENDEE_DATA, 1); // Put Account reference AccountInfo account = getCalendarAccount(calendarId); cv.put(Events._SYNC_ACCOUNT, account.name); cv.put(Events._SYNC_ACCOUNT_TYPE, account.type); // Set the hasAlarm property Reminder rem = event.getReminder(); cv.put(Events.HAS_ALARM, rem != null); try { putRecurrence(event, cv); } catch (Exception e) { Log.error(TAG_LOG, "Cannot convert recurrence rule", e); throw new IOException("Cannot write recurrence rule"); } // Put Calendar references cv.put(Events.CALENDAR_ID, calendarId); */ return cv; } /** * Put a String property to the given ContentValues. * @param column the culumn to be written * @param property the property to be written into the column * @param cv the content values related to the property */ private void putStringProperty(String column, Property property, ContentValues cv) { if(property != null) { String value = property.getPropertyValueAsString(); if(value != null) { value = StringUtil.replaceAll(value, "\r\n", "\n"); value = StringUtil.replaceAll(value, "\r", "\n"); cv.put(column, value); } } } /** * Put a date time property to the given ContentValues. * @param column the culumn to be written * @param property the property to be written into the column * @param cv the content values related to the property */ private void putDateTimeProperty(String column, PropertyWithTimeZone property, boolean addOneDay, ContentValues cv) { if(property != null) { String value = property.getPropertyValueAsString(); if(value != null) { long time = CalendarUtils.getLocalDateTime(value, property.getTimeZone()); if(addOneDay) { time += CalendarUtils.DAY_FACTOR; } cv.put(column, time); } } } /** * Put the allday property to the given ContentValues. * @param event the event that contains the all day property * @param cv the content values related to the event */ private boolean putAllDay(Event event, ContentValues cv) { //int allday = event.isAllDay() ? 1 : 0; //cv.put(Events.ALL_DAY, allday); //return event.isAllDay(); return false; } /** * Put the timezone property to the given ContentValues. * @param property the TZ to be set * @param cv the contentValues where to put the TZ */ /* private void putTimeZone(PropertyWithTimeZone property, ContentValues cv) { if(property != null) { String tz = property.getTimeZone(); if(!StringUtil.isNullOrEmpty(tz)) { cv.put(Events.TIMEZONE, tz); } } } */ /** * Put the visibility class property to the given ContentValues. * @param property the visibility property container * @param cv the Content value to be updated */ /* private void putVisibilityClass(Property property, ContentValues cv) { if(property != null) { String vclass = property.getPropertyValueAsString(); if(!StringUtil.isNullOrEmpty(vclass)) { if(Events.VISIBILITY_CLASS_PRIVATE_S.equals(vclass)) { cv.put(Events.VISIBILITY_CLASS, Events.VISIBILITY_CLASS_PRIVATE); } else if(Events.VISIBILITY_CLASS_PUBLIC_S.equals(vclass)) { cv.put(Events.VISIBILITY_CLASS, Events.VISIBILITY_CLASS_PUBLIC); } } } } */ private void putRecurrence(Task task, ContentValues cv) throws ConverterException { // We basically need to transform a vCal rec rule into an iCal rec rule // This can be done in different ways. One possibility was to used the // VCalendarConverter and the VComponentWriter, but this would not give // us anu ability to modify the generated RRULE to fix issues. For this // reason that method has been discarded even if implementation wise it // would have been simpler. if (Log.isLoggable(Log.TRACE)) { Log.trace(TAG_LOG, "Saving recurrence"); } RecurrencePattern rp = task.getRecurrencePattern(); if (rp != null) { StringBuffer result = new StringBuffer(60); // Estimate 60 is needed String typeDesc = rp.getTypeDesc(); if (typeDesc != null) { result.append("FREQ="); if ("D".equals(typeDesc)) { result.append("DAILY"); } else if ("W".equals(typeDesc)) { result.append("WEEKLY"); } else if ("YM".equals(typeDesc)) { result.append("YEARLY"); } else if ("D".equals(typeDesc)) { result.append("YEARLY"); } else if ("YD".equals(typeDesc)) { result.append("YEARLY"); } else if ("MP".equals(typeDesc) || "MD".equals(typeDesc)) { // This ia by position recurrence result.append("MONTHLY"); } result.append(";INTERVAL=").append(rp.getInterval()); } if (rp.getOccurrences() != -1 && rp.isNoEndDate()) { result.append(";COUNT=").append(rp.getOccurrences()); } if (!rp.isNoEndDate() && rp.getEndDatePattern() != null && !rp.getEndDatePattern().equals("")) { result.append(";UNTIL=").append(rp.getEndDatePattern()); } if ("W".equals(typeDesc)) { StringBuffer days = new StringBuffer(); for (int i=0; i<rp.getDayOfWeek().size(); i++) { if (days.length() > 0) { days.append(","); } days.append(rp.getDayOfWeek().get(i)); } if (days.length() > 0) { result.append(";BYDAY=").append(days.toString()); } } else if ("MD".equals(typeDesc)) { if (Log.isLoggable(Log.TRACE)) { Log.trace(TAG_LOG, "getDayOfMonth=" + rp.getDayOfMonth()); } result.append(";BYMONTHDAY=").append(rp.getDayOfMonth()); } else if ("MP".equals(typeDesc)) { int instance = rp.getInstance(); short mask = rp.getDayOfWeekMask(); StringBuffer daysOfWeek = new StringBuffer(); if ((mask & RecurrencePattern.DAY_OF_WEEK_SUNDAY) != 0) { addDayOfWeek(daysOfWeek, instance, "SU"); } if ((mask & RecurrencePattern.DAY_OF_WEEK_MONDAY) != 0) { addDayOfWeek(daysOfWeek, instance, "MO"); } if ((mask & RecurrencePattern.DAY_OF_WEEK_TUESDAY) != 0) { addDayOfWeek(daysOfWeek, instance, "TU"); } if ((mask & RecurrencePattern.DAY_OF_WEEK_WEDNESDAY) != 0) { addDayOfWeek(daysOfWeek, instance, "WE"); } if ((mask & RecurrencePattern.DAY_OF_WEEK_THURSDAY) != 0) { addDayOfWeek(daysOfWeek, instance, "TH"); } if ((mask & RecurrencePattern.DAY_OF_WEEK_FRIDAY) != 0) { addDayOfWeek(daysOfWeek, instance, "FR"); } if ((mask & RecurrencePattern.DAY_OF_WEEK_SATURDAY) != 0) { addDayOfWeek(daysOfWeek, instance, "SA"); } if (daysOfWeek.length() > 0) { result.append(";BYDAY=").append(daysOfWeek.toString()); } } else if ("YM".equals(typeDesc)) { short monthOfYear = rp.getMonthOfYear(); if (monthOfYear > 0) { result.append(";BYMONTH=").append(monthOfYear); } } else if ("YD".equals(typeDesc)) { // This is not supported by the calendar model } // Add the RRULE field String rule = result.toString(); if (Log.isLoggable(Log.INFO)) { Log.info(TAG_LOG, "Setting rrule in task to: " + rule); } cv.put(Tasks.RECURRENCE, rule); } } private void addDayOfWeek(StringBuffer dayOfWeek, int instance, String day) { if (dayOfWeek.length() > 0) { dayOfWeek.append(","); } if (instance != 1 && instance != 0) { dayOfWeek.append(instance); } dayOfWeek.append(day); } private RecurrencePattern createRecurrencePattern(String rrule, String dueDate) throws Exception { // We must parse an ICalendar recurrence StringBuffer todo = new StringBuffer(); todo.append("BEGIN:VCALENDAR\r\n") .append("VERSION:2.0\r\n") .append("BEGIN:VTODO\r\n") .append("SUMMARY:Conversion todo\r\n") .append("DUE:").append(dueDate).append("\r\n") .append("RRULE:").append(rrule).append("\r\n") .append("END:VTODO\r\n") .append("END:VCALENDAR\r\n"); ByteArrayInputStream buffer = new ByteArrayInputStream(todo.toString().getBytes()); VCalendar vcalendar = new VCalendar(); ICalendarSyntaxParserListenerImpl listener = new ICalendarSyntaxParserListenerImpl(vcalendar); ICalendarSyntaxParser parser = new ICalendarSyntaxParser(buffer); parser.setListener(listener); parser.parse(); vcalendar.addProperty("VERSION", "2.0"); VCalendarConverter vcf = new VCalendarConverter(TimeZone.getDefault(), "UTF-8", false); Task t = vcf.vcalendar2calendar(vcalendar).getTask(); return t.getRecurrencePattern(); } }