diff options
author | Rohit Yengisetty <rohit@cyngn.com> | 2014-11-20 14:09:53 -0800 |
---|---|---|
committer | Rohit Yengisetty <rohit@cyngn.com> | 2014-12-04 11:20:00 -0800 |
commit | dd9e1fe5cdd5799256696b88ef42c5e7f701af58 (patch) | |
tree | ddaa526aeecc1a23311d9d29f58233e95d841aaf | |
parent | 88cda7db95cc9e05ea1a3a2cd355f89ca98dada3 (diff) | |
download | android_packages_apps_Calendar-dd9e1fe5cdd5799256696b88ef42c5e7f701af58.tar.gz android_packages_apps_Calendar-dd9e1fe5cdd5799256696b88ef42c5e7f701af58.tar.bz2 android_packages_apps_Calendar-dd9e1fe5cdd5799256696b88ef42c5e7f701af58.zip |
Calendar - Add the ability to share calendar events through an ics file.
iCal specification rescources:
http://build.mnode.org/projects/ical4j/apidocs/index.html
http://www.kanzaki.com/docs/ical/
iCal validators:
http://icalvalid.cloudapp.net
http://severinghaus.org/projects/icv/
Change-Id: I87b12fd37aa7e3ad29bea79d4dbb152f868ff4f8
-rw-r--r-- | res/drawable-hdpi/ic_action_share.png | bin | 0 -> 647 bytes | |||
-rw-r--r-- | res/drawable-mdpi/ic_action_share.png | bin | 0 -> 472 bytes | |||
-rw-r--r-- | res/drawable-xhdpi/ic_action_share.png | bin | 0 -> 785 bytes | |||
-rw-r--r-- | res/drawable-xxhdpi/ic_action_share.png | bin | 0 -> 1094 bytes | |||
-rw-r--r-- | res/menu/event_info_title_bar.xml | 5 | ||||
-rw-r--r-- | res/values/strings.xml | 3 | ||||
-rw-r--r-- | src/com/android/calendar/AllInOneActivity.java | 37 | ||||
-rw-r--r-- | src/com/android/calendar/EventInfoFragment.java | 103 | ||||
-rw-r--r-- | src/com/android/calendar/icalendar/Attendee.java | 77 | ||||
-rw-r--r-- | src/com/android/calendar/icalendar/IcalendarUtils.java | 183 | ||||
-rw-r--r-- | src/com/android/calendar/icalendar/Organizer.java | 42 | ||||
-rw-r--r-- | src/com/android/calendar/icalendar/VCalendar.java | 113 | ||||
-rw-r--r-- | src/com/android/calendar/icalendar/VEvent.java | 176 |
13 files changed, 737 insertions, 2 deletions
diff --git a/res/drawable-hdpi/ic_action_share.png b/res/drawable-hdpi/ic_action_share.png Binary files differnew file mode 100644 index 00000000..8a6cbfea --- /dev/null +++ b/res/drawable-hdpi/ic_action_share.png diff --git a/res/drawable-mdpi/ic_action_share.png b/res/drawable-mdpi/ic_action_share.png Binary files differnew file mode 100644 index 00000000..bff81179 --- /dev/null +++ b/res/drawable-mdpi/ic_action_share.png diff --git a/res/drawable-xhdpi/ic_action_share.png b/res/drawable-xhdpi/ic_action_share.png Binary files differnew file mode 100644 index 00000000..2f6dc413 --- /dev/null +++ b/res/drawable-xhdpi/ic_action_share.png diff --git a/res/drawable-xxhdpi/ic_action_share.png b/res/drawable-xxhdpi/ic_action_share.png Binary files differnew file mode 100644 index 00000000..3e441000 --- /dev/null +++ b/res/drawable-xxhdpi/ic_action_share.png diff --git a/res/menu/event_info_title_bar.xml b/res/menu/event_info_title_bar.xml index 8dbcfa43..75623a44 100644 --- a/res/menu/event_info_title_bar.xml +++ b/res/menu/event_info_title_bar.xml @@ -16,6 +16,11 @@ <menu xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:id="@+id/info_action_share_event" + android:alphabeticShortcut="s" + android:title="@string/share_label" + android:icon="@drawable/ic_action_share" + android:showAsAction="always" /> <item android:id='@+id/info_action_change_color' android:alphabeticShortcut="c" android:title="@string/choose_event_color_label" diff --git a/res/values/strings.xml b/res/values/strings.xml index caec61b5..1bd97e50 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -208,6 +208,8 @@ <string name="creating_event_with_guest">"Invitations will be sent."</string> <!-- Toast message displayed when an existing event with guests is saved after being modified --> <string name="saving_event_with_guest">"Updates will be sent."</string> + <!-- Toast message displayed when ics file to share an event can't be generated --> + <string name="error_generating_ics">"Problems encountered while sharing event"</string> <!-- Toast message displayed responding to an event from an email as accepted [CHAR LIMIT=50] --> @@ -389,6 +391,7 @@ <!-- This is a menu button for switching into edit mode when viewing an event. [CHAR LIMIT=10] --> <string name="edit_label">"Edit"</string> + <string name="share_label">"Share"</string> <!-- The button label for deleting an event --> <string name="delete_label">"Delete"</string> <!-- A menu item for deleting an event --> diff --git a/src/com/android/calendar/AllInOneActivity.java b/src/com/android/calendar/AllInOneActivity.java index 17aaff1b..fcf2b0cd 100644 --- a/src/com/android/calendar/AllInOneActivity.java +++ b/src/com/android/calendar/AllInOneActivity.java @@ -37,7 +37,6 @@ import android.content.AsyncQueryHandler; import android.content.BroadcastReceiver; import android.content.ContentResolver; import android.content.ContentUris; -import android.content.Context; import android.content.CursorLoader; import android.content.DialogInterface; import android.content.Loader; @@ -51,6 +50,7 @@ import android.database.Cursor; import android.graphics.drawable.LayerDrawable; import android.net.Uri; import android.os.Bundle; +import android.os.Environment; import android.os.Handler; import android.provider.CalendarContract; import android.provider.CalendarContract.Attendees; @@ -81,6 +81,7 @@ import com.android.calendar.agenda.AgendaFragment; import com.android.calendar.month.MonthByWeekFragment; import com.android.calendar.selectcalendars.SelectVisibleCalendarsFragment; +import java.io.File; import java.io.IOException; import java.util.Calendar; import java.util.List; @@ -446,6 +447,9 @@ public class AllInOneActivity extends AbstractCalendarActivity implements EventH if (getResources().getBoolean(R.bool.show_delete_events_menu)) { getLoaderManager().initLoader(0, null, this); } + + // clean up cached ics files - in case onDestroy() didn't run the last time + cleanupCachedIcsFiles(); } private long parseViewAction(final Intent intent) { @@ -633,6 +637,37 @@ public class AllInOneActivity extends AbstractCalendarActivity implements EventH mController.deregisterAllEventHandlers(); CalendarController.removeInstance(this); + + // clean up cached ics files + cleanupCachedIcsFiles(); + } + + /** + * Cleans up the temporarily generated ics files in the cache directory + * The files are of the format *.ics + */ + private void cleanupCachedIcsFiles() { + if (!isExternalStorageWritable()) return; + File cacheDir = getExternalCacheDir(); + File[] files = cacheDir.listFiles(); + if (files == null) return; + for (File file : files) { + String filename = file.getName(); + if (filename.endsWith(".ics")) { + file.delete(); + } + } + } + + /** + * Checks if external storage is available for read and write + */ + public boolean isExternalStorageWritable() { + String state = Environment.getExternalStorageState(); + if (Environment.MEDIA_MOUNTED.equals(state)) { + return true; + } + return false; } private void initFragments(long timeMillis, int viewType, Bundle icicle) { diff --git a/src/com/android/calendar/EventInfoFragment.java b/src/com/android/calendar/EventInfoFragment.java index f72938e7..85be389d 100644 --- a/src/com/android/calendar/EventInfoFragment.java +++ b/src/com/android/calendar/EventInfoFragment.java @@ -103,12 +103,18 @@ import com.android.calendar.event.EditEventActivity; import com.android.calendar.event.EditEventHelper; import com.android.calendar.event.EventColorPickerDialog; import com.android.calendar.event.EventViewUtils; +import com.android.calendar.icalendar.IcalendarUtils; +import com.android.calendar.icalendar.Organizer; +import com.android.calendar.icalendar.VCalendar; +import com.android.calendar.icalendar.VEvent; import com.android.calendarcommon2.DateException; import com.android.calendarcommon2.Duration; import com.android.calendarcommon2.EventRecurrence; import com.android.colorpicker.ColorPickerSwatch.OnColorSelectedListener; import com.android.colorpicker.HsvColorComparator; +import java.io.File; +import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -412,7 +418,6 @@ public class EventInfoFragment extends DialogFragment implements OnCheckedChange private QueryHandler mHandler; - private final Runnable mTZUpdater = new Runnable() { @Override public void run() { @@ -1252,10 +1257,106 @@ public class EventInfoFragment extends DialogFragment implements OnCheckedChange mDeleteHelper.delete(mStartMillis, mEndMillis, mEventId, -1, onDeleteRunnable); } else if (itemId == R.id.info_action_change_color) { showEventColorPickerDialog(); + } else if (itemId == R.id.info_action_share_event) { + shareEvent(); } return super.onOptionsItemSelected(item); } + /** + * Generates an .ics formatted file with the event info and launches intent chooser to + * share said file + */ + private void shareEvent() { + // Create the respective ICalendar objects from the event info + VCalendar calendar = new VCalendar(); + calendar.addProperty(VCalendar.VERSION, "2.0"); + calendar.addProperty(VCalendar.PRODID, VCalendar.PRODUCT_IDENTIFIER); + calendar.addProperty(VCalendar.CALSCALE, "GREGORIAN"); + calendar.addProperty(VCalendar.METHOD, "REQUEST"); + + VEvent event = new VEvent(); + mEventCursor.moveToFirst(); + // add event start and end datetime + if (!mAllDay) { + String eventTimeZone = mEventCursor.getString(EVENT_INDEX_EVENT_TIMEZONE); + event.addEventStart(mStartMillis, eventTimeZone); + event.addEventEnd(mEndMillis, eventTimeZone); + } else { + // All-day events' start and end time are stored as UTC. + // Treat the event start and end time as being in the local time zone and convert them + // to the corresponding UTC datetime. If the UTC time is used as is, the ical recipients + // will report the wrong start and end time (+/- 1 day) for the event as they will + // convert the UTC time to their respective local time-zones + String localTimeZone = Utils.getTimeZone(mActivity, mTZUpdater); + long eventStart = IcalendarUtils.convertTimeToUtc(mStartMillis, localTimeZone); + long eventEnd = IcalendarUtils.convertTimeToUtc(mEndMillis, localTimeZone); + event.addEventStart(eventStart, "UTC"); + event.addEventEnd(eventEnd, "UTC"); + } + + event.addProperty(VEvent.LOCATION, mEventCursor.getString(EVENT_INDEX_EVENT_LOCATION)); + event.addProperty(VEvent.DESCRIPTION, mEventCursor.getString(EVENT_INDEX_DESCRIPTION)); + event.addProperty(VEvent.SUMMARY, mEventCursor.getString(EVENT_INDEX_TITLE)); + event.addOrganizer(new Organizer(mEventOrganizerDisplayName, mEventOrganizerEmail)); + + // Add Attendees to event + for (Attendee attendee : mAcceptedAttendees) { + IcalendarUtils.addAttendeeToEvent(attendee, event); + } + + for (Attendee attendee : mDeclinedAttendees) { + IcalendarUtils.addAttendeeToEvent(attendee, event); + } + + for (Attendee attendee : mTentativeAttendees) { + IcalendarUtils.addAttendeeToEvent(attendee, event); + } + + for (Attendee attendee : mNoResponseAttendees) { + IcalendarUtils.addAttendeeToEvent(attendee, event); + } + + // compose all of the ICalendar objects + calendar.addEvent(event); + + // create and share ics file + boolean isShareSuccessful = false; + try { + // event title serves as the file name prefix + String filePrefix = event.getProperty(VEvent.SUMMARY); + if (filePrefix == null || filePrefix.length() < 3) { + // default to a generic filename if event title doesn't qualify + // prefix length constraint is imposed by File#createTempFile + filePrefix = "invite"; + } + File inviteFile = File.createTempFile(filePrefix, ".ics", + mActivity.getExternalCacheDir()); + if (IcalendarUtils.writeCalendarToFile(calendar, inviteFile)) { + inviteFile.setReadable(true,false); // set world-readable + Intent shareIntent = new Intent(); + shareIntent.setAction(Intent.ACTION_SEND); + shareIntent.putExtra(Intent.EXTRA_STREAM, Uri.fromFile(inviteFile)); + // the ics file is sent as an extra, the receiving application decides whether to + // parse the file to extract calendar events or treat it as a regular file + shareIntent.setType("application/octet-stream"); + startActivity(shareIntent); + isShareSuccessful = true; + + } else { + // error writing event info to file + isShareSuccessful = false; + } + } catch (IOException e) { + isShareSuccessful = false; + } + + if (!isShareSuccessful) { + Log.e(TAG, "Couldn't generate ics file"); + Toast.makeText(mActivity, R.string.error_generating_ics, Toast.LENGTH_SHORT).show(); + } + } + private void showEventColorPickerDialog() { if (mColorPickerDialog == null) { mColorPickerDialog = EventColorPickerDialog.newInstance(mColors, mCurrentColor, diff --git a/src/com/android/calendar/icalendar/Attendee.java b/src/com/android/calendar/icalendar/Attendee.java new file mode 100644 index 00000000..fc8c78bb --- /dev/null +++ b/src/com/android/calendar/icalendar/Attendee.java @@ -0,0 +1,77 @@ +/** + * Copyright (C) 2014 The CyanogenMod Project + */ + +package com.android.calendar.icalendar; + +import java.util.HashMap; + +/** + * Models the Attendee component of a calendar event + */ +public class Attendee { + + // property strings + // TODO: only a partial list of attributes have been implemented, implement the rest + public static String CN = "CN"; // Attendee Name + public static String PARTSTAT = "PARTSTAT"; // Participant Status (Attending , Declined .. ) + public static String RSVP = "RSVP"; + public static String ROLE = "ROLE"; + public static String CUTYPE = "CUTYPE"; + + + private static HashMap<String, Integer> sPropertyList = new HashMap<String, Integer>(); + // initialize the approved list of mProperties for a calendar event + static { + sPropertyList.put(CN,1); + sPropertyList.put(PARTSTAT, 1); + sPropertyList.put(RSVP, 1); + sPropertyList.put(ROLE, 1); + sPropertyList.put(CUTYPE, 1); + } + + public HashMap<String, String> mProperties; // stores (property, value) pairs + public String mEmail; + + public Attendee() { + mProperties = new HashMap<String, String>(); + } + + /** + * Add Attendee properties + * @param property + * @param value + * @return + */ + public boolean addProperty(String property, String value) { + // only unary properties for now + if (sPropertyList.containsKey(property) && sPropertyList.get(property) == 1 && + value != null) { + mProperties.put(property, value); + return true; + } + return false; + } + + /** + * Returns an iCal formatted string of the Attendee component + * @return + */ + public String getICalFormattedString() { + StringBuilder output = new StringBuilder(); + + // Add Event mProperties + output.append("ATTENDEE;"); + for (String property : mProperties.keySet() ) { + // append properties in the following format: attribute=value; + output.append(property + "=" + mProperties.get(property) + ";"); + } + output.append("X-NUM-GUESTS=0:mailto:" + mEmail); + + output = IcalendarUtils.enforceICalLineLength(output); + + output.append("\n"); + return output.toString(); + } + +} diff --git a/src/com/android/calendar/icalendar/IcalendarUtils.java b/src/com/android/calendar/icalendar/IcalendarUtils.java new file mode 100644 index 00000000..ef96674b --- /dev/null +++ b/src/com/android/calendar/icalendar/IcalendarUtils.java @@ -0,0 +1,183 @@ +/** + * Copyright (C) 2014 The CyanogenMod Project + */ + +package com.android.calendar.icalendar; + +import android.provider.CalendarContract; +import android.util.Log; +import com.android.calendar.CalendarEventModel; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.TimeZone; + +/** + * Helper functions to help adhere to the iCalendar format. + */ +public class IcalendarUtils { + + private static int sPermittedLineLength = 75; // Line length mandated by iCalendar format + + /** + * ensure the string conforms to the iCalendar encoding requirements + * escape line breaks , commas and semicolons + * @param sequence + * @return + */ + public static String cleanseString(CharSequence sequence) { + if (sequence == null) return null; + String input = sequence.toString(); + + // replace new lines with the literal '\n' + input = input.replaceAll("\\r|\\n|\\r\\n", "\\\\n"); + // escape semicolons and commas + input = input.replace(";", "\\;"); + input = input.replace(",", "\\,"); + + return input; + } + + /** + * Stringify VCalendar object and write to file + * @param calendar + * @param file + * @return success status of the file write operation + */ + public static boolean writeCalendarToFile(VCalendar calendar, File file) { + if (calendar == null || file == null) return false; + String icsFormattedString = calendar.getICalFormattedString(); + FileOutputStream outStream = null; + try { + outStream = new FileOutputStream(file); + outStream.write(icsFormattedString.getBytes()); + } catch (FileNotFoundException e) { + return false; + } catch (IOException e) { + return false; + } finally { + try { + if (outStream != null) outStream.close(); + } catch (IOException ioe) { + return false; + } + } + return true; + } + + /** + * Formats the given input to adhere to the iCal line length and formatting requirements + * @param input + * @return + */ + public static StringBuilder enforceICalLineLength(StringBuilder input) { + if (input == null) return null; + StringBuilder output = new StringBuilder(); + int length = input.length(); + + // bail if no work needs to be done + if (length <= sPermittedLineLength) { + return input; + } + + for (int i = 0, currentLineLength = 0; i < length; i++) { + char currentChar = input.charAt(i); + if (currentChar == '\n') { // new line encountered + output.append(currentChar); + currentLineLength = 0; // reset char counter + + } else if (currentChar != '\n' && currentLineLength <= sPermittedLineLength) { + // a non-newline char that can be part of the current line + output.append(currentChar); + currentLineLength++; + + } else if (currentLineLength > sPermittedLineLength) { + // need to branch out to a new line + // add a new line and a space - iCal requirement + output.append("\n "); + output.append(currentChar); + currentLineLength = 2; // already has 2 chars : space and currentChar + } + } + + return output; + } + + /** + * create an iCal Attendee with properties from CalendarModel attendee + * + * @param attendee + * @param event + */ + public static void addAttendeeToEvent(CalendarEventModel.Attendee attendee, VEvent event) { + if (attendee == null || event == null) return; + Attendee vAttendee = new Attendee(); + vAttendee.addProperty(Attendee.CN, attendee.mName); + + String participationStatus; + switch (attendee.mStatus) { + case CalendarContract.Attendees.ATTENDEE_STATUS_ACCEPTED: + participationStatus = "ACCEPTED"; + break; + case CalendarContract.Attendees.ATTENDEE_STATUS_DECLINED: + participationStatus = "DECLINED"; + break; + case CalendarContract.Attendees.ATTENDEE_STATUS_TENTATIVE: + participationStatus = "TENTATIVE"; + break; + case CalendarContract.Attendees.ATTENDEE_STATUS_NONE: + default: + participationStatus = "NEEDS-ACTION"; + break; + } + vAttendee.addProperty(Attendee.PARTSTAT, participationStatus); + vAttendee.mEmail = attendee.mEmail; + + event.addAttendee(vAttendee); + } + + /** + * returns an iCalendar formatted UTC date-time + * ex: 20141120T120000Z for noon on Nov 20, 2014 + * + * @param millis in epoch time + * @param timeZone indicates the time zone of the input epoch time + * @return + */ + public static String getICalFormattedDateTime(long millis, String timeZone) { + if (millis < 0) return null; + + Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone(timeZone)); + calendar.setTimeInMillis(millis); + + SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMddHHmmss"); + simpleDateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); + String dateTime = simpleDateFormat.format(calendar.getTime()); + StringBuilder output = new StringBuilder(16); + + // iCal UTC date format : <yyyyMMdd>T<HHmmss>Z + return output.append(dateTime.subSequence(0,8)) + .append("T") + .append(dateTime.substring(8)) + .append("Z") + .toString(); + } + + /** + * Converts the time in a local time zone to UTC time + * @param millis epoch time in the local timezone + * @param localTimeZone string id of the local time zone + * @return + */ + public static long convertTimeToUtc(long millis, String localTimeZone) { + if (millis < 0) return 0; + + // remove the local time zone's UTC offset + return millis - TimeZone.getTimeZone(localTimeZone).getRawOffset(); + } +} diff --git a/src/com/android/calendar/icalendar/Organizer.java b/src/com/android/calendar/icalendar/Organizer.java new file mode 100644 index 00000000..2e039849 --- /dev/null +++ b/src/com/android/calendar/icalendar/Organizer.java @@ -0,0 +1,42 @@ +/** + * Copyright (C) 2014 The CyanogenMod Project + */ + +package com.android.calendar.icalendar; + +/** + * Event Organizer component + * Fulfils the ORGANIZER property of an Event + */ +public class Organizer { + + public String mName; + public String mEmail; + + public Organizer(String name, String email) { + if (name != null) { + mName = name; + } else { + mName = "UNKNOWN"; + } + if (email != null) { + mEmail = email; + } else { + mEmail = "UNKNOWN"; + } + } + + /** + * Returns an iCal formatted string + */ + public String getICalFormattedString() { + StringBuilder output = new StringBuilder(); + // add the organizer info + output.append("ORGANIZER;CN=" + mName + ":mailto:" + mEmail); + // enforce line length constraints + output = IcalendarUtils.enforceICalLineLength(output); + output.append("\n"); + return output.toString(); + } + +} diff --git a/src/com/android/calendar/icalendar/VCalendar.java b/src/com/android/calendar/icalendar/VCalendar.java new file mode 100644 index 00000000..00c7b81b --- /dev/null +++ b/src/com/android/calendar/icalendar/VCalendar.java @@ -0,0 +1,113 @@ +/** + * Copyright (C) 2014 The CyanogenMod Project + */ + +package com.android.calendar.icalendar; + +import java.util.HashMap; +import java.util.LinkedList; + +/** + * Models the Calendar/VCalendar component of the iCalendar format + */ +public class VCalendar { + + // valid property identifiers of the component + // TODO: only a partial list of attributes have been implemented, implement the rest + public static String VERSION = "VERSION"; + public static String PRODID = "PRODID"; + public static String CALSCALE = "CALSCALE"; + public static String METHOD = "METHOD"; + + public final static String PRODUCT_IDENTIFIER = "-//Cyanogen Inc//com.android.calendar"; + + // stores the -arity of the attributes that this component can have + private final static HashMap<String, Integer> sPropertyList = new HashMap<String, Integer>(); + + // initialize approved list of iCal Calendar properties + static { + sPropertyList.put(VERSION, 1); + sPropertyList.put(PRODID, 1); + sPropertyList.put(CALSCALE, 1); + sPropertyList.put(METHOD, 1); + } + + // stores attributes and their corresponding values belonging to the Calendar object + public HashMap<String, String> mProperties; + public LinkedList<VEvent> mEvents; // events that belong to this Calendar object + + /** + * Constructor + */ + public VCalendar() { + mProperties = new HashMap<String, String>(); + mEvents = new LinkedList<VEvent>(); + } + + /** + * Add specified property + * @param property + * @param value + * @return + */ + public boolean addProperty(String property, String value) { + // since all the required mProperties are unary (only one can exist) , taking a shortcut here + // when multiples of a property can exist , enforce that here .. cleverly + if (sPropertyList.containsKey(property) && value != null) { + mProperties.put(property, IcalendarUtils.cleanseString(value)); + return true; + } + return false; + } + + /** + * Add Event to calendar + * @param event + */ + public void addEvent(VEvent event) { + if (event != null) mEvents.add(event); + } + + /** + * + * @return + */ + public LinkedList<VEvent> getAllEvents() { + return mEvents; + } + + /** + * Returns the iCal representation of the calendar and all of its inherent components + * @return + */ + public String getICalFormattedString() { + StringBuilder output = new StringBuilder(); + + // Add Event properties + // TODO: add the ability to specify the order in which to compose the properties + output.append("BEGIN:VCALENDAR\n"); + for (String property : mProperties.keySet() ) { + output.append(property + ":" + mProperties.get(property) + "\n"); + } + + // enforce line length requirements + output = IcalendarUtils.enforceICalLineLength(output); + // add event + for (VEvent event : mEvents) { + output.append(event.getICalFormattedString()); + } + + output.append("END:VCALENDAR\n"); + + return output.toString(); + } + + /** + * TODO: Aggressive validation of VCalendar and all of its components to ensure they conform + * to the ical specification + * @return + */ + private boolean validate() { + return false; + } +}
\ No newline at end of file diff --git a/src/com/android/calendar/icalendar/VEvent.java b/src/com/android/calendar/icalendar/VEvent.java new file mode 100644 index 00000000..1aff0bd4 --- /dev/null +++ b/src/com/android/calendar/icalendar/VEvent.java @@ -0,0 +1,176 @@ +/** + * Copyright (C) 2014 The CyanogenMod Project + */ + +package com.android.calendar.icalendar; + +import java.util.HashMap; +import java.util.LinkedList; +import java.util.UUID; + +/** + * Models the Event/VEvent component of the iCalendar format + */ +public class VEvent { + + // valid property identifiers for an event component + // TODO: only a partial list of attributes has been implemented, implement the rest + public static String CLASS = "CLASS"; + public static String CREATED = "CREATED"; + public static String LOCATION = "LOCATION"; + public static String ORGANIZER = "ORGANIZER"; + public static String PRIORITY = "PRIORITY"; + public static String SEQ = "SEQ"; + public static String STATUS = "STATUS"; + public static String UID = "UID"; + public static String URL = "URL"; + public static String DTSTART = "DTSTART"; + public static String DTEND = "DTEND"; + public static String DURATION = "DURATION"; + public static String DTSTAMP = "DTSTAMP"; + public static String SUMMARY = "SUMMARY"; + public static String DESCRIPTION = "DESCRIPTION"; + public static String ATTENDEE = "ATTENDEE"; + public static String CATEGORIES = "CATEGORIES"; + + // stores the -arity of the attributes that this component can have + private static HashMap<String, Integer> sPropertyList = new HashMap<String, Integer>(); + + // initialize the approved list of mProperties for a calendar event + static { + sPropertyList.put(CLASS,1); + sPropertyList.put(CREATED,1); + sPropertyList.put(LOCATION,1); + sPropertyList.put(ORGANIZER,1); + sPropertyList.put(PRIORITY,1); + sPropertyList.put(SEQ,1); + sPropertyList.put(STATUS,1); + sPropertyList.put(UID,1); + sPropertyList.put(URL,1); + sPropertyList.put(DTSTART,1); + sPropertyList.put(DTEND,1); + sPropertyList.put(DURATION, 1); + sPropertyList.put(DTSTAMP,1); + sPropertyList.put(SUMMARY,1); + sPropertyList.put(DESCRIPTION,1); + + sPropertyList.put(ATTENDEE, Integer.MAX_VALUE); + sPropertyList.put(CATEGORIES, Integer.MAX_VALUE); + sPropertyList.put(CATEGORIES, Integer.MAX_VALUE); + } + + // stores attributes and their corresponding values belonging to the Event component + public HashMap<String, String> mProperties; + + public LinkedList<Attendee> mAttendees; + public Organizer mOrganizer; + + /** + * Constructor + */ + public VEvent() { + mProperties = new HashMap<String, String>(); + mAttendees = new LinkedList<Attendee>(); + + // generate and add a unique identifier to this event - ical requisite + addProperty(UID , UUID.randomUUID().toString() + "@cyanogenmod.com"); + addTimeStamp(); + } + + /** + * For adding unary properties. For adding other property attributes , use the respective + * component methods to create and add these special components. + * @param property + * @param value + * @return + */ + public boolean addProperty(String property, String value) { + // only unary-properties for now + if (sPropertyList.containsKey(property) && sPropertyList.get(property) == 1 && + value != null) { + mProperties.put(property, IcalendarUtils.cleanseString(value)); + return true; + } + return false; + } + + /** + * returns the value of the requested event property or null if there isn't one + */ + public String getProperty(String property) { + return mProperties.get(property); + } + + /** + * Add attendees to the event + * @param attendee + */ + public void addAttendee(Attendee attendee) { + if(attendee != null) mAttendees.add(attendee); + } + + /** + * Add an Organizer to the Event + * @param organizer + */ + public void addOrganizer(Organizer organizer) { + if (organizer != null) mOrganizer = organizer; + } + + /** + * Add an start date-time to the event + */ + public void addEventStart(long startMillis, String timeZone) { + if (startMillis < 0) return; + + String formattedDateTime = IcalendarUtils.getICalFormattedDateTime(startMillis, timeZone); + addProperty(DTSTART, formattedDateTime); + } + + /** + * Add an end date-time for event + */ + public void addEventEnd(long endMillis, String timeZone) { + if (endMillis < 0) return; + + String formattedDateTime = IcalendarUtils.getICalFormattedDateTime(endMillis, timeZone); + addProperty(DTEND, formattedDateTime); + } + + /** + * Timestamps the events with the current date-time + */ + private void addTimeStamp() { + String formattedDateTime = IcalendarUtils.getICalFormattedDateTime( + System.currentTimeMillis(), "UTC"); + addProperty(DTSTAMP, formattedDateTime); + } + + /** + * Returns the iCal representation of the Event component + */ + public String getICalFormattedString() { + StringBuilder sb = new StringBuilder(); + + // Add Event properties + sb.append("BEGIN:VEVENT\n"); + for (String property : mProperties.keySet() ) { + sb.append(property + ":" + mProperties.get(property) + "\n"); + } + + // Enforce line length requirements + sb = IcalendarUtils.enforceICalLineLength(sb); + + sb.append(mOrganizer.getICalFormattedString()); + + // add event Attendees + for (Attendee attendee : mAttendees) { + sb.append(attendee.getICalFormattedString()); + } + + sb.append("END:VEVENT\n"); + + return sb.toString(); + } + +} |