diff --git a/.gitignore b/.gitignore index 41799c93..c9f525f0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,4 @@ demo/platforms demo/plugins *.ipr -*.iml .idea/ \ No newline at end of file diff --git a/.npmignore b/.npmignore index efd261bf..8fecf43c 100644 --- a/.npmignore +++ b/.npmignore @@ -1 +1,3 @@ -demo \ No newline at end of file +.idea +demo/ +test/ \ No newline at end of file diff --git a/README.md b/README.md index 39b7eafc..a670d69f 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,9 @@ cordova plugin add cordova-plugin-calendar --variable CALENDAR_USAGE_DESCRIPTION * Supported methods on Android 4: `find`, `create` (silent and interactive), `delete`, .. * Supported methods on Android 2 and 3: `create` interactive only: the user is presented a prefilled Calendar event. Pressing the hardware back button will give control back to your app. +### Windows 10 Mobile +* Supported methods: `createEvent`, `createEventWithOptions`, `createEventInteractively`, `createEventInteractivelyWithOptions` only interactively + ## 2. Installation ### Automatically @@ -130,24 +133,28 @@ Also, make sure you're building with Gradle by adding this to your `config.xml` The table gives an overview of basic operation compatibility: -Operation | Comment | iOS | Android ------------------------------------ | ----------- | --- | ------- -createCalendar | | yes | yes -deleteCalendar | | yes | yes -createEvent | silent | yes | yes (on Android < 4 dialog is shown) -createEventWithOptions | silent | yes | yes (on Android < 4 dialog is shown) -createEventInteractively | interactive | yes | yes -createEventInteractivelyWithOptions | interactive | yes | yes -findEvent | | yes | yes -findEventWithOptions | | yes | yes -listEventsInRange | | | yes -listCalendars | | yes | yes -findAllEventsInNamedCalendars | | yes | -modifyEvent | | yes | -modifyEventWithOptions | | yes | -deleteEvent | | yes | yes -deleteEventFromNamedCalendar | | yes | -openCalendar | | yes | yes +Operation | Comment | iOS | Android | Windows | +----------------------------------- | ----------- | --- | ------- | ------- | +createCalendar | | yes | yes | | +deleteCalendar | | yes | yes | | +createEvent | silent | yes | yes * | yes ** | +createEventWithOptions | silent | yes | yes * | yes ** | +createEventInteractively | interactive | yes | yes | yes ** | +createEventInteractivelyWithOptions | interactive | yes | yes | yes ** | +findEvent | | yes | yes | | +findEventWithOptions | | yes | yes | | +listEventsInRange | | | yes | | +listCalendars | | yes | yes | | +findAllEventsInNamedCalendars | | yes | | | +modifyEvent | | yes | | | +modifyEventWithOptions | | yes | | | +deleteEvent | | yes | yes | | +deleteEventFromNamedCalendar | | yes | | | +deleteEventById | | yes | yes | | +openCalendar | | yes | yes | | + +* \* on Android < 4 dialog is shown +* \** only interactively on windows mobile Basic operations, you'll want to copy-paste this for testing purposes: ```js @@ -235,7 +242,7 @@ Basic operations, you'll want to copy-paste this for testing purposes: newOptions.firstReminderMinutes = 120; window.plugins.calendar.modifyEventWithOptions(title,eventLocation,notes,startDate,endDate,newTitle,eventLocation,notes,startDate,endDate,filterOptions,newOptions,success,error); - // delete an event (you can pass nulls for irrelevant parameters, note that on Android `notes` is ignored). The dates are mandatory and represent a date range to delete events in. + // delete an event (you can pass nulls for irrelevant parameters). The dates are mandatory and represent a date range to delete events in. // note that on iOS there is a bug where the timespan must not be larger than 4 years, see issue 102 for details.. call this method multiple times if need be // since 4.3.0 you can match events starting with a prefix title, so if your event title is 'My app - cool event' then 'My app -' will match. window.plugins.calendar.deleteEvent(newTitle,eventLocation,notes,startDate,endDate,success,error); @@ -243,6 +250,9 @@ Basic operations, you'll want to copy-paste this for testing purposes: // delete an event, as above, but for a specific calendar (iOS only) window.plugins.calendar.deleteEventFromNamedCalendar(newTitle,eventLocation,notes,startDate,endDate,calendarName,success,error); + // delete an event by id. If the event has recurring instances, all will be deleted unless `fromDate` is specified, which will delete from that date onward. (iOS and android only) + window.plugins.calendar.deleteEventById(id,fromDate,success,error); + // open the calendar app (added in 4.2.8): // - open it at 'today' window.plugins.calendar.openCalendar(); diff --git a/package.json b/package.json index 9864aac4..52cdb80b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cordova-plugin-calendar", - "version": "4.5.5", + "version": "5.1.3", "description": "This plugin allows you to manipulate the native calendar.", "cordova": { "id": "cordova-plugin-calendar", @@ -21,12 +21,13 @@ "cordova-ios", "cordova-android" ], - "engines": [ - { - "name": "cordova", - "version": ">=3.0.0" + "engines": { + "cordovaDependencies": { + "3.0.0": { + "cordova-android": ">=6.3.0" + } } - ], + }, "author": "Eddy Verbruggen (https://github.com/EddyVerbruggen)", "license": "MIT", "bugs": { diff --git a/plugin.xml b/plugin.xml index 1cf8f36d..e9273785 100644 --- a/plugin.xml +++ b/plugin.xml @@ -3,7 +3,7 @@ xmlns="http://apache.org/cordova/ns/plugins/1.0" xmlns:android="http://schemas.android.com/apk/res/android" id="cordova-plugin-calendar" - version="4.5.5"> + version="5.1.3"> Calendar @@ -22,7 +22,7 @@ https://github.com/EddyVerbruggen/Calendar-PhoneGap-Plugin/issues - + @@ -47,6 +47,7 @@ nl fr it + pt-BR @@ -78,6 +79,15 @@ target-dir="src/nl/xservices/plugins/accessor"/> - + + + + + + + + + + diff --git a/src/android/nl/xservices/plugins/Calendar.java b/src/android/nl/xservices/plugins/Calendar.java index a62fd1ef..bf1aa2e7 100644 --- a/src/android/nl/xservices/plugins/Calendar.java +++ b/src/android/nl/xservices/plugins/Calendar.java @@ -43,6 +43,7 @@ public class Calendar extends CordovaPlugin { private static final String ACTION_CREATE_EVENT_WITH_OPTIONS = "createEventWithOptions"; private static final String ACTION_CREATE_EVENT_INTERACTIVELY = "createEventInteractively"; private static final String ACTION_DELETE_EVENT = "deleteEvent"; + private static final String ACTION_DELETE_EVENT_BY_ID = "deleteEventById"; private static final String ACTION_FIND_EVENT_WITH_OPTIONS = "findEventWithOptions"; private static final String ACTION_LIST_EVENTS_IN_RANGE = "listEventsInRange"; private static final String ACTION_LIST_CALENDARS = "listCalendars"; @@ -54,6 +55,7 @@ public class Calendar extends CordovaPlugin { private static final int PERMISSION_REQCODE_DELETE_CALENDAR = 101; private static final int PERMISSION_REQCODE_CREATE_EVENT = 102; private static final int PERMISSION_REQCODE_DELETE_EVENT = 103; + private static final int PERMISSION_REQCODE_DELETE_EVENT_BY_ID = 104; // read permissions private static final int PERMISSION_REQCODE_FIND_EVENTS = 200; @@ -84,7 +86,6 @@ public boolean execute(String action, JSONArray args, CallbackContext callbackCo return true; } else if (ACTION_CREATE_EVENT_WITH_OPTIONS.equals(action)) { if (hasLimitedSupport) { - // TODO investigate this option some day: http://stackoverflow.com/questions/3721963/how-to-add-calendar-events-in-android createEventInteractively(args); } else { createEvent(args); @@ -102,6 +103,9 @@ public boolean execute(String action, JSONArray args, CallbackContext callbackCo } else if (!hasLimitedSupport && ACTION_DELETE_EVENT.equals(action)) { deleteEvent(args); return true; + } else if (!hasLimitedSupport && ACTION_DELETE_EVENT_BY_ID.equals(action)) { + deleteEventById(args); + return true; } else if (ACTION_LIST_CALENDARS.equals(action)) { listCalendars(); return true; @@ -135,17 +139,17 @@ public boolean execute(String action, JSONArray args, CallbackContext callbackCo private void hasReadPermission() { this.callback.sendPluginResult(new PluginResult(PluginResult.Status.OK, - calendarPermissionGranted(Manifest.permission.READ_CALENDAR))); + calendarPermissionGranted(Manifest.permission.READ_CALENDAR))); } private void hasWritePermission() { this.callback.sendPluginResult(new PluginResult(PluginResult.Status.OK, - calendarPermissionGranted(Manifest.permission.WRITE_CALENDAR))); + calendarPermissionGranted(Manifest.permission.WRITE_CALENDAR))); } private void hasReadWritePermission() { this.callback.sendPluginResult(new PluginResult(PluginResult.Status.OK, - calendarPermissionGranted(Manifest.permission.READ_CALENDAR, Manifest.permission.WRITE_CALENDAR))); + calendarPermissionGranted(Manifest.permission.READ_CALENDAR, Manifest.permission.WRITE_CALENDAR))); } private void requestReadPermission(int requestCode) { @@ -196,6 +200,8 @@ public void onRequestPermissionResult(int requestCode, String[] permissions, int createEvent(requestArgs); } else if (requestCode == PERMISSION_REQCODE_DELETE_EVENT) { deleteEvent(requestArgs); + } else if (requestCode == PERMISSION_REQCODE_DELETE_EVENT_BY_ID) { + deleteEventById(requestArgs); } else if (requestCode == PERMISSION_REQCODE_FIND_EVENTS) { findEvents(requestArgs); } else if (requestCode == PERMISSION_REQCODE_LIST_CALENDARS) { @@ -267,9 +273,12 @@ public void run() { } callback.sendPluginResult(new PluginResult(PluginResult.Status.OK, activeCalendars)); } catch (JSONException e) { - System.err.println("Exception: " + e.getMessage()); + System.err.println("JSONException: " + e.getMessage()); callback.error(e.getMessage()); - } + } catch (Exception ex) { + System.err.println("Exception: " + ex.getMessage()); + callback.error(ex.getMessage()); + } } }); } @@ -280,8 +289,8 @@ private void createCalendar(JSONArray args) { return; } - if (!calendarPermissionGranted(Manifest.permission.WRITE_CALENDAR)) { - requestWritePermission(PERMISSION_REQCODE_CREATE_CALENDAR); + if (!calendarPermissionGranted(Manifest.permission.WRITE_CALENDAR, Manifest.permission.READ_CALENDAR)) { + requestReadWritePermission(PERMISSION_REQCODE_CREATE_CALENDAR); return; } @@ -313,8 +322,8 @@ private void deleteCalendar(JSONArray args) { return; } - if (!calendarPermissionGranted(Manifest.permission.WRITE_CALENDAR)) { - requestWritePermission(PERMISSION_REQCODE_DELETE_CALENDAR); + if (!calendarPermissionGranted(Manifest.permission.WRITE_CALENDAR, Manifest.permission.READ_CALENDAR)) { + requestReadWritePermission(PERMISSION_REQCODE_DELETE_CALENDAR); return; } @@ -329,8 +338,13 @@ private void deleteCalendar(JSONArray args) { cordova.getThreadPool().execute(new Runnable() { @Override public void run() { - getCalendarAccessor().deleteCalendar(calendarName); - callback.sendPluginResult(new PluginResult(PluginResult.Status.OK, "yes")); + try { + getCalendarAccessor().deleteCalendar(calendarName); + callback.sendPluginResult(new PluginResult(PluginResult.Status.OK, "yes")); + } catch (Exception e) { + System.err.println("Exception: " + e.getMessage()); + callback.error(e.getMessage()); + } } }); } catch (JSONException e) { @@ -347,21 +361,27 @@ private void createEventInteractively(JSONArray args) { cordova.getThreadPool().execute(new Runnable() { @Override public void run() { - final boolean isAllDayEvent = AbstractCalendarAccessor.isAllDayEvent(new Date(jsonFilter.optLong("startTime")), new Date(jsonFilter.optLong("endTime"))); - final Intent calIntent = new Intent(Intent.ACTION_EDIT) - .setType("vnd.android.cursor.item/event") - .putExtra("title", getPossibleNullString("title", jsonFilter)) - .putExtra("hasAlarm", 1); - if(isAllDayEvent){ - calIntent - .putExtra("allDay", isAllDayEvent) - .putExtra("beginTime", jsonFilter.optLong("startTime") + TimeZone.getDefault().getOffset(jsonFilter.optLong("startTime"))) - .putExtra("endTime", jsonFilter.optLong("endTime") + TimeZone.getDefault().getOffset(jsonFilter.optLong("endTime"))) + + boolean isAllDayEvent = false; + String allDayOption = getPossibleNullString("allday", argOptionsObject); + if (allDayOption != null) { + isAllDayEvent = Boolean.parseBoolean(allDayOption); + } else { + isAllDayEvent = AbstractCalendarAccessor.isAllDayEvent(new Date(jsonFilter.optLong("startTime")), + new Date(jsonFilter.optLong("endTime"))); + } + final Intent calIntent = new Intent(Intent.ACTION_EDIT).setType("vnd.android.cursor.item/event") + .putExtra("title", getPossibleNullString("title", jsonFilter)).putExtra("hasAlarm", 1); + if (isAllDayEvent) { + calIntent.putExtra("allDay", isAllDayEvent) + .putExtra("beginTime", + jsonFilter.optLong("startTime") + TimeZone.getDefault().getOffset(jsonFilter.optLong("startTime"))) + .putExtra("endTime", + jsonFilter.optLong("endTime") + TimeZone.getDefault().getOffset(jsonFilter.optLong("endTime"))) .putExtra("eventTimezone", "TIMEZONE_UTC"); } else { - calIntent - .putExtra("beginTime", jsonFilter.optLong("startTime")) - .putExtra("endTime", jsonFilter.optLong("endTime")); + calIntent.putExtra("beginTime", jsonFilter.optLong("startTime")).putExtra("endTime", + jsonFilter.optLong("endTime")); } // TODO can we pass a reminder here? @@ -393,8 +413,7 @@ public void run() { if (recurrenceEndTime == null) { calIntent.putExtra(Events.RRULE, "FREQ=" + recurrence.toUpperCase() + ";INTERVAL=" + recurrenceInterval); } else { - final SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd'T'hhmmss'Z'"); - calIntent.putExtra(Events.RRULE, "FREQ=" + recurrence.toUpperCase() + ";INTERVAL=" + recurrenceInterval + ";UNTIL=" + sdf.format(new Date(recurrenceEndTime))); + calIntent.putExtra(Events.RRULE, "FREQ=" + recurrence.toUpperCase() + ";INTERVAL=" + recurrenceInterval + ";UNTIL=" + formatICalDateTime(new Date(recurrenceEndTime))); } } @@ -431,8 +450,8 @@ private void deleteEvent(JSONArray args) { // note that if the dev didn't call requestWritePermission before calling this method and calendarPermissionGranted returns false, // the app will ask permission and this method needs to be invoked again (done for backward compat). - if (!calendarPermissionGranted(Manifest.permission.WRITE_CALENDAR)) { - requestWritePermission(PERMISSION_REQCODE_DELETE_EVENT); + if (!calendarPermissionGranted(Manifest.permission.WRITE_CALENDAR, Manifest.permission.READ_CALENDAR)) { + requestReadWritePermission(PERMISSION_REQCODE_DELETE_EVENT); return; } @@ -444,11 +463,12 @@ private void deleteEvent(JSONArray args) { public void run() { boolean deleteResult = getCalendarAccessor().deleteEvent( - null, - jsonFilter.optLong("startTime"), - jsonFilter.optLong("endTime"), - getPossibleNullString("title", jsonFilter), - getPossibleNullString("location", jsonFilter)); + null, + jsonFilter.optLong("startTime"), + jsonFilter.optLong("endTime"), + getPossibleNullString("title", jsonFilter), + getPossibleNullString("location", jsonFilter), + getPossibleNullString("notes", jsonFilter)); callback.sendPluginResult(new PluginResult(PluginResult.Status.OK, deleteResult)); } @@ -459,6 +479,31 @@ public void run() { } } + private void deleteEventById(final JSONArray args) { + + // note that if the dev didn't call requestWritePermission before calling this method and calendarPermissionGranted returns false, + // the app will ask permission and this method needs to be invoked again (done for backward compat). + if (!calendarPermissionGranted(Manifest.permission.WRITE_CALENDAR)) { + requestWritePermission(PERMISSION_REQCODE_DELETE_EVENT_BY_ID); + return; + } + + cordova.getThreadPool().execute(new Runnable() { @Override public void run() { + try { + final JSONObject opts = args.optJSONObject(0); + final long id = opts != null ? opts.optLong("id", -1) : -1; + final long fromTime = opts != null ? opts.optLong("fromTime", -1) : -1; + + boolean deleteResult = getCalendarAccessor().deleteEventById(null, id, fromTime); + + callback.sendPluginResult(new PluginResult(PluginResult.Status.OK, deleteResult)); + } catch (Exception e) { + System.err.println("Exception: " + e.getMessage()); + callback.error(e.getMessage()); + } + }}); + } + private void findEvents(JSONArray args) { if (args.length() == 0) { System.err.println("Exception: No Arguments passed"); @@ -480,12 +525,14 @@ private void findEvents(JSONArray args) { @Override public void run() { JSONArray jsonEvents = getCalendarAccessor().findEvents( - getPossibleNullString("id", argOptionsObject), - getPossibleNullString("title", jsonFilter), - getPossibleNullString("location", jsonFilter), - getPossibleNullString("notes", jsonFilter), - jsonFilter.optLong("startTime"), - jsonFilter.optLong("endTime")); + getPossibleNullString("id", argOptionsObject), + getPossibleNullString("title", jsonFilter), + getPossibleNullString("location", jsonFilter), + getPossibleNullString("notes", jsonFilter), + jsonFilter.optLong("startTime"), + jsonFilter.optLong("endTime"), + getPossibleNullString("calendarId", argOptionsObject)) + ; callback.sendPluginResult(new PluginResult(PluginResult.Status.OK, jsonEvents)); } @@ -499,8 +546,8 @@ public void run() { private void createEvent(JSONArray args) { // note that if the dev didn't call requestWritePermission before calling this method and calendarPermissionGranted returns false, // the app will ask permission and this method needs to be invoked again (done for backward compat). - if (!calendarPermissionGranted(Manifest.permission.WRITE_CALENDAR)) { - requestWritePermission(PERMISSION_REQCODE_CREATE_EVENT); + if (!calendarPermissionGranted(Manifest.permission.WRITE_CALENDAR, Manifest.permission.READ_CALENDAR)) { + requestReadWritePermission(PERMISSION_REQCODE_CREATE_EVENT); return; } @@ -513,19 +560,24 @@ private void createEvent(JSONArray args) { public void run() { try { final String createdEventID = getCalendarAccessor().createEvent( - null, - getPossibleNullString("title", argObject), - argObject.getLong("startTime"), - argObject.getLong("endTime"), - getPossibleNullString("notes", argObject), - getPossibleNullString("location", argObject), - argOptionsObject.optLong("firstReminderMinutes", -1), - argOptionsObject.optLong("secondReminderMinutes", -1), - getPossibleNullString("recurrence", argOptionsObject), - argOptionsObject.optInt("recurrenceInterval"), - argOptionsObject.optLong("recurrenceEndTime"), - argOptionsObject.optInt("calendarId", 1), - getPossibleNullString("url", argOptionsObject)); + null, + getPossibleNullString("title", argObject), + argObject.getLong("startTime"), + argObject.getLong("endTime"), + getPossibleNullString("notes", argObject), + getPossibleNullString("location", argObject), + argOptionsObject.optLong("firstReminderMinutes", -1), + argOptionsObject.optLong("secondReminderMinutes", -1), + getPossibleNullString("recurrence", argOptionsObject), + argOptionsObject.optInt("recurrenceInterval", -1), + getPossibleNullString("recurrenceWeekstart", argOptionsObject), + getPossibleNullString("recurrenceByDay", argOptionsObject), + getPossibleNullString("recurrenceByMonthDay", argOptionsObject), + argOptionsObject.optLong("recurrenceEndTime", -1), + argOptionsObject.optLong("recurrenceCount", -1), + getPossibleNullString("allday", argOptionsObject), + argOptionsObject.optInt("calendarId", 1), + getPossibleNullString("url", argOptionsObject)); if (createdEventID != null) { callback.success(createdEventID); } else { @@ -589,37 +641,37 @@ public void run() { //actual query Cursor cursor = contentResolver.query( - l_eventUri, - l_projection, - "(deleted = 0 AND" + - " (" + - // all day events are stored in UTC, others in the user's timezone - " (eventTimezone = 'UTC' AND begin >=" + (calendar_start.getTimeInMillis() + TimeZone.getDefault().getOffset(calendar_start.getTimeInMillis())) + " AND end <=" + (calendar_end.getTimeInMillis() + TimeZone.getDefault().getOffset(calendar_end.getTimeInMillis())) + ")" + - " OR " + - " (eventTimezone <> 'UTC' AND begin >=" + calendar_start.getTimeInMillis() + " AND end <=" + calendar_end.getTimeInMillis() + ")" + - " )" + - ")", - null, - "begin ASC"); + l_eventUri, + l_projection, + "(deleted = 0 AND" + + " (" + + // all day events are stored in UTC, others in the user's timezone + " (eventTimezone = 'UTC' AND begin >=" + (calendar_start.getTimeInMillis() + TimeZone.getDefault().getOffset(calendar_start.getTimeInMillis())) + " AND end <=" + (calendar_end.getTimeInMillis() + TimeZone.getDefault().getOffset(calendar_end.getTimeInMillis())) + ")" + + " OR " + + " (eventTimezone <> 'UTC' AND begin >=" + calendar_start.getTimeInMillis() + " AND end <=" + calendar_end.getTimeInMillis() + ")" + + " )" + + ")", + null, + "begin ASC"); int i = 0; if (cursor != null) { while (cursor.moveToNext()) { try { result.put( - i++, - new JSONObject() - .put("calendar_id", cursor.getString(cursor.getColumnIndex("calendar_id"))) - .put("id", cursor.getString(cursor.getColumnIndex("_id"))) - .put("event_id", cursor.getString(cursor.getColumnIndex("event_id"))) - .put("rrule", cursor.getString(cursor.getColumnIndex("rrule"))) - .put("rdate", cursor.getString(cursor.getColumnIndex("rdate"))) - .put("exdate", cursor.getString(cursor.getColumnIndex("exdate"))) - .put("title", cursor.getString(cursor.getColumnIndex("title"))) - .put("dtstart", cursor.getLong(cursor.getColumnIndex("begin"))) - .put("dtend", cursor.getLong(cursor.getColumnIndex("end"))) - .put("eventLocation", cursor.getString(cursor.getColumnIndex("eventLocation")) != null ? cursor.getString(cursor.getColumnIndex("eventLocation")) : "") - .put("allDay", cursor.getInt(cursor.getColumnIndex("allDay"))) + i++, + new JSONObject() + .put("calendar_id", cursor.getString(cursor.getColumnIndex("calendar_id"))) + .put("id", cursor.getString(cursor.getColumnIndex("_id"))) + .put("event_id", cursor.getString(cursor.getColumnIndex("event_id"))) + .put("rrule", cursor.getString(cursor.getColumnIndex("rrule"))) + .put("rdate", cursor.getString(cursor.getColumnIndex("rdate"))) + .put("exdate", cursor.getString(cursor.getColumnIndex("exdate"))) + .put("title", cursor.getString(cursor.getColumnIndex("title"))) + .put("dtstart", cursor.getLong(cursor.getColumnIndex("begin"))) + .put("dtend", cursor.getLong(cursor.getColumnIndex("end"))) + .put("eventLocation", cursor.getString(cursor.getColumnIndex("eventLocation")) != null ? cursor.getString(cursor.getColumnIndex("eventLocation")) : "") + .put("allDay", cursor.getInt(cursor.getColumnIndex("allDay"))) ); } catch (JSONException e) { e.printStackTrace(); @@ -656,4 +708,10 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) { callback.error("Unable to add event (" + resultCode + ")."); } } + + public static String formatICalDateTime(Date date) { + final SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'"); + sdf.setTimeZone(TimeZone.getTimeZone("UTC")); + return sdf.format(date); + } } diff --git a/src/android/nl/xservices/plugins/accessor/AbstractCalendarAccessor.java b/src/android/nl/xservices/plugins/accessor/AbstractCalendarAccessor.java index 320eab58..4db0c055 100644 --- a/src/android/nl/xservices/plugins/accessor/AbstractCalendarAccessor.java +++ b/src/android/nl/xservices/plugins/accessor/AbstractCalendarAccessor.java @@ -9,6 +9,7 @@ import android.provider.CalendarContract; import android.text.TextUtils; import android.util.Log; + import org.apache.cordova.CordovaInterface; import org.json.JSONArray; import org.json.JSONException; @@ -18,587 +19,760 @@ import java.util.*; import static android.provider.CalendarContract.Events; +import android.provider.CalendarContract.Instances; public abstract class AbstractCalendarAccessor { - public static final String LOG_TAG = "Calendar"; - public static final String CONTENT_PROVIDER = "content://com.android.calendar"; - public static final String CONTENT_PROVIDER_PRE_FROYO = "content://calendar"; - - public static final String CONTENT_PROVIDER_PATH_CALENDARS = "/calendars"; - public static final String CONTENT_PROVIDER_PATH_EVENTS = "/events"; - public static final String CONTENT_PROVIDER_PATH_REMINDERS = "/reminders"; - public static final String CONTENT_PROVIDER_PATH_INSTANCES_WHEN = "/instances/when"; - public static final String CONTENT_PROVIDER_PATH_ATTENDEES = "/attendees"; - - protected static class Event { - String id; - String message; - String location; - String title; - String startDate; - String endDate; - //attribute DOMString status; - // attribute DOMString transparency; - // attribute CalendarRepeatRule recurrence; - // attribute DOMString reminder; - - String eventId; - boolean recurring = false; - boolean allDay; - ArrayList attendees; - - public JSONObject toJSONObject() { - JSONObject obj = new JSONObject(); - try { - obj.put("id", this.eventId); - obj.putOpt("message", this.message); - obj.putOpt("location", this.location); - obj.putOpt("title", this.title); - SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); - sdf.setTimeZone(TimeZone.getDefault()); - if (this.startDate != null) { - obj.put("startDate", sdf.format(new Date(Long.parseLong(this.startDate)))); - } - if (this.endDate != null) { - obj.put("endDate", sdf.format(new Date(Long.parseLong(this.endDate)))); - } - obj.put("allday", this.allDay); - if (this.attendees != null) { - JSONArray arr = new JSONArray(); - for (Attendee attendee : this.attendees) { - arr.put(attendee.toJSONObject()); - } - obj.put("attendees", arr); - } - } catch (JSONException e) { - throw new RuntimeException(e); - } - return obj; - } - } - - protected static class Attendee { - String id; - String name; - String email; - String status; - - public JSONObject toJSONObject() { - JSONObject obj = new JSONObject(); - try { - obj.put("id", this.id); - obj.putOpt("name", this.name); - obj.putOpt("email", this.email); - obj.putOpt("status", this.status); - } catch (JSONException e) { - throw new RuntimeException(e); - } - return obj; - } - } - - protected CordovaInterface cordova; - - private EnumMap calendarKeys; - - public AbstractCalendarAccessor(CordovaInterface cordova) { - this.cordova = cordova; - this.calendarKeys = initContentProviderKeys(); - } - - protected enum KeyIndex { - CALENDARS_ID, - CALENDARS_NAME, - CALENDARS_VISIBLE, - EVENTS_ID, - EVENTS_CALENDAR_ID, - EVENTS_DESCRIPTION, - EVENTS_LOCATION, - EVENTS_SUMMARY, - EVENTS_START, - EVENTS_END, - EVENTS_RRULE, - EVENTS_ALL_DAY, - INSTANCES_ID, - INSTANCES_EVENT_ID, - INSTANCES_BEGIN, - INSTANCES_END, - ATTENDEES_ID, - ATTENDEES_EVENT_ID, - ATTENDEES_NAME, - ATTENDEES_EMAIL, - ATTENDEES_STATUS - } - - protected abstract EnumMap initContentProviderKeys(); - - protected String getKey(KeyIndex index) { - return this.calendarKeys.get(index); - } - - protected abstract Cursor queryAttendees(String[] projection, - String selection, String[] selectionArgs, String sortOrder); - - protected abstract Cursor queryCalendars(String[] projection, - String selection, String[] selectionArgs, String sortOrder); - - protected abstract Cursor queryEvents(String[] projection, - String selection, String[] selectionArgs, String sortOrder); - - protected abstract Cursor queryEventInstances(long startFrom, long startTo, - String[] projection, String selection, String[] selectionArgs, - String sortOrder); - - private Event[] fetchEventInstances(String eventId, String title, String location, String notes, long startFrom, long startTo) { - String[] projection = { - this.getKey(KeyIndex.INSTANCES_ID), - this.getKey(KeyIndex.INSTANCES_EVENT_ID), - this.getKey(KeyIndex.INSTANCES_BEGIN), - this.getKey(KeyIndex.INSTANCES_END) - }; - - String sortOrder = this.getKey(KeyIndex.INSTANCES_BEGIN) + " ASC, " + this.getKey(KeyIndex.INSTANCES_END) + " ASC"; - // Fetch events from instances table in ascending order by time. - - // filter - String selection = ""; - List selectionList = new ArrayList(); - - if (eventId != null) { - selection += CalendarContract.Instances.EVENT_ID + " = ?"; - selectionList.add(eventId); - } else { - if (title != null) { - //selection += Events.TITLE + "=?"; - selection += Events.TITLE + " LIKE ?"; - selectionList.add("%" + title + "%"); - } - if (location != null && !location.equals("")) { - if (!"".equals(selection)) { - selection += " AND "; - } - selection += Events.EVENT_LOCATION + " LIKE ?"; - selectionList.add("%" + location + "%"); - } - if (notes != null && !notes.equals("")) { - if (!"".equals(selection)) { - selection += " AND "; - } - selection += Events.DESCRIPTION + " LIKE ?"; - selectionList.add("%" + notes + "%"); - } + public static final String LOG_TAG = "Calendar"; + public static final String CONTENT_PROVIDER = "content://com.android.calendar"; + public static final String CONTENT_PROVIDER_PRE_FROYO = "content://calendar"; + + public static final String CONTENT_PROVIDER_PATH_CALENDARS = "/calendars"; + public static final String CONTENT_PROVIDER_PATH_EVENTS = "/events"; + public static final String CONTENT_PROVIDER_PATH_REMINDERS = "/reminders"; + public static final String CONTENT_PROVIDER_PATH_INSTANCES_WHEN = "/instances/when"; + public static final String CONTENT_PROVIDER_PATH_ATTENDEES = "/attendees"; + + protected static class Event { + String id; + String message; + String location; + String title; + String startDate; + String endDate; + String recurrenceFreq; + String recurrenceInterval; + String recurrenceWeekstart; + String recurrenceByDay; + String recurrenceByMonthDay; + String recurrenceUntil; + String recurrenceCount; + //attribute DOMString status; + // attribute DOMString transparency; + // attribute CalendarRepeatRule recurrence; + // attribute DOMString reminder; + + String eventId; + boolean recurring = false; + boolean allDay; + ArrayList attendees; + + public JSONObject toJSONObject() { + JSONObject obj = new JSONObject(); + try { + obj.put("id", this.eventId); + obj.putOpt("message", this.message); + obj.putOpt("location", this.location); + obj.putOpt("title", this.title); + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + sdf.setTimeZone(TimeZone.getDefault()); + if (this.startDate != null) { + obj.put("startDate", sdf.format(new Date(Long.parseLong(this.startDate)))); + } + if (this.endDate != null) { + obj.put("endDate", sdf.format(new Date(Long.parseLong(this.endDate)))); + } + obj.put("allday", this.allDay); + if (this.attendees != null) { + JSONArray arr = new JSONArray(); + for (Attendee attendee : this.attendees) { + arr.put(attendee.toJSONObject()); + } + obj.put("attendees", arr); + } + if (this.recurring) { + JSONObject objRecurrence = new JSONObject(); + + objRecurrence.putOpt("freq", this.recurrenceFreq); + objRecurrence.putOpt("interval", this.recurrenceInterval); + objRecurrence.putOpt("wkst", this.recurrenceWeekstart); + objRecurrence.putOpt("byday", this.recurrenceByDay); + objRecurrence.putOpt("bymonthday", this.recurrenceByMonthDay); + objRecurrence.putOpt("until", this.recurrenceUntil); + objRecurrence.putOpt("count", this.recurrenceCount); + + obj.put("recurrence", objRecurrence); + } + } catch (JSONException e) { + throw new RuntimeException(e); + } + return obj; + } } - String[] selectionArgs = new String[selectionList.size()]; - Cursor cursor = queryEventInstances(startFrom, startTo, projection, selection, selectionList.toArray(selectionArgs), sortOrder); - if (cursor == null) { - return null; - } - Event[] instances = null; - if (cursor.moveToFirst()) { - int idCol = cursor.getColumnIndex(this.getKey(KeyIndex.INSTANCES_ID)); - int eventIdCol = cursor.getColumnIndex(this.getKey(KeyIndex.INSTANCES_EVENT_ID)); - int beginCol = cursor.getColumnIndex(this.getKey(KeyIndex.INSTANCES_BEGIN)); - int endCol = cursor.getColumnIndex(this.getKey(KeyIndex.INSTANCES_END)); - int count = cursor.getCount(); - int i = 0; - instances = new Event[count]; - do { - // Use the startDate/endDate time from the instances table. For recurring - // events the events table contain the startDate/endDate time for the - // origin event (as you would expect). - instances[i] = new Event(); - instances[i].id = cursor.getString(idCol); - instances[i].eventId = cursor.getString(eventIdCol); - instances[i].startDate = cursor.getString(beginCol); - instances[i].endDate = cursor.getString(endCol); - i += 1; - } while (cursor.moveToNext()); + protected static class Attendee { + String id; + String name; + String email; + String status; + + public JSONObject toJSONObject() { + JSONObject obj = new JSONObject(); + try { + obj.put("id", this.id); + obj.putOpt("name", this.name); + obj.putOpt("email", this.email); + obj.putOpt("status", this.status); + } catch (JSONException e) { + throw new RuntimeException(e); + } + return obj; + } } - // if we don't find the event by id, try again by title etc - inline with iOS logic - if ((instances == null || instances.length == 0) && eventId != null) { - return fetchEventInstances(null, title, location, notes, startFrom, startTo); - } else { - return instances; - } - } - - private String[] getActiveCalendarIds() { - Cursor cursor = queryCalendars(new String[]{ - this.getKey(KeyIndex.CALENDARS_ID) - }, - this.getKey(KeyIndex.CALENDARS_VISIBLE) + "=1", null, null); - String[] calendarIds = null; - if (cursor.moveToFirst()) { - calendarIds = new String[cursor.getCount()]; - int i = 0; - do { - int col = cursor.getColumnIndex(this.getKey(KeyIndex.CALENDARS_ID)); - calendarIds[i] = cursor.getString(col); - i += 1; - } while (cursor.moveToNext()); - cursor.close(); - } - return calendarIds; - } - - public final JSONArray getActiveCalendars() throws JSONException { - Cursor cursor = queryCalendars( - new String[]{ - this.getKey(KeyIndex.CALENDARS_ID), - this.getKey(KeyIndex.CALENDARS_NAME) - }, - this.getKey(KeyIndex.CALENDARS_VISIBLE) + "=1", null, null - ); - if (cursor == null) { - return null; - } - JSONArray calendarsWrapper = new JSONArray(); - if (cursor.moveToFirst()) { - do { - JSONObject calendar = new JSONObject(); - calendar.put("id", cursor.getString(cursor.getColumnIndex(this.getKey(KeyIndex.CALENDARS_ID)))); - calendar.put("name", cursor.getString(cursor.getColumnIndex(this.getKey(KeyIndex.CALENDARS_NAME)))); - calendarsWrapper.put(calendar); - } while (cursor.moveToNext()); - cursor.close(); - } - return calendarsWrapper; - } - - private Map fetchEventsAsMap(Event[] instances) { - // Only selecting from active calendars, no active calendars = no events. - String[] activeCalendarIds = getActiveCalendarIds(); - if (activeCalendarIds.length == 0) { - return null; - } - String[] projection = new String[]{ - this.getKey(KeyIndex.EVENTS_ID), - this.getKey(KeyIndex.EVENTS_DESCRIPTION), - this.getKey(KeyIndex.EVENTS_LOCATION), - this.getKey(KeyIndex.EVENTS_SUMMARY), - this.getKey(KeyIndex.EVENTS_START), - this.getKey(KeyIndex.EVENTS_END), - this.getKey(KeyIndex.EVENTS_RRULE), - this.getKey(KeyIndex.EVENTS_ALL_DAY) - }; - // Get all the ids at once from active calendars. - StringBuffer select = new StringBuffer(); - select.append(this.getKey(KeyIndex.EVENTS_ID) + " IN ("); - select.append(instances[0].eventId); - for (int i = 1; i < instances.length; i++) { - select.append(","); - select.append(instances[i].eventId); + protected CordovaInterface cordova; + + private EnumMap calendarKeys; + + public AbstractCalendarAccessor(CordovaInterface cordova) { + this.cordova = cordova; + this.calendarKeys = initContentProviderKeys(); } - select.append(") AND " + this.getKey(KeyIndex.EVENTS_CALENDAR_ID) + - " IN ("); - select.append(activeCalendarIds[0]); - for (int i = 1; i < activeCalendarIds.length; i++) { - select.append(","); - select.append(activeCalendarIds[i]); + + protected enum KeyIndex { + CALENDARS_ID, + IS_PRIMARY, + CALENDARS_NAME, + CALENDARS_VISIBLE, + CALENDARS_DISPLAY_NAME, + EVENTS_ID, + EVENTS_CALENDAR_ID, + EVENTS_DESCRIPTION, + EVENTS_LOCATION, + EVENTS_SUMMARY, + EVENTS_START, + EVENTS_END, + EVENTS_RRULE, + EVENTS_ALL_DAY, + INSTANCES_ID, + INSTANCES_EVENT_ID, + INSTANCES_BEGIN, + INSTANCES_END, + ATTENDEES_ID, + ATTENDEES_EVENT_ID, + ATTENDEES_NAME, + ATTENDEES_EMAIL, + ATTENDEES_STATUS } - select.append(")"); - Cursor cursor = queryEvents(projection, select.toString(), null, null); - Map eventsMap = new HashMap(); - if (cursor.moveToFirst()) { - int[] cols = new int[projection.length]; - for (int i = 0; i < cols.length; i++) { - cols[i] = cursor.getColumnIndex(projection[i]); - } - do { - Event event = new Event(); - event.id = cursor.getString(cols[0]); - event.message = cursor.getString(cols[1]); - event.location = cursor.getString(cols[2]); - event.title = cursor.getString(cols[3]); - event.startDate = cursor.getString(cols[4]); - event.endDate = cursor.getString(cols[5]); - event.recurring = !TextUtils.isEmpty(cursor.getString(cols[6])); - event.allDay = cursor.getInt(cols[7]) != 0; - eventsMap.put(event.id, event); - } while (cursor.moveToNext()); - cursor.close(); + + protected abstract EnumMap initContentProviderKeys(); + + protected String getKey(KeyIndex index) { + return this.calendarKeys.get(index); } - return eventsMap; - } - - private Map> fetchAttendeesForEventsAsMap( - String[] eventIds) { - // At least one id. - if (eventIds.length == 0) { - return null; + + protected abstract Cursor queryAttendees(String[] projection, + String selection, String[] selectionArgs, String sortOrder); + + protected abstract Cursor queryCalendars(String[] projection, + String selection, String[] selectionArgs, String sortOrder); + + protected abstract Cursor queryEvents(String[] projection, + String selection, String[] selectionArgs, String sortOrder); + + protected abstract Cursor queryEventInstances(long startFrom, long startTo, + String[] projection, String selection, String[] selectionArgs, + String sortOrder); + + private Event[] fetchEventInstances(String eventId, String title, String location, String notes, long startFrom, long startTo) { + String[] projection = { + this.getKey(KeyIndex.INSTANCES_ID), + this.getKey(KeyIndex.INSTANCES_EVENT_ID), + this.getKey(KeyIndex.INSTANCES_BEGIN), + this.getKey(KeyIndex.INSTANCES_END) + }; + + String sortOrder = this.getKey(KeyIndex.INSTANCES_BEGIN) + " ASC, " + this.getKey(KeyIndex.INSTANCES_END) + " ASC"; + // Fetch events from instances table in ascending order by time. + + // filter + String selection = ""; + List selectionList = new ArrayList(); + + if (eventId != null) { + selection += CalendarContract.Instances.EVENT_ID + " = ?"; + selectionList.add(eventId); + } else { + if (title != null) { + //selection += Events.TITLE + "=?"; + selection += Events.TITLE + " LIKE ?"; + selectionList.add("%" + title + "%"); + } + if (location != null && !location.equals("")) { + if (!"".equals(selection)) { + selection += " AND "; + } + selection += Events.EVENT_LOCATION + " LIKE ?"; + selectionList.add("%" + location + "%"); + } + if (notes != null && !notes.equals("")) { + if (!"".equals(selection)) { + selection += " AND "; + } + selection += Events.DESCRIPTION + " LIKE ?"; + selectionList.add("%" + notes + "%"); + } + } + + String[] selectionArgs = new String[selectionList.size()]; + Cursor cursor = queryEventInstances(startFrom, startTo, projection, selection, selectionList.toArray(selectionArgs), sortOrder); + if (cursor == null) { + return null; + } + Event[] instances = null; + if (cursor.moveToFirst()) { + int idCol = cursor.getColumnIndex(this.getKey(KeyIndex.INSTANCES_ID)); + int eventIdCol = cursor.getColumnIndex(this.getKey(KeyIndex.INSTANCES_EVENT_ID)); + int beginCol = cursor.getColumnIndex(this.getKey(KeyIndex.INSTANCES_BEGIN)); + int endCol = cursor.getColumnIndex(this.getKey(KeyIndex.INSTANCES_END)); + int count = cursor.getCount(); + int i = 0; + instances = new Event[count]; + do { + // Use the startDate/endDate time from the instances table. For recurring + // events the events table contain the startDate/endDate time for the + // origin event (as you would expect). + instances[i] = new Event(); + instances[i].id = cursor.getString(idCol); + instances[i].eventId = cursor.getString(eventIdCol); + instances[i].startDate = cursor.getString(beginCol); + instances[i].endDate = cursor.getString(endCol); + i += 1; + } while (cursor.moveToNext()); + } + + // if we don't find the event by id, try again by title etc - inline with iOS logic + if ((instances == null || instances.length == 0) && eventId != null) { + return fetchEventInstances(null, title, location, notes, startFrom, startTo); + } else { + return instances; + } } - String[] projection = new String[]{ - this.getKey(KeyIndex.ATTENDEES_EVENT_ID), - this.getKey(KeyIndex.ATTENDEES_ID), - this.getKey(KeyIndex.ATTENDEES_NAME), - this.getKey(KeyIndex.ATTENDEES_EMAIL), - this.getKey(KeyIndex.ATTENDEES_STATUS) - }; - StringBuffer select = new StringBuffer(); - select.append(this.getKey(KeyIndex.ATTENDEES_EVENT_ID) + " IN ("); - select.append(eventIds[0]); - for (int i = 1; i < eventIds.length; i++) { - select.append(","); - select.append(eventIds[i]); + + private String[] getActiveCalendarIds() { + Cursor cursor = queryCalendars(new String[]{ + this.getKey(KeyIndex.CALENDARS_ID) + }, + this.getKey(KeyIndex.CALENDARS_VISIBLE) + "=1", null, null); + String[] calendarIds = null; + if (cursor.moveToFirst()) { + calendarIds = new String[cursor.getCount()]; + int i = 0; + do { + int col = cursor.getColumnIndex(this.getKey(KeyIndex.CALENDARS_ID)); + calendarIds[i] = cursor.getString(col); + i += 1; + } while (cursor.moveToNext()); + cursor.close(); + } + return calendarIds; } - select.append(")"); - // Group the events together for easy iteration. - Cursor cursor = queryAttendees(projection, select.toString(), null, - this.getKey(KeyIndex.ATTENDEES_EVENT_ID) + " ASC"); - Map> attendeeMap = - new HashMap>(); - if (cursor.moveToFirst()) { - int[] cols = new int[projection.length]; - for (int i = 0; i < cols.length; i++) { - cols[i] = cursor.getColumnIndex(projection[i]); - } - ArrayList array = null; - String currentEventId = null; - do { - String eventId = cursor.getString(cols[0]); - if (currentEventId == null || !currentEventId.equals(eventId)) { - currentEventId = eventId; - array = new ArrayList(); - attendeeMap.put(currentEventId, array); - } - Attendee attendee = new Attendee(); - attendee.id = cursor.getString(cols[1]); - attendee.name = cursor.getString(cols[2]); - attendee.email = cursor.getString(cols[3]); - attendee.status = cursor.getString(cols[4]); - array.add(attendee); - } while (cursor.moveToNext()); - cursor.close(); + + public final JSONArray getActiveCalendars() throws JSONException { + Cursor cursor = queryCalendars( + new String[]{ + this.getKey(KeyIndex.CALENDARS_ID), + this.getKey(KeyIndex.CALENDARS_NAME), + this.getKey(KeyIndex.CALENDARS_DISPLAY_NAME), + this.getKey(KeyIndex.IS_PRIMARY) + }, + this.getKey(KeyIndex.CALENDARS_VISIBLE) + "=1", null, null + ); + if (cursor == null) { + return null; + } + JSONArray calendarsWrapper = new JSONArray(); + int primaryColumnIndex; + if (cursor.moveToFirst()) { + do { + JSONObject calendar = new JSONObject(); + calendar.put("id", cursor.getString(cursor.getColumnIndex(this.getKey(KeyIndex.CALENDARS_ID)))); + calendar.put("name", cursor.getString(cursor.getColumnIndex(this.getKey(KeyIndex.CALENDARS_NAME)))); + calendar.put("displayname", cursor.getString(cursor.getColumnIndex(this.getKey(KeyIndex.CALENDARS_DISPLAY_NAME)))); + primaryColumnIndex = cursor.getColumnIndex(this.getKey((KeyIndex.IS_PRIMARY))); + if (primaryColumnIndex == -1) { + primaryColumnIndex = cursor.getColumnIndex("COALESCE(isPrimary, ownerAccount = account_name)"); + } + calendar.put("isPrimary", "1".equals(cursor.getString(primaryColumnIndex))); + calendarsWrapper.put(calendar); + } while (cursor.moveToNext()); + cursor.close(); + } + return calendarsWrapper; } - return attendeeMap; - } - - public JSONArray findEvents(String eventId, String title, String location, String notes, long startFrom, long startTo) { - JSONArray result = new JSONArray(); - // Fetch events from the instance table. - Event[] instances = fetchEventInstances(eventId, title, location, notes, startFrom, startTo); - if (instances == null) { - return result; + + private Map fetchEventsAsMap(Event[] instances, String calendarId) { + // Only selecting from active calendars, no active calendars = no events. + List activeCalendarIds = Arrays.asList(getActiveCalendarIds()); + if (activeCalendarIds.isEmpty()) { + return null; + } + + List calendarsToSearch; + + if(calendarId!=null){ + calendarsToSearch = new ArrayList(); + if(activeCalendarIds.contains(calendarId)){ + calendarsToSearch.add(calendarId); + } + + }else{ + calendarsToSearch = activeCalendarIds; + } + + if(calendarsToSearch.isEmpty()){ + return null; + } + + + String[] projection = new String[]{ + this.getKey(KeyIndex.EVENTS_ID), + this.getKey(KeyIndex.EVENTS_DESCRIPTION), + this.getKey(KeyIndex.EVENTS_LOCATION), + this.getKey(KeyIndex.EVENTS_SUMMARY), + this.getKey(KeyIndex.EVENTS_START), + this.getKey(KeyIndex.EVENTS_END), + this.getKey(KeyIndex.EVENTS_RRULE), + this.getKey(KeyIndex.EVENTS_ALL_DAY) + }; + // Get all the ids at once from active calendars. + StringBuffer select = new StringBuffer(); + select.append(this.getKey(KeyIndex.EVENTS_ID) + " IN ("); + select.append(instances[0].eventId); + for (int i = 1; i < instances.length; i++) { + select.append(","); + select.append(instances[i].eventId); + } + select.append(") AND " + this.getKey(KeyIndex.EVENTS_CALENDAR_ID) + + " IN ("); + + String prefix =""; + for (String calendarToFilterId:calendarsToSearch) { + select.append(prefix); + prefix = ","; + select.append(calendarToFilterId); + } + + select.append(")"); + Cursor cursor = queryEvents(projection, select.toString(), null, null); + Map eventsMap = new HashMap(); + if (cursor.moveToFirst()) { + int[] cols = new int[projection.length]; + for (int i = 0; i < cols.length; i++) { + cols[i] = cursor.getColumnIndex(projection[i]); + } + + do { + Event event = new Event(); + event.id = cursor.getString(cols[0]); + event.message = cursor.getString(cols[1]); + event.location = cursor.getString(cols[2]); + event.title = cursor.getString(cols[3]); + event.startDate = cursor.getString(cols[4]); + event.endDate = cursor.getString(cols[5]); + + String rrule = cursor.getString(cols[6]); + if (!TextUtils.isEmpty(rrule)) { + event.recurring = true; + String[] rrule_rules = cursor.getString(cols[6]).split(";"); + for (String rule : rrule_rules) { + String rule_type = rule.split("=")[0]; + if (rule_type.equals("FREQ")) { + event.recurrenceFreq = rule.split("=")[1]; + } else if (rule_type.equals("INTERVAL")) { + event.recurrenceInterval = rule.split("=")[1]; + } else if (rule_type.equals("WKST")) { + event.recurrenceWeekstart = rule.split("=")[1]; + } else if (rule_type.equals("BYDAY")) { + event.recurrenceByDay = rule.split("=")[1]; + } else if (rule_type.equals("BYMONTHDAY")) { + event.recurrenceByMonthDay = rule.split("=")[1]; + } else if (rule_type.equals("UNTIL")) { + event.recurrenceUntil = rule.split("=")[1]; + } else if (rule_type.equals("COUNT")) { + event.recurrenceCount = rule.split("=")[1]; + } else { + Log.d(LOG_TAG, "Missing handler for " + rule); + } + } + } else { + event.recurring = false; + } + event.allDay = cursor.getInt(cols[7]) != 0; + eventsMap.put(event.id, event); + } while (cursor.moveToNext()); + cursor.close(); + } + return eventsMap; } - // Fetch events from the events table for more event info. - Map eventMap = fetchEventsAsMap(instances); - // Fetch event attendees - Map> attendeeMap = - fetchAttendeesForEventsAsMap(eventMap.keySet().toArray(new String[0])); - // Merge the event info with the instances and turn it into a JSONArray. - for (Event instance : instances) { - Event event = eventMap.get(instance.eventId); - if (event != null) { - instance.message = event.message; - instance.location = event.location; - instance.title = event.title; - if (!event.recurring) { - instance.startDate = event.startDate; - instance.endDate = event.endDate; - } - instance.allDay = event.allDay; - instance.attendees = attendeeMap.get(instance.eventId); - result.put(instance.toJSONObject()); - } + + private Map> fetchAttendeesForEventsAsMap( + String[] eventIds) { + // At least one id. + if (eventIds.length == 0) { + return null; + } + String[] projection = new String[]{ + this.getKey(KeyIndex.ATTENDEES_EVENT_ID), + this.getKey(KeyIndex.ATTENDEES_ID), + this.getKey(KeyIndex.ATTENDEES_NAME), + this.getKey(KeyIndex.ATTENDEES_EMAIL), + this.getKey(KeyIndex.ATTENDEES_STATUS) + }; + StringBuffer select = new StringBuffer(); + select.append(this.getKey(KeyIndex.ATTENDEES_EVENT_ID) + " IN ("); + select.append(eventIds[0]); + for (int i = 1; i < eventIds.length; i++) { + select.append(","); + select.append(eventIds[i]); + } + select.append(")"); + // Group the events together for easy iteration. + Cursor cursor = queryAttendees(projection, select.toString(), null, + this.getKey(KeyIndex.ATTENDEES_EVENT_ID) + " ASC"); + Map> attendeeMap = + new HashMap>(); + if (cursor.moveToFirst()) { + int[] cols = new int[projection.length]; + for (int i = 0; i < cols.length; i++) { + cols[i] = cursor.getColumnIndex(projection[i]); + } + ArrayList array = null; + String currentEventId = null; + do { + String eventId = cursor.getString(cols[0]); + if (currentEventId == null || !currentEventId.equals(eventId)) { + currentEventId = eventId; + array = new ArrayList(); + attendeeMap.put(currentEventId, array); + } + Attendee attendee = new Attendee(); + attendee.id = cursor.getString(cols[1]); + attendee.name = cursor.getString(cols[2]); + attendee.email = cursor.getString(cols[3]); + attendee.status = cursor.getString(cols[4]); + array.add(attendee); + } while (cursor.moveToNext()); + cursor.close(); + } + return attendeeMap; } - return result; - } - - public boolean deleteEvent(Uri eventsUri, long startFrom, long startTo, String title, String location) { - ContentResolver resolver = this.cordova.getActivity().getApplicationContext().getContentResolver(); - Event[] events = fetchEventInstances(null, title, location, "", startFrom, startTo); - int nrDeletedRecords = 0; - if (events != null) { - for (Event event : events) { - Uri eventUri = ContentUris.withAppendedId(eventsUri, Integer.parseInt(event.eventId)); - nrDeletedRecords = resolver.delete(eventUri, null, null); - } + + public JSONArray findEvents(String eventId, String title, String location, String notes, long startFrom, long startTo, String calendarId) { + JSONArray result = new JSONArray(); + // Fetch events from the instance table. + Event[] instances = fetchEventInstances(eventId, title, location, notes, startFrom, startTo); + if (instances == null) { + return result; + } + // Fetch events from the events table for more event info. + Map eventMap = fetchEventsAsMap(instances, calendarId); + // Fetch event attendees + Map> attendeeMap = + fetchAttendeesForEventsAsMap(eventMap.keySet().toArray(new String[0])); + // Merge the event info with the instances and turn it into a JSONArray. + /*for (Event event : eventMap.values()) { + result.put(event.toJSONObject()); + }*/ + + for (Event instance : instances) { + Event event = eventMap.get(instance.eventId); + if (event != null) { + instance.message = event.message; + instance.location = event.location; + instance.title = event.title; + if (!event.recurring) { + instance.startDate = event.startDate; + instance.endDate = event.endDate; + } + + instance.recurring = event.recurring; + instance.recurrenceFreq = event.recurrenceFreq; + instance.recurrenceInterval = event.recurrenceInterval; + instance.recurrenceWeekstart = event.recurrenceWeekstart; + instance.recurrenceByDay = event.recurrenceByDay; + instance.recurrenceByMonthDay = event.recurrenceByMonthDay; + instance.recurrenceUntil = event.recurrenceUntil; + instance.recurrenceCount = event.recurrenceCount; + + instance.allDay = event.allDay; + instance.attendees = attendeeMap.get(instance.eventId); + result.put(instance.toJSONObject()); + } + } + + return result; } - return nrDeletedRecords > 0; - } - - public String createEvent(Uri eventsUri, String title, long startTime, long endTime, String description, - String location, Long firstReminderMinutes, Long secondReminderMinutes, - String recurrence, int recurrenceInterval, Long recurrenceEndTime, Integer calendarId, String url) { - ContentResolver cr = this.cordova.getActivity().getContentResolver(); - ContentValues values = new ContentValues(); - final boolean allDayEvent = isAllDayEvent(new Date(startTime), new Date(endTime)); - if (allDayEvent) { - //all day events must be in UTC time zone per Android specification, getOffset accounts for daylight savings time - values.put(Events.EVENT_TIMEZONE, "UTC"); - values.put(Events.DTSTART, startTime + TimeZone.getDefault().getOffset(startTime)); - values.put(Events.DTEND, endTime + TimeZone.getDefault().getOffset(endTime)); - } else { - values.put(Events.EVENT_TIMEZONE, TimeZone.getDefault().getID()); - values.put(Events.DTSTART, startTime); - values.put(Events.DTEND, endTime); + + public boolean deleteEvent(Uri eventsUri, long startFrom, long startTo, String title, String location, String notes) { + ContentResolver resolver = this.cordova.getActivity().getApplicationContext().getContentResolver(); + Event[] events = fetchEventInstances(null, title, location, notes, startFrom, startTo); + int nrDeletedRecords = 0; + if (events != null) { + for (Event event : events) { + Uri eventUri = ContentUris.withAppendedId(eventsUri, Integer.parseInt(event.eventId)); + nrDeletedRecords = resolver.delete(eventUri, null, null); + } + } + return nrDeletedRecords > 0; } - values.put(Events.ALL_DAY, allDayEvent ? 1 : 0); - values.put(Events.TITLE, title); - // there's no separate url field, so adding it to the notes - if (url != null) { - if (description == null) { - description = url; - } else { - description += " " + url; - } + + public boolean deleteEventById(Uri eventsUri, long id, long fromTime) { + if (id == -1) + throw new IllegalArgumentException("Event id not specified."); + + // Find event + long evDtStart = -1; + String evRRule = null; + { + Cursor cur = queryEvents(new String[] { Events.DTSTART, Events.RRULE }, + Events._ID + " = ?", + new String[] { Long.toString(id) }, + Events.DTSTART); + if (cur.moveToNext()) { + evDtStart = cur.getLong(0); + evRRule = cur.getString(1); + } + cur.close(); + } + if (evDtStart == -1) + throw new RuntimeException("Could not find event."); + + // If targeted, delete initial event + if (fromTime == -1 || evDtStart >= fromTime) { + ContentResolver resolver = this.cordova.getActivity().getContentResolver(); + int deleted = this.cordova.getActivity().getContentResolver() + .delete(ContentUris.withAppendedId(eventsUri, id), null, null); + return deleted > 0; + } + + // Find target instance + long targDtStart = -1; + { + // Scans just over a year. + // Not using a wider range because it can corrupt the Calendar Storage state! https://issuetracker.google.com/issues/36980229 + Cursor cur = queryEventInstances(fromTime, + fromTime + 1000L * 60L * 60L * 24L * 367L, + new String[] { Instances.DTSTART }, + Instances.EVENT_ID + " = ?", + new String[] { Long.toString(id) }, + Instances.DTSTART); + if (cur.moveToNext()) { + targDtStart = cur.getLong(0); + } + cur.close(); + } + if (targDtStart == -1) { + // Nothing to delete + return false; + } + + // Set UNTIL + if (evRRule == null) + evRRule = ""; + + // Remove any existing COUNT or UNTIL + List recurRuleParts = new ArrayList(Arrays.asList(evRRule.split(";"))); + Iterator iter = recurRuleParts.iterator(); + while (iter.hasNext()) { + String rulePart = iter.next(); + if (rulePart.startsWith("COUNT=") || rulePart.startsWith("UNTIL=")) { + iter.remove(); + } + } + + evRRule = TextUtils.join(";", recurRuleParts) + ";UNTIL=" + nl.xservices.plugins.Calendar.formatICalDateTime(new Date(fromTime - 1000)); + + // Update event + ContentValues values = new ContentValues(); + values.put(Events.RRULE, evRRule); + int updated = this.cordova.getActivity().getContentResolver() + .update(ContentUris.withAppendedId(eventsUri, id), values, null, null); + + return updated > 0; } - values.put(Events.DESCRIPTION, description); - values.put(Events.HAS_ALARM, firstReminderMinutes > -1 || secondReminderMinutes > -1 ? 1 : 0); - values.put(Events.CALENDAR_ID, calendarId); - values.put(Events.EVENT_LOCATION, location); - - if (recurrence != null) { - if (recurrenceEndTime == null) { - values.put(Events.RRULE, "FREQ=" + recurrence.toUpperCase() + ";INTERVAL=" + recurrenceInterval); - } else { - final SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd'T'hhmmss'Z'"); - values.put(Events.RRULE, "FREQ=" + recurrence.toUpperCase() + ";INTERVAL=" + recurrenceInterval + ";UNTIL=" + sdf.format(new Date(recurrenceEndTime))); - } + + public String createEvent(Uri eventsUri, String title, long startTime, long endTime, String description, + String location, Long firstReminderMinutes, Long secondReminderMinutes, + String recurrence, int recurrenceInterval, String recurrenceWeekstart, + String recurrenceByDay, String recurrenceByMonthDay, Long recurrenceEndTime, Long recurrenceCount, + String allday, + Integer calendarId, String url) { + ContentResolver cr = this.cordova.getActivity().getContentResolver(); + ContentValues values = new ContentValues(); + final boolean allDayEvent = "true".equals(allday) && isAllDayEvent(new Date(startTime), new Date(endTime)); + if (allDayEvent) { + //all day events must be in UTC time zone per Android specification, getOffset accounts for daylight savings time + values.put(Events.EVENT_TIMEZONE, "UTC"); + values.put(Events.DTSTART, startTime + TimeZone.getDefault().getOffset(startTime)); + values.put(Events.DTEND, endTime + TimeZone.getDefault().getOffset(endTime)); + } else { + values.put(Events.EVENT_TIMEZONE, TimeZone.getDefault().getID()); + values.put(Events.DTSTART, startTime); + values.put(Events.DTEND, endTime); + } + values.put(Events.ALL_DAY, allDayEvent ? 1 : 0); + values.put(Events.TITLE, title); + // there's no separate url field, so adding it to the notes + if (url != null) { + if (description == null) { + description = url; + } else { + description += " " + url; + } + } + values.put(Events.DESCRIPTION, description); + values.put(Events.HAS_ALARM, firstReminderMinutes > -1 || secondReminderMinutes > -1 ? 1 : 0); + values.put(Events.CALENDAR_ID, calendarId); + values.put(Events.EVENT_LOCATION, location); + + if (recurrence != null) { + String rrule = "FREQ=" + recurrence.toUpperCase() + + ((recurrenceInterval > -1) ? ";INTERVAL=" + recurrenceInterval : "") + + ((recurrenceWeekstart != null) ? ";WKST=" + recurrenceWeekstart : "") + + ((recurrenceByDay != null) ? ";BYDAY=" + recurrenceByDay : "") + + ((recurrenceByMonthDay != null) ? ";BYMONTHDAY=" + recurrenceByMonthDay : "") + + ((recurrenceEndTime > -1) ? ";UNTIL=" + nl.xservices.plugins.Calendar.formatICalDateTime(new Date(recurrenceEndTime)) : "") + + ((recurrenceCount > -1) ? ";COUNT=" + recurrenceCount : ""); + values.put(Events.RRULE, rrule); + } + + String createdEventID = null; + try { + Uri uri = cr.insert(eventsUri, values); + createdEventID = uri.getLastPathSegment(); + Log.d(LOG_TAG, "Created event with ID " + createdEventID); + + if (firstReminderMinutes > -1) { + ContentValues reminderValues = new ContentValues(); + reminderValues.put("event_id", Long.parseLong(uri.getLastPathSegment())); + reminderValues.put("minutes", firstReminderMinutes); + reminderValues.put("method", 1); + cr.insert(Uri.parse(CONTENT_PROVIDER + CONTENT_PROVIDER_PATH_REMINDERS), reminderValues); + } + + if (secondReminderMinutes > -1) { + ContentValues reminderValues = new ContentValues(); + reminderValues.put("event_id", Long.parseLong(uri.getLastPathSegment())); + reminderValues.put("minutes", secondReminderMinutes); + reminderValues.put("method", 1); + cr.insert(Uri.parse(CONTENT_PROVIDER + CONTENT_PROVIDER_PATH_REMINDERS), reminderValues); + } + } catch (Exception e) { + Log.e(LOG_TAG, "Creating reminders failed, ignoring since the event was created.", e); + } + return createdEventID; } - String createdEventID = null; - try { - Uri uri = cr.insert(eventsUri, values); - createdEventID = uri.getLastPathSegment(); - Log.d(LOG_TAG, "Created event with ID " + createdEventID); - - if (firstReminderMinutes > -1) { - ContentValues reminderValues = new ContentValues(); - reminderValues.put("event_id", Long.parseLong(uri.getLastPathSegment())); - reminderValues.put("minutes", firstReminderMinutes); - reminderValues.put("method", 1); - cr.insert(Uri.parse(CONTENT_PROVIDER + CONTENT_PROVIDER_PATH_REMINDERS), reminderValues); - } - - if (secondReminderMinutes > -1) { - ContentValues reminderValues = new ContentValues(); - reminderValues.put("event_id", Long.parseLong(uri.getLastPathSegment())); - reminderValues.put("minutes", secondReminderMinutes); - reminderValues.put("method", 1); - cr.insert(Uri.parse(CONTENT_PROVIDER + CONTENT_PROVIDER_PATH_REMINDERS), reminderValues); - } - } catch (Exception e) { - Log.e(LOG_TAG, "Creating reminders failed, ignoring since the event was created.", e); + @SuppressWarnings("MissingPermission") // already requested in calling method + public String createCalendar(String calendarName, String calendarColor) { + try { + // don't create if it already exists + Uri evuri = CalendarContract.Calendars.CONTENT_URI; + final ContentResolver contentResolver = cordova.getActivity().getContentResolver(); + Cursor result = contentResolver.query(evuri, new String[]{CalendarContract.Calendars._ID, CalendarContract.Calendars.NAME, CalendarContract.Calendars.CALENDAR_DISPLAY_NAME}, null, null, null); + if (result != null) { + while (result.moveToNext()) { + if ((result.getString(1) != null && result.getString(1).equals(calendarName)) || (result.getString(2) != null && result.getString(2).equals(calendarName))) { + result.close(); + return null; + } + } + result.close(); + } + + // doesn't exist yet, so create + Uri calUri = CalendarContract.Calendars.CONTENT_URI; + ContentValues cv = new ContentValues(); + cv.put(CalendarContract.Calendars.ACCOUNT_NAME, "AccountName"); + cv.put(CalendarContract.Calendars.ACCOUNT_TYPE, CalendarContract.ACCOUNT_TYPE_LOCAL); + cv.put(CalendarContract.Calendars.NAME, calendarName); + cv.put(CalendarContract.Calendars.CALENDAR_DISPLAY_NAME, calendarName); + if (calendarColor != null) { + cv.put(CalendarContract.Calendars.CALENDAR_COLOR, Color.parseColor(calendarColor)); + } + cv.put(CalendarContract.Calendars.VISIBLE, 1); + cv.put(CalendarContract.Calendars.CALENDAR_ACCESS_LEVEL, CalendarContract.Calendars.CAL_ACCESS_OWNER); + cv.put(CalendarContract.Calendars.OWNER_ACCOUNT, "AccountName" ); + cv.put(CalendarContract.Calendars.SYNC_EVENTS, 0); + + calUri = calUri.buildUpon() + .appendQueryParameter(CalendarContract.CALLER_IS_SYNCADAPTER, "true") + .appendQueryParameter(CalendarContract.Calendars.ACCOUNT_NAME, "AccountName") + .appendQueryParameter(CalendarContract.Calendars.ACCOUNT_TYPE, CalendarContract.ACCOUNT_TYPE_LOCAL) + .build(); + + Uri created = contentResolver.insert(calUri, cv); + if (created != null) { + return created.getLastPathSegment(); + } + } catch (Exception e) { + Log.e(LOG_TAG, "Creating calendar failed.", e); + } + return null; } - return createdEventID; - } - - @SuppressWarnings("MissingPermission") // already requested in calling method - public String createCalendar(String calendarName, String calendarColor) { - try { - // don't create if it already exists - Uri evuri = CalendarContract.Calendars.CONTENT_URI; - final ContentResolver contentResolver = cordova.getActivity().getContentResolver(); - Cursor result = contentResolver.query(evuri, new String[] {CalendarContract.Calendars._ID, CalendarContract.Calendars.NAME, CalendarContract.Calendars.CALENDAR_DISPLAY_NAME}, null, null, null); - if (result != null) { - while (result.moveToNext()) { - if (result.getString(1).equals(calendarName) || result.getString(2).equals(calendarName)) { - result.close(); - return null; - } - } - result.close(); - } - - // doesn't exist yet, so create - Uri calUri = CalendarContract.Calendars.CONTENT_URI; - ContentValues cv = new ContentValues(); - cv.put(CalendarContract.Calendars.ACCOUNT_NAME, "AccountName"); - cv.put(CalendarContract.Calendars.ACCOUNT_TYPE, CalendarContract.ACCOUNT_TYPE_LOCAL); - cv.put(CalendarContract.Calendars.NAME, calendarName); - cv.put(CalendarContract.Calendars.CALENDAR_DISPLAY_NAME, calendarName); - if (calendarColor != null) { - cv.put(CalendarContract.Calendars.CALENDAR_COLOR, Color.parseColor(calendarColor)); - } - cv.put(CalendarContract.Calendars.VISIBLE, 1); - cv.put(CalendarContract.Calendars.SYNC_EVENTS, 0); - - calUri = calUri.buildUpon() - .appendQueryParameter(CalendarContract.CALLER_IS_SYNCADAPTER, "true") - .appendQueryParameter(CalendarContract.Calendars.ACCOUNT_NAME, "AccountName") - .appendQueryParameter(CalendarContract.Calendars.ACCOUNT_TYPE, CalendarContract.ACCOUNT_TYPE_LOCAL) - .build(); - - Uri created = contentResolver.insert(calUri, cv); - if (created != null) { - return created.getLastPathSegment(); - } - } catch (Exception e) { - Log.e(LOG_TAG, "Creating calendar failed.", e); + + ; + + @SuppressWarnings("MissingPermission") // already requested in calling method + public void deleteCalendar(String calendarName) { + try { + Uri evuri = CalendarContract.Calendars.CONTENT_URI; + final ContentResolver contentResolver = cordova.getActivity().getContentResolver(); + Cursor result = contentResolver.query(evuri, new String[]{CalendarContract.Calendars._ID, CalendarContract.Calendars.NAME, CalendarContract.Calendars.CALENDAR_DISPLAY_NAME}, null, null, null); + if (result != null) { + while (result.moveToNext()) { + if (result.getString(1) != null && result.getString(1).equals(calendarName) || result.getString(2) != null && result.getString(2).equals(calendarName)) { + long calid = result.getLong(0); + Uri deleteUri = ContentUris.withAppendedId(evuri, calid); + contentResolver.delete(deleteUri, null, null); + } + } + result.close(); + } + + // also delete previously crashing calendars, see https://github.com/EddyVerbruggen/Calendar-PhoneGap-Plugin/issues/241 + deleteCrashingCalendars(contentResolver); + } catch (Throwable t) { + System.err.println(t.getMessage()); + t.printStackTrace(); + } } - return null; - }; - - @SuppressWarnings("MissingPermission") // already requested in calling method - public void deleteCalendar(String calendarName) { - try { - Uri evuri = CalendarContract.Calendars.CONTENT_URI; - final ContentResolver contentResolver = cordova.getActivity().getContentResolver(); - Cursor result = contentResolver.query(evuri, new String[] {CalendarContract.Calendars._ID, CalendarContract.Calendars.NAME, CalendarContract.Calendars.CALENDAR_DISPLAY_NAME}, null, null, null); - if (result != null) { - while (result.moveToNext()) { - if (result.getString(1).equals(calendarName) || result.getString(2).equals(calendarName)) { - long calid = result.getLong(0); - Uri deleteUri = ContentUris.withAppendedId(evuri, calid); - contentResolver.delete(deleteUri, null, null); - } - } - result.close(); - } - - // also delete previously crashing calendars, see https://github.com/EddyVerbruggen/Calendar-PhoneGap-Plugin/issues/241 - deleteCrashingCalendars(contentResolver); - } catch (Throwable t) { - System.err.println(t.getMessage()); - t.printStackTrace(); + + @SuppressWarnings("MissingPermission") // already requested in calling method + private void deleteCrashingCalendars(ContentResolver contentResolver) { + // first find any faulty Calendars + final String fixingAccountName = "FixingAccountName"; + String selection = CalendarContract.Calendars.ACCOUNT_NAME + " IS NULL"; + Uri uri = CalendarContract.Calendars.CONTENT_URI; + uri = uri.buildUpon() + .appendQueryParameter(CalendarContract.CALLER_IS_SYNCADAPTER, "true") + .appendQueryParameter(CalendarContract.Calendars.ACCOUNT_NAME, fixingAccountName) + .appendQueryParameter(CalendarContract.Calendars.ACCOUNT_TYPE, CalendarContract.ACCOUNT_TYPE_LOCAL) + .build(); + ContentValues values = new ContentValues(); + values.put(CalendarContract.Calendars.ACCOUNT_NAME, fixingAccountName); + values.put(CalendarContract.Calendars.ACCOUNT_TYPE, CalendarContract.ACCOUNT_TYPE_LOCAL); + int count = contentResolver.update(uri, values, selection, null); + + // now delete any faulty Calendars + if (count > 0) { + Uri evuri = CalendarContract.Calendars.CONTENT_URI; + Cursor result = contentResolver.query(evuri, new String[]{CalendarContract.Calendars._ID, CalendarContract.Calendars.ACCOUNT_NAME}, null, null, null); + if (result != null) { + while (result.moveToNext()) { + if (result.getString(1) != null && result.getString(1).equals(fixingAccountName)) { + long calid = result.getLong(0); + Uri deleteUri = ContentUris.withAppendedId(evuri, calid); + contentResolver.delete(deleteUri, null, null); + } + } + result.close(); + } + } } - } - - @SuppressWarnings("MissingPermission") // already requested in calling method - private void deleteCrashingCalendars(ContentResolver contentResolver) { - // first find any faulty Calendars - final String fixingAccountName = "FixingAccountName"; - String selection = CalendarContract.Calendars.ACCOUNT_NAME + " IS NULL"; - Uri uri = CalendarContract.Calendars.CONTENT_URI; - uri = uri.buildUpon() - .appendQueryParameter(CalendarContract.CALLER_IS_SYNCADAPTER, "true") - .appendQueryParameter(CalendarContract.Calendars.ACCOUNT_NAME, fixingAccountName) - .appendQueryParameter(CalendarContract.Calendars.ACCOUNT_TYPE, CalendarContract.ACCOUNT_TYPE_LOCAL) - .build(); - ContentValues values = new ContentValues(); - values.put(CalendarContract.Calendars.ACCOUNT_NAME, fixingAccountName); - values.put(CalendarContract.Calendars.ACCOUNT_TYPE, CalendarContract.ACCOUNT_TYPE_LOCAL); - int count = contentResolver.update(uri, values, selection, null); - - // now delete any faulty Calendars - if (count > 0) { - Uri evuri = CalendarContract.Calendars.CONTENT_URI; - Cursor result = contentResolver.query(evuri, new String[] {CalendarContract.Calendars._ID, CalendarContract.Calendars.ACCOUNT_NAME}, null, null, null); - if (result != null) { - while (result.moveToNext()) { - if (result.getString(1).equals(fixingAccountName)) { - long calid = result.getLong(0); - Uri deleteUri = ContentUris.withAppendedId(evuri, calid); - contentResolver.delete(deleteUri, null, null); - } - } - result.close(); - } + + public static boolean isAllDayEvent(final Date startDate, final Date endDate) { + return ((endDate.getTime() - startDate.getTime()) % (24 * 60 * 60 * 1000) == 0); } - } - - public static boolean isAllDayEvent(final Date startDate, final Date endDate) { - return - ((endDate.getTime() - startDate.getTime()) % (24*60*60*1000) == 0) && - startDate.getHours() == 0 && - startDate.getMinutes() == 0 && - startDate.getSeconds() == 0 && - endDate.getHours() == 0 && - endDate.getMinutes() == 0 && - endDate.getSeconds() == 0; - } } diff --git a/src/android/nl/xservices/plugins/accessor/CalendarProviderAccessor.java b/src/android/nl/xservices/plugins/accessor/CalendarProviderAccessor.java index 15704e74..21551c38 100644 --- a/src/android/nl/xservices/plugins/accessor/CalendarProviderAccessor.java +++ b/src/android/nl/xservices/plugins/accessor/CalendarProviderAccessor.java @@ -7,6 +7,7 @@ import android.provider.CalendarContract.Calendars; import android.provider.CalendarContract.Events; import android.provider.CalendarContract.Instances; + import org.apache.cordova.CordovaInterface; import java.lang.Integer; @@ -20,10 +21,11 @@ public CalendarProviderAccessor(CordovaInterface cordova) { @Override protected EnumMap initContentProviderKeys() { - EnumMap keys = new EnumMap( - KeyIndex.class); + EnumMap keys = new EnumMap(KeyIndex.class); keys.put(KeyIndex.CALENDARS_ID, Calendars._ID); + keys.put(KeyIndex.IS_PRIMARY, Calendars.IS_PRIMARY); keys.put(KeyIndex.CALENDARS_NAME, Calendars.NAME); + keys.put(KeyIndex.CALENDARS_DISPLAY_NAME, Calendars.CALENDAR_DISPLAY_NAME); keys.put(KeyIndex.CALENDARS_VISIBLE, Calendars.VISIBLE); keys.put(KeyIndex.EVENTS_ID, Events._ID); keys.put(KeyIndex.EVENTS_CALENDAR_ID, Events.CALENDAR_ID); @@ -50,23 +52,23 @@ protected EnumMap initContentProviderKeys() { protected Cursor queryAttendees(String[] projection, String selection, String[] selectionArgs, String sortOrder) { return this.cordova.getActivity().getContentResolver().query( - Attendees.CONTENT_URI, projection, selection, selectionArgs, - sortOrder); + Attendees.CONTENT_URI, projection, selection, selectionArgs, + sortOrder); } @Override protected Cursor queryCalendars(String[] projection, String selection, String[] selectionArgs, String sortOrder) { return this.cordova.getActivity().getContentResolver().query( - Calendars.CONTENT_URI, projection, selection, selectionArgs, - sortOrder); + Calendars.CONTENT_URI, projection, selection, selectionArgs, + sortOrder); } @Override protected Cursor queryEvents(String[] projection, String selection, String[] selectionArgs, String sortOrder) { return this.cordova.getActivity().getContentResolver().query( - Events.CONTENT_URI, projection, selection, selectionArgs, sortOrder); + Events.CONTENT_URI, projection, selection, selectionArgs, sortOrder); } @Override @@ -77,22 +79,30 @@ protected Cursor queryEventInstances(long startFrom, long startTo, ContentUris.appendId(builder, startFrom); ContentUris.appendId(builder, startTo); return this.cordova.getActivity().getContentResolver().query( - builder.build(), projection, selection, selectionArgs, sortOrder); + builder.build(), projection, selection, selectionArgs, sortOrder); + } + + @Override + public boolean deleteEvent(Uri eventsUri, long startFrom, long startTo, String title, String location, String notes) { + eventsUri = eventsUri == null ? Uri.parse(CONTENT_PROVIDER + CONTENT_PROVIDER_PATH_EVENTS) : eventsUri; + return super.deleteEvent(eventsUri, startFrom, startTo, title, location, notes); } @Override - public boolean deleteEvent(Uri eventsUri, long startFrom, long startTo, String title, String location) { + public boolean deleteEventById(Uri eventsUri, long id, long fromDate) { eventsUri = eventsUri == null ? Uri.parse(CONTENT_PROVIDER + CONTENT_PROVIDER_PATH_EVENTS) : eventsUri; - return super.deleteEvent(eventsUri, startFrom, startTo, title, location); + return super.deleteEventById(eventsUri, id, fromDate); } @Override public String createEvent(Uri eventsUri, String title, long startTime, long endTime, - String description, String location, Long firstReminderMinutes, Long secondReminderMinutes, - String recurrence, int recurrenceInterval, Long recurrenceEndTime, Integer calendarId, - String url) { + String description, String location, Long firstReminderMinutes, Long secondReminderMinutes, + String recurrence, int recurrenceInterval, String recurrenceWeekstart, + String recurrenceByDay, String recurrenceByMonthDay, Long recurrenceEndTime, Long recurrenceCount, + String allday, Integer calendarId, String url) { eventsUri = eventsUri == null ? Uri.parse(CONTENT_PROVIDER + CONTENT_PROVIDER_PATH_EVENTS) : eventsUri; return super.createEvent(eventsUri, title, startTime, endTime, description, location, - firstReminderMinutes, secondReminderMinutes, recurrence, recurrenceInterval, recurrenceEndTime, calendarId, url); + firstReminderMinutes, secondReminderMinutes, recurrence, recurrenceInterval, recurrenceWeekstart, + recurrenceByDay, recurrenceByMonthDay, recurrenceEndTime, recurrenceCount, allday, calendarId, url); } } diff --git a/src/android/nl/xservices/plugins/accessor/LegacyCalendarAccessor.java b/src/android/nl/xservices/plugins/accessor/LegacyCalendarAccessor.java index 794e51e7..00492dcf 100644 --- a/src/android/nl/xservices/plugins/accessor/LegacyCalendarAccessor.java +++ b/src/android/nl/xservices/plugins/accessor/LegacyCalendarAccessor.java @@ -3,6 +3,7 @@ import android.database.Cursor; import android.net.Uri; import android.os.Build; + import org.apache.cordova.CordovaInterface; import java.util.EnumMap; @@ -17,7 +18,9 @@ public LegacyCalendarAccessor(CordovaInterface cordova) { protected EnumMap initContentProviderKeys() { EnumMap keys = new EnumMap(KeyIndex.class); keys.put(KeyIndex.CALENDARS_ID, "_id"); + keys.put(KeyIndex.IS_PRIMARY, "isPrimary"); keys.put(KeyIndex.CALENDARS_NAME, "name"); + keys.put(KeyIndex.CALENDARS_DISPLAY_NAME, "displayname"); keys.put(KeyIndex.CALENDARS_VISIBLE, "selected"); keys.put(KeyIndex.EVENTS_ID, "_id"); keys.put(KeyIndex.EVENTS_CALENDAR_ID, "calendar_id"); @@ -53,7 +56,7 @@ protected Cursor queryAttendees(String[] projection, String selection, String[] selectionArgs, String sortOrder) { String uri = getContentProviderUri(CONTENT_PROVIDER_PATH_ATTENDEES); return this.cordova.getActivity().managedQuery(Uri.parse(uri), projection, - selection, selectionArgs, sortOrder); + selection, selectionArgs, sortOrder); } @Override @@ -61,7 +64,7 @@ protected Cursor queryCalendars(String[] projection, String selection, String[] selectionArgs, String sortOrder) { String uri = getContentProviderUri(CONTENT_PROVIDER_PATH_CALENDARS); return this.cordova.getActivity().managedQuery(Uri.parse(uri), projection, - selection, selectionArgs, sortOrder); + selection, selectionArgs, sortOrder); } @Override @@ -69,7 +72,7 @@ protected Cursor queryEvents(String[] projection, String selection, String[] selectionArgs, String sortOrder) { String uri = getContentProviderUri(CONTENT_PROVIDER_PATH_EVENTS); return this.cordova.getActivity().managedQuery(Uri.parse(uri), projection, - selection, selectionArgs, sortOrder); + selection, selectionArgs, sortOrder); } @Override @@ -77,25 +80,33 @@ protected Cursor queryEventInstances(long startFrom, long startTo, String[] projection, String selection, String[] selectionArgs, String sortOrder) { String uri = getContentProviderUri(CONTENT_PROVIDER_PATH_INSTANCES_WHEN) + - "/" + Long.toString(startFrom) + "/" + Long.toString(startTo); + "/" + Long.toString(startFrom) + "/" + Long.toString(startTo); return this.cordova.getActivity().managedQuery(Uri.parse(uri), projection, - selection, selectionArgs, sortOrder); + selection, selectionArgs, sortOrder); + } + + @Override + public boolean deleteEvent(Uri eventsUri, long startFrom, long startTo, String title, String location, String notes) { + eventsUri = eventsUri == null ? Uri.parse(CONTENT_PROVIDER_PRE_FROYO + CONTENT_PROVIDER_PATH_EVENTS) : eventsUri; + return super.deleteEvent(eventsUri, startFrom, startTo, title, location, notes); } @Override - public boolean deleteEvent(Uri eventsUri, long startFrom, long startTo, String title, String location) { + public boolean deleteEventById(Uri eventsUri, long id, long fromDate) { eventsUri = eventsUri == null ? Uri.parse(CONTENT_PROVIDER_PRE_FROYO + CONTENT_PROVIDER_PATH_EVENTS) : eventsUri; - return super.deleteEvent(eventsUri, startFrom, startTo, title, location); + return super.deleteEventById(eventsUri, id, fromDate); } @Override public String createEvent(Uri eventsUri, String title, long startTime, long endTime, - String description, String location, Long firstReminderMinutes, Long secondReminderMinutes, - String recurrence, int recurrenceInterval, Long recurrenceEndTime, Integer calendarId, - String url) { + String description, String location, Long firstReminderMinutes, Long secondReminderMinutes, + String recurrence, int recurrenceInterval, String recurrenceWeekstart, + String recurrenceByDay, String recurrenceByMonthDay, Long recurrenceEndTime, Long recurrenceCount, + String allday, Integer calendarId, String url) { eventsUri = eventsUri == null ? Uri.parse(CONTENT_PROVIDER_PRE_FROYO + CONTENT_PROVIDER_PATH_EVENTS) : eventsUri; return super.createEvent(eventsUri, title, startTime, endTime, description, location, - firstReminderMinutes, secondReminderMinutes, recurrence, recurrenceInterval, recurrenceEndTime, calendarId, url); + firstReminderMinutes, secondReminderMinutes, recurrence, recurrenceInterval, recurrenceWeekstart, + recurrenceByDay, recurrenceByMonthDay, recurrenceEndTime, recurrenceCount, allday, calendarId, url); } } diff --git a/src/ios/Calendar.h b/src/ios/Calendar.h index 29ff2339..bd61a402 100644 --- a/src/ios/Calendar.h +++ b/src/ios/Calendar.h @@ -41,6 +41,7 @@ - (void)deleteEvent:(CDVInvokedUrlCommand*)command; - (void)deleteEventFromNamedCalendar:(CDVInvokedUrlCommand*)command; - (void)deleteEventFromCalendar:(CDVInvokedUrlCommand*)command calendar:(EKCalendar*)calendar; +- (void)deleteEventById:(CDVInvokedUrlCommand*)command; - (void)eventEditViewController:(EKEventEditViewController*)controller didCompleteWithAction:(EKEventEditViewAction) action; @end diff --git a/src/ios/Calendar.m b/src/ios/Calendar.m index 336e9b1e..c6c22202 100644 --- a/src/ios/Calendar.m +++ b/src/ios/Calendar.m @@ -345,6 +345,9 @@ - (EKCalendar*) findEKCalendar: (NSString *)calendarName { if ([thisCalendar.title isEqualToString:calendarName]) { return thisCalendar; } + if ([thisCalendar.calendarIdentifier isEqualToString:calendarName]) { + return thisCalendar; + } } } NSLog(@"No match found for calendar with name: %@", calendarName); @@ -378,7 +381,7 @@ - (NSMutableArray*) eventsToDataArray: (NSArray*)matchingEvents { NSMutableDictionary *entry = [[NSMutableDictionary alloc] initWithObjectsAndKeys: event.title, @"title", event.calendar.title, @"calendar", - event.eventIdentifier, @"id", + event.calendarItemIdentifier , @"id", [df stringFromDate:event.startDate], @"startDate", [df stringFromDate:event.endDate], @"endDate", [df stringFromDate:event.lastModifiedDate], @"lastModifiedDate", @@ -457,7 +460,6 @@ - (NSMutableArray*) eventsToDataArray: (NSArray*)matchingEvents { } } - [entry setObject:event.calendarItemIdentifier forKey:@"id"]; [results addObject:entry]; } return results; @@ -499,6 +501,32 @@ - (void) listCalendars:(CDVInvokedUrlCommand*)command { } - (void) listEventsInRange:(CDVInvokedUrlCommand*)command { + NSDictionary* options = [command.arguments objectAtIndex:0]; + NSNumber* startTime = [options objectForKey:@"startTime"]; + NSNumber* endTime = [options objectForKey:@"endTime"]; + + [self.commandDelegate runInBackground: ^{ + NSLog(@"listEventsInRange invoked"); + NSTimeInterval _startInterval = [startTime doubleValue] / 1000; // strip millis + NSDate *startDate = [NSDate dateWithTimeIntervalSince1970:_startInterval]; + + NSTimeInterval _endInterval = [endTime doubleValue] / 1000; // strip millis + NSDate *endDate = [NSDate dateWithTimeIntervalSince1970:_endInterval]; + + NSLog(@"startDate: %@", startDate); + NSLog(@"endDate: %@", endDate); + + CDVPluginResult *pluginResult = nil; + + NSArray *calendarArray = nil; + NSPredicate *fetchCalendarEvents = [eventStore predicateForEventsWithStartDate:startDate endDate:endDate calendars:calendarArray]; + NSArray *matchingEvents = [eventStore eventsMatchingPredicate:fetchCalendarEvents]; + NSMutableArray * eventsDataArray = [self eventsToDataArray:matchingEvents]; + + pluginResult = [CDVPluginResult resultWithStatus: CDVCommandStatus_OK messageAsArray:eventsDataArray]; + + [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId]; + }]; } - (void)createEventWithOptions:(CDVInvokedUrlCommand*)command { @@ -745,6 +773,57 @@ - (void) deleteEvent:(CDVInvokedUrlCommand*)command { } } +- (void) deleteEventById:(CDVInvokedUrlCommand*)command { + NSDictionary* options = [command.arguments objectAtIndex:0]; + NSString* ciid = [options objectForKey:@"id"]; + NSNumber* fromTime = [options objectForKey:@"fromTime"]; + + [self.commandDelegate runInBackground: ^{ + + // Get original instance + EKEvent* firstEvent = (EKEvent *)[eventStore calendarItemWithIdentifier:ciid]; + if (firstEvent == nil) { + // Fail + [self.commandDelegate + sendPluginResult:[CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"Could not find event."] + callbackId:command.callbackId]; + return; + } else { + EKEvent* instance; + if (fromTime != nil && fromTime != (id)NSNull.null) { + // Find target instance + NSDate* fromDate = [NSDate dateWithTimeIntervalSince1970:(fromTime.doubleValue / 1000)]; // strip millis + NSArray* toDelete = [eventStore eventsMatchingPredicate:[eventStore predicateForEventsWithStartDate:fromDate endDate:NSDate.distantFuture calendars:@[firstEvent.calendar]]]; + if (toDelete.count < 1) { + // Nothing to delete + [self.commandDelegate sendPluginResult:[CDVPluginResult resultWithStatus:CDVCommandStatus_OK] callbackId:command.callbackId]; + return; + } + NSArray* toDeleteSorted = [toDelete sortedArrayUsingSelector:@selector(compareStartDateWithEvent:)]; + instance = toDeleteSorted.firstObject; + } else { + // First instance is target + instance = firstEvent; + } + + // Delete + NSError *error = nil; + [eventStore removeEvent:instance span:EKSpanFutureEvents error:&error]; + if (error != nil) { + // Fail + [self.commandDelegate + sendPluginResult:[CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"Could not delete event."] + callbackId:command.callbackId]; + return; + } else { + // Succeed + [self.commandDelegate sendPluginResult:[CDVPluginResult resultWithStatus:CDVCommandStatus_OK] callbackId:command.callbackId]; + return; + } + } + }]; +} + - (void) findAllEventsInNamedCalendar:(CDVInvokedUrlCommand*)command { NSDictionary* options = [command.arguments objectAtIndex:0]; NSString* calendarName = [options objectForKey:@"calendarName"]; diff --git a/src/windows/appointment.js b/src/windows/appointment.js new file mode 100644 index 00000000..e5a856de --- /dev/null +++ b/src/windows/appointment.js @@ -0,0 +1,58 @@ +var appointment = { + createEventWithOptions: function (successCallback, errorCallback, options) { + var o = { + title: null, + location: null, + notes: null, + startTime: null, + endTime: null, + options: { // Defaults: Calendar.prototype.getCalendarOptions() + firstReminderMinutes: null, + secondReminderMinutes: null, + recurrence: null, + recurrenceInterval: null, + recurrenceWeekstart: null, + recurrenceByDay: null, + recurrenceByMonthDay: null, + recurrenceEndDate: null, + recurrenceCount: null, + calendarName: null, + calendarId: null, + url: null + } + }; + o = options[0]; + var appointment = new Windows.ApplicationModel.Appointments.Appointment(); + + appointment.startTime = new Date(o.startTime); + if (o.endTime) + appointment.duration = Math.abs(o.endTime - o.startTime); //(60 * 60 * 100000) / 100; // 1 hour in 100ms units + else + appointment.allDay = true; + appointment.location = o.location || ''; + appointment.subject = o.title || ''; + appointment.details = o.notes || ''; + appointment.reminder = o.options.firstReminderMinutes; + appointment.onlineMeetingLink = o.options.url || ''; + + var boundingRect = window.document; // e.srcElement.getBoundingClientRect(); + var selectionRect = { + x: boundingRect.left, + y: boundingRect.top, + width: boundingRect.width, + height: boundingRect.height + }; + + var appointmentId = Windows.ApplicationModel.Appointments.AppointmentManager.showAddAppointmentAsync( + appointment, selectionRect, Windows.UI.Popups.Placement.default) + .done(function (appointmentId) { + if (appointmentId) { + successCallback(appointmentId); + } else { + errorCallback(); + } + }); + } +} + +cordova.commandProxy.add("Calendar", appointment); \ No newline at end of file diff --git a/test/package.json b/test/package.json new file mode 100644 index 00000000..19fce855 --- /dev/null +++ b/test/package.json @@ -0,0 +1,14 @@ +{ + "name": "cordova-plugin-calendar-tests", + "version": "5.0.0", + "description": "", + "cordova": { + "id": "cordova-plugin-calendar-tests", + "platforms": [] + }, + "keywords": [ + "ecosystem:cordova" + ], + "author": "", + "license": "Apache 2.0" +} diff --git a/test/plugin.xml b/test/plugin.xml new file mode 100644 index 00000000..701967b5 --- /dev/null +++ b/test/plugin.xml @@ -0,0 +1,44 @@ + + + + + Cordova Calendar Plugin Tests + Apache 2.0 + + + + + + + + + + + + + + + + + diff --git a/test/src/android/org/apache/cordova/calendartests/Utility.java b/test/src/android/org/apache/cordova/calendartests/Utility.java new file mode 100644 index 00000000..206901e8 --- /dev/null +++ b/test/src/android/org/apache/cordova/calendartests/Utility.java @@ -0,0 +1,46 @@ +package org.apache.cordova.calendartests; + +import android.content.ContentResolver; +import android.os.Bundle; +import org.apache.cordova.CallbackContext; +import org.apache.cordova.CordovaPlugin; +import org.apache.cordova.PluginResult; +import org.json.JSONArray; +import org.json.JSONException; + +public class Utility extends CordovaPlugin { + + private CallbackContext callback; + + @Override + public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException { + this.callback = callbackContext; + + if ("syncAndroidGoogleCalendar".equals(action)) { + syncAndroidGoogleCalendar(); + return true; + } + return false; + } + + private void syncAndroidGoogleCalendar() { + cordova.getThreadPool().execute(new Runnable() { @Override public void run() { + String authority = "com.android.calendar"; + + Bundle settingsBundle = new Bundle(); + settingsBundle.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true); + settingsBundle.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true); + for (android.accounts.Account acc : android.accounts.AccountManager.get(cordova.getActivity()).getAccountsByType("com.google")) { + ContentResolver.requestSync(acc, authority, settingsBundle); + + // Wait for completion + try { Thread.sleep(1000); } catch (InterruptedException e) {} + int ii = 0; + while (++ii < 30 && ContentResolver.isSyncActive(acc, authority)) { + try { Thread.sleep(1000); } catch (InterruptedException e) {} + } + } + callback.sendPluginResult(new PluginResult(PluginResult.Status.OK)); + }}); + } +} diff --git a/test/tests.js b/test/tests.js index 3f0f52dd..048117a1 100644 --- a/test/tests.js +++ b/test/tests.js @@ -1,14 +1,71 @@ exports.defineAutoTests = function() { - var fail = function (done) { - expect(true).toBe(false); - done(); - }, - succeed = function (done) { - expect(true).toBe(true); - done(); + var itP = function (description, fn, timeout) { + it(description, function (done) { + Promise.resolve(fn()) + .catch(fail) + .then(done, done); + }, timeout); }; + var runTime = new Date(); + var runId = Math.floor((runTime - new Date(runTime).setHours(0, 0, 0, 0)) / 1000); + var runTag = ' [cpctZQX' + runId + ']'; + + var delay = function (t, v) { + return new Promise(function (resolve) { + setTimeout(resolve.bind(null, v), t) + }); + }; + var promisifyScbEcb = function (func) { + if (typeof (func) != 'function') + throw 'not a function: ' + func; + return function () { + var args = Array.prototype.slice.call(arguments); + if (args.length > func.length - 2) + throw 'too many arguments; expected at most ' + func.length - 2; + return new Promise(function (resolve, reject) { + args[func.length - 2] = resolve; + args[func.length - 1] = reject; + func.apply(null, args); + }); + }; + }; + var deleteEventP = promisifyScbEcb(plugins.calendar.deleteEvent); + var findEventP = promisifyScbEcb(plugins.calendar.findEvent); + var createEventP = promisifyScbEcb(plugins.calendar.createEvent); + var createEventWithOptionsP = promisifyScbEcb(plugins.calendar.createEventWithOptions); + var deleteEventByIdP = promisifyScbEcb(plugins.calendar.deleteEventById); + var syncAndroidGoogleCalendarP = promisifyScbEcb(function(successCallback, errorCallback) { + if (cordova.platformId == 'android') { + cordova.exec(successCallback, errorCallback, "CalendarTestsUtility", "syncAndroidGoogleCalendar", []); + } else { + successCallback(); + } + }); + var parseEventDate = plugins.calendar.parseEventDate; + + var newDate = function (dd, hh, mm) { + return new Date(2018, 0, 21 + dd, hh || 0, mm || 0); + }; + + + jasmine.DEFAULT_TIMEOUT_INTERVAL= 60000; + + beforeEach(function (done) { + /* clean up autotest data */ + return deleteEventP('[cpctZQX', null, null, newDate(1), newDate(8)) + .then(function () { + return findEventP(runTag, null, null, newDate(1), newDate(8)); + }) + .then(function (events) { + expect(events.length).toBe(0, 'if test data was cleaned up'); + }) + .catch(fail) + .then(done, done); + }); + + describe('Plugin availability', function () { it("window.plugins.calendar should exist", function() { expect(window.plugins.calendar).toBeDefined(); @@ -21,16 +78,256 @@ exports.defineAutoTests = function() { }); }); - /* - TODO extend - this is a copy-paste example of Toast - describe('Invalid usage', function () { - it("should fail due to an invalid position", function(done) { - window.plugins.toast.show('hi', 'short', 'nowhere', fail.bind(null, done), succeed.bind(null, done)); + // subsequent tests cover functionality specific to iOS and android + if (cordova.platformId != 'android' && cordova.platformId != 'ios') + return; + + describe('createEvent / findEvent / deleteEvent', function () { + itP('should create, find, then delete an event', function () { + var title = 'CFD event' + runTag; + + // create + return createEventP(title, null, null, newDate(2, 18), newDate(2, 19)) + .then(function (id) { + // find + return findEventP(title, null, null, newDate(2, 17), newDate(2, 20)) + .then(function (events) { + expect(events.length).toBe(1); + expect(events[0].title).toBe(title); + expect(parseEventDate(events[0].startDate)).toEqual(newDate(2, 18)); + expect(parseEventDate(events[0].endDate)).toEqual(newDate(2, 19)); + expect(events[0].id).toBe(id); + }); + }) + .then(function () { + // delete + return deleteEventP(title, null, null, newDate(2, 17), newDate(2, 20)) + .then(function () { + return findEventP(title, null, null, newDate(2, 17), newDate(2, 20)) + .then(function (events) { + expect(events.length).toBe(0); + }); + }); + }); + }); + + itP('should support delete by title/date/location/notes', function () { + var title = 'DF event' + runTag + ' '; + var first, bytitle, bydate, bylocation, bynotes, last; + + var expectedIds; + var removeExpectedId = function (id) { + expectedIds = expectedIds.filter(function (x) { return x != id; }); + }; + var expectIds = function (events) { + expect(events.map(function (e) { return e.id; })).toEqual(expectedIds); + }; + + // create + return createEventP(title + 'first', null, null, newDate(1, 7), newDate(1, 8)) + .then(function (id) { + first = id; + return createEventP(title + 'bytitle', null, null, newDate(1, 9), newDate(1, 10)); + }) + .then(function (id) { + bytitle = id; + return createEventP(title + 'bydate', null, null, newDate(1, 11), newDate(1, 13)); + }) + .then(function (id) { + bydate = id; + return createEventP(title, 'bylocation', null, newDate(1, 14), newDate(1, 15)); + }) + .then(function (id) { + bylocation = id; + return createEventP(title, null, 'bynotes', newDate(1, 16), newDate(1, 17)); + }) + .then(function (id) { + bynotes = id; + return createEventP(title + 'last', null, null, newDate(1, 18), newDate(1, 19)); + }) + .then(function (id) { + last = id; + + // find + expectedIds = [first, bytitle, bydate, bylocation, bynotes, last]; + return findEventP(title, null, null, newDate(1, 0), newDate(2, 0)); + }) + .then(function (events) { + expectIds(events); + + // delete by title + removeExpectedId(bytitle); + return deleteEventP(title + 'bytitle', null, null, newDate(1, 0), newDate(2, 0)); + }) + .then(function () { + // find + return findEventP(title, null, null, newDate(1, 0), newDate(2, 0)); + }) + .then(function (events) { + expectIds(events); + + // delete by date + removeExpectedId(bydate); + return deleteEventP(null, null, null, newDate(1, 11), newDate(1, 13)); + }) + .then(function () { + // find + return findEventP(title, null, null, newDate(1, 0), newDate(2, 0)); + }) + .then(function (events) { + expectIds(events); + + // delete by location + removeExpectedId(bylocation); + return deleteEventP(null, 'bylocation', null, newDate(1, 0), newDate(2, 0)); + }) + .then(function () { + // find + return findEventP(title, null, null, newDate(1, 0), newDate(2, 0)); + }) + .then(function (events) { + expectIds(events); + + // delete by notes + removeExpectedId(bynotes); + return deleteEventP(null, null, 'bynotes', newDate(1, 0), newDate(2, 0)); + }) + .then(function () { + // find + return findEventP(title, null, null, newDate(1, 0), newDate(2, 0)); + }) + .then(function (events) { + expectIds(events); + + // delete the rest + return deleteEventP(title, null, null, newDate(1, 0), newDate(2, 0)); + }); + }); + }); + + describe('deleteEventById', function () { + + var createRecurring = function (title, withCount) { + // create + var createOpts = withCount + ? { recurrence: "daily", recurrenceCount: 4 } + : { recurrence: "daily", recurrenceEndDate: newDate(6, 0) }; + return createEventWithOptionsP(title, null, null, newDate(2, 18), newDate(2, 19), createOpts) + .then(function (id) { + // find + return findEventP(title, null, null, newDate(2, 0), newDate(8, 0)) + .then(function (events) { + expect(events.length).toBe(4); + expect(events.every(function (x) { return x.id == id; })).toBe(true); + + // pedantic checks + expect(parseEventDate(events[2].startDate)).toEqual(newDate(4, 18)); + if (!withCount) { + var ev = events[0]; + var until = ev.recurrence ? ev.recurrence.until : ev.rrule.until.date; + expect(parseEventDate(until)).toEqual(newDate(6, 0)); + } + + return id; + }); + }); + }; + + itP('should support removing all instances', function () { + var title = 'delIdAll event' + runTag; + + return createRecurring(title) + .then(function (id) { + return deleteEventByIdP(id); + }) + .then(function () { + return findEventP(title, null, null, newDate(2, 0), newDate(8, 0)); + }) + .then(function (events) { + expect(events.length).toBe(0); + }); }); - it("should fail due to an invalid duration", function(done) { - window.plugins.toast.show('hi', 'medium', 'top', fail.bind(null, done), succeed.bind(null, done)); + itP('should support truncating series', function () { + var title = 'delIdDate event' + runTag; + + return createRecurring(title) + .then(function (id) { + return deleteEventByIdP(id, newDate(4, 18)); + }) + .then(function () { + return syncAndroidGoogleCalendarP(); + }) + .then(function () { + return findEventP(title, null, null, newDate(2, 0), newDate(8, 0)); + }) + .then(function (events) { + expect(events.length).toBe(2); + expect(parseEventDate(events[1].startDate)).toEqual(newDate(3, 18)); + }); + }); + + itP('should fail on invalid id', function () { + var failed = false; + return deleteEventByIdP('3826806B-1678-46DE-96B5-0748014AD920') + .catch(function () { + failed = true; + }) + .then(function () { + expect(failed).toBe(true); + }); + }); + + itP('should succeed if already truncated', function () { + var title = 'delIdAgain event' + runTag; + + return createRecurring(title) + .then(function (id) { + return deleteEventByIdP(id, newDate(5, 19)); + }) + .then(function () { + return findEventP(title, null, null, newDate(2, 0), newDate(8, 0)); + }) + .then(function (events) { + expect(events.length).toBe(4); + }); }); + + if (cordova.platformId == 'android') { + itP('should support truncating recurrences defined by a count in android', function() { + var title = 'delIdCtDate event' + runTag; + + return createRecurring(title, true) + .then(function (id) { + return deleteEventByIdP(id, newDate(4, 18)); + }) + .then(function () { + return syncAndroidGoogleCalendarP(); + }) + .then(function () { + return findEventP(title, null, null, newDate(2, 0), newDate(8, 0)); + }) + .then(function (events) { + expect(events.length).toBe(2); + expect(parseEventDate(events[1].startDate)).toEqual(newDate(3, 18)); + }); + }); + + itP('should succeed if already truncated by a count in android', function () { + var title = 'delIdCtAgain event' + runTag; + + return createRecurring(title, true) + .then(function (id) { + return deleteEventByIdP(id, newDate(5, 19)); + }) + .then(function () { + return findEventP(title, null, null, newDate(2, 0), newDate(8, 0)); + }) + .then(function (events) { + expect(events.length).toBe(4); + }); + }); + }; }); - */ + }; diff --git a/www/Calendar.js b/www/Calendar.js index f059f3f6..4b0b5d62 100644 --- a/www/Calendar.js +++ b/www/Calendar.js @@ -9,28 +9,28 @@ Calendar.prototype.getCreateCalendarOptions = function () { }; }; -Calendar.prototype.hasReadPermission = function (callback) { - cordova.exec(callback, null, "Calendar", "hasReadPermission", []); +Calendar.prototype.hasReadPermission = function (successCallback, errorCallback) { + cordova.exec(successCallback, errorCallback, "Calendar", "hasReadPermission", []); }; -Calendar.prototype.requestReadPermission = function (callback) { - cordova.exec(callback, null, "Calendar", "requestReadPermission", []); +Calendar.prototype.requestReadPermission = function (successCallback, errorCallback) { + cordova.exec(successCallback, errorCallback, "Calendar", "requestReadPermission", []); }; -Calendar.prototype.hasWritePermission = function (callback) { - cordova.exec(callback, null, "Calendar", "hasWritePermission", []); +Calendar.prototype.hasWritePermission = function (successCallback, errorCallback) { + cordova.exec(successCallback, errorCallback, "Calendar", "hasWritePermission", []); }; -Calendar.prototype.requestWritePermission = function (callback) { - cordova.exec(callback, null, "Calendar", "requestWritePermission", []); +Calendar.prototype.requestWritePermission = function (successCallback, errorCallback) { + cordova.exec(successCallback, errorCallback, "Calendar", "requestWritePermission", []); }; -Calendar.prototype.hasReadWritePermission = function (callback) { - cordova.exec(callback, null, "Calendar", "hasReadWritePermission", []); +Calendar.prototype.hasReadWritePermission = function (successCallback, errorCallback) { + cordova.exec(successCallback, errorCallback, "Calendar", "hasReadWritePermission", []); }; -Calendar.prototype.requestReadWritePermission = function (callback) { - cordova.exec(callback, null, "Calendar", "requestReadWritePermission", []); +Calendar.prototype.requestReadWritePermission = function (successCallback, errorCallback) { + cordova.exec(successCallback, errorCallback, "Calendar", "requestReadWritePermission", []); }; Calendar.prototype.createCalendar = function (calendarNameOrOptionsObject, successCallback, errorCallback) { @@ -74,7 +74,11 @@ Calendar.prototype.getCalendarOptions = function () { secondReminderMinutes: null, recurrence: null, // options are: 'daily', 'weekly', 'monthly', 'yearly' recurrenceInterval: 1, // only used when recurrence is set + recurrenceWeekstart: "MO", + recurrenceByDay: null, + recurrenceByMonthDay: null, recurrenceEndDate: null, + recurrenceCount: null, calendarName: null, calendarId: null, url: null @@ -204,6 +208,13 @@ Calendar.prototype.deleteEventFromNamedCalendar = function (title, location, not }]) }; +Calendar.prototype.deleteEventById = function (id, fromDate, successCallback, errorCallback) { + cordova.exec(successCallback, errorCallback, "Calendar", "deleteEventById", [{ + "id": id, + "fromTime": fromDate instanceof Date ? fromDate.getTime() : null + }]); +}; + Calendar.prototype.modifyEventWithOptions = function (title, location, notes, startDate, endDate, newTitle, newLocation, newNotes, newStartDate, newEndDate, options, newOptions, successCallback, errorCallback) { if (!(newStartDate instanceof Date && newEndDate instanceof Date)) { errorCallback("newStartDate and newEndDate must be JavaScript Date Objects"); @@ -266,6 +277,19 @@ Calendar.prototype.listCalendars = function (successCallback, errorCallback) { cordova.exec(successCallback, errorCallback, "Calendar", "listCalendars", []); }; +Calendar.prototype.parseEventDate = function (dateStr) { + // Handle yyyyMMddTHHmmssZ iCalendar UTC format + var icalRegExp = /\b(\d{4})(\d{2})(\d{2}T\d{2})(\d{2})(\d{2}Z)\b/; + if (icalRegExp.test(dateStr)) + return new Date(String(dateStr).replace(icalRegExp, '$1-$2-$3:$4:$5')); + + var spl; + // Handle yyyy-MM-dd HH:mm:ss format returned by AbstractCalendarAccessor.java L66 and Calendar.m L378, and yyyyMMddTHHmmss iCalendar local format, and similar + return (spl = /^\s*(\d{4})\D?(\d{2})\D?(\d{2})\D?(\d{2})\D?(\d{2})\D?(\d{2})\s*$/.exec(dateStr)) + && new Date(spl[1], spl[2] - 1, spl[3], spl[4], spl[5], spl[6]) + || new Date(dateStr); +}; + Calendar.install = function () { if (!window.plugins) { window.plugins = {};