diff options
Diffstat (limited to 'core/java/android/provider/Calendar.java')
-rw-r--r-- | core/java/android/provider/Calendar.java | 740 |
1 files changed, 453 insertions, 287 deletions
diff --git a/core/java/android/provider/Calendar.java b/core/java/android/provider/Calendar.java index f046cef..9a09805 100644 --- a/core/java/android/provider/Calendar.java +++ b/core/java/android/provider/Calendar.java @@ -16,34 +16,27 @@ package android.provider; +import android.accounts.Account; import android.app.AlarmManager; import android.app.PendingIntent; +import android.content.ContentProviderClient; import android.content.ContentResolver; import android.content.ContentUris; import android.content.ContentValues; import android.content.Context; +import android.content.CursorEntityIterator; +import android.content.Entity; +import android.content.EntityIterator; import android.content.Intent; import android.database.Cursor; +import android.database.DatabaseUtils; import android.net.Uri; +import android.os.RemoteException; import android.pim.ICalendar; -import android.pim.RecurrenceSet; import android.text.TextUtils; import android.text.format.DateUtils; import android.text.format.Time; -import android.util.Config; import android.util.Log; -import android.accounts.Account; -import com.android.internal.database.ArrayListCursor; -import com.google.android.gdata.client.AndroidGDataClient; -import com.google.android.gdata.client.AndroidXmlParserFactory; -import com.google.wireless.gdata.calendar.client.CalendarClient; -import com.google.wireless.gdata.calendar.data.EventEntry; -import com.google.wireless.gdata.calendar.data.Who; -import com.google.wireless.gdata.calendar.parser.xml.XmlCalendarGDataParserFactory; -import com.google.wireless.gdata.data.StringUtils; - -import java.util.ArrayList; -import java.util.Vector; /** * The Calendar provider contains all calendar events. @@ -57,8 +50,7 @@ public final class Calendar { /** * Broadcast Action: An event reminder. */ - public static final String - EVENT_REMINDER_ACTION = "android.intent.action.EVENT_REMINDER"; + public static final String EVENT_REMINDER_ACTION = "android.intent.action.EVENT_REMINDER"; /** * These are the symbolic names for the keys used in the extra data @@ -67,7 +59,7 @@ public final class Calendar { public static final String EVENT_BEGIN_TIME = "beginTime"; public static final String EVENT_END_TIME = "endTime"; - public static final String AUTHORITY = "calendar"; + public static final String AUTHORITY = "com.android.calendar"; /** * The content:// style URL for the top-level calendar authority @@ -76,16 +68,20 @@ public final class Calendar { Uri.parse("content://" + AUTHORITY); /** + * An optional insert, update or delete URI parameter that allows the caller + * to specify that it is a sync adapter. The default value is false. If true + * the dirty flag is not automatically set and the "syncToNetwork" parameter + * is set to false when calling + * {@link ContentResolver#notifyChange(android.net.Uri, android.database.ContentObserver, boolean)}. + */ + public static final String CALLER_IS_SYNCADAPTER = "caller_is_syncadapter"; + + /** * Columns from the Calendars table that other tables join into themselves. */ public interface CalendarsColumns { /** - * A string that uniquely identifies this contact to its source - */ - public static final String SOURCE_ID = "sourceid"; - - /** * The color of the calendar * <P>Type: INTEGER (color value)</P> */ @@ -137,13 +133,81 @@ public final class Calendar { * <p>Type: String (blob)</p> */ public static final String SYNC_STATE = "sync_state"; + + /** + * The account that was used to sync the entry to the device. + * <P>Type: TEXT</P> + */ + public static final String _SYNC_ACCOUNT = "_sync_account"; + + /** + * The type of the account that was used to sync the entry to the device. + * <P>Type: TEXT</P> + */ + public static final String _SYNC_ACCOUNT_TYPE = "_sync_account_type"; + + /** + * The unique ID for a row assigned by the sync source. NULL if the row has never been synced. + * <P>Type: TEXT</P> + */ + public static final String _SYNC_ID = "_sync_id"; + + /** + * The last time, from the sync source's point of view, that this row has been synchronized. + * <P>Type: INTEGER (long)</P> + */ + public static final String _SYNC_TIME = "_sync_time"; + + /** + * The version of the row, as assigned by the server. + * <P>Type: TEXT</P> + */ + public static final String _SYNC_VERSION = "_sync_version"; + + /** + * For use by sync adapter at its discretion; not modified by CalendarProvider + * Note that this column was formerly named _SYNC_LOCAL_ID. We are using it to avoid a + * schema change. + * TODO Replace this with something more general in the future. + * <P>Type: INTEGER (long)</P> + */ + public static final String _SYNC_DATA = "_sync_local_id"; + + /** + * Used only in persistent providers, and only during merging. + * <P>Type: INTEGER (long)</P> + */ + public static final String _SYNC_MARK = "_sync_mark"; + + /** + * Used to indicate that local, unsynced, changes are present. + * <P>Type: INTEGER (long)</P> + */ + public static final String _SYNC_DIRTY = "_sync_dirty"; + + /** + * The name of the account instance to which this row belongs, which when paired with + * {@link #ACCOUNT_TYPE} identifies a specific account. + * <P>Type: TEXT</P> + */ + public static final String ACCOUNT_NAME = "account_name"; + + /** + * The type of account to which this row belongs, which when paired with + * {@link #ACCOUNT_NAME} identifies a specific account. + * <P>Type: TEXT</P> + */ + public static final String ACCOUNT_TYPE = "account_type"; } /** * Contains a list of available calendars. */ - public static class Calendars implements BaseColumns, SyncConstValue, CalendarsColumns + public static class Calendars implements BaseColumns, CalendarsColumns { + private static final String WHERE_DELETE_FOR_ACCOUNT = Calendars._SYNC_ACCOUNT + "=?" + + " AND " + Calendars._SYNC_ACCOUNT_TYPE + "=?"; + public static final Cursor query(ContentResolver cr, String[] projection, String where, String orderBy) { @@ -173,16 +237,14 @@ public final class Calendar { public static int deleteCalendarsForAccount(ContentResolver cr, Account account) { // delete all calendars that match this account return Calendar.Calendars.delete(cr, - Calendar.Calendars._SYNC_ACCOUNT + "=? AND " - + Calendar.Calendars._SYNC_ACCOUNT_TYPE + "=?", - new String[] {account.name, account.type}); + WHERE_DELETE_FOR_ACCOUNT, + new String[] { account.name, account.type }); } /** * The content:// style URL for this table */ - public static final Uri CONTENT_URI = - Uri.parse("content://calendar/calendars"); + public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/calendars"); /** * The default sort order for this table @@ -225,6 +287,13 @@ public final class Calendar { * <P>Type: String</P> */ public static final String OWNER_ACCOUNT = "ownerAccount"; + + /** + * Can the organizer respond to the event? If no, the status of the + * organizer should not be shown by the UI. Defaults to 1 + * <P>Type: INTEGER (boolean)</P> + */ + public static final String ORGANIZER_CAN_RESPOND = "organizerCanRespond"; } public interface AttendeesColumns { @@ -282,10 +351,8 @@ public final class Calendar { public static final int ATTENDEE_STATUS_TENTATIVE = 4; } - public static final class Attendees implements BaseColumns, - AttendeesColumns, EventsColumns { - public static final Uri CONTENT_URI = - Uri.parse("content://calendar/attendees"); + public static final class Attendees implements BaseColumns, AttendeesColumns, EventsColumns { + public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/attendees"); // TODO: fill out this class when we actually start utilizing attendees // in the calendar application. @@ -341,11 +408,17 @@ public final class Calendar { * This field is copied here so that we can efficiently filter out * events that are declined without having to look in the Attendees * table. - * + * * <P>Type: INTEGER (int)</P> */ public static final String SELF_ATTENDEE_STATUS = "selfAttendeeStatus"; - + + /** + * This column is available for use by sync adapters + * <P>Type: TEXT</P> + */ + public static final String SYNC_ADAPTER_DATA = "syncAdapterData"; + /** * The comments feed uri. * <P>Type: TEXT</P> @@ -514,13 +587,215 @@ public final class Calendar { * <P>Type: String</P> */ public static final String OWNER_ACCOUNT = "ownerAccount"; + + /** + * Whether the row has been deleted. A deleted row should be ignored. + * <P>Type: INTEGER (boolean)</P> + */ + public static final String DELETED = "deleted"; + } + + /** + * Contains one entry per calendar event. Recurring events show up as a single entry. + */ + public static final class EventsEntity implements BaseColumns, EventsColumns, CalendarsColumns { + /** + * The content:// style URL for this table + */ + public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + + "/event_entities"); + + public static EntityIterator newEntityIterator(Cursor cursor, ContentResolver resolver) { + return new EntityIteratorImpl(cursor, resolver); + } + + public static EntityIterator newEntityIterator(Cursor cursor, + ContentProviderClient provider) { + return new EntityIteratorImpl(cursor, provider); + } + + private static class EntityIteratorImpl extends CursorEntityIterator { + private final ContentResolver mResolver; + private final ContentProviderClient mProvider; + + private static final String[] REMINDERS_PROJECTION = new String[] { + Reminders.MINUTES, + Reminders.METHOD, + }; + private static final int COLUMN_MINUTES = 0; + private static final int COLUMN_METHOD = 1; + + private static final String[] ATTENDEES_PROJECTION = new String[] { + Attendees.ATTENDEE_NAME, + Attendees.ATTENDEE_EMAIL, + Attendees.ATTENDEE_RELATIONSHIP, + Attendees.ATTENDEE_TYPE, + Attendees.ATTENDEE_STATUS, + }; + private static final int COLUMN_ATTENDEE_NAME = 0; + private static final int COLUMN_ATTENDEE_EMAIL = 1; + private static final int COLUMN_ATTENDEE_RELATIONSHIP = 2; + private static final int COLUMN_ATTENDEE_TYPE = 3; + private static final int COLUMN_ATTENDEE_STATUS = 4; + private static final String[] EXTENDED_PROJECTION = new String[] { + ExtendedProperties._ID, + ExtendedProperties.NAME, + ExtendedProperties.VALUE + }; + private static final int COLUMN_ID = 0; + private static final int COLUMN_NAME = 1; + private static final int COLUMN_VALUE = 2; + + private static final String WHERE_EVENT_ID = "event_id=?"; + + public EntityIteratorImpl(Cursor cursor, ContentResolver resolver) { + super(cursor); + mResolver = resolver; + mProvider = null; + } + + public EntityIteratorImpl(Cursor cursor, ContentProviderClient provider) { + super(cursor); + mResolver = null; + mProvider = provider; + } + + @Override + public Entity getEntityAndIncrementCursor(Cursor cursor) throws RemoteException { + // we expect the cursor is already at the row we need to read from + final long eventId = cursor.getLong(cursor.getColumnIndexOrThrow(Events._ID)); + ContentValues cv = new ContentValues(); + cv.put(Events._ID, eventId); + DatabaseUtils.cursorIntToContentValuesIfPresent(cursor, cv, CALENDAR_ID); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, HTML_URI); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, TITLE); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, DESCRIPTION); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, EVENT_LOCATION); + DatabaseUtils.cursorIntToContentValuesIfPresent(cursor, cv, STATUS); + DatabaseUtils.cursorIntToContentValuesIfPresent(cursor, cv, SELF_ATTENDEE_STATUS); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, COMMENTS_URI); + DatabaseUtils.cursorLongToContentValuesIfPresent(cursor, cv, DTSTART); + DatabaseUtils.cursorLongToContentValuesIfPresent(cursor, cv, DTEND); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, DURATION); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, EVENT_TIMEZONE); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, ALL_DAY); + DatabaseUtils.cursorIntToContentValuesIfPresent(cursor, cv, VISIBILITY); + DatabaseUtils.cursorIntToContentValuesIfPresent(cursor, cv, TRANSPARENCY); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, HAS_ALARM); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, + HAS_EXTENDED_PROPERTIES); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, RRULE); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, RDATE); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, EXRULE); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, EXDATE); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, ORIGINAL_EVENT); + DatabaseUtils.cursorLongToContentValuesIfPresent(cursor, cv, + ORIGINAL_INSTANCE_TIME); + DatabaseUtils.cursorIntToContentValuesIfPresent(cursor, cv, ORIGINAL_ALL_DAY); + DatabaseUtils.cursorLongToContentValuesIfPresent(cursor, cv, LAST_DATE); + DatabaseUtils.cursorIntToContentValuesIfPresent(cursor, cv, HAS_ATTENDEE_DATA); + DatabaseUtils.cursorIntToContentValuesIfPresent(cursor, cv, + GUESTS_CAN_INVITE_OTHERS); + DatabaseUtils.cursorIntToContentValuesIfPresent(cursor, cv, GUESTS_CAN_MODIFY); + DatabaseUtils.cursorIntToContentValuesIfPresent(cursor, cv, GUESTS_CAN_SEE_GUESTS); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, ORGANIZER); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, _SYNC_ID); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, _SYNC_DATA); + DatabaseUtils.cursorLongToContentValuesIfPresent(cursor, cv, _SYNC_DIRTY); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, _SYNC_VERSION); + DatabaseUtils.cursorIntToContentValuesIfPresent(cursor, cv, DELETED); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, Calendars.URL); + + Entity entity = new Entity(cv); + Cursor subCursor; + if (mResolver != null) { + subCursor = mResolver.query(Reminders.CONTENT_URI, REMINDERS_PROJECTION, + WHERE_EVENT_ID, + new String[] { Long.toString(eventId) } /* selectionArgs */, + null /* sortOrder */); + } else { + subCursor = mProvider.query(Reminders.CONTENT_URI, REMINDERS_PROJECTION, + WHERE_EVENT_ID, + new String[] { Long.toString(eventId) } /* selectionArgs */, + null /* sortOrder */); + } + try { + while (subCursor.moveToNext()) { + ContentValues reminderValues = new ContentValues(); + reminderValues.put(Reminders.MINUTES, subCursor.getInt(COLUMN_MINUTES)); + reminderValues.put(Reminders.METHOD, subCursor.getInt(COLUMN_METHOD)); + entity.addSubValue(Reminders.CONTENT_URI, reminderValues); + } + } finally { + subCursor.close(); + } + + if (mResolver != null) { + subCursor = mResolver.query(Attendees.CONTENT_URI, ATTENDEES_PROJECTION, + WHERE_EVENT_ID, + new String[] { Long.toString(eventId) } /* selectionArgs */, + null /* sortOrder */); + } else { + subCursor = mProvider.query(Attendees.CONTENT_URI, ATTENDEES_PROJECTION, + WHERE_EVENT_ID, + new String[] { Long.toString(eventId) } /* selectionArgs */, + null /* sortOrder */); + } + try { + while (subCursor.moveToNext()) { + ContentValues attendeeValues = new ContentValues(); + attendeeValues.put(Attendees.ATTENDEE_NAME, + subCursor.getString(COLUMN_ATTENDEE_NAME)); + attendeeValues.put(Attendees.ATTENDEE_EMAIL, + subCursor.getString(COLUMN_ATTENDEE_EMAIL)); + attendeeValues.put(Attendees.ATTENDEE_RELATIONSHIP, + subCursor.getInt(COLUMN_ATTENDEE_RELATIONSHIP)); + attendeeValues.put(Attendees.ATTENDEE_TYPE, + subCursor.getInt(COLUMN_ATTENDEE_TYPE)); + attendeeValues.put(Attendees.ATTENDEE_STATUS, + subCursor.getInt(COLUMN_ATTENDEE_STATUS)); + entity.addSubValue(Attendees.CONTENT_URI, attendeeValues); + } + } finally { + subCursor.close(); + } + + if (mResolver != null) { + subCursor = mResolver.query(ExtendedProperties.CONTENT_URI, EXTENDED_PROJECTION, + WHERE_EVENT_ID, + new String[] { Long.toString(eventId) } /* selectionArgs */, + null /* sortOrder */); + } else { + subCursor = mProvider.query(ExtendedProperties.CONTENT_URI, EXTENDED_PROJECTION, + WHERE_EVENT_ID, + new String[] { Long.toString(eventId) } /* selectionArgs */, + null /* sortOrder */); + } + try { + while (subCursor.moveToNext()) { + ContentValues extendedValues = new ContentValues(); + extendedValues.put(ExtendedProperties._ID, + subCursor.getString(COLUMN_ID)); + extendedValues.put(ExtendedProperties.NAME, + subCursor.getString(COLUMN_NAME)); + extendedValues.put(ExtendedProperties.VALUE, + subCursor.getString(COLUMN_VALUE)); + entity.addSubValue(ExtendedProperties.CONTENT_URI, extendedValues); + } + } finally { + subCursor.close(); + } + + cursor.moveToNext(); + return entity; + } + } } /** * Contains one entry per calendar event. Recurring events show up as a single entry. */ - public static final class Events implements BaseColumns, SyncConstValue, - EventsColumns, CalendarsColumns { + public static final class Events implements BaseColumns, EventsColumns, CalendarsColumns { private static final String[] FETCH_ENTRY_COLUMNS = new String[] { Events._SYNC_ACCOUNT, Events._SYNC_ID }; @@ -532,8 +807,6 @@ public final class Calendar { AttendeesColumns.ATTENDEE_TYPE, AttendeesColumns.ATTENDEE_STATUS }; - private static CalendarClient sCalendarClient = null; - public static final Cursor query(ContentResolver cr, String[] projection) { return cr.query(CONTENT_URI, projection, null, null, DEFAULT_SORT_ORDER); } @@ -554,172 +827,14 @@ public final class Calendar { return null; } - public static final Uri insertVEvent(ContentResolver cr, - ICalendar.Component event, long calendarId, int status, - ContentValues values) { - - // TODO: define VEVENT component names as constants in some - // appropriate class (ICalendar.Component?). - - values.clear(); - - // title - String title = extractValue(event, "SUMMARY"); - if (TextUtils.isEmpty(title)) { - if (Config.LOGD) { - Log.d(TAG, "No SUMMARY provided for event. " - + "Cannot import."); - } - return null; - } - values.put(TITLE, title); - - // status - values.put(STATUS, status); - - // description - String description = extractValue(event, "DESCRIPTION"); - if (!TextUtils.isEmpty(description)) { - values.put(DESCRIPTION, description); - } - - // where - String where = extractValue(event, "LOCATION"); - if (!StringUtils.isEmpty(where)) { - values.put(EVENT_LOCATION, where); - } - - // Calendar ID - values.put(CALENDAR_ID, calendarId); - - boolean timesSet = false; - - // TODO: deal with VALARMs - - // dtstart & dtend - Time time = new Time(Time.TIMEZONE_UTC); - String dtstart = null; - String dtend = null; - String duration = null; - ICalendar.Property dtstartProp = event.getFirstProperty("DTSTART"); - // TODO: handle "floating" timezone (no timezone specified). - if (dtstartProp != null) { - dtstart = dtstartProp.getValue(); - if (!TextUtils.isEmpty(dtstart)) { - ICalendar.Parameter tzidParam = - dtstartProp.getFirstParameter("TZID"); - if (tzidParam != null && tzidParam.value != null) { - time.clear(tzidParam.value); - } - try { - time.parse(dtstart); - } catch (Exception e) { - if (Config.LOGD) { - Log.d(TAG, "Cannot parse dtstart " + dtstart, e); - } - return null; - } - if (time.allDay) { - values.put(ALL_DAY, 1); - } - values.put(DTSTART, time.toMillis(false /* use isDst */)); - values.put(EVENT_TIMEZONE, time.timezone); - } - - ICalendar.Property dtendProp = event.getFirstProperty("DTEND"); - if (dtendProp != null) { - dtend = dtendProp.getValue(); - if (!TextUtils.isEmpty(dtend)) { - // TODO: make sure the timezones are the same for - // start, end. - try { - time.parse(dtend); - } catch (Exception e) { - if (Config.LOGD) { - Log.d(TAG, "Cannot parse dtend " + dtend, e); - } - return null; - } - values.put(DTEND, time.toMillis(false /* use isDst */)); - } - } else { - // look for a duration - ICalendar.Property durationProp = - event.getFirstProperty("DURATION"); - if (durationProp != null) { - duration = durationProp.getValue(); - if (!TextUtils.isEmpty(duration)) { - // TODO: check that it is valid? - values.put(DURATION, duration); - } - } - } - } - if (TextUtils.isEmpty(dtstart) || - (TextUtils.isEmpty(dtend) && TextUtils.isEmpty(duration))) { - if (Config.LOGD) { - Log.d(TAG, "No DTSTART or DTEND/DURATION defined."); - } - return null; - } - - // rrule - if (!RecurrenceSet.populateContentValues(event, values)) { - return null; - } - - return cr.insert(CONTENT_URI, values); - } - - /** - * Returns a singleton instance of the CalendarClient used to fetch entries from the - * calendar server. - * @param cr The ContentResolver used to lookup the address of the calendar server in the - * settings database. - * @return The singleton instance of the CalendarClient used to fetch entries from the - * calendar server. - */ - private static synchronized CalendarClient getCalendarClient(ContentResolver cr) { - if (sCalendarClient == null) { - sCalendarClient = new CalendarClient( - new AndroidGDataClient(cr), - new XmlCalendarGDataParserFactory(new AndroidXmlParserFactory())); - } - return sCalendarClient; - } - - /** - * Extracts the attendees information out of event and adds it to a new ArrayList of columns - * within the supplied ArrayList of rows. These rows are expected to be used within an - * {@link ArrayListCursor}. - */ - private static final void extractAttendeesIntoArrayList(EventEntry event, - ArrayList<ArrayList> rows) { - Log.d(TAG, "EVENT: " + event.toString()); - Vector<Who> attendees = (Vector<Who>) event.getAttendees(); - - int numAttendees = attendees == null ? 0 : attendees.size(); - - for (int i = 0; i < numAttendees; ++i) { - Who attendee = attendees.elementAt(i); - ArrayList row = new ArrayList(); - row.add(attendee.getValue()); - row.add(attendee.getEmail()); - row.add(attendee.getRelationship()); - row.add(attendee.getType()); - row.add(attendee.getStatus()); - rows.add(row); - } - } - /** * The content:// style URL for this table */ public static final Uri CONTENT_URI = - Uri.parse("content://calendar/events"); + Uri.parse("content://" + AUTHORITY + "/events"); public static final Uri DELETED_CONTENT_URI = - Uri.parse("content://calendar/deleted_events"); + Uri.parse("content://" + AUTHORITY + "/deleted_events"); /** * The default sort order for this table @@ -733,12 +848,14 @@ public final class Calendar { */ public static final class Instances implements BaseColumns, EventsColumns, CalendarsColumns { + private static final String WHERE_CALENDARS_SELECTED = Calendars.SELECTED + "=1"; + public static final Cursor query(ContentResolver cr, String[] projection, long begin, long end) { Uri.Builder builder = CONTENT_URI.buildUpon(); ContentUris.appendId(builder, begin); ContentUris.appendId(builder, end); - return cr.query(builder.build(), projection, Calendars.SELECTED + "=1", + return cr.query(builder.build(), projection, WHERE_CALENDARS_SELECTED, null, DEFAULT_SORT_ORDER); } @@ -748,9 +865,9 @@ public final class Calendar { ContentUris.appendId(builder, begin); ContentUris.appendId(builder, end); if (TextUtils.isEmpty(where)) { - where = Calendars.SELECTED + "=1"; + where = WHERE_CALENDARS_SELECTED; } else { - where = "(" + where + ") AND " + Calendars.SELECTED + "=1"; + where = "(" + where + ") AND " + WHERE_CALENDARS_SELECTED; } return cr.query(builder.build(), projection, where, null, orderBy == null ? DEFAULT_SORT_ORDER : orderBy); @@ -759,9 +876,10 @@ public final class Calendar { /** * The content:// style URL for this table */ - public static final Uri CONTENT_URI = Uri.parse("content://calendar/instances/when"); + public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + + "/instances/when"); public static final Uri CONTENT_BY_DAY_URI = - Uri.parse("content://calendar/instances/whenbyday"); + Uri.parse("content://" + AUTHORITY + "/instances/whenbyday"); /** * The default sort order for this table. @@ -851,60 +969,42 @@ public final class Calendar { public static final String MAX_INSTANCE = "maxInstance"; /** - * The minimum Julian day in the BusyBits table. + * The minimum Julian day in the EventDays table. * <P>Type: INTEGER</P> */ - public static final String MIN_BUSYBITS = "minBusyBits"; + public static final String MIN_EVENTDAYS = "minEventDays"; /** - * The maximum Julian day in the BusyBits table. + * The maximum Julian day in the EventDays table. * <P>Type: INTEGER</P> */ - public static final String MAX_BUSYBITS = "maxBusyBits"; + public static final String MAX_EVENTDAYS = "maxEventDays"; } - + public static final class CalendarMetaData implements CalendarMetaDataColumns { } - - public interface BusyBitsColumns { - /** - * The Julian day number. - * <P>Type: INTEGER (int)</P> - */ - public static final String DAY = "day"; + public interface EventDaysColumns { /** - * The 24 bits representing the 24 1-hour time slots in a day. - * If an event in the Instances table overlaps part of a 1-hour - * time slot then the corresponding bit is set. The first time slot - * (12am to 1am) is bit 0. The last time slot (11pm to midnight) - * is bit 23. + * The Julian starting day number. * <P>Type: INTEGER (int)</P> */ - public static final String BUSYBITS = "busyBits"; + public static final String STARTDAY = "startDay"; + public static final String ENDDAY = "endDay"; - /** - * The number of all-day events that occur on this day. - * <P>Type: INTEGER (int)</P> - */ - public static final String ALL_DAY_COUNT = "allDayCount"; } - - public static final class BusyBits implements BusyBitsColumns { - public static final Uri CONTENT_URI = Uri.parse("content://calendar/busybits/when"); - public static final String[] PROJECTION = { DAY, BUSYBITS, ALL_DAY_COUNT }; - - // The number of minutes represented by one busy bit - public static final int MINUTES_PER_BUSY_INTERVAL = 60; - - // The number of intervals in a day - public static final int INTERVALS_PER_DAY = 24 * 60 / MINUTES_PER_BUSY_INTERVAL; + public static final class EventDays implements EventDaysColumns { + public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + + "/instances/groupbyday"); + + public static final String[] PROJECTION = { STARTDAY, ENDDAY }; + public static final String SELECTION = "selected=1"; /** - * Retrieves the busy bits for the Julian days starting at "startDay" + * Retrieves the days with events for the Julian days starting at "startDay" * for "numDays". - * + * * @param cr the ContentResolver * @param startDay the first Julian day in the range * @param numDays the number of days to load (must be at least 1) @@ -918,8 +1018,8 @@ public final class Calendar { Uri.Builder builder = CONTENT_URI.buildUpon(); ContentUris.appendId(builder, startDay); ContentUris.appendId(builder, endDay); - return cr.query(builder.build(), PROJECTION, null /* selection */, - null /* selection args */, DAY); + return cr.query(builder.build(), PROJECTION, SELECTION, + null /* selection args */, STARTDAY); } } @@ -955,7 +1055,7 @@ public final class Calendar { public static final class Reminders implements BaseColumns, RemindersColumns, EventsColumns { public static final String TABLE_NAME = "Reminders"; - public static final Uri CONTENT_URI = Uri.parse("content://calendar/reminders"); + public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/reminders"); } public interface CalendarAlertsColumns { @@ -1025,22 +1125,38 @@ public final class Calendar { /** * The default sort order for this table */ - public static final String DEFAULT_SORT_ORDER = "alarmTime ASC,begin ASC,title ASC"; + public static final String DEFAULT_SORT_ORDER = "begin ASC,title ASC"; } public static final class CalendarAlerts implements BaseColumns, CalendarAlertsColumns, EventsColumns, CalendarsColumns { + public static final String TABLE_NAME = "CalendarAlerts"; - public static final Uri CONTENT_URI = Uri.parse("content://calendar/calendar_alerts"); - + public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + + "/calendar_alerts"); + + private static final String WHERE_ALARM_EXISTS = EVENT_ID + "=?" + + " AND " + BEGIN + "=?" + + " AND " + ALARM_TIME + "=?"; + + private static final String WHERE_FINDNEXTALARMTIME = ALARM_TIME + ">=?"; + private static final String SORT_ORDER_ALARMTIME_ASC = ALARM_TIME + " ASC"; + + private static final String WHERE_RESCHEDULE_MISSED_ALARMS = STATE + "=" + SCHEDULED + + " AND " + ALARM_TIME + "<?" + + " AND " + ALARM_TIME + ">?" + + " AND " + END + ">=?"; + /** * This URI is for grouping the query results by event_id and begin * time. This will return one result per instance of an event. So * events with multiple alarms will appear just once, but multiple * instances of a repeating event will show up multiple times. */ - public static final Uri CONTENT_URI_BY_INSTANCE = - Uri.parse("content://calendar/calendar_alerts/by_instance"); + public static final Uri CONTENT_URI_BY_INSTANCE = + Uri.parse("content://" + AUTHORITY + "/calendar_alerts/by_instance"); + + private static final boolean DEBUG = true; public static final Uri insert(ContentResolver cr, long eventId, long begin, long end, long alarmTime, int minutes) { @@ -1059,15 +1175,15 @@ public final class Calendar { } public static final Cursor query(ContentResolver cr, String[] projection, - String selection, String[] selectionArgs) { + String selection, String[] selectionArgs, String sortOrder) { return cr.query(CONTENT_URI, projection, selection, selectionArgs, - DEFAULT_SORT_ORDER); + sortOrder); } - + /** * Finds the next alarm after (or equal to) the given time and returns * the time of that alarm or -1 if no such alarm exists. - * + * * @param cr the ContentResolver * @param millis the time in UTC milliseconds * @return the next alarm time greater than or equal to "millis", or -1 @@ -1078,7 +1194,12 @@ public final class Calendar { // TODO: construct an explicit SQL query so that we can add // "LIMIT 1" to the end and get just one result. String[] projection = new String[] { ALARM_TIME }; - Cursor cursor = query(cr, projection, selection, null); + Cursor cursor = query(cr, projection, + WHERE_FINDNEXTALARMTIME, + new String[] { + Long.toString(millis) + }, + SORT_ORDER_ALARMTIME_ASC); long alarmTime = -1; try { if (cursor != null && cursor.moveToFirst()) { @@ -1091,13 +1212,13 @@ public final class Calendar { } return alarmTime; } - + /** * Searches the CalendarAlerts table for alarms that should have fired * but have not and then reschedules them. This method can be called * at boot time to restore alarms that may have been lost due to a * phone reboot. - * + * * @param cr the ContentResolver * @param context the Context * @param manager the AlarmManager @@ -1107,53 +1228,72 @@ public final class Calendar { // Get all the alerts that have been scheduled but have not fired // and should have fired by now and are not too old. long now = System.currentTimeMillis(); - long ancient = now - 24 * DateUtils.HOUR_IN_MILLIS; - String selection = CalendarAlerts.STATE + "=" + CalendarAlerts.SCHEDULED - + " AND " + CalendarAlerts.ALARM_TIME + "<" + now - + " AND " + CalendarAlerts.ALARM_TIME + ">" + ancient - + " AND " + CalendarAlerts.END + ">=" + now; + long ancient = now - DateUtils.DAY_IN_MILLIS; String[] projection = new String[] { - _ID, - BEGIN, - END, ALARM_TIME, }; - Cursor cursor = CalendarAlerts.query(cr, projection, selection, null); + + // TODO: construct an explicit SQL query so that we can add + // "GROUPBY" instead of doing a sort and de-dup + Cursor cursor = CalendarAlerts.query(cr, + projection, + WHERE_RESCHEDULE_MISSED_ALARMS, + new String[] { + Long.toString(now), + Long.toString(ancient), + Long.toString(now) + }, + SORT_ORDER_ALARMTIME_ASC); if (cursor == null) { return; } - if (Log.isLoggable(TAG, Log.DEBUG)) { + + if (DEBUG) { Log.d(TAG, "missed alarms found: " + cursor.getCount()); } - + try { + long alarmTime = -1; + while (cursor.moveToNext()) { - long id = cursor.getLong(0); - long begin = cursor.getLong(1); - long end = cursor.getLong(2); - long alarmTime = cursor.getLong(3); - Uri uri = ContentUris.withAppendedId(CONTENT_URI, id); - Intent intent = new Intent(android.provider.Calendar.EVENT_REMINDER_ACTION); - intent.setData(uri); - intent.putExtra(android.provider.Calendar.EVENT_BEGIN_TIME, begin); - intent.putExtra(android.provider.Calendar.EVENT_END_TIME, end); - PendingIntent sender = PendingIntent.getBroadcast(context, - 0, intent, PendingIntent.FLAG_CANCEL_CURRENT); - Log.w(TAG, "rescheduling missed alarm, id: " + id + " begin: " + begin - + " end: " + end + " alarmTime: " + alarmTime); - manager.set(AlarmManager.RTC_WAKEUP, alarmTime, sender); + long newAlarmTime = cursor.getLong(0); + if (alarmTime != newAlarmTime) { + if (DEBUG) { + Log.w(TAG, "rescheduling missed alarm. alarmTime: " + newAlarmTime); + } + scheduleAlarm(context, manager, newAlarmTime); + alarmTime = newAlarmTime; + } } } finally { cursor.close(); } - } - + + public static void scheduleAlarm(Context context, AlarmManager manager, long alarmTime) { + if (DEBUG) { + Time time = new Time(); + time.set(alarmTime); + String schedTime = time.format(" %a, %b %d, %Y %I:%M%P"); + Log.d(TAG, "Schedule alarm at " + alarmTime + " " + schedTime); + } + + if (manager == null) { + manager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); + } + + Intent intent = new Intent(EVENT_REMINDER_ACTION); + intent.setData(ContentUris.withAppendedId(Calendar.CONTENT_URI, alarmTime)); + intent.putExtra(ALARM_TIME, alarmTime); + PendingIntent pi = PendingIntent.getBroadcast(context, 0, intent, 0); + manager.set(AlarmManager.RTC_WAKEUP, alarmTime, pi); + } + /** * Searches for an entry in the CalendarAlerts table that matches * the given event id, begin time and alarm time. If one is found * then this alarm already exists and this method returns true. - * + * * @param cr the ContentResolver * @param eventId the event id to match * @param begin the start time of the event in UTC millis @@ -1163,13 +1303,18 @@ public final class Calendar { */ public static final boolean alarmExists(ContentResolver cr, long eventId, long begin, long alarmTime) { - String selection = CalendarAlerts.EVENT_ID + "=" + eventId - + " AND " + CalendarAlerts.BEGIN + "=" + begin - + " AND " + CalendarAlerts.ALARM_TIME + "=" + alarmTime; // TODO: construct an explicit SQL query so that we can add // "LIMIT 1" to the end and get just one result. - String[] projection = new String[] { CalendarAlerts.ALARM_TIME }; - Cursor cursor = query(cr, projection, selection, null); + String[] projection = new String[] { ALARM_TIME }; + Cursor cursor = query(cr, + projection, + WHERE_ALARM_EXISTS, + new String[] { + Long.toString(eventId), + Long.toString(begin), + Long.toString(alarmTime) + }, + null); boolean found = false; try { if (cursor != null && cursor.getCount() > 0) { @@ -1208,9 +1353,30 @@ public final class Calendar { public static final class ExtendedProperties implements BaseColumns, ExtendedPropertiesColumns, EventsColumns { public static final Uri CONTENT_URI = - Uri.parse("content://calendar/extendedproperties"); + Uri.parse("content://" + AUTHORITY + "/extendedproperties"); // TODO: fill out this class when we actually start utilizing extendedproperties // in the calendar application. } + + /** + * A table provided for sync adapters to use for storing private sync state data. + * + * @see SyncStateContract + */ + public static final class SyncState implements SyncStateContract.Columns { + /** + * This utility class cannot be instantiated + */ + private SyncState() {} + + public static final String CONTENT_DIRECTORY = + SyncStateContract.Constants.CONTENT_DIRECTORY; + + /** + * The content:// style URI for this table + */ + public static final Uri CONTENT_URI = + Uri.withAppendedPath(Calendar.CONTENT_URI, CONTENT_DIRECTORY); + } } |