Home | History | Annotate | Download | only in provider
      1 /*
      2  * Copyright (C) 2006 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package android.provider;
     18 
     19 import android.accounts.Account;
     20 import android.app.AlarmManager;
     21 import android.app.PendingIntent;
     22 import android.content.ContentProviderClient;
     23 import android.content.ContentResolver;
     24 import android.content.ContentUris;
     25 import android.content.ContentValues;
     26 import android.content.Context;
     27 import android.content.CursorEntityIterator;
     28 import android.content.Entity;
     29 import android.content.EntityIterator;
     30 import android.content.Intent;
     31 import android.database.Cursor;
     32 import android.database.DatabaseUtils;
     33 import android.net.Uri;
     34 import android.os.RemoteException;
     35 import android.pim.ICalendar;
     36 import android.text.TextUtils;
     37 import android.text.format.DateUtils;
     38 import android.text.format.Time;
     39 import android.util.Log;
     40 
     41 /**
     42  * The Calendar provider contains all calendar events.
     43  *
     44  * @hide
     45  */
     46 public final class Calendar {
     47 
     48     public static final String TAG = "Calendar";
     49 
     50     /**
     51      * Broadcast Action: An event reminder.
     52      */
     53     public static final String EVENT_REMINDER_ACTION = "android.intent.action.EVENT_REMINDER";
     54 
     55     /**
     56      * These are the symbolic names for the keys used in the extra data
     57      * passed in the intent for event reminders.
     58      */
     59     public static final String EVENT_BEGIN_TIME = "beginTime";
     60     public static final String EVENT_END_TIME = "endTime";
     61 
     62     public static final String AUTHORITY = "com.android.calendar";
     63 
     64     /**
     65      * The content:// style URL for the top-level calendar authority
     66      */
     67     public static final Uri CONTENT_URI =
     68         Uri.parse("content://" + AUTHORITY);
     69 
     70     /**
     71      * An optional insert, update or delete URI parameter that allows the caller
     72      * to specify that it is a sync adapter. The default value is false. If true
     73      * the dirty flag is not automatically set and the "syncToNetwork" parameter
     74      * is set to false when calling
     75      * {@link ContentResolver#notifyChange(android.net.Uri, android.database.ContentObserver, boolean)}.
     76      */
     77     public static final String CALLER_IS_SYNCADAPTER = "caller_is_syncadapter";
     78 
     79     /**
     80      * Columns from the Calendars table that other tables join into themselves.
     81      */
     82     public interface CalendarsColumns
     83     {
     84         /**
     85          * The color of the calendar
     86          * <P>Type: INTEGER (color value)</P>
     87          */
     88         public static final String COLOR = "color";
     89 
     90         /**
     91          * The level of access that the user has for the calendar
     92          * <P>Type: INTEGER (one of the values below)</P>
     93          */
     94         public static final String ACCESS_LEVEL = "access_level";
     95 
     96         /** Cannot access the calendar */
     97         public static final int NO_ACCESS = 0;
     98         /** Can only see free/busy information about the calendar */
     99         public static final int FREEBUSY_ACCESS = 100;
    100         /** Can read all event details */
    101         public static final int READ_ACCESS = 200;
    102         public static final int RESPOND_ACCESS = 300;
    103         public static final int OVERRIDE_ACCESS = 400;
    104         /** Full access to modify the calendar, but not the access control settings */
    105         public static final int CONTRIBUTOR_ACCESS = 500;
    106         public static final int EDITOR_ACCESS = 600;
    107         /** Full access to the calendar */
    108         public static final int OWNER_ACCESS = 700;
    109         /** Domain admin */
    110         public static final int ROOT_ACCESS = 800;
    111 
    112         /**
    113          * Is the calendar selected to be displayed?
    114          * <P>Type: INTEGER (boolean)</P>
    115          */
    116         public static final String SELECTED = "selected";
    117 
    118         /**
    119          * The timezone the calendar's events occurs in
    120          * <P>Type: TEXT</P>
    121          */
    122         public static final String TIMEZONE = "timezone";
    123 
    124         /**
    125          * If this calendar is in the list of calendars that are selected for
    126          * syncing then "sync_events" is 1, otherwise 0.
    127          * <p>Type: INTEGER (boolean)</p>
    128          */
    129         public static final String SYNC_EVENTS = "sync_events";
    130 
    131         /**
    132          * Sync state data.
    133          * <p>Type: String (blob)</p>
    134          */
    135         public static final String SYNC_STATE = "sync_state";
    136 
    137         /**
    138          * The account that was used to sync the entry to the device.
    139          * <P>Type: TEXT</P>
    140          */
    141         public static final String _SYNC_ACCOUNT = "_sync_account";
    142 
    143         /**
    144          * The type of the account that was used to sync the entry to the device.
    145          * <P>Type: TEXT</P>
    146          */
    147         public static final String _SYNC_ACCOUNT_TYPE = "_sync_account_type";
    148 
    149         /**
    150          * The unique ID for a row assigned by the sync source. NULL if the row has never been synced.
    151          * <P>Type: TEXT</P>
    152          */
    153         public static final String _SYNC_ID = "_sync_id";
    154 
    155         /**
    156          * The last time, from the sync source's point of view, that this row has been synchronized.
    157          * <P>Type: INTEGER (long)</P>
    158          */
    159         public static final String _SYNC_TIME = "_sync_time";
    160 
    161         /**
    162          * The version of the row, as assigned by the server.
    163          * <P>Type: TEXT</P>
    164          */
    165         public static final String _SYNC_VERSION = "_sync_version";
    166 
    167         /**
    168          * For use by sync adapter at its discretion; not modified by CalendarProvider
    169          * Note that this column was formerly named _SYNC_LOCAL_ID.  We are using it to avoid a
    170          * schema change.
    171          * TODO Replace this with something more general in the future.
    172          * <P>Type: INTEGER (long)</P>
    173          */
    174         public static final String _SYNC_DATA = "_sync_local_id";
    175 
    176         /**
    177          * Used only in persistent providers, and only during merging.
    178          * <P>Type: INTEGER (long)</P>
    179          */
    180         public static final String _SYNC_MARK = "_sync_mark";
    181 
    182         /**
    183          * Used to indicate that local, unsynced, changes are present.
    184          * <P>Type: INTEGER (long)</P>
    185          */
    186         public static final String _SYNC_DIRTY = "_sync_dirty";
    187 
    188         /**
    189          * The name of the account instance to which this row belongs, which when paired with
    190          * {@link #ACCOUNT_TYPE} identifies a specific account.
    191          * <P>Type: TEXT</P>
    192          */
    193         public static final String ACCOUNT_NAME = "account_name";
    194 
    195         /**
    196          * The type of account to which this row belongs, which when paired with
    197          * {@link #ACCOUNT_NAME} identifies a specific account.
    198          * <P>Type: TEXT</P>
    199          */
    200         public static final String ACCOUNT_TYPE = "account_type";
    201     }
    202 
    203     /**
    204      * Contains a list of available calendars.
    205      */
    206     public static class Calendars implements BaseColumns, CalendarsColumns
    207     {
    208         private static final String WHERE_DELETE_FOR_ACCOUNT = Calendars._SYNC_ACCOUNT + "=?"
    209                 + " AND " + Calendars._SYNC_ACCOUNT_TYPE + "=?";
    210 
    211         public static final Cursor query(ContentResolver cr, String[] projection,
    212                                        String where, String orderBy)
    213         {
    214             return cr.query(CONTENT_URI, projection, where,
    215                                          null, orderBy == null ? DEFAULT_SORT_ORDER : orderBy);
    216         }
    217 
    218         /**
    219          * Convenience method perform a delete on the Calendar provider
    220          *
    221          * @param cr the ContentResolver
    222          * @param selection the rows to delete
    223          * @return the count of rows that were deleted
    224          */
    225         public static int delete(ContentResolver cr, String selection, String[] selectionArgs)
    226         {
    227             return cr.delete(CONTENT_URI, selection, selectionArgs);
    228         }
    229 
    230         /**
    231          * Convenience method to delete all calendars that match the account.
    232          *
    233          * @param cr the ContentResolver
    234          * @param account the account whose rows should be deleted
    235          * @return the count of rows that were deleted
    236          */
    237         public static int deleteCalendarsForAccount(ContentResolver cr, Account account) {
    238             // delete all calendars that match this account
    239             return Calendar.Calendars.delete(cr,
    240                     WHERE_DELETE_FOR_ACCOUNT,
    241                     new String[] { account.name, account.type });
    242         }
    243 
    244         /**
    245          * The content:// style URL for this table
    246          */
    247         public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/calendars");
    248 
    249         /**
    250          * The default sort order for this table
    251          */
    252         public static final String DEFAULT_SORT_ORDER = "displayName";
    253 
    254         /**
    255          * The URL to the calendar
    256          * <P>Type: TEXT (URL)</P>
    257          */
    258         public static final String URL = "url";
    259 
    260         /**
    261          * The name of the calendar
    262          * <P>Type: TEXT</P>
    263          */
    264         public static final String NAME = "name";
    265 
    266         /**
    267          * The display name of the calendar
    268          * <P>Type: TEXT</P>
    269          */
    270         public static final String DISPLAY_NAME = "displayName";
    271 
    272         /**
    273          * The location the of the events in the calendar
    274          * <P>Type: TEXT</P>
    275          */
    276         public static final String LOCATION = "location";
    277 
    278         /**
    279          * Should the calendar be hidden in the calendar selection panel?
    280          * <P>Type: INTEGER (boolean)</P>
    281          */
    282         public static final String HIDDEN = "hidden";
    283 
    284         /**
    285          * The owner account for this calendar, based on the calendar feed.
    286          * This will be different from the _SYNC_ACCOUNT for delegated calendars.
    287          * <P>Type: String</P>
    288          */
    289         public static final String OWNER_ACCOUNT = "ownerAccount";
    290 
    291         /**
    292          * Can the organizer respond to the event?  If no, the status of the
    293          * organizer should not be shown by the UI.  Defaults to 1
    294          * <P>Type: INTEGER (boolean)</P>
    295          */
    296         public static final String ORGANIZER_CAN_RESPOND = "organizerCanRespond";
    297     }
    298 
    299     public interface AttendeesColumns {
    300 
    301         /**
    302          * The id of the event.
    303          * <P>Type: INTEGER</P>
    304          */
    305         public static final String EVENT_ID = "event_id";
    306 
    307         /**
    308          * The name of the attendee.
    309          * <P>Type: STRING</P>
    310          */
    311         public static final String ATTENDEE_NAME = "attendeeName";
    312 
    313         /**
    314          * The email address of the attendee.
    315          * <P>Type: STRING</P>
    316          */
    317         public static final String ATTENDEE_EMAIL = "attendeeEmail";
    318 
    319         /**
    320          * The relationship of the attendee to the user.
    321          * <P>Type: INTEGER (one of {@link #RELATIONSHIP_ATTENDEE}, ...}.
    322          */
    323         public static final String ATTENDEE_RELATIONSHIP = "attendeeRelationship";
    324 
    325         public static final int RELATIONSHIP_NONE = 0;
    326         public static final int RELATIONSHIP_ATTENDEE = 1;
    327         public static final int RELATIONSHIP_ORGANIZER = 2;
    328         public static final int RELATIONSHIP_PERFORMER = 3;
    329         public static final int RELATIONSHIP_SPEAKER = 4;
    330 
    331         /**
    332          * The type of attendee.
    333          * <P>Type: Integer (one of {@link #TYPE_REQUIRED}, {@link #TYPE_OPTIONAL})
    334          */
    335         public static final String ATTENDEE_TYPE = "attendeeType";
    336 
    337         public static final int TYPE_NONE = 0;
    338         public static final int TYPE_REQUIRED = 1;
    339         public static final int TYPE_OPTIONAL = 2;
    340 
    341         /**
    342          * The attendance status of the attendee.
    343          * <P>Type: Integer (one of {@link #ATTENDEE_STATUS_ACCEPTED}, ...}.
    344          */
    345         public static final String ATTENDEE_STATUS = "attendeeStatus";
    346 
    347         public static final int ATTENDEE_STATUS_NONE = 0;
    348         public static final int ATTENDEE_STATUS_ACCEPTED = 1;
    349         public static final int ATTENDEE_STATUS_DECLINED = 2;
    350         public static final int ATTENDEE_STATUS_INVITED = 3;
    351         public static final int ATTENDEE_STATUS_TENTATIVE = 4;
    352     }
    353 
    354     public static final class Attendees implements BaseColumns, AttendeesColumns, EventsColumns {
    355         public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/attendees");
    356 
    357         // TODO: fill out this class when we actually start utilizing attendees
    358         // in the calendar application.
    359     }
    360 
    361     /**
    362      * Columns from the Events table that other tables join into themselves.
    363      */
    364     public interface EventsColumns
    365     {
    366         /**
    367          * The calendar the event belongs to
    368          * <P>Type: INTEGER (foreign key to the Calendars table)</P>
    369          */
    370         public static final String CALENDAR_ID = "calendar_id";
    371 
    372         /**
    373          * The URI for an HTML version of this event.
    374          * <P>Type: TEXT</P>
    375          */
    376         public static final String HTML_URI = "htmlUri";
    377 
    378         /**
    379          * The title of the event
    380          * <P>Type: TEXT</P>
    381          */
    382         public static final String TITLE = "title";
    383 
    384         /**
    385          * The description of the event
    386          * <P>Type: TEXT</P>
    387          */
    388         public static final String DESCRIPTION = "description";
    389 
    390         /**
    391          * Where the event takes place.
    392          * <P>Type: TEXT</P>
    393          */
    394         public static final String EVENT_LOCATION = "eventLocation";
    395 
    396         /**
    397          * The event status
    398          * <P>Type: INTEGER (int)</P>
    399          */
    400         public static final String STATUS = "eventStatus";
    401 
    402         public static final int STATUS_TENTATIVE = 0;
    403         public static final int STATUS_CONFIRMED = 1;
    404         public static final int STATUS_CANCELED = 2;
    405 
    406         /**
    407          * This is a copy of the attendee status for the owner of this event.
    408          * This field is copied here so that we can efficiently filter out
    409          * events that are declined without having to look in the Attendees
    410          * table.
    411          *
    412          * <P>Type: INTEGER (int)</P>
    413          */
    414         public static final String SELF_ATTENDEE_STATUS = "selfAttendeeStatus";
    415 
    416         /**
    417          * This column is available for use by sync adapters
    418          * <P>Type: TEXT</P>
    419          */
    420         public static final String SYNC_ADAPTER_DATA = "syncAdapterData";
    421 
    422         /**
    423          * The comments feed uri.
    424          * <P>Type: TEXT</P>
    425          */
    426         public static final String COMMENTS_URI = "commentsUri";
    427 
    428         /**
    429          * The time the event starts
    430          * <P>Type: INTEGER (long; millis since epoch)</P>
    431          */
    432         public static final String DTSTART = "dtstart";
    433 
    434         /**
    435          * The time the event ends
    436          * <P>Type: INTEGER (long; millis since epoch)</P>
    437          */
    438         public static final String DTEND = "dtend";
    439 
    440         /**
    441          * The duration of the event
    442          * <P>Type: TEXT (duration in RFC2445 format)</P>
    443          */
    444         public static final String DURATION = "duration";
    445 
    446         /**
    447          * The timezone for the event.
    448          * <P>Type: TEXT
    449          */
    450         public static final String EVENT_TIMEZONE = "eventTimezone";
    451 
    452         /**
    453          * Whether the event lasts all day or not
    454          * <P>Type: INTEGER (boolean)</P>
    455          */
    456         public static final String ALL_DAY = "allDay";
    457 
    458         /**
    459          * Visibility for the event.
    460          * <P>Type: INTEGER</P>
    461          */
    462         public static final String VISIBILITY = "visibility";
    463 
    464         public static final int VISIBILITY_DEFAULT = 0;
    465         public static final int VISIBILITY_CONFIDENTIAL = 1;
    466         public static final int VISIBILITY_PRIVATE = 2;
    467         public static final int VISIBILITY_PUBLIC = 3;
    468 
    469         /**
    470          * Transparency for the event -- does the event consume time on the calendar?
    471          * <P>Type: INTEGER</P>
    472          */
    473         public static final String TRANSPARENCY = "transparency";
    474 
    475         public static final int TRANSPARENCY_OPAQUE = 0;
    476 
    477         public static final int TRANSPARENCY_TRANSPARENT = 1;
    478 
    479         /**
    480          * Whether the event has an alarm or not
    481          * <P>Type: INTEGER (boolean)</P>
    482          */
    483         public static final String HAS_ALARM = "hasAlarm";
    484 
    485         /**
    486          * Whether the event has extended properties or not
    487          * <P>Type: INTEGER (boolean)</P>
    488          */
    489         public static final String HAS_EXTENDED_PROPERTIES = "hasExtendedProperties";
    490 
    491         /**
    492          * The recurrence rule for the event.
    493          * than one.
    494          * <P>Type: TEXT</P>
    495          */
    496         public static final String RRULE = "rrule";
    497 
    498         /**
    499          * The recurrence dates for the event.
    500          * <P>Type: TEXT</P>
    501          */
    502         public static final String RDATE = "rdate";
    503 
    504         /**
    505          * The recurrence exception rule for the event.
    506          * <P>Type: TEXT</P>
    507          */
    508         public static final String EXRULE = "exrule";
    509 
    510         /**
    511          * The recurrence exception dates for the event.
    512          * <P>Type: TEXT</P>
    513          */
    514         public static final String EXDATE = "exdate";
    515 
    516         /**
    517          * The _sync_id of the original recurring event for which this event is
    518          * an exception.
    519          * <P>Type: TEXT</P>
    520          */
    521         public static final String ORIGINAL_EVENT = "originalEvent";
    522 
    523         /**
    524          * The original instance time of the recurring event for which this
    525          * event is an exception.
    526          * <P>Type: INTEGER (long; millis since epoch)</P>
    527          */
    528         public static final String ORIGINAL_INSTANCE_TIME = "originalInstanceTime";
    529 
    530         /**
    531          * The allDay status (true or false) of the original recurring event
    532          * for which this event is an exception.
    533          * <P>Type: INTEGER (boolean)</P>
    534          */
    535         public static final String ORIGINAL_ALL_DAY = "originalAllDay";
    536 
    537         /**
    538          * The last date this event repeats on, or NULL if it never ends
    539          * <P>Type: INTEGER (long; millis since epoch)</P>
    540          */
    541         public static final String LAST_DATE = "lastDate";
    542 
    543         /**
    544          * Whether the event has attendee information.  True if the event
    545          * has full attendee data, false if the event has information about
    546          * self only.
    547          * <P>Type: INTEGER (boolean)</P>
    548          */
    549         public static final String HAS_ATTENDEE_DATA = "hasAttendeeData";
    550 
    551         /**
    552          * Whether guests can modify the event.
    553          * <P>Type: INTEGER (boolean)</P>
    554          */
    555         public static final String GUESTS_CAN_MODIFY = "guestsCanModify";
    556 
    557         /**
    558          * Whether guests can invite other guests.
    559          * <P>Type: INTEGER (boolean)</P>
    560          */
    561         public static final String GUESTS_CAN_INVITE_OTHERS = "guestsCanInviteOthers";
    562 
    563         /**
    564          * Whether guests can see the list of attendees.
    565          * <P>Type: INTEGER (boolean)</P>
    566          */
    567         public static final String GUESTS_CAN_SEE_GUESTS = "guestsCanSeeGuests";
    568 
    569         /**
    570          * Email of the organizer (owner) of the event.
    571          * <P>Type: STRING</P>
    572          */
    573         public static final String ORGANIZER = "organizer";
    574 
    575         /**
    576          * Whether the user can invite others to the event.
    577          * The GUESTS_CAN_INVITE_OTHERS is a setting that applies to an arbitrary guest,
    578          * while CAN_INVITE_OTHERS indicates if the user can invite others (either through
    579          * GUESTS_CAN_INVITE_OTHERS or because the user has modify access to the event).
    580          * <P>Type: INTEGER (boolean, readonly)</P>
    581          */
    582         public static final String CAN_INVITE_OTHERS = "canInviteOthers";
    583 
    584         /**
    585          * The owner account for this calendar, based on the calendar (foreign
    586          * key into the calendars table).
    587          * <P>Type: String</P>
    588          */
    589         public static final String OWNER_ACCOUNT = "ownerAccount";
    590 
    591         /**
    592          * Whether the row has been deleted.  A deleted row should be ignored.
    593          * <P>Type: INTEGER (boolean)</P>
    594          */
    595         public static final String DELETED = "deleted";
    596     }
    597 
    598     /**
    599      * Contains one entry per calendar event. Recurring events show up as a single entry.
    600      */
    601     public static final class EventsEntity implements BaseColumns, EventsColumns, CalendarsColumns {
    602         /**
    603          * The content:// style URL for this table
    604          */
    605         public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY +
    606                 "/event_entities");
    607 
    608         public static EntityIterator newEntityIterator(Cursor cursor, ContentResolver resolver) {
    609             return new EntityIteratorImpl(cursor, resolver);
    610         }
    611 
    612         public static EntityIterator newEntityIterator(Cursor cursor,
    613                 ContentProviderClient provider) {
    614             return new EntityIteratorImpl(cursor, provider);
    615         }
    616 
    617         private static class EntityIteratorImpl extends CursorEntityIterator {
    618             private final ContentResolver mResolver;
    619             private final ContentProviderClient mProvider;
    620 
    621             private static final String[] REMINDERS_PROJECTION = new String[] {
    622                     Reminders.MINUTES,
    623                     Reminders.METHOD,
    624             };
    625             private static final int COLUMN_MINUTES = 0;
    626             private static final int COLUMN_METHOD = 1;
    627 
    628             private static final String[] ATTENDEES_PROJECTION = new String[] {
    629                     Attendees.ATTENDEE_NAME,
    630                     Attendees.ATTENDEE_EMAIL,
    631                     Attendees.ATTENDEE_RELATIONSHIP,
    632                     Attendees.ATTENDEE_TYPE,
    633                     Attendees.ATTENDEE_STATUS,
    634             };
    635             private static final int COLUMN_ATTENDEE_NAME = 0;
    636             private static final int COLUMN_ATTENDEE_EMAIL = 1;
    637             private static final int COLUMN_ATTENDEE_RELATIONSHIP = 2;
    638             private static final int COLUMN_ATTENDEE_TYPE = 3;
    639             private static final int COLUMN_ATTENDEE_STATUS = 4;
    640             private static final String[] EXTENDED_PROJECTION = new String[] {
    641                     ExtendedProperties._ID,
    642                     ExtendedProperties.NAME,
    643                     ExtendedProperties.VALUE
    644             };
    645             private static final int COLUMN_ID = 0;
    646             private static final int COLUMN_NAME = 1;
    647             private static final int COLUMN_VALUE = 2;
    648 
    649             private static final String WHERE_EVENT_ID = "event_id=?";
    650 
    651             public EntityIteratorImpl(Cursor cursor, ContentResolver resolver) {
    652                 super(cursor);
    653                 mResolver = resolver;
    654                 mProvider = null;
    655             }
    656 
    657             public EntityIteratorImpl(Cursor cursor, ContentProviderClient provider) {
    658                 super(cursor);
    659                 mResolver = null;
    660                 mProvider = provider;
    661             }
    662 
    663             @Override
    664             public Entity getEntityAndIncrementCursor(Cursor cursor) throws RemoteException {
    665                 // we expect the cursor is already at the row we need to read from
    666                 final long eventId = cursor.getLong(cursor.getColumnIndexOrThrow(Events._ID));
    667                 ContentValues cv = new ContentValues();
    668                 cv.put(Events._ID, eventId);
    669                 DatabaseUtils.cursorIntToContentValuesIfPresent(cursor, cv, CALENDAR_ID);
    670                 DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, HTML_URI);
    671                 DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, TITLE);
    672                 DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, DESCRIPTION);
    673                 DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, EVENT_LOCATION);
    674                 DatabaseUtils.cursorIntToContentValuesIfPresent(cursor, cv, STATUS);
    675                 DatabaseUtils.cursorIntToContentValuesIfPresent(cursor, cv, SELF_ATTENDEE_STATUS);
    676                 DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, COMMENTS_URI);
    677                 DatabaseUtils.cursorLongToContentValuesIfPresent(cursor, cv, DTSTART);
    678                 DatabaseUtils.cursorLongToContentValuesIfPresent(cursor, cv, DTEND);
    679                 DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, DURATION);
    680                 DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, EVENT_TIMEZONE);
    681                 DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, ALL_DAY);
    682                 DatabaseUtils.cursorIntToContentValuesIfPresent(cursor, cv, VISIBILITY);
    683                 DatabaseUtils.cursorIntToContentValuesIfPresent(cursor, cv, TRANSPARENCY);
    684                 DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, HAS_ALARM);
    685                 DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv,
    686                         HAS_EXTENDED_PROPERTIES);
    687                 DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, RRULE);
    688                 DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, RDATE);
    689                 DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, EXRULE);
    690                 DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, EXDATE);
    691                 DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, ORIGINAL_EVENT);
    692                 DatabaseUtils.cursorLongToContentValuesIfPresent(cursor, cv,
    693                         ORIGINAL_INSTANCE_TIME);
    694                 DatabaseUtils.cursorIntToContentValuesIfPresent(cursor, cv, ORIGINAL_ALL_DAY);
    695                 DatabaseUtils.cursorLongToContentValuesIfPresent(cursor, cv, LAST_DATE);
    696                 DatabaseUtils.cursorIntToContentValuesIfPresent(cursor, cv, HAS_ATTENDEE_DATA);
    697                 DatabaseUtils.cursorIntToContentValuesIfPresent(cursor, cv,
    698                         GUESTS_CAN_INVITE_OTHERS);
    699                 DatabaseUtils.cursorIntToContentValuesIfPresent(cursor, cv, GUESTS_CAN_MODIFY);
    700                 DatabaseUtils.cursorIntToContentValuesIfPresent(cursor, cv, GUESTS_CAN_SEE_GUESTS);
    701                 DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, ORGANIZER);
    702                 DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, _SYNC_ID);
    703                 DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, _SYNC_DATA);
    704                 DatabaseUtils.cursorLongToContentValuesIfPresent(cursor, cv, _SYNC_DIRTY);
    705                 DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, _SYNC_VERSION);
    706                 DatabaseUtils.cursorIntToContentValuesIfPresent(cursor, cv, DELETED);
    707                 DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, Calendars.URL);
    708 
    709                 Entity entity = new Entity(cv);
    710                 Cursor subCursor;
    711                 if (mResolver != null) {
    712                     subCursor = mResolver.query(Reminders.CONTENT_URI, REMINDERS_PROJECTION,
    713                             WHERE_EVENT_ID,
    714                             new String[] { Long.toString(eventId) }  /* selectionArgs */,
    715                             null /* sortOrder */);
    716                 } else {
    717                     subCursor = mProvider.query(Reminders.CONTENT_URI, REMINDERS_PROJECTION,
    718                             WHERE_EVENT_ID,
    719                             new String[] { Long.toString(eventId) }  /* selectionArgs */,
    720                             null /* sortOrder */);
    721                 }
    722                 try {
    723                     while (subCursor.moveToNext()) {
    724                         ContentValues reminderValues = new ContentValues();
    725                         reminderValues.put(Reminders.MINUTES, subCursor.getInt(COLUMN_MINUTES));
    726                         reminderValues.put(Reminders.METHOD, subCursor.getInt(COLUMN_METHOD));
    727                         entity.addSubValue(Reminders.CONTENT_URI, reminderValues);
    728                     }
    729                 } finally {
    730                     subCursor.close();
    731                 }
    732 
    733                 if (mResolver != null) {
    734                     subCursor = mResolver.query(Attendees.CONTENT_URI, ATTENDEES_PROJECTION,
    735                             WHERE_EVENT_ID,
    736                             new String[] { Long.toString(eventId) } /* selectionArgs */,
    737                             null /* sortOrder */);
    738                 } else {
    739                     subCursor = mProvider.query(Attendees.CONTENT_URI, ATTENDEES_PROJECTION,
    740                             WHERE_EVENT_ID,
    741                             new String[] { Long.toString(eventId) } /* selectionArgs */,
    742                             null /* sortOrder */);
    743                 }
    744                 try {
    745                     while (subCursor.moveToNext()) {
    746                         ContentValues attendeeValues = new ContentValues();
    747                         attendeeValues.put(Attendees.ATTENDEE_NAME,
    748                                 subCursor.getString(COLUMN_ATTENDEE_NAME));
    749                         attendeeValues.put(Attendees.ATTENDEE_EMAIL,
    750                                 subCursor.getString(COLUMN_ATTENDEE_EMAIL));
    751                         attendeeValues.put(Attendees.ATTENDEE_RELATIONSHIP,
    752                                 subCursor.getInt(COLUMN_ATTENDEE_RELATIONSHIP));
    753                         attendeeValues.put(Attendees.ATTENDEE_TYPE,
    754                                 subCursor.getInt(COLUMN_ATTENDEE_TYPE));
    755                         attendeeValues.put(Attendees.ATTENDEE_STATUS,
    756                                 subCursor.getInt(COLUMN_ATTENDEE_STATUS));
    757                         entity.addSubValue(Attendees.CONTENT_URI, attendeeValues);
    758                     }
    759                 } finally {
    760                     subCursor.close();
    761                 }
    762 
    763                 if (mResolver != null) {
    764                     subCursor = mResolver.query(ExtendedProperties.CONTENT_URI, EXTENDED_PROJECTION,
    765                             WHERE_EVENT_ID,
    766                             new String[] { Long.toString(eventId) } /* selectionArgs */,
    767                             null /* sortOrder */);
    768                 } else {
    769                     subCursor = mProvider.query(ExtendedProperties.CONTENT_URI, EXTENDED_PROJECTION,
    770                             WHERE_EVENT_ID,
    771                             new String[] { Long.toString(eventId) } /* selectionArgs */,
    772                             null /* sortOrder */);
    773                 }
    774                 try {
    775                     while (subCursor.moveToNext()) {
    776                         ContentValues extendedValues = new ContentValues();
    777                         extendedValues.put(ExtendedProperties._ID,
    778                                 subCursor.getString(COLUMN_ID));
    779                         extendedValues.put(ExtendedProperties.NAME,
    780                                 subCursor.getString(COLUMN_NAME));
    781                         extendedValues.put(ExtendedProperties.VALUE,
    782                                 subCursor.getString(COLUMN_VALUE));
    783                         entity.addSubValue(ExtendedProperties.CONTENT_URI, extendedValues);
    784                     }
    785                 } finally {
    786                     subCursor.close();
    787                 }
    788 
    789                 cursor.moveToNext();
    790                 return entity;
    791             }
    792         }
    793     }
    794 
    795     /**
    796      * Contains one entry per calendar event. Recurring events show up as a single entry.
    797      */
    798     public static final class Events implements BaseColumns, EventsColumns, CalendarsColumns {
    799 
    800         private static final String[] FETCH_ENTRY_COLUMNS =
    801                 new String[] { Events._SYNC_ACCOUNT, Events._SYNC_ID };
    802 
    803         private static final String[] ATTENDEES_COLUMNS =
    804                 new String[] { AttendeesColumns.ATTENDEE_NAME,
    805                                AttendeesColumns.ATTENDEE_EMAIL,
    806                                AttendeesColumns.ATTENDEE_RELATIONSHIP,
    807                                AttendeesColumns.ATTENDEE_TYPE,
    808                                AttendeesColumns.ATTENDEE_STATUS };
    809 
    810         public static final Cursor query(ContentResolver cr, String[] projection) {
    811             return cr.query(CONTENT_URI, projection, null, null, DEFAULT_SORT_ORDER);
    812         }
    813 
    814         public static final Cursor query(ContentResolver cr, String[] projection,
    815                                        String where, String orderBy) {
    816             return cr.query(CONTENT_URI, projection, where,
    817                                          null, orderBy == null ? DEFAULT_SORT_ORDER : orderBy);
    818         }
    819 
    820         private static String extractValue(ICalendar.Component component,
    821                                            String propertyName) {
    822             ICalendar.Property property =
    823                     component.getFirstProperty(propertyName);
    824             if (property != null) {
    825                 return property.getValue();
    826             }
    827             return null;
    828         }
    829 
    830         /**
    831          * The content:// style URL for this table
    832          */
    833         public static final Uri CONTENT_URI =
    834                 Uri.parse("content://" + AUTHORITY + "/events");
    835 
    836         public static final Uri DELETED_CONTENT_URI =
    837                 Uri.parse("content://" + AUTHORITY + "/deleted_events");
    838 
    839         /**
    840          * The default sort order for this table
    841          */
    842         public static final String DEFAULT_SORT_ORDER = "";
    843     }
    844 
    845     /**
    846      * Contains one entry per calendar event instance. Recurring events show up every time
    847      * they occur.
    848      */
    849     public static final class Instances implements BaseColumns, EventsColumns, CalendarsColumns {
    850 
    851         private static final String WHERE_CALENDARS_SELECTED = Calendars.SELECTED + "=1";
    852 
    853         public static final Cursor query(ContentResolver cr, String[] projection,
    854                                          long begin, long end) {
    855             Uri.Builder builder = CONTENT_URI.buildUpon();
    856             ContentUris.appendId(builder, begin);
    857             ContentUris.appendId(builder, end);
    858             return cr.query(builder.build(), projection, WHERE_CALENDARS_SELECTED,
    859                          null, DEFAULT_SORT_ORDER);
    860         }
    861 
    862         public static final Cursor query(ContentResolver cr, String[] projection,
    863                                          long begin, long end, String where, String orderBy) {
    864             Uri.Builder builder = CONTENT_URI.buildUpon();
    865             ContentUris.appendId(builder, begin);
    866             ContentUris.appendId(builder, end);
    867             if (TextUtils.isEmpty(where)) {
    868                 where = WHERE_CALENDARS_SELECTED;
    869             } else {
    870                 where = "(" + where + ") AND " + WHERE_CALENDARS_SELECTED;
    871             }
    872             return cr.query(builder.build(), projection, where,
    873                          null, orderBy == null ? DEFAULT_SORT_ORDER : orderBy);
    874         }
    875 
    876         /**
    877          * The content:// style URL for this table
    878          */
    879         public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY +
    880                 "/instances/when");
    881         public static final Uri CONTENT_BY_DAY_URI =
    882             Uri.parse("content://" + AUTHORITY + "/instances/whenbyday");
    883 
    884         /**
    885          * The default sort order for this table.
    886          */
    887         public static final String DEFAULT_SORT_ORDER = "begin ASC";
    888 
    889         /**
    890          * The sort order is: events with an earlier start time occur
    891          * first and if the start times are the same, then events with
    892          * a later end time occur first. The later end time is ordered
    893          * first so that long-running events in the calendar views appear
    894          * first.  If the start and end times of two events are
    895          * the same then we sort alphabetically on the title.  This isn't
    896          * required for correctness, it just adds a nice touch.
    897          */
    898         public static final String SORT_CALENDAR_VIEW = "begin ASC, end DESC, title ASC";
    899 
    900         /**
    901          * The beginning time of the instance, in UTC milliseconds
    902          * <P>Type: INTEGER (long; millis since epoch)</P>
    903          */
    904         public static final String BEGIN = "begin";
    905 
    906         /**
    907          * The ending time of the instance, in UTC milliseconds
    908          * <P>Type: INTEGER (long; millis since epoch)</P>
    909          */
    910         public static final String END = "end";
    911 
    912         /**
    913          * The event for this instance
    914          * <P>Type: INTEGER (long, foreign key to the Events table)</P>
    915          */
    916         public static final String EVENT_ID = "event_id";
    917 
    918         /**
    919          * The Julian start day of the instance, relative to the local timezone
    920          * <P>Type: INTEGER (int)</P>
    921          */
    922         public static final String START_DAY = "startDay";
    923 
    924         /**
    925          * The Julian end day of the instance, relative to the local timezone
    926          * <P>Type: INTEGER (int)</P>
    927          */
    928         public static final String END_DAY = "endDay";
    929 
    930         /**
    931          * The start minute of the instance measured from midnight in the
    932          * local timezone.
    933          * <P>Type: INTEGER (int)</P>
    934          */
    935         public static final String START_MINUTE = "startMinute";
    936 
    937         /**
    938          * The end minute of the instance measured from midnight in the
    939          * local timezone.
    940          * <P>Type: INTEGER (int)</P>
    941          */
    942         public static final String END_MINUTE = "endMinute";
    943     }
    944 
    945     /**
    946      * A few Calendar globals are needed in the CalendarProvider for expanding
    947      * the Instances table and these are all stored in the first (and only)
    948      * row of the CalendarMetaData table.
    949      */
    950     public interface CalendarMetaDataColumns {
    951         /**
    952          * The local timezone that was used for precomputing the fields
    953          * in the Instances table.
    954          */
    955         public static final String LOCAL_TIMEZONE = "localTimezone";
    956 
    957         /**
    958          * The minimum time used in expanding the Instances table,
    959          * in UTC milliseconds.
    960          * <P>Type: INTEGER</P>
    961          */
    962         public static final String MIN_INSTANCE = "minInstance";
    963 
    964         /**
    965          * The maximum time used in expanding the Instances table,
    966          * in UTC milliseconds.
    967          * <P>Type: INTEGER</P>
    968          */
    969         public static final String MAX_INSTANCE = "maxInstance";
    970 
    971         /**
    972          * The minimum Julian day in the EventDays table.
    973          * <P>Type: INTEGER</P>
    974          */
    975         public static final String MIN_EVENTDAYS = "minEventDays";
    976 
    977         /**
    978          * The maximum Julian day in the EventDays table.
    979          * <P>Type: INTEGER</P>
    980          */
    981         public static final String MAX_EVENTDAYS = "maxEventDays";
    982     }
    983 
    984     public static final class CalendarMetaData implements CalendarMetaDataColumns {
    985     }
    986 
    987     public interface EventDaysColumns {
    988         /**
    989          * The Julian starting day number.
    990          * <P>Type: INTEGER (int)</P>
    991          */
    992         public static final String STARTDAY = "startDay";
    993         public static final String ENDDAY = "endDay";
    994 
    995     }
    996 
    997     public static final class EventDays implements EventDaysColumns {
    998         public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY +
    999                 "/instances/groupbyday");
   1000 
   1001         public static final String[] PROJECTION = { STARTDAY, ENDDAY };
   1002         public static final String SELECTION = "selected=1";
   1003 
   1004         /**
   1005          * Retrieves the days with events for the Julian days starting at "startDay"
   1006          * for "numDays".
   1007          *
   1008          * @param cr the ContentResolver
   1009          * @param startDay the first Julian day in the range
   1010          * @param numDays the number of days to load (must be at least 1)
   1011          * @return a database cursor
   1012          */
   1013         public static final Cursor query(ContentResolver cr, int startDay, int numDays) {
   1014             if (numDays < 1) {
   1015                 return null;
   1016             }
   1017             int endDay = startDay + numDays - 1;
   1018             Uri.Builder builder = CONTENT_URI.buildUpon();
   1019             ContentUris.appendId(builder, startDay);
   1020             ContentUris.appendId(builder, endDay);
   1021             return cr.query(builder.build(), PROJECTION, SELECTION,
   1022                     null /* selection args */, STARTDAY);
   1023         }
   1024     }
   1025 
   1026     public interface RemindersColumns {
   1027         /**
   1028          * The event the reminder belongs to
   1029          * <P>Type: INTEGER (foreign key to the Events table)</P>
   1030          */
   1031         public static final String EVENT_ID = "event_id";
   1032 
   1033         /**
   1034          * The minutes prior to the event that the alarm should ring.  -1
   1035          * specifies that we should use the default value for the system.
   1036          * <P>Type: INTEGER</P>
   1037          */
   1038         public static final String MINUTES = "minutes";
   1039 
   1040         public static final int MINUTES_DEFAULT = -1;
   1041 
   1042         /**
   1043          * The alarm method, as set on the server.  DEFAULT, ALERT, EMAIL, and
   1044          * SMS are possible values; the device will only process DEFAULT and
   1045          * ALERT reminders (the other types are simply stored so we can send the
   1046          * same reminder info back to the server when we make changes).
   1047          */
   1048         public static final String METHOD = "method";
   1049 
   1050         public static final int METHOD_DEFAULT = 0;
   1051         public static final int METHOD_ALERT = 1;
   1052         public static final int METHOD_EMAIL = 2;
   1053         public static final int METHOD_SMS = 3;
   1054     }
   1055 
   1056     public static final class Reminders implements BaseColumns, RemindersColumns, EventsColumns {
   1057         public static final String TABLE_NAME = "Reminders";
   1058         public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/reminders");
   1059     }
   1060 
   1061     public interface CalendarAlertsColumns {
   1062         /**
   1063          * The event that the alert belongs to
   1064          * <P>Type: INTEGER (foreign key to the Events table)</P>
   1065          */
   1066         public static final String EVENT_ID = "event_id";
   1067 
   1068         /**
   1069          * The start time of the event, in UTC
   1070          * <P>Type: INTEGER (long; millis since epoch)</P>
   1071          */
   1072         public static final String BEGIN = "begin";
   1073 
   1074         /**
   1075          * The end time of the event, in UTC
   1076          * <P>Type: INTEGER (long; millis since epoch)</P>
   1077          */
   1078         public static final String END = "end";
   1079 
   1080         /**
   1081          * The alarm time of the event, in UTC
   1082          * <P>Type: INTEGER (long; millis since epoch)</P>
   1083          */
   1084         public static final String ALARM_TIME = "alarmTime";
   1085 
   1086         /**
   1087          * The creation time of this database entry, in UTC.
   1088          * (Useful for debugging missed reminders.)
   1089          * <P>Type: INTEGER (long; millis since epoch)</P>
   1090          */
   1091         public static final String CREATION_TIME = "creationTime";
   1092 
   1093         /**
   1094          * The time that the alarm broadcast was received by the Calendar app,
   1095          * in UTC. (Useful for debugging missed reminders.)
   1096          * <P>Type: INTEGER (long; millis since epoch)</P>
   1097          */
   1098         public static final String RECEIVED_TIME = "receivedTime";
   1099 
   1100         /**
   1101          * The time that the notification was created by the Calendar app,
   1102          * in UTC. (Useful for debugging missed reminders.)
   1103          * <P>Type: INTEGER (long; millis since epoch)</P>
   1104          */
   1105         public static final String NOTIFY_TIME = "notifyTime";
   1106 
   1107         /**
   1108          * The state of this alert.  It starts out as SCHEDULED, then when
   1109          * the alarm goes off, it changes to FIRED, and then when the user
   1110          * dismisses the alarm it changes to DISMISSED.
   1111          * <P>Type: INTEGER</P>
   1112          */
   1113         public static final String STATE = "state";
   1114 
   1115         public static final int SCHEDULED = 0;
   1116         public static final int FIRED = 1;
   1117         public static final int DISMISSED = 2;
   1118 
   1119         /**
   1120          * The number of minutes that this alarm precedes the start time
   1121          * <P>Type: INTEGER </P>
   1122          */
   1123         public static final String MINUTES = "minutes";
   1124 
   1125         /**
   1126          * The default sort order for this table
   1127          */
   1128         public static final String DEFAULT_SORT_ORDER = "begin ASC,title ASC";
   1129     }
   1130 
   1131     public static final class CalendarAlerts implements BaseColumns,
   1132             CalendarAlertsColumns, EventsColumns, CalendarsColumns {
   1133 
   1134         public static final String TABLE_NAME = "CalendarAlerts";
   1135         public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY +
   1136                 "/calendar_alerts");
   1137 
   1138         private static final String WHERE_ALARM_EXISTS = EVENT_ID + "=?"
   1139                 + " AND " + BEGIN + "=?"
   1140                 + " AND " + ALARM_TIME + "=?";
   1141 
   1142         private static final String WHERE_FINDNEXTALARMTIME = ALARM_TIME + ">=?";
   1143         private static final String SORT_ORDER_ALARMTIME_ASC = ALARM_TIME + " ASC";
   1144 
   1145         private static final String WHERE_RESCHEDULE_MISSED_ALARMS = STATE + "=" + SCHEDULED
   1146                 + " AND " + ALARM_TIME + "<?"
   1147                 + " AND " + ALARM_TIME + ">?"
   1148                 + " AND " + END + ">=?";
   1149 
   1150         /**
   1151          * This URI is for grouping the query results by event_id and begin
   1152          * time.  This will return one result per instance of an event.  So
   1153          * events with multiple alarms will appear just once, but multiple
   1154          * instances of a repeating event will show up multiple times.
   1155          */
   1156         public static final Uri CONTENT_URI_BY_INSTANCE =
   1157             Uri.parse("content://" + AUTHORITY + "/calendar_alerts/by_instance");
   1158 
   1159         private static final boolean DEBUG = true;
   1160 
   1161         public static final Uri insert(ContentResolver cr, long eventId,
   1162                 long begin, long end, long alarmTime, int minutes) {
   1163             ContentValues values = new ContentValues();
   1164             values.put(CalendarAlerts.EVENT_ID, eventId);
   1165             values.put(CalendarAlerts.BEGIN, begin);
   1166             values.put(CalendarAlerts.END, end);
   1167             values.put(CalendarAlerts.ALARM_TIME, alarmTime);
   1168             long currentTime = System.currentTimeMillis();
   1169             values.put(CalendarAlerts.CREATION_TIME, currentTime);
   1170             values.put(CalendarAlerts.RECEIVED_TIME, 0);
   1171             values.put(CalendarAlerts.NOTIFY_TIME, 0);
   1172             values.put(CalendarAlerts.STATE, SCHEDULED);
   1173             values.put(CalendarAlerts.MINUTES, minutes);
   1174             return cr.insert(CONTENT_URI, values);
   1175         }
   1176 
   1177         public static final Cursor query(ContentResolver cr, String[] projection,
   1178                 String selection, String[] selectionArgs, String sortOrder) {
   1179             return cr.query(CONTENT_URI, projection, selection, selectionArgs,
   1180                     sortOrder);
   1181         }
   1182 
   1183         /**
   1184          * Finds the next alarm after (or equal to) the given time and returns
   1185          * the time of that alarm or -1 if no such alarm exists.
   1186          *
   1187          * @param cr the ContentResolver
   1188          * @param millis the time in UTC milliseconds
   1189          * @return the next alarm time greater than or equal to "millis", or -1
   1190          *     if no such alarm exists.
   1191          */
   1192         public static final long findNextAlarmTime(ContentResolver cr, long millis) {
   1193             String selection = ALARM_TIME + ">=" + millis;
   1194             // TODO: construct an explicit SQL query so that we can add
   1195             // "LIMIT 1" to the end and get just one result.
   1196             String[] projection = new String[] { ALARM_TIME };
   1197             Cursor cursor = query(cr, projection,
   1198                     WHERE_FINDNEXTALARMTIME,
   1199                     new String[] {
   1200                         Long.toString(millis)
   1201                     },
   1202                     SORT_ORDER_ALARMTIME_ASC);
   1203             long alarmTime = -1;
   1204             try {
   1205                 if (cursor != null && cursor.moveToFirst()) {
   1206                     alarmTime = cursor.getLong(0);
   1207                 }
   1208             } finally {
   1209                 if (cursor != null) {
   1210                     cursor.close();
   1211                 }
   1212             }
   1213             return alarmTime;
   1214         }
   1215 
   1216         /**
   1217          * Searches the CalendarAlerts table for alarms that should have fired
   1218          * but have not and then reschedules them.  This method can be called
   1219          * at boot time to restore alarms that may have been lost due to a
   1220          * phone reboot.
   1221          *
   1222          * @param cr the ContentResolver
   1223          * @param context the Context
   1224          * @param manager the AlarmManager
   1225          */
   1226         public static final void rescheduleMissedAlarms(ContentResolver cr,
   1227                 Context context, AlarmManager manager) {
   1228             // Get all the alerts that have been scheduled but have not fired
   1229             // and should have fired by now and are not too old.
   1230             long now = System.currentTimeMillis();
   1231             long ancient = now - DateUtils.DAY_IN_MILLIS;
   1232             String[] projection = new String[] {
   1233                     ALARM_TIME,
   1234             };
   1235 
   1236             // TODO: construct an explicit SQL query so that we can add
   1237             // "GROUPBY" instead of doing a sort and de-dup
   1238             Cursor cursor = CalendarAlerts.query(cr,
   1239                     projection,
   1240                     WHERE_RESCHEDULE_MISSED_ALARMS,
   1241                     new String[] {
   1242                         Long.toString(now),
   1243                         Long.toString(ancient),
   1244                         Long.toString(now)
   1245                     },
   1246                     SORT_ORDER_ALARMTIME_ASC);
   1247             if (cursor == null) {
   1248                 return;
   1249             }
   1250 
   1251             if (DEBUG) {
   1252                 Log.d(TAG, "missed alarms found: " + cursor.getCount());
   1253             }
   1254 
   1255             try {
   1256                 long alarmTime = -1;
   1257 
   1258                 while (cursor.moveToNext()) {
   1259                     long newAlarmTime = cursor.getLong(0);
   1260                     if (alarmTime != newAlarmTime) {
   1261                         if (DEBUG) {
   1262                             Log.w(TAG, "rescheduling missed alarm. alarmTime: " + newAlarmTime);
   1263                         }
   1264                         scheduleAlarm(context, manager, newAlarmTime);
   1265                         alarmTime = newAlarmTime;
   1266                     }
   1267                 }
   1268             } finally {
   1269                 cursor.close();
   1270             }
   1271         }
   1272 
   1273         public static void scheduleAlarm(Context context, AlarmManager manager, long alarmTime) {
   1274             if (DEBUG) {
   1275                 Time time = new Time();
   1276                 time.set(alarmTime);
   1277                 String schedTime = time.format(" %a, %b %d, %Y %I:%M%P");
   1278                 Log.d(TAG, "Schedule alarm at " + alarmTime + " " + schedTime);
   1279             }
   1280 
   1281             if (manager == null) {
   1282                 manager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
   1283             }
   1284 
   1285             Intent intent = new Intent(EVENT_REMINDER_ACTION);
   1286             intent.setData(ContentUris.withAppendedId(Calendar.CONTENT_URI, alarmTime));
   1287             intent.putExtra(ALARM_TIME, alarmTime);
   1288             PendingIntent pi = PendingIntent.getBroadcast(context, 0, intent, 0);
   1289             manager.set(AlarmManager.RTC_WAKEUP, alarmTime, pi);
   1290         }
   1291 
   1292         /**
   1293          * Searches for an entry in the CalendarAlerts table that matches
   1294          * the given event id, begin time and alarm time.  If one is found
   1295          * then this alarm already exists and this method returns true.
   1296          *
   1297          * @param cr the ContentResolver
   1298          * @param eventId the event id to match
   1299          * @param begin the start time of the event in UTC millis
   1300          * @param alarmTime the alarm time of the event in UTC millis
   1301          * @return true if there is already an alarm for the given event
   1302          *   with the same start time and alarm time.
   1303          */
   1304         public static final boolean alarmExists(ContentResolver cr, long eventId,
   1305                 long begin, long alarmTime) {
   1306             // TODO: construct an explicit SQL query so that we can add
   1307             // "LIMIT 1" to the end and get just one result.
   1308             String[] projection = new String[] { ALARM_TIME };
   1309             Cursor cursor = query(cr,
   1310                     projection,
   1311                     WHERE_ALARM_EXISTS,
   1312                     new String[] {
   1313                         Long.toString(eventId),
   1314                         Long.toString(begin),
   1315                         Long.toString(alarmTime)
   1316                     },
   1317                     null);
   1318             boolean found = false;
   1319             try {
   1320                 if (cursor != null && cursor.getCount() > 0) {
   1321                     found = true;
   1322                 }
   1323             } finally {
   1324                 if (cursor != null) {
   1325                     cursor.close();
   1326                 }
   1327             }
   1328             return found;
   1329         }
   1330     }
   1331 
   1332     public interface ExtendedPropertiesColumns {
   1333         /**
   1334          * The event the extended property belongs to
   1335          * <P>Type: INTEGER (foreign key to the Events table)</P>
   1336          */
   1337         public static final String EVENT_ID = "event_id";
   1338 
   1339         /**
   1340          * The name of the extended property.  This is a uri of the form
   1341          * {scheme}#{local-name} convention.
   1342          * <P>Type: TEXT</P>
   1343          */
   1344         public static final String NAME = "name";
   1345 
   1346         /**
   1347          * The value of the extended property.
   1348          * <P>Type: TEXT</P>
   1349          */
   1350         public static final String VALUE = "value";
   1351     }
   1352 
   1353    public static final class ExtendedProperties implements BaseColumns,
   1354             ExtendedPropertiesColumns, EventsColumns {
   1355         public static final Uri CONTENT_URI =
   1356                 Uri.parse("content://" + AUTHORITY + "/extendedproperties");
   1357 
   1358         // TODO: fill out this class when we actually start utilizing extendedproperties
   1359         // in the calendar application.
   1360    }
   1361 
   1362     /**
   1363      * A table provided for sync adapters to use for storing private sync state data.
   1364      *
   1365      * @see SyncStateContract
   1366      */
   1367     public static final class SyncState implements SyncStateContract.Columns {
   1368         /**
   1369          * This utility class cannot be instantiated
   1370          */
   1371         private SyncState() {}
   1372 
   1373         public static final String CONTENT_DIRECTORY =
   1374                 SyncStateContract.Constants.CONTENT_DIRECTORY;
   1375 
   1376         /**
   1377          * The content:// style URI for this table
   1378          */
   1379         public static final Uri CONTENT_URI =
   1380                 Uri.withAppendedPath(Calendar.CONTENT_URI, CONTENT_DIRECTORY);
   1381     }
   1382 }
   1383