/**
* Copyright (c) 2012, Twist and Shout, Inc. http://www.twist.com/
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
* IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
/**
* @author yvonne@twist.com (Yvonne Yip)
*/
package nl.xservices.plugins.accessor;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;
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;
import org.json.JSONObject;
import java.text.SimpleDateFormat;
import java.util.*;
import static android.provider.CalendarContract.Events;
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<Attendee> attendees;
public JSONObject toJSONObject() {
JSONObject obj = new JSONObject();
try {
obj.put("id", this.id);
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<KeyIndex, String> 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<KeyIndex, String> 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 title, String location, 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<String> selectionList = new ArrayList<String>();
if (title != null) {
selection += Events.TITLE + "=?";
selectionList.add(title);
}
if (location != null) {
if (!"".equals(selection)) {
selection += " AND ";
}
selection += Events.EVENT_LOCATION + "=?";
selectionList.add(location);
}
String[] selectionArgs = new String[selectionList.size()];
Cursor cursor = queryEventInstances(startFrom, startTo, projection, selection, selectionList.toArray(selectionArgs), sortOrder);
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());
}
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());
}
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
);
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());
}
return calendarsWrapper;
}
private Map<String, Event> 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);
}
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]);
}
select.append(")");
Cursor cursor = queryEvents(projection, select.toString(), null, null);
Map<String, Event> eventsMap = new HashMap<String, Event>();
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());
}
return eventsMap;
}
private Map<String, ArrayList<Attendee>> 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<String, ArrayList<Attendee>> attendeeMap =
new HashMap<String, ArrayList<Attendee>>();
if (cursor.moveToFirst()) {
int[] cols = new int[projection.length];
for (int i = 0; i < cols.length; i++) {
cols[i] = cursor.getColumnIndex(projection[i]);
}
ArrayList<Attendee> array = null;
String currentEventId = null;
do {
String eventId = cursor.getString(cols[0]);
if (currentEventId == null || !currentEventId.equals(eventId)) {
currentEventId = eventId;
array = new ArrayList<Attendee>();
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());
}
return attendeeMap;
}
public JSONArray findEvents(String title, String location, long startFrom, long startTo) {
JSONArray result = new JSONArray();
// Fetch events from the instance table.
Event[] instances = fetchEventInstances(title, location, startFrom, startTo);
if (instances == null) {
return result;
}
// Fetch events from the events table for more event info.
Map<String, Event> eventMap = fetchEventsAsMap(instances);
// Fetch event attendees
Map<String, ArrayList<Attendee>> 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());
}
}
return result;
}
public boolean deleteEvent(Uri eventsUri, long startFrom, long startTo, String title, String location) {
// filter
String where = "";
List<String> selectionList = new ArrayList<String>();
if (title != null) {
where += Events.TITLE + "=?";
selectionList.add(title);
}
if (location != null) {
if (!"".equals(where)) {
where += " AND ";
}
where += Events.EVENT_LOCATION + "=?";
selectionList.add(location);
}
if (startFrom > 0) {
if (!"".equals(where)) {
where += " AND ";
}
where += Events.DTSTART + "=?";
selectionList.add(""+startFrom);
}
if (startTo > 0) {
if (!"".equals(where)) {
where += " AND ";
}
where += Events.DTEND + "=?";
selectionList.add(""+startTo);
}
String[] selectionArgs = new String[selectionList.size()];
ContentResolver resolver = this.cordova.getActivity().getApplicationContext().getContentResolver();
int nrDeletedRecords = resolver.delete(eventsUri, where, selectionList.toArray(selectionArgs));
return nrDeletedRecords > 0;
}
public void createEvent(Uri eventsUri, String title, long startTime, long endTime, String description,
String location, Long firstReminderMinutes, Long secondReminderMinutes,
String recurrence, Long recurrenceEndTime) {
ContentResolver cr = this.cordova.getActivity().getContentResolver();
ContentValues values = new ContentValues();
final boolean allDayEvent = isAllDayEvent(new Date(startTime), new Date(endTime));
values.put(Events.EVENT_TIMEZONE, TimeZone.getDefault().getID());
values.put(Events.ALL_DAY, allDayEvent ? 1 : 0);
values.put(Events.DTSTART, allDayEvent ? startTime+(1000*60*60*24) : startTime);
values.put(Events.DTEND, endTime);
values.put(Events.TITLE, title);
values.put(Events.DESCRIPTION, description);
values.put(Events.HAS_ALARM, 1);
values.put(Events.CALENDAR_ID, 1);
values.put(Events.EVENT_LOCATION, location);
if (recurrence != null) {
if (recurrenceEndTime == null) {
values.put(Events.RRULE, "FREQ=" + recurrence.toUpperCase());
} else {
final SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd");
values.put(Events.RRULE, "FREQ=" + recurrence.toUpperCase() + ";UNTIL=" + sdf.format(new Date(recurrenceEndTime)));
}
}
Uri uri = cr.insert(eventsUri, values);
Log.d(LOG_TAG, "Added to ContentResolver");
// TODO ?
getActiveCalendarIds();
if (firstReminderMinutes != null) {
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 != null) {
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);
}
}
public void createCalendar(String calendarName) {
Uri calUri = CalendarContract.Calendars.CONTENT_URI;
ContentValues cv = new ContentValues();
// cv.put(CalendarContract.Calendars.ACCOUNT_NAME, yourAccountName);
// cv.put(CalendarContract.Calendars.ACCOUNT_TYPE, CalendarContract.ACCOUNT_TYPE_LOCAL);
// cv.put(CalendarContract.Calendars.NAME, "myname");
cv.put(CalendarContract.Calendars.CALENDAR_DISPLAY_NAME, calendarName);
// cv.put(CalendarContract.Calendars.CALENDAR_COLOR, yourColor);
// cv.put(CalendarContract.Calendars.CALENDAR_ACCESS_LEVEL, CalendarContract.Calendars.CAL_ACCESS_OWNER);
// cv.put(CalendarContract.Calendars.OWNER_ACCOUNT, true);
cv.put(CalendarContract.Calendars.VISIBLE, 1);
cv.put(CalendarContract.Calendars.SYNC_EVENTS, 0);
calUri = calUri.buildUpon()
// .appendQueryParameter(CalendarContract.CALLER_IS_SYNCADAPTER, "false")
// .appendQueryParameter(CalendarContract.Calendars.ACCOUNT_NAME, ACCOUNT_NAME)
// .appendQueryParameter(CalendarContract.Calendars.ACCOUNT_TYPE, CalendarContract.ACCOUNT_TYPE_LOCAL)
.build();
Uri result = this.cordova.getActivity().getApplicationContext().getContentResolver().insert(calUri, cv);
int i=0;
}
public static boolean isAllDayEvent(final Date startDate, final Date endDate) {
return
endDate.getTime() - startDate.getTime() == (24*60*60*1000) &&
startDate.getHours() == 0 &&
startDate.getMinutes() == 0 &&
startDate.getSeconds() == 0 &&
endDate.getHours() == 0 &&
endDate.getMinutes() == 0 &&
endDate.getSeconds() == 0;
}
}