• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2 **
3 ** Copyright 2006, The Android Open Source Project
4 **
5 ** Licensed under the Apache License, Version 2.0 (the "License");
6 ** you may not use this file except in compliance with the License.
7 ** You may obtain a copy of the License at
8 **
9 **     http://www.apache.org/licenses/LICENSE-2.0
10 **
11 ** Unless required by applicable law or agreed to in writing, software
12 ** distributed under the License is distributed on an "AS IS" BASIS,
13 ** See the License for the specific language governing permissions and
14 ** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 ** limitations under the License.
16 */
17 
18 package com.android.providers.calendar;
19 
20 import android.accounts.Account;
21 import android.accounts.AccountManager;
22 import android.accounts.AccountManagerCallback;
23 import android.accounts.AccountManagerFuture;
24 import android.accounts.OperationCanceledException;
25 import android.accounts.AuthenticatorException;
26 import android.app.AlarmManager;
27 import android.app.PendingIntent;
28 import android.content.AbstractSyncableContentProvider;
29 import android.content.AbstractTableMerger;
30 import android.content.BroadcastReceiver;
31 import android.content.ContentProvider;
32 import android.content.ContentProviderOperation;
33 import android.content.ContentProviderResult;
34 import android.content.ContentResolver;
35 import android.content.ContentUris;
36 import android.content.ContentValues;
37 import android.content.Context;
38 import android.content.Entity;
39 import android.content.EntityIterator;
40 import android.content.Intent;
41 import android.content.IntentFilter;
42 import android.content.OperationApplicationException;
43 import android.content.SyncContext;
44 import android.content.UriMatcher;
45 import android.database.Cursor;
46 import android.database.DatabaseUtils;
47 import android.database.SQLException;
48 import android.database.sqlite.SQLiteCursor;
49 import android.database.sqlite.SQLiteDatabase;
50 import android.database.sqlite.SQLiteQueryBuilder;
51 import android.net.Uri;
52 import android.os.Bundle;
53 import android.os.Debug;
54 import android.os.Process;
55 import android.os.RemoteException;
56 import android.pim.DateException;
57 import android.pim.RecurrenceSet;
58 import android.provider.Calendar;
59 import android.provider.Calendar.Attendees;
60 import android.provider.Calendar.BusyBits;
61 import android.provider.Calendar.CalendarAlerts;
62 import android.provider.Calendar.Calendars;
63 import android.provider.Calendar.Events;
64 import android.provider.Calendar.ExtendedProperties;
65 import android.provider.Calendar.Instances;
66 import android.provider.Calendar.Reminders;
67 import android.text.TextUtils;
68 import android.text.format.Time;
69 import android.util.Config;
70 import android.util.Log;
71 import android.util.TimeFormatException;
72 import com.google.android.collect.Maps;
73 import com.google.android.collect.Sets;
74 import com.google.android.gdata.client.AndroidGDataClient;
75 import com.google.android.gdata.client.AndroidXmlParserFactory;
76 import com.google.android.providers.AbstractGDataSyncAdapter;
77 import com.google.android.providers.AbstractGDataSyncAdapter.GDataSyncData;
78 import com.google.android.googlelogin.GoogleLoginServiceConstants;
79 import com.google.wireless.gdata.calendar.client.CalendarClient;
80 import com.google.wireless.gdata.calendar.parser.xml.XmlCalendarGDataParserFactory;
81 
82 import java.util.ArrayList;
83 import java.util.Collections;
84 import java.util.HashMap;
85 import java.util.Map;
86 import java.util.Set;
87 import java.util.TimeZone;
88 import java.io.IOException;
89 
90 public class CalendarProvider extends AbstractSyncableContentProvider {
91     private static final boolean PROFILE = false;
92     private static final boolean MULTIPLE_ATTENDEES_PER_EVENT = true;
93     private static final String[] ACCOUNTS_PROJECTION =
94             new String[] {Calendars._SYNC_ACCOUNT, Calendars._SYNC_ACCOUNT_TYPE};
95 
96     private static final String[] EVENTS_PROJECTION = new String[] {
97             Events._SYNC_ID,
98             Events._SYNC_VERSION,
99             Events._SYNC_ACCOUNT,
100             Events._SYNC_ACCOUNT_TYPE,
101             Events.CALENDAR_ID,
102             Events.RRULE,
103             Events.RDATE,
104             Events.ORIGINAL_EVENT,
105     };
106     private static final int EVENTS_SYNC_ID_INDEX = 0;
107     private static final int EVENTS_SYNC_VERSION_INDEX = 1;
108     private static final int EVENTS_SYNC_ACCOUNT_NAME_INDEX = 2;
109     private static final int EVENTS_SYNC_ACCOUNT_TYPE_INDEX = 3;
110     private static final int EVENTS_CALENDAR_ID_INDEX = 4;
111     private static final int EVENTS_RRULE_INDEX = 5;
112     private static final int EVENTS_RDATE_INDEX = 6;
113     private static final int EVENTS_ORIGINAL_EVENT_INDEX = 7;
114 
115     private DatabaseUtils.InsertHelper mCalendarsInserter;
116     private DatabaseUtils.InsertHelper mEventsInserter;
117     private DatabaseUtils.InsertHelper mEventsRawTimesInserter;
118     private DatabaseUtils.InsertHelper mDeletedEventsInserter;
119     private DatabaseUtils.InsertHelper mInstancesInserter;
120     private DatabaseUtils.InsertHelper mAttendeesInserter;
121     private DatabaseUtils.InsertHelper mRemindersInserter;
122     private DatabaseUtils.InsertHelper mCalendarAlertsInserter;
123     private DatabaseUtils.InsertHelper mExtendedPropertiesInserter;
124 
125     /**
126      * The cached copy of the CalendarMetaData database table.
127      * Make this "package private" instead of "private" so that test code
128      * can access it.
129      */
130     MetaData mMetaData;
131 
132     // The interval in minutes for calculating busy bits
133     private static final int BUSYBIT_INTERVAL = 60;
134 
135     // A lookup table for getting a bit mask of length N, for N <= 32
136     // For example, BIT_MASKS[4] gives 0xf (which has 4 bits set to 1).
137     // We use this for computing the busy bits for events.
138     private static final int[] BIT_MASKS = {
139             0,
140             0x00000001, 0x00000003, 0x00000007, 0x0000000f,
141             0x0000001f, 0x0000003f, 0x0000007f, 0x000000ff,
142             0x000001ff, 0x000003ff, 0x000007ff, 0x00000fff,
143             0x00001fff, 0x00003fff, 0x00007fff, 0x0000ffff,
144             0x0001ffff, 0x0003ffff, 0x0007ffff, 0x000fffff,
145             0x001fffff, 0x003fffff, 0x007fffff, 0x00ffffff,
146             0x01ffffff, 0x03ffffff, 0x07ffffff, 0x0fffffff,
147             0x1fffffff, 0x3fffffff, 0x7fffffff, 0xffffffff,
148     };
149 
150     // To determine if a recurrence exception originally overlapped the
151     // window, we need to assume a maximum duration, since we only know
152     // the original start time.
153     private static final int MAX_ASSUMED_DURATION = 7*24*60*60*1000;
154 
155     public static final class TimeRange {
156         public long begin;
157         public long end;
158         public boolean allDay;
159     }
160 
161     public static final class InstancesRange {
162         public long begin;
163         public long end;
164 
InstancesRange(long begin, long end)165         public InstancesRange(long begin, long end) {
166             this.begin = begin;
167             this.end = end;
168         }
169     }
170 
171     public static final class InstancesList
172             extends ArrayList<ContentValues> {
173     }
174 
175     public static final class EventInstancesMap
176             extends HashMap<String, InstancesList> {
add(String syncId, ContentValues values)177         public void add(String syncId, ContentValues values) {
178             InstancesList instances = get(syncId);
179             if (instances == null) {
180                 instances = new InstancesList();
181                 put(syncId, instances);
182             }
183             instances.add(values);
184         }
185     }
186 
187     // A thread that runs in the background and schedules the next
188     // calendar event alarm.
189     private class AlarmScheduler extends Thread {
190         boolean mRemoveAlarms;
191 
AlarmScheduler(boolean removeAlarms)192         public AlarmScheduler(boolean removeAlarms) {
193             mRemoveAlarms = removeAlarms;
194         }
195 
run()196         public void run() {
197             try {
198                 Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
199                 runScheduleNextAlarm(mRemoveAlarms);
200             } catch (SQLException e) {
201                 Log.e(TAG, "runScheduleNextAlarm() failed", e);
202             }
203         }
204     }
205 
206     /**
207      * We search backward in time for event reminders that we may have missed
208      * and schedule them if the event has not yet expired.  The amount in
209      * the past to search backwards is controlled by this constant.  It
210      * should be at least a few minutes to allow for an event that was
211      * recently created on the web to make its way to the phone.  Two hours
212      * might seem like overkill, but it is useful in the case where the user
213      * just crossed into a new timezone and might have just missed an alarm.
214      */
215     private static final long SCHEDULE_ALARM_SLACK = 2 * android.text.format.DateUtils.HOUR_IN_MILLIS;
216 
217     /**
218      * Alarms older than this threshold will be deleted from the CalendarAlerts
219      * table.  This should be at least a day because if the timezone is
220      * wrong and the user corrects it we might delete good alarms that
221      * appear to be old because the device time was incorrectly in the future.
222      * This threshold must also be larger than SCHEDULE_ALARM_SLACK.  We add
223      * the SCHEDULE_ALARM_SLACK to ensure this.
224      *
225      * To make it easier to find and debug problems with missed reminders,
226      * set this to something greater than a day.
227      */
228     private static final long CLEAR_OLD_ALARM_THRESHOLD =
229             7 * android.text.format.DateUtils.DAY_IN_MILLIS + SCHEDULE_ALARM_SLACK;
230 
231     // A lock for synchronizing access to fields that are shared
232     // with the AlarmScheduler thread.
233     private Object mAlarmLock = new Object();
234 
235     private static final String TAG = "CalendarProvider";
236     private static final String DATABASE_NAME = "calendar.db";
237 
238     // Note: if you update the version number, you must also update the code
239     // in upgradeDatabase() to modify the database (gracefully, if possible).
240     private static final int DATABASE_VERSION = 57;
241 
242     // Make sure we load at least two months worth of data.
243     // Client apps can load more data in a background thread.
244     private static final long MINIMUM_EXPANSION_SPAN =
245             2L * 31 * 24 * 60 * 60 * 1000;
246 
247     private static final String[] sCalendarsIdProjection = new String[] { Calendars._ID };
248     private static final int CALENDARS_INDEX_ID = 0;
249 
250     // Allocate the string constant once here instead of on the heap
251     private static final String CALENDAR_ID_SELECTION = "calendar_id=?";
252 
253     private static final String[] sInstancesProjection =
254             new String[] { Instances.START_DAY, Instances.END_DAY,
255                     Instances.START_MINUTE, Instances.END_MINUTE, Instances.ALL_DAY };
256 
257     private static final int INSTANCES_INDEX_START_DAY = 0;
258     private static final int INSTANCES_INDEX_END_DAY = 1;
259     private static final int INSTANCES_INDEX_START_MINUTE = 2;
260     private static final int INSTANCES_INDEX_END_MINUTE = 3;
261     private static final int INSTANCES_INDEX_ALL_DAY = 4;
262 
263     private static final String[] sBusyBitProjection = new String[] {
264             BusyBits.DAY, BusyBits.BUSYBITS, BusyBits.ALL_DAY_COUNT };
265 
266     private static final int BUSYBIT_INDEX_DAY = 0;
267     private static final int BUSYBIT_INDEX_BUSYBITS= 1;
268     private static final int BUSYBIT_INDEX_ALL_DAY_COUNT = 2;
269 
270     private CalendarClient mCalendarClient = null;
271 
272     private AlarmManager mAlarmManager;
273 
274     private CalendarAppWidgetProvider mAppWidgetProvider = CalendarAppWidgetProvider.getInstance();
275 
276     /**
277      * Listens for timezone changes and disk-no-longer-full events
278      */
279     private BroadcastReceiver mIntentReceiver = new BroadcastReceiver() {
280         @Override
281         public void onReceive(Context context, Intent intent) {
282             String action = intent.getAction();
283             if (Log.isLoggable(TAG, Log.DEBUG)) {
284                 Log.d(TAG, "onReceive() " + action);
285             }
286             if (Intent.ACTION_TIMEZONE_CHANGED.equals(action)) {
287                 updateTimezoneDependentFields();
288                 scheduleNextAlarm(false /* do not remove alarms */);
289             } else if (Intent.ACTION_DEVICE_STORAGE_OK.equals(action)) {
290                 // Try to clean up if things were screwy due to a full disk
291                 updateTimezoneDependentFields();
292                 scheduleNextAlarm(false /* do not remove alarms */);
293             } else if (Intent.ACTION_TIME_CHANGED.equals(action)) {
294                 scheduleNextAlarm(false /* do not remove alarms */);
295             }
296         }
297     };
298 
CalendarProvider()299     public CalendarProvider() {
300         super(DATABASE_NAME, DATABASE_VERSION, Calendars.CONTENT_URI);
301     }
302 
303     @Override
onCreate()304     public boolean onCreate() {
305         super.onCreate();
306 
307         setTempProviderSyncAdapter(new CalendarSyncAdapter(getContext(), this));
308 
309         // Register for Intent broadcasts
310         IntentFilter filter = new IntentFilter();
311 
312         filter.addAction(Intent.ACTION_TIMEZONE_CHANGED);
313         filter.addAction(Intent.ACTION_DEVICE_STORAGE_OK);
314         filter.addAction(Intent.ACTION_TIME_CHANGED);
315         final Context c = getContext();
316 
317         // We don't ever unregister this because this thread always wants
318         // to receive notifications, even in the background.  And if this
319         // thread is killed then the whole process will be killed and the
320         // memory resources will be reclaimed.
321         c.registerReceiver(mIntentReceiver, filter);
322 
323         mMetaData = new MetaData(mOpenHelper);
324         updateTimezoneDependentFields();
325 
326         return true;
327     }
328 
329     /**
330      * This creates a background thread to check the timezone and update
331      * the timezone dependent fields in the Instances table if the timezone
332      * has changes.
333      */
updateTimezoneDependentFields()334     private void updateTimezoneDependentFields() {
335         Thread thread = new TimezoneCheckerThread();
336         thread.start();
337     }
338 
339     private class TimezoneCheckerThread extends Thread {
340         @Override
run()341         public void run() {
342             Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
343             try {
344                 doUpdateTimezoneDependentFields();
345             } catch (SQLException e) {
346                 Log.e(TAG, "doUpdateTimezoneDependentFields() failed", e);
347                 try {
348                     // Clear at least the in-memory data (and if possible the
349                     // database fields) to force a re-computation of Instances.
350                     mMetaData.clearInstanceRange();
351                 } catch (SQLException e2) {
352                     Log.e(TAG, "clearInstanceRange() also failed: " + e2);
353                 }
354             }
355         }
356     }
357 
358     /**
359      * This method runs in a background thread.  If the timezone has changed
360      * then the Instances table will be regenerated.
361      */
doUpdateTimezoneDependentFields()362     private void doUpdateTimezoneDependentFields() {
363         MetaData.Fields fields = mMetaData.getFields();
364         String localTimezone = TimeZone.getDefault().getID();
365         if (TextUtils.equals(fields.timezone, localTimezone)) {
366             // Even if the timezone hasn't changed, check for missed alarms.
367             // This code executes when the CalendarProvider is created and
368             // helps to catch missed alarms when the Calendar process is
369             // killed (because of low-memory conditions) and then restarted.
370             rescheduleMissedAlarms();
371             return;
372         }
373 
374         // The database timezone is different from the current timezone.
375         // Regenerate the Instances table for this month.  Include events
376         // starting at the beginning of this month.
377         long now = System.currentTimeMillis();
378         Time time = new Time();
379         time.set(now);
380         time.monthDay = 1;
381         time.hour = 0;
382         time.minute = 0;
383         time.second = 0;
384         long begin = time.normalize(true);
385         long end = begin + MINIMUM_EXPANSION_SPAN;
386         SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
387         handleInstanceQuery(qb, begin, end, new String[] { Instances._ID },
388                 null /* selection */, null /* sort */, false /* searchByDayInsteadOfMillis */);
389 
390         // Also pre-compute the BusyBits table for this month.
391         int startDay = Time.getJulianDay(begin, time.gmtoff);
392         int endDay = startDay + 31;
393         qb = new SQLiteQueryBuilder();
394         handleBusyBitsQuery(qb, startDay, endDay, sBusyBitProjection,
395                 null /* selection */, null /* sort */);
396         rescheduleMissedAlarms();
397     }
398 
rescheduleMissedAlarms()399     private void rescheduleMissedAlarms() {
400         AlarmManager manager = getAlarmManager();
401         if (manager != null) {
402             Context context = getContext();
403             ContentResolver cr = context.getContentResolver();
404             CalendarAlerts.rescheduleMissedAlarms(cr, context, manager);
405         }
406     }
407 
408     @Override
onDatabaseOpened(SQLiteDatabase db)409     protected void onDatabaseOpened(SQLiteDatabase db) {
410         db.markTableSyncable("Events", "DeletedEvents");
411 
412         if (!isTemporary()) {
413             mCalendarClient = new CalendarClient(
414                     new AndroidGDataClient(getContext(), CalendarSyncAdapter.USER_AGENT_APP_VERSION),
415                     new XmlCalendarGDataParserFactory(
416                             new AndroidXmlParserFactory()));
417         }
418 
419         mCalendarsInserter = new DatabaseUtils.InsertHelper(db, "Calendars");
420         mEventsInserter = new DatabaseUtils.InsertHelper(db, "Events");
421         mEventsRawTimesInserter = new DatabaseUtils.InsertHelper(db, "EventsRawTimes");
422         mDeletedEventsInserter = new DatabaseUtils.InsertHelper(db, "DeletedEvents");
423         mInstancesInserter = new DatabaseUtils.InsertHelper(db, "Instances");
424         mAttendeesInserter = new DatabaseUtils.InsertHelper(db, "Attendees");
425         mRemindersInserter = new DatabaseUtils.InsertHelper(db, "Reminders");
426         mCalendarAlertsInserter = new DatabaseUtils.InsertHelper(db, "CalendarAlerts");
427         mExtendedPropertiesInserter =
428                 new DatabaseUtils.InsertHelper(db, "ExtendedProperties");
429     }
430 
431     @Override
upgradeDatabase(SQLiteDatabase db, int oldVersion, int newVersion)432     protected boolean upgradeDatabase(SQLiteDatabase db, int oldVersion, int newVersion) {
433         Log.i(TAG, "Upgrading DB from version " + oldVersion
434                 + " to " + newVersion);
435         if (oldVersion < 46) {
436             dropTables(db);
437             bootstrapDatabase(db);
438             return false; // this was lossy
439         }
440 
441         if (oldVersion == 46) {
442             Log.w(TAG, "Upgrading CalendarAlerts table");
443             db.execSQL("UPDATE CalendarAlerts SET reminder_id=NULL;");
444             db.execSQL("ALTER TABLE CalendarAlerts ADD COLUMN minutes INTEGER DEFAULT 0;");
445             oldVersion += 1;
446         }
447 
448         if (oldVersion == 47) {
449             // Changing to version 48 was intended to force a data wipe
450             dropTables(db);
451             bootstrapDatabase(db);
452             return false; // this was lossy
453         }
454 
455         if (oldVersion == 48) {
456             // Changing to version 49 was intended to force a data wipe
457             dropTables(db);
458             bootstrapDatabase(db);
459             return false; // this was lossy
460         }
461 
462         if (oldVersion == 49) {
463             Log.w(TAG, "Upgrading DeletedEvents table");
464 
465             // We don't have enough information to fill in the correct
466             // value of the calendar_id for old rows in the DeletedEvents
467             // table, but rows in that table are transient so it is unlikely
468             // that there are any rows.  Plus, the calendar_id is used only
469             // when deleting a calendar, which is a rare event.  All new rows
470             // will have the correct calendar_id.
471             db.execSQL("ALTER TABLE DeletedEvents ADD COLUMN calendar_id INTEGER;");
472 
473             // Trigger to remove a calendar's events when we delete the calendar
474             db.execSQL("DROP TRIGGER IF EXISTS calendar_cleanup");
475             db.execSQL("CREATE TRIGGER calendar_cleanup DELETE ON Calendars " +
476                     "BEGIN " +
477                     "DELETE FROM Events WHERE calendar_id = old._id;" +
478                     "DELETE FROM DeletedEvents WHERE calendar_id = old._id;" +
479                     "END");
480             db.execSQL("DROP TRIGGER IF EXISTS event_to_deleted");
481             oldVersion += 1;
482         }
483 
484         if (oldVersion == 50) {
485             // This should have been deleted in the upgrade from version 49
486             // but we missed it.
487             db.execSQL("DROP TRIGGER IF EXISTS event_to_deleted");
488             oldVersion += 1;
489         }
490 
491         if (oldVersion == 51) {
492             // We added "originalAllDay" to the Events table to keep track of
493             // the allDay status of the original recurring event for entries
494             // that are exceptions to that recurring event.  We need this so
495             // that we can format the date correctly for the "originalInstanceTime"
496             // column when we make a change to the recurrence exception and
497             // send it to the server.
498             db.execSQL("ALTER TABLE Events ADD COLUMN originalAllDay INTEGER;");
499 
500             // Iterate through the Events table and for each recurrence
501             // exception, fill in the correct value for "originalAllDay",
502             // if possible.  The only times where this might not be possible
503             // are (1) the original recurring event no longer exists, or
504             // (2) the original recurring event does not yet have a _sync_id
505             // because it was created on the phone and hasn't been synced to the
506             // server yet.  In both cases the originalAllDay field will be set
507             // to null.  In the first case we don't care because the recurrence
508             // exception will not be displayed and we won't be able to make
509             // any changes to it (and even if we did, the server should ignore
510             // them, right?).  In the second case, the calendar client already
511             // disallows making changes to an instance of a recurring event
512             // until the recurring event has been synced to the server so the
513             // second case should never occur.
514 
515             // "cursor" iterates over all the recurrences exceptions.
516             Cursor cursor = db.rawQuery("SELECT _id,originalEvent FROM Events"
517                     + " WHERE originalEvent IS NOT NULL", null /* selection args */);
518             if (cursor != null) {
519                 try {
520                     while (cursor.moveToNext()) {
521                         long id = cursor.getLong(0);
522                         String originalEvent = cursor.getString(1);
523 
524                         // Find the original recurring event (if it exists)
525                         Cursor recur = db.rawQuery("SELECT allDay FROM Events"
526                                 + " WHERE _sync_id=?", new String[] {originalEvent});
527                         if (recur == null) {
528                             continue;
529                         }
530 
531                         try {
532                             // Fill in the "originalAllDay" field of the
533                             // recurrence exception with the "allDay" value
534                             // from the recurring event.
535                             if (recur.moveToNext()) {
536                                 int allDay = recur.getInt(0);
537                                 db.execSQL("UPDATE Events SET originalAllDay=" + allDay
538                                         + " WHERE _id="+id);
539                             }
540                         } finally {
541                             recur.close();
542                         }
543                     }
544                 } finally {
545                     cursor.close();
546                 }
547             }
548             oldVersion += 1;
549         }
550 
551         if (oldVersion == 52) {
552             Log.w(TAG, "Upgrading CalendarAlerts table");
553             db.execSQL("ALTER TABLE CalendarAlerts ADD COLUMN creationTime INTEGER DEFAULT 0;");
554             db.execSQL("ALTER TABLE CalendarAlerts ADD COLUMN receivedTime INTEGER DEFAULT 0;");
555             db.execSQL("ALTER TABLE CalendarAlerts ADD COLUMN notifyTime INTEGER DEFAULT 0;");
556             oldVersion += 1;
557         }
558 
559         if (oldVersion == 53) {
560             Log.w(TAG, "adding eventSyncAccountAndIdIndex");
561             db.execSQL("CREATE INDEX eventSyncAccountAndIdIndex ON Events ("
562                     + Events._SYNC_ACCOUNT + ", " + Events._SYNC_ID + ");");
563             oldVersion += 1;
564         }
565 
566         if (oldVersion == 54) {
567             db.execSQL("ALTER TABLE Calendars ADD COLUMN _sync_account_type TEXT;");
568             db.execSQL("ALTER TABLE Events ADD COLUMN _sync_account_type TEXT;");
569             db.execSQL("ALTER TABLE DeletedEvents ADD COLUMN _sync_account_type TEXT;");
570             db.execSQL("UPDATE Calendars"
571                     + " SET _sync_account_type='com.google'"
572                     + " WHERE _sync_account IS NOT NULL");
573             db.execSQL("UPDATE Events"
574                     + " SET _sync_account_type='com.google'"
575                     + " WHERE _sync_account IS NOT NULL");
576             db.execSQL("UPDATE DeletedEvents"
577                     + " SET _sync_account_type='com.google'"
578                     + " WHERE _sync_account IS NOT NULL");
579             Log.w(TAG, "re-creating eventSyncAccountAndIdIndex");
580             db.execSQL("DROP INDEX eventSyncAccountAndIdIndex");
581             db.execSQL("CREATE INDEX eventSyncAccountAndIdIndex ON Events ("
582                     + Events._SYNC_ACCOUNT_TYPE + ", " + Events._SYNC_ACCOUNT + ", "
583                     + Events._SYNC_ID + ");");
584             oldVersion += 1;
585         }
586         if (oldVersion == 55 || oldVersion == 56) {  // Both require resync
587             // Delete sync state, so all records will be re-synced.
588             db.execSQL("DELETE FROM _sync_state;");
589 
590             // "cursor" iterates over all the calendars
591             Cursor cursor = db.rawQuery("SELECT _sync_account,_sync_account_type,url "
592                     + "FROM Calendars",
593                     null /* selection args */);
594             if (cursor != null) {
595                 try {
596                     while (cursor.moveToNext()) {
597                         String accountName = cursor.getString(0);
598                         String accountType = cursor.getString(1);
599                         final Account account = new Account(accountName, accountType);
600                         String calendarUrl = cursor.getString(2);
601                         scheduleSync(account, false /* two-way sync */, calendarUrl);
602                     }
603                 } finally {
604                     cursor.close();
605                 }
606             }
607         }
608         if (oldVersion == 55) {
609             db.execSQL("ALTER TABLE Calendars ADD COLUMN ownerAccount TEXT;");
610             db.execSQL("ALTER TABLE Events ADD COLUMN hasAttendeeData INTEGER;");
611             // Clear _sync_dirty to avoid a client-to-server sync that could blow away
612             // server attendees.
613             // Clear _sync_version to pull down the server's event (with attendees)
614             // Change the URLs from full-selfattendance to full
615             db.execSQL("UPDATE Events"
616                     + " SET _sync_dirty=0,"
617                     + " _sync_version=NULL,"
618                     + " _sync_id="
619                     + "REPLACE(_sync_id, '/private/full-selfattendance', '/private/full'),"
620                     + " commentsUri ="
621                     + "REPLACE(commentsUri, '/private/full-selfattendance', '/private/full');");
622             db.execSQL("UPDATE Calendars"
623                     + " SET url="
624                     + "REPLACE(url, '/private/full-selfattendance', '/private/full');");
625 
626             // "cursor" iterates over all the calendars
627             Cursor cursor = db.rawQuery("SELECT _id, url FROM Calendars",
628                     null /* selection args */);
629             // Add the owner column.
630             if (cursor != null) {
631                 try {
632                     while (cursor.moveToNext()) {
633                         Long id = cursor.getLong(0);
634                         String url = cursor.getString(1);
635                         String owner = CalendarSyncAdapter.calendarEmailAddressFromFeedUrl(url);
636                         db.execSQL("UPDATE Calendars SET ownerAccount=? WHERE _id=?",
637                                 new Object[] {owner, id});
638                     }
639                 } finally {
640                     cursor.close();
641                 }
642             }
643             oldVersion += 1;
644         }
645         if (oldVersion == 56) {
646             db.execSQL("ALTER TABLE Events ADD COLUMN guestsCanModify"
647                     + " INTEGER NOT NULL DEFAULT 0;");
648             db.execSQL("ALTER TABLE Events ADD COLUMN guestsCanInviteOthers"
649                     + " INTEGER NOT NULL DEFAULT 1;");
650             db.execSQL("ALTER TABLE Events ADD COLUMN guestsCanSeeGuests"
651                     + " INTEGER NOT NULL DEFAULT 1;");
652             db.execSQL("ALTER TABLE Events ADD COLUMN organizer STRING;");
653             db.execSQL("UPDATE Events SET organizer="
654                     + "(SELECT attendeeEmail FROM Attendees WHERE "
655                     + "Attendees.event_id = Events._id AND Attendees.attendeeRelationship=2);");
656 
657 
658             oldVersion += 1;
659         }
660 
661         return true; // this was lossless
662     }
663 
dropTables(SQLiteDatabase db)664     private void dropTables(SQLiteDatabase db) {
665         db.execSQL("DROP TABLE IF EXISTS Calendars;");
666         db.execSQL("DROP TABLE IF EXISTS Events;");
667         db.execSQL("DROP TABLE IF EXISTS EventsRawTimes;");
668         db.execSQL("DROP TABLE IF EXISTS DeletedEvents;");
669         db.execSQL("DROP TABLE IF EXISTS Instances;");
670         db.execSQL("DROP TABLE IF EXISTS CalendarMetaData;");
671         db.execSQL("DROP TABLE IF EXISTS BusyBits;");
672         db.execSQL("DROP TABLE IF EXISTS Attendees;");
673         db.execSQL("DROP TABLE IF EXISTS Reminders;");
674         db.execSQL("DROP TABLE IF EXISTS CalendarAlerts;");
675         db.execSQL("DROP TABLE IF EXISTS ExtendedProperties;");
676     }
677 
678     @Override
bootstrapDatabase(SQLiteDatabase db)679     protected void bootstrapDatabase(SQLiteDatabase db) {
680         super.bootstrapDatabase(db);
681         db.execSQL("CREATE TABLE Calendars (" +
682                 "_id INTEGER PRIMARY KEY," +
683                 "_sync_account TEXT," +
684                 "_sync_account_type TEXT," +
685                 "_sync_id TEXT," +
686                 "_sync_version TEXT," +
687                 "_sync_time TEXT," +            // UTC
688                 "_sync_local_id INTEGER," +
689                 "_sync_dirty INTEGER," +
690                 "_sync_mark INTEGER," + // Used to filter out new rows
691                 "url TEXT," +
692                 "name TEXT," +
693                 "displayName TEXT," +
694                 "hidden INTEGER NOT NULL DEFAULT 0," +
695                 "color INTEGER," +
696                 "access_level INTEGER," +
697                 "selected INTEGER NOT NULL DEFAULT 1," +
698                 "sync_events INTEGER NOT NULL DEFAULT 0," +
699                 "location TEXT," +
700                 "timezone TEXT," +
701                 "ownerAccount TEXT" +
702                 ");");
703 
704         // Trigger to remove a calendar's events when we delete the calendar
705         db.execSQL("CREATE TRIGGER calendar_cleanup DELETE ON Calendars " +
706                 "BEGIN " +
707                 "DELETE FROM Events WHERE calendar_id = old._id;" +
708                 "DELETE FROM DeletedEvents WHERE calendar_id = old._id;" +
709                 "END");
710 
711         // TODO: do we need both dtend and duration?
712         db.execSQL("CREATE TABLE Events (" +
713                 "_id INTEGER PRIMARY KEY," +
714                 "_sync_account TEXT," +
715                 "_sync_account_type TEXT," +
716                 "_sync_id TEXT," +
717                 "_sync_version TEXT," +
718                 "_sync_time TEXT," +            // UTC
719                 "_sync_local_id INTEGER," +
720                 "_sync_dirty INTEGER," +
721                 "_sync_mark INTEGER," + // To filter out new rows
722                 "calendar_id INTEGER NOT NULL," +
723                 "htmlUri TEXT," +
724                 "title TEXT," +
725                 "eventLocation TEXT," +
726                 "description TEXT," +
727                 "eventStatus INTEGER," +
728                 "selfAttendeeStatus INTEGER NOT NULL DEFAULT 0," +
729                 "commentsUri TEXT," +
730                 "dtstart INTEGER," +               // millis since epoch
731                 "dtend INTEGER," +                 // millis since epoch
732                 "eventTimezone TEXT," +         // timezone for event
733                 "duration TEXT," +
734                 "allDay INTEGER NOT NULL DEFAULT 0," +
735                 "visibility INTEGER NOT NULL DEFAULT 0," +
736                 "transparency INTEGER NOT NULL DEFAULT 0," +
737                 "hasAlarm INTEGER NOT NULL DEFAULT 0," +
738                 "hasExtendedProperties INTEGER NOT NULL DEFAULT 0," +
739                 "rrule TEXT," +
740                 "rdate TEXT," +
741                 "exrule TEXT," +
742                 "exdate TEXT," +
743                 "originalEvent TEXT," +  // _sync_id of recurring event
744                 "originalInstanceTime INTEGER," +  // millis since epoch
745                 "originalAllDay INTEGER," +
746                 "lastDate INTEGER," +               // millis since epoch
747                 "hasAttendeeData INTEGER NOT NULL DEFAULT 0," +
748                 "guestsCanModify INTEGER NOT NULL DEFAULT 0," +
749                 "guestsCanInviteOthers INTEGER NOT NULL DEFAULT 1," +
750                 "guestsCanSeeGuests INTEGER NOT NULL DEFAULT 1," +
751                 "organizer STRING" +
752                 ");");
753 
754         db.execSQL("CREATE INDEX eventSyncAccountAndIdIndex ON Events ("
755                 + Events._SYNC_ACCOUNT_TYPE + ", " + Events._SYNC_ACCOUNT + ", "
756                 + Events._SYNC_ID + ");");
757 
758         db.execSQL("CREATE INDEX eventsCalendarIdIndex ON Events (" +
759                 Events.CALENDAR_ID +
760                 ");");
761 
762         db.execSQL("CREATE TABLE EventsRawTimes (" +
763                 "_id INTEGER PRIMARY KEY," +
764                 "event_id INTEGER NOT NULL," +
765                 "dtstart2445 TEXT," +
766                 "dtend2445 TEXT," +
767                 "originalInstanceTime2445 TEXT," +
768                 "lastDate2445 TEXT," +
769                 "UNIQUE (event_id)" +
770                 ");");
771 
772         // NOTE: we do not create a trigger to delete an event's instances upon update,
773         // as all rows currently get updated during a merge.
774 
775         db.execSQL("CREATE TABLE DeletedEvents (" +
776                 "_sync_id TEXT," +
777                 "_sync_version TEXT," +
778                 "_sync_account TEXT," +
779                 "_sync_account_type TEXT," +
780                 (isTemporary() ? "_sync_local_id INTEGER," : "") + // Used while syncing,
781                 "_sync_mark INTEGER," + // To filter out new rows
782                 "calendar_id INTEGER" +
783                 ");");
784 
785         db.execSQL("CREATE TABLE Instances (" +
786                 "_id INTEGER PRIMARY KEY," +
787                 "event_id INTEGER," +
788                 "begin INTEGER," +         // UTC millis
789                 "end INTEGER," +           // UTC millis
790                 "startDay INTEGER," +      // Julian start day
791                 "endDay INTEGER," +        // Julian end day
792                 "startMinute INTEGER," +   // minutes from midnight
793                 "endMinute INTEGER," +     // minutes from midnight
794                 "UNIQUE (event_id, begin, end)" +
795                 ");");
796 
797         db.execSQL("CREATE INDEX instancesStartDayIndex ON Instances (" +
798                 Instances.START_DAY +
799                 ");");
800 
801         db.execSQL("CREATE TABLE CalendarMetaData (" +
802                 "_id INTEGER PRIMARY KEY," +
803                 "localTimezone TEXT," +
804                 "minInstance INTEGER," +      // UTC millis
805                 "maxInstance INTEGER," +      // UTC millis
806                 "minBusyBits INTEGER," +      // UTC millis
807                 "maxBusyBits INTEGER" +       // UTC millis
808                 ");");
809 
810         db.execSQL("CREATE TABLE BusyBits(" +
811                 "day INTEGER PRIMARY KEY," +  // the Julian day
812                 "busyBits INTEGER," +         // 24 bits for 60-minute intervals
813                 "allDayCount INTEGER" +       // number of all-day events
814                 ");");
815 
816         db.execSQL("CREATE TABLE Attendees (" +
817                 "_id INTEGER PRIMARY KEY," +
818                 "event_id INTEGER," +
819                 "attendeeName TEXT," +
820                 "attendeeEmail TEXT," +
821                 "attendeeStatus INTEGER," +
822                 "attendeeRelationship INTEGER," +
823                 "attendeeType INTEGER" +
824                 ");");
825 
826         db.execSQL("CREATE INDEX attendeesEventIdIndex ON Attendees (" +
827                 Attendees.EVENT_ID +
828                 ");");
829 
830         db.execSQL("CREATE TABLE Reminders (" +
831                 "_id INTEGER PRIMARY KEY," +
832                 "event_id INTEGER," +
833                 "minutes INTEGER," +
834                 "method INTEGER NOT NULL" +
835                 " DEFAULT " + Reminders.METHOD_DEFAULT +
836                 ");");
837 
838         db.execSQL("CREATE INDEX remindersEventIdIndex ON Reminders (" +
839                 Reminders.EVENT_ID +
840                 ");");
841 
842         // This table stores the Calendar notifications that have gone off.
843         db.execSQL("CREATE TABLE CalendarAlerts (" +
844                 "_id INTEGER PRIMARY KEY," +
845                 "event_id INTEGER," +
846                 "begin INTEGER NOT NULL," +         // UTC millis
847                 "end INTEGER NOT NULL," +           // UTC millis
848                 "alarmTime INTEGER NOT NULL," +     // UTC millis
849                 "creationTime INTEGER NOT NULL," +  // UTC millis
850                 "receivedTime INTEGER NOT NULL," +  // UTC millis
851                 "notifyTime INTEGER NOT NULL," +    // UTC millis
852                 "state INTEGER NOT NULL," +
853                 "minutes INTEGER," +
854                 "UNIQUE (alarmTime, begin, event_id)" +
855                 ");");
856 
857         db.execSQL("CREATE INDEX calendarAlertsEventIdIndex ON CalendarAlerts (" +
858                 CalendarAlerts.EVENT_ID +
859                 ");");
860 
861         db.execSQL("CREATE TABLE ExtendedProperties (" +
862                 "_id INTEGER PRIMARY KEY," +
863                 "event_id INTEGER," +
864                 "name TEXT," +
865                 "value TEXT" +
866                 ");");
867 
868         db.execSQL("CREATE INDEX extendedPropertiesEventIdIndex ON ExtendedProperties (" +
869                 ExtendedProperties.EVENT_ID +
870                 ");");
871 
872         // Trigger to remove data tied to an event when we delete that event.
873         db.execSQL("CREATE TRIGGER events_cleanup_delete DELETE ON Events " +
874                 "BEGIN " +
875                 "DELETE FROM Instances WHERE event_id = old._id;" +
876                 "DELETE FROM EventsRawTimes WHERE event_id = old._id;" +
877                 "DELETE FROM Attendees WHERE event_id = old._id;" +
878                 "DELETE FROM Reminders WHERE event_id = old._id;" +
879                 "DELETE FROM CalendarAlerts WHERE event_id = old._id;" +
880                 "DELETE FROM ExtendedProperties WHERE event_id = old._id;" +
881                 "END");
882 
883         // Triggers to set the _sync_dirty flag when an attendee is changed,
884         // inserted or deleted
885         db.execSQL("CREATE TRIGGER attendees_update UPDATE ON Attendees " +
886                 "BEGIN " +
887                 "UPDATE Events SET _sync_dirty=1 WHERE Events._id=old.event_id;" +
888                 "END");
889         db.execSQL("CREATE TRIGGER attendees_insert INSERT ON Attendees " +
890                 "BEGIN " +
891                 "UPDATE Events SET _sync_dirty=1 WHERE Events._id=new.event_id;" +
892                 "END");
893         db.execSQL("CREATE TRIGGER attendees_delete DELETE ON Attendees " +
894                 "BEGIN " +
895                 "UPDATE Events SET _sync_dirty=1 WHERE Events._id=old.event_id;" +
896                 "END");
897 
898         // Triggers to set the _sync_dirty flag when a reminder is changed,
899         // inserted or deleted
900         db.execSQL("CREATE TRIGGER reminders_update UPDATE ON Reminders " +
901                 "BEGIN " +
902                 "UPDATE Events SET _sync_dirty=1 WHERE Events._id=old.event_id;" +
903                 "END");
904         db.execSQL("CREATE TRIGGER reminders_insert INSERT ON Reminders " +
905                 "BEGIN " +
906                 "UPDATE Events SET _sync_dirty=1 WHERE Events._id=new.event_id;" +
907                 "END");
908         db.execSQL("CREATE TRIGGER reminders_delete DELETE ON Reminders " +
909                 "BEGIN " +
910                 "UPDATE Events SET _sync_dirty=1 WHERE Events._id=old.event_id;" +
911                 "END");
912         // Triggers to set the _sync_dirty flag when an extended property is changed,
913         // inserted or deleted
914         db.execSQL("CREATE TRIGGER extended_properties_update UPDATE ON ExtendedProperties " +
915                 "BEGIN " +
916                 "UPDATE Events SET _sync_dirty=1 WHERE Events._id=old.event_id;" +
917                 "END");
918         db.execSQL("CREATE TRIGGER extended_properties_insert UPDATE ON ExtendedProperties " +
919                 "BEGIN " +
920                 "UPDATE Events SET _sync_dirty=1 WHERE Events._id=new.event_id;" +
921                 "END");
922         db.execSQL("CREATE TRIGGER extended_properties_delete UPDATE ON ExtendedProperties " +
923                 "BEGIN " +
924                 "UPDATE Events SET _sync_dirty=1 WHERE Events._id=old.event_id;" +
925                 "END");
926     }
927 
928     /**
929      * Make sure that there are no entries for accounts that no longer
930      * exist. We are overriding this since we need to delete from the
931      * Calendars table, which is not syncable, which has triggers that
932      * will delete from the Events and DeletedEvents tables, which are
933      * syncable.
934      */
935     @Override
onAccountsChanged(final Account[] accountsArray)936     protected void onAccountsChanged(final Account[] accountsArray) {
937         super.onAccountsChanged(accountsArray);
938 
939         final Map<Account, Boolean> accounts = Maps.newHashMap();
940         for (Account account : accountsArray) {
941             accounts.put(account, false);
942         }
943 
944         mDb.beginTransaction();
945         try {
946             deleteRowsForRemovedAccounts(accounts, "Calendars");
947             mDb.setTransactionSuccessful();
948         } finally {
949             mDb.endTransaction();
950         }
951 
952         if (mCalendarClient == null) {
953             return;
954         }
955 
956         // If we have calendars for unknown accounts, delete them.
957         // If there are no calendars at all for a given account, add the
958         // default calendar.
959 
960         // TODO: allow caller to specify which account's feeds should be updated
961         String[] features = new String[]{
962                 GoogleLoginServiceConstants.FEATURE_LEGACY_HOSTED_OR_GOOGLE};
963         AccountManagerCallback<Account[]> callback = new AccountManagerCallback<Account[]>() {
964             public void run(AccountManagerFuture<Account[]> accountManagerFuture) {
965                 Account[] currentAccounts = new Account[0];
966                 try {
967                     currentAccounts = accountManagerFuture.getResult();
968                 } catch (OperationCanceledException e) {
969                     Log.w(TAG, "onAccountsChanged", e);
970                     return;
971                 } catch (IOException e) {
972                     Log.w(TAG, "onAccountsChanged", e);
973                     return;
974                 } catch (AuthenticatorException e) {
975                     Log.w(TAG, "onAccountsChanged", e);
976                     return;
977                 }
978                 if (currentAccounts.length < 1) {
979                     Log.w(TAG, "getPrimaryAccount: no primary account configured.");
980                     return;
981                 }
982                 Account primaryAccount = currentAccounts[0];
983 
984                 for (Map.Entry<Account, Boolean> entry : accounts.entrySet()) {
985                     // TODO: change this when Calendar supports multiple accounts. Until then
986                     // pretend that only the primary exists.
987                     boolean ignore = primaryAccount == null ||
988                             !primaryAccount.equals(entry.getKey());
989                     entry.setValue(ignore);
990                 }
991 
992                 Set<Account> handledAccounts = Sets.newHashSet();
993                 if (Config.LOGV) Log.v(TAG, "querying calendars");
994                 Cursor c = queryInternal(Calendars.CONTENT_URI, ACCOUNTS_PROJECTION, null, null,
995                         null);
996                 try {
997                     while (c.moveToNext()) {
998                         final String accountName = c.getString(0);
999                         final String accountType = c.getString(1);
1000                         final Account account = new Account(accountName, accountType);
1001                         if (handledAccounts.contains(account)) {
1002                             continue;
1003                         }
1004                         handledAccounts.add(account);
1005                         if (accounts.containsKey(account)) {
1006                             if (Config.LOGV) {
1007                                 Log.v(TAG, "calendars for account " + account + " exist");
1008                             }
1009                             accounts.put(account, true /* hasCalendar */);
1010                         }
1011                     }
1012                 } finally {
1013                     c.close();
1014                     c = null;
1015                 }
1016 
1017                 if (Config.LOGV) {
1018                     Log.v(TAG, "scanning over " + accounts.size() + " account(s)");
1019                 }
1020                 for (Map.Entry<Account, Boolean> entry : accounts.entrySet()) {
1021                     final Account account = entry.getKey();
1022                     boolean hasCalendar = entry.getValue();
1023                     if (hasCalendar) {
1024                         if (Config.LOGV) {
1025                             Log.v(TAG, "ignoring account " + account +
1026                                     " since it matched an existing calendar");
1027                         }
1028                         continue;
1029                     }
1030                     String feedUrl = mCalendarClient.getDefaultCalendarUrl(account.name,
1031                             CalendarClient.PROJECTION_PRIVATE_FULL, null/* query params */);
1032                     feedUrl = CalendarSyncAdapter.rewriteUrlforAccount(account, feedUrl);
1033                     if (Config.LOGV) {
1034                         Log.v(TAG, "adding default calendar for account " + account);
1035                     }
1036                     ContentValues values = new ContentValues();
1037                     values.put(Calendars._SYNC_ACCOUNT, account.name);
1038                     values.put(Calendars._SYNC_ACCOUNT_TYPE, account.type);
1039                     values.put(Calendars.URL, feedUrl);
1040                     values.put(Calendars.OWNER_ACCOUNT,
1041                             CalendarSyncAdapter.calendarEmailAddressFromFeedUrl(feedUrl));
1042                     values.put(Calendars.DISPLAY_NAME,
1043                             getContext().getString(R.string.calendar_default_name));
1044                     values.put(Calendars.SYNC_EVENTS, 1);
1045                     values.put(Calendars.SELECTED, 1);
1046                     values.put(Calendars.HIDDEN, 0);
1047                     values.put(Calendars.COLOR, -14069085 /* blue */);
1048                     // this is just our best guess.  the real value will get updated
1049                     // when the user does a sync.
1050                     values.put(Calendars.TIMEZONE, Time.getCurrentTimezone());
1051                     values.put(Calendars.ACCESS_LEVEL, Calendars.OWNER_ACCESS);
1052                     insertInternal(Calendars.CONTENT_URI, values);
1053 
1054                     scheduleSync(account, false /* do a full sync */, null /* no url */);
1055 
1056                 }
1057                 // Call the CalendarSyncAdapter's onAccountsChanged
1058                 getTempProviderSyncAdapter().onAccountsChanged(accountsArray);
1059             }
1060         };
1061 
1062         AccountManager.get(getContext()).getAccountsByTypeAndFeatures(
1063                 GoogleLoginServiceConstants.ACCOUNT_TYPE, features, callback, null);
1064     }
1065 
1066     @Override
queryInternal(Uri url, String[] projectionIn, String selection, String[] selectionArgs, String sort)1067     public Cursor queryInternal(Uri url, String[] projectionIn,
1068             String selection, String[] selectionArgs, String sort) {
1069         final SQLiteDatabase db = getDatabase();
1070         SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
1071 
1072         Cursor ret;
1073 
1074         // Generate the body of the query
1075         int match = sURLMatcher.match(url);
1076         switch (match)
1077         {
1078             case EVENTS:
1079                 qb.setTables("Events, Calendars");
1080                 qb.setProjectionMap(sEventsProjectionMap);
1081                 qb.appendWhere("Events.calendar_id=Calendars._id");
1082                 break;
1083             case EVENTS_ID:
1084                 qb.setTables("Events, Calendars");
1085                 qb.setProjectionMap(sEventsProjectionMap);
1086                 qb.appendWhere("Events.calendar_id=Calendars._id");
1087                 qb.appendWhere(" AND Events._id=");
1088                 qb.appendWhere(url.getPathSegments().get(1));
1089                 break;
1090             case DELETED_EVENTS:
1091                 if (isTemporary()) {
1092                     qb.setTables("DeletedEvents");
1093                     break;
1094                 } else {
1095                     throw new IllegalArgumentException("Unknown URL " + url);
1096                 }
1097             case CALENDARS:
1098                 qb.setTables("Calendars");
1099                 break;
1100             case CALENDARS_ID:
1101                 qb.setTables("Calendars");
1102                 qb.appendWhere("_id=");
1103                 qb.appendWhere(url.getPathSegments().get(1));
1104                 break;
1105             case INSTANCES:
1106             case INSTANCES_BY_DAY:
1107                 long begin;
1108                 long end;
1109                 try {
1110                     begin = Long.valueOf(url.getPathSegments().get(2));
1111                 } catch (NumberFormatException nfe) {
1112                     throw new IllegalArgumentException("Cannot parse begin "
1113                             + url.getPathSegments().get(2));
1114                 }
1115                 try {
1116                     end = Long.valueOf(url.getPathSegments().get(3));
1117                 } catch (NumberFormatException nfe) {
1118                     throw new IllegalArgumentException("Cannot parse end "
1119                             + url.getPathSegments().get(3));
1120                 }
1121                 return handleInstanceQuery(qb, begin, end, projectionIn,
1122                         selection, sort, match == INSTANCES_BY_DAY);
1123             case BUSYBITS:
1124                 int startDay;
1125                 int endDay;
1126                 try {
1127                     startDay = Integer.valueOf(url.getPathSegments().get(2));
1128                 } catch (NumberFormatException nfe) {
1129                     throw new IllegalArgumentException("Cannot parse start day "
1130                             + url.getPathSegments().get(2));
1131                 }
1132                 try {
1133                     endDay = Integer.valueOf(url.getPathSegments().get(3));
1134                 } catch (NumberFormatException nfe) {
1135                     throw new IllegalArgumentException("Cannot parse end day "
1136                             + url.getPathSegments().get(3));
1137                 }
1138                 return handleBusyBitsQuery(qb, startDay, endDay, projectionIn,
1139                         selection, sort);
1140             case ATTENDEES:
1141                 qb.setTables("Attendees, Events, Calendars");
1142                 qb.setProjectionMap(sAttendeesProjectionMap);
1143                 qb.appendWhere("Events.calendar_id=Calendars._id");
1144                 qb.appendWhere(" AND Events._id=Attendees.event_id");
1145                 break;
1146             case ATTENDEES_ID:
1147                 qb.setTables("Attendees, Events, Calendars");
1148                 qb.setProjectionMap(sAttendeesProjectionMap);
1149                 qb.appendWhere("Attendees._id=");
1150                 qb.appendWhere(url.getPathSegments().get(1));
1151                 qb.appendWhere(" AND Events.calendar_id=Calendars._id");
1152                 qb.appendWhere(" AND Events._id=Attendees.event_id");
1153                 break;
1154             case REMINDERS:
1155                 qb.setTables("Reminders");
1156                 break;
1157             case REMINDERS_ID:
1158                 qb.setTables("Reminders, Events, Calendars");
1159                 qb.setProjectionMap(sRemindersProjectionMap);
1160                 qb.appendWhere("Reminders._id=");
1161                 qb.appendWhere(url.getLastPathSegment());
1162                 qb.appendWhere(" AND Events.calendar_id=Calendars._id");
1163                 qb.appendWhere(" AND Events._id=Reminders.event_id");
1164                 break;
1165             case CALENDAR_ALERTS:
1166                 qb.setTables("CalendarAlerts, Events, Calendars");
1167                 qb.setProjectionMap(sCalendarAlertsProjectionMap);
1168                 qb.appendWhere("Events.calendar_id=Calendars._id");
1169                 qb.appendWhere(" AND Events._id=CalendarAlerts.event_id");
1170                 break;
1171             case CALENDAR_ALERTS_BY_INSTANCE:
1172                 qb.setTables("CalendarAlerts, Events, Calendars");
1173                 qb.setProjectionMap(sCalendarAlertsProjectionMap);
1174                 qb.appendWhere("Events.calendar_id=Calendars._id");
1175                 qb.appendWhere(" AND Events._id=CalendarAlerts.event_id");
1176                 String groupBy = CalendarAlerts.EVENT_ID + "," + CalendarAlerts.BEGIN;
1177                 return qb.query(db, projectionIn, selection, selectionArgs,
1178                         groupBy, null, sort);
1179             case CALENDAR_ALERTS_ID:
1180                 qb.setTables("CalendarAlerts, Events, Calendars");
1181                 qb.setProjectionMap(sCalendarAlertsProjectionMap);
1182                 qb.appendWhere("CalendarAlerts._id=");
1183                 qb.appendWhere(url.getLastPathSegment());
1184                 qb.appendWhere(" AND Events.calendar_id=Calendars._id");
1185                 qb.appendWhere(" AND Events._id=CalendarAlerts.event_id");
1186                 break;
1187             case EXTENDED_PROPERTIES:
1188                 qb.setTables("ExtendedProperties");
1189                 break;
1190             case EXTENDED_PROPERTIES_ID:
1191                 qb.setTables("ExtendedProperties, Events, Calendars");
1192                 // not sure if we need a projection map or a join.  see what callers want.
1193 //                qb.setProjectionMap(sExtendedPropertiesProjectionMap);
1194                 qb.appendWhere("ExtendedProperties._id=");
1195                 qb.appendWhere(url.getPathSegments().get(1));
1196 //                qb.appendWhere(" AND Events.calendar_id = Calendars._id");
1197 //                qb.appendWhere(" AND Events._id=ExtendedProperties.event_id");
1198                 break;
1199 
1200             default:
1201                 throw new IllegalArgumentException("Unknown URL " + url);
1202         }
1203 
1204         // run the query
1205         ret = qb.query(db, projectionIn, selection, selectionArgs, null, null, sort);
1206 
1207         return ret;
1208     }
1209 
1210     /*
1211      * Fills the Instances table, if necessary, for the given range and then
1212      * queries the Instances table.
1213      *
1214      * @param qb The query
1215      * @param rangeBegin start of range (Julian days or ms)
1216      * @param rangeEnd end of range (Julian days or ms)
1217      * @param projectionIn The projection
1218      * @param selection The selection
1219      * @param sort How to sort
1220      * @param searchByDay if true, range is in Julian days, if false, range is in ms
1221      * @return
1222      */
handleInstanceQuery(SQLiteQueryBuilder qb, long rangeBegin, long rangeEnd, String[] projectionIn, String selection, String sort, boolean searchByDay)1223     private Cursor handleInstanceQuery(SQLiteQueryBuilder qb, long rangeBegin,
1224             long rangeEnd, String[] projectionIn,
1225             String selection, String sort, boolean searchByDay) {
1226         final SQLiteDatabase db = getDatabase();
1227 
1228         qb.setTables("Instances INNER JOIN Events ON (Instances.event_id=Events._id) " +
1229                 "INNER JOIN Calendars ON (Events.calendar_id = Calendars._id)");
1230         qb.setProjectionMap(sInstancesProjectionMap);
1231         if (searchByDay) {
1232             // Convert the first and last Julian day range to a range that uses
1233             // UTC milliseconds.
1234             Time time = new Time();
1235             long beginMs = time.setJulianDay((int) rangeBegin);
1236             // We add one to lastDay because the time is set to 12am on the given
1237             // Julian day and we want to include all the events on the last day.
1238             long endMs = time.setJulianDay((int) rangeEnd + 1);
1239             // will lock the database.
1240             acquireInstanceRange(beginMs, endMs, true /* use minimum expansion window */);
1241             qb.appendWhere("startDay <= ");
1242             qb.appendWhere(String.valueOf(rangeEnd));
1243             qb.appendWhere(" AND endDay >= ");
1244         } else {
1245             // will lock the database.
1246             acquireInstanceRange(rangeBegin, rangeEnd, true /* use minimum expansion window */);
1247             qb.appendWhere("begin <= ");
1248             qb.appendWhere(String.valueOf(rangeEnd));
1249             qb.appendWhere(" AND end >= ");
1250         }
1251         qb.appendWhere(String.valueOf(rangeBegin));
1252         return qb.query(db, projectionIn, selection, null, null, null, sort);
1253     }
1254 
handleBusyBitsQuery(SQLiteQueryBuilder qb, int startDay, int endDay, String[] projectionIn, String selection, String sort)1255     private Cursor handleBusyBitsQuery(SQLiteQueryBuilder qb, int startDay,
1256             int endDay, String[] projectionIn,
1257             String selection, String sort) {
1258         final SQLiteDatabase db = getDatabase();
1259         acquireBusyBitRange(startDay, endDay);
1260         qb.setTables("BusyBits");
1261         qb.setProjectionMap(sBusyBitsProjectionMap);
1262         qb.appendWhere("day >= ");
1263         qb.appendWhere(String.valueOf(startDay));
1264         qb.appendWhere(" AND day <= ");
1265         qb.appendWhere(String.valueOf(endDay));
1266         return qb.query(db, projectionIn, selection, null, null, null, sort);
1267     }
1268 
1269     /**
1270      * Ensure that the date range given has all elements in the instance
1271      * table.  Acquires the database lock and calls {@link #acquireInstanceRangeLocked}.
1272      *
1273      * @param begin start of range (ms)
1274      * @param end end of range (ms)
1275      * @param useMinimumExpansionWindow expand by at least MINIMUM_EXPANSION_SPAN
1276      */
acquireInstanceRange(final long begin, final long end, final boolean useMinimumExpansionWindow)1277     private void acquireInstanceRange(final long begin,
1278             final long end,
1279             final boolean useMinimumExpansionWindow) {
1280         mDb.beginTransaction();
1281         try {
1282             acquireInstanceRangeLocked(begin, end, useMinimumExpansionWindow);
1283             mDb.setTransactionSuccessful();
1284         } finally {
1285             mDb.endTransaction();
1286         }
1287     }
1288 
1289     /**
1290      * Expands the Instances table (if needed) and the BusyBits table.
1291      * Acquires the database lock and calls {@link #acquireBusyBitRangeLocked}.
1292      */
acquireBusyBitRange(final int startDay, final int endDay)1293     private void acquireBusyBitRange(final int startDay, final int endDay) {
1294         mDb.beginTransaction();
1295         try {
1296             acquireBusyBitRangeLocked(startDay, endDay);
1297             mDb.setTransactionSuccessful();
1298         } finally {
1299             mDb.endTransaction();
1300         }
1301     }
1302 
1303     /**
1304      * Ensure that the date range given has all elements in the instance
1305      * table.  The database lock must be held when calling this method.
1306      *
1307      * @param begin start of range (ms)
1308      * @param end end of range (ms)
1309      * @param useMinimumExpansionWindow expand by at least MINIMUM_EXPANSION_SPAN
1310      */
acquireInstanceRangeLocked(long begin, long end, boolean useMinimumExpansionWindow)1311     private void acquireInstanceRangeLocked(long begin, long end,
1312             boolean useMinimumExpansionWindow) {
1313         long expandBegin = begin;
1314         long expandEnd = end;
1315 
1316         if (useMinimumExpansionWindow) {
1317             // if we end up having to expand events into the instances table, expand
1318             // events for a minimal amount of time, so we do not have to perform
1319             // expansions frequently.
1320             long span = end - begin;
1321             if (span < MINIMUM_EXPANSION_SPAN) {
1322                 long additionalRange = (MINIMUM_EXPANSION_SPAN - span) / 2;
1323                 expandBegin -= additionalRange;
1324                 expandEnd += additionalRange;
1325             }
1326         }
1327 
1328         // Check if the timezone has changed.
1329         // We do this check here because the database is locked and we can
1330         // safely delete all the entries in the Instances table.
1331         MetaData.Fields fields = mMetaData.getFieldsLocked();
1332         String dbTimezone = fields.timezone;
1333         long maxInstance = fields.maxInstance;
1334         long minInstance = fields.minInstance;
1335         String localTimezone = TimeZone.getDefault().getID();
1336         boolean timezoneChanged = (dbTimezone == null) || !dbTimezone.equals(localTimezone);
1337 
1338         if (maxInstance == 0 || timezoneChanged) {
1339             // Empty the Instances table and expand from scratch.
1340             mDb.execSQL("DELETE FROM Instances;");
1341             mDb.execSQL("DELETE FROM BusyBits;");
1342             if (Config.LOGV) {
1343                 Log.v(TAG, "acquireInstanceRangeLocked() deleted Instances and Busybits,"
1344                         + " timezone changed: " + timezoneChanged);
1345             }
1346             expandInstanceRangeLocked(expandBegin, expandEnd, localTimezone);
1347 
1348             mMetaData.writeLocked(localTimezone, expandBegin, expandEnd,
1349                     0 /* startDay */, 0 /* endDay */);
1350             return;
1351         }
1352 
1353         // If the desired range [begin, end] has already been
1354         // expanded, then simply return.  The range is inclusive, that is,
1355         // events that touch either endpoint are included in the expansion.
1356         // This means that a zero-duration event that starts and ends at
1357         // the endpoint will be included.
1358         // We use [begin, end] here and not [expandBegin, expandEnd] for
1359         // checking the range because a common case is for the client to
1360         // request successive days or weeks, for example.  If we checked
1361         // that the expanded range [expandBegin, expandEnd] then we would
1362         // always be expanding because there would always be one more day
1363         // or week that hasn't been expanded.
1364         if ((begin >= minInstance) && (end <= maxInstance)) {
1365             if (Config.LOGV) {
1366                 Log.v(TAG, "Canceled instance query (" + expandBegin + ", " + expandEnd
1367                         + ") falls within previously expanded range.");
1368             }
1369             return;
1370         }
1371 
1372         // If the requested begin point has not been expanded, then include
1373         // more events than requested in the expansion (use "expandBegin").
1374         if (begin < minInstance) {
1375             expandInstanceRangeLocked(expandBegin, minInstance, localTimezone);
1376             minInstance = expandBegin;
1377         }
1378 
1379         // If the requested end point has not been expanded, then include
1380         // more events than requested in the expansion (use "expandEnd").
1381         if (end > maxInstance) {
1382             expandInstanceRangeLocked(maxInstance, expandEnd, localTimezone);
1383             maxInstance = expandEnd;
1384         }
1385 
1386         // Update the bounds on the Instances table.
1387         mMetaData.writeLocked(localTimezone, minInstance, maxInstance,
1388                 fields.minBusyBit, fields.maxBusyBit);
1389     }
1390 
acquireBusyBitRangeLocked(int firstDay, int lastDay)1391     private void acquireBusyBitRangeLocked(int firstDay, int lastDay) {
1392         if (firstDay > lastDay) {
1393             throw new IllegalArgumentException("firstDay must not be greater than lastDay");
1394         }
1395         String localTimezone = TimeZone.getDefault().getID();
1396         MetaData.Fields fields = mMetaData.getFieldsLocked();
1397         String dbTimezone = fields.timezone;
1398         int minBusyBit = fields.minBusyBit;
1399         int maxBusyBit = fields.maxBusyBit;
1400         boolean timezoneChanged = (dbTimezone == null) || !dbTimezone.equals(localTimezone);
1401         if (firstDay >= minBusyBit && lastDay <= maxBusyBit && !timezoneChanged) {
1402             if (Config.LOGV) {
1403                 Log.v(TAG, "acquireBusyBitRangeLocked() no expansion needed");
1404             }
1405             return;
1406         }
1407 
1408         // Avoid gaps in the BusyBit table and avoid recomputing the busy bits
1409         // that are already in the table.  If the busy bit range has been cleared,
1410         // don't bother checking.
1411         if (maxBusyBit != 0) {
1412             if (firstDay > maxBusyBit) {
1413                 firstDay = maxBusyBit;
1414             } else if (lastDay < minBusyBit) {
1415                 lastDay = minBusyBit;
1416             } else if (firstDay < minBusyBit && lastDay <= maxBusyBit) {
1417                 lastDay = minBusyBit;
1418             } else if (lastDay > maxBusyBit && firstDay >= minBusyBit) {
1419                 firstDay = maxBusyBit;
1420             }
1421         }
1422 
1423         // Allocate space for the busy bits, one 32-bit integer for each day.
1424         int numDays = lastDay - firstDay + 1;
1425         int[] busybits = new int[numDays];
1426         int[] allDayCounts = new int[numDays];
1427 
1428         // Convert the first and last Julian day range to a range that uses
1429         // UTC milliseconds.
1430         Time time = new Time();
1431         long begin = time.setJulianDay(firstDay);
1432 
1433         // We add one to lastDay because the time is set to 12am on the given
1434         // Julian day and we want to include all the events on the last day.
1435         long end = time.setJulianDay(lastDay + 1);
1436 
1437         // Make sure the Instances table includes events in the range
1438         // [begin, end].
1439         acquireInstanceRange(begin, end, true /* use minimum expansion window */);
1440 
1441         SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
1442         qb.setTables("Instances INNER JOIN Events ON (Instances.event_id=Events._id) " +
1443                 "INNER JOIN Calendars ON (Events.calendar_id = Calendars._id)");
1444         qb.setProjectionMap(sInstancesProjectionMap);
1445         qb.appendWhere("begin <= ");
1446         qb.appendWhere(String.valueOf(end));
1447         qb.appendWhere(" AND end >= ");
1448         qb.appendWhere(String.valueOf(begin));
1449         qb.appendWhere(" AND ");
1450         qb.appendWhere(Instances.SELECTED);
1451         qb.appendWhere("=1");
1452 
1453         final SQLiteDatabase db = getDatabase();
1454         // Get all the instances that overlap the range [begin,end]
1455         Cursor cursor = qb.query(db, sInstancesProjection, null, null, null, null, null);
1456         int count = 0;
1457         try {
1458             count = cursor.getCount();
1459             while (cursor.moveToNext()) {
1460                 int startDay = cursor.getInt(INSTANCES_INDEX_START_DAY);
1461                 int endDay = cursor.getInt(INSTANCES_INDEX_END_DAY);
1462                 int startMinute = cursor.getInt(INSTANCES_INDEX_START_MINUTE);
1463                 int endMinute = cursor.getInt(INSTANCES_INDEX_END_MINUTE);
1464                 boolean allDay = cursor.getInt(INSTANCES_INDEX_ALL_DAY) != 0;
1465                 fillBusyBits(firstDay, startDay, endDay, startMinute, endMinute,
1466                         allDay, busybits, allDayCounts);
1467             }
1468         } finally {
1469             if (cursor != null) {
1470                 cursor.close();
1471             }
1472         }
1473 
1474         if (count == 0) {
1475             return;
1476         }
1477 
1478         // Read the busybit range again because that may have changed when we
1479         // called acquireInstanceRange().
1480         fields = mMetaData.getFieldsLocked();
1481         minBusyBit = fields.minBusyBit;
1482         maxBusyBit = fields.maxBusyBit;
1483 
1484         // If the busybit range was cleared, then delete all the entries.
1485         if (maxBusyBit == 0) {
1486             mDb.execSQL("DELETE FROM BusyBits;");
1487         }
1488 
1489         // Merge the busy bits with the database.
1490         mergeBusyBits(firstDay, lastDay, busybits, allDayCounts);
1491         if (maxBusyBit == 0) {
1492             minBusyBit = firstDay;
1493             maxBusyBit = lastDay;
1494         } else {
1495             if (firstDay < minBusyBit) {
1496                 minBusyBit = firstDay;
1497             }
1498             if (lastDay > maxBusyBit) {
1499                 maxBusyBit = lastDay;
1500             }
1501         }
1502         // Update the busy bit range
1503         mMetaData.writeLocked(fields.timezone, fields.minInstance, fields.maxInstance,
1504                 minBusyBit, maxBusyBit);
1505     }
1506 
1507     private static final String[] EXPAND_COLUMNS = new String[] {
1508             Events._ID,
1509             Events._SYNC_ID,
1510             Events.STATUS,
1511             Events.DTSTART,
1512             Events.DTEND,
1513             Events.EVENT_TIMEZONE,
1514             Events.RRULE,
1515             Events.RDATE,
1516             Events.EXRULE,
1517             Events.EXDATE,
1518             Events.DURATION,
1519             Events.ALL_DAY,
1520             Events.ORIGINAL_EVENT,
1521             Events.ORIGINAL_INSTANCE_TIME
1522     };
1523 
1524     /**
1525      * Make instances for the given range.
1526      */
expandInstanceRangeLocked(long begin, long end, String localTimezone)1527     private void expandInstanceRangeLocked(long begin, long end, String localTimezone) {
1528 
1529         if (PROFILE) {
1530             Debug.startMethodTracing("expandInstanceRangeLocked");
1531         }
1532 
1533         if (Log.isLoggable(TAG, Log.VERBOSE)) {
1534             Log.v(TAG, "Expanding events between " + begin + " and " + end);
1535         }
1536 
1537         Cursor entries = getEntries(begin, end);
1538         try {
1539             performInstanceExpansion(begin, end, localTimezone, entries);
1540         } finally {
1541             if (entries != null) {
1542                 entries.close();
1543             }
1544         }
1545         if (PROFILE) {
1546             Debug.stopMethodTracing();
1547         }
1548     }
1549 
1550     /**
1551      * Get all entries affecting the given window.
1552      * @param begin Window start (ms).
1553      * @param end Window end (ms).
1554      * @return Cursor for the entries; caller must close it.
1555      */
getEntries(long begin, long end)1556     private Cursor getEntries(long begin, long end) {
1557         final SQLiteDatabase db = getDatabase();
1558         SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
1559         qb.setTables("Events INNER JOIN Calendars ON (calendar_id = Calendars._id)");
1560         qb.setProjectionMap(sEventsProjectionMap);
1561 
1562         String beginString = String.valueOf(begin);
1563         String endString = String.valueOf(end);
1564 
1565         qb.appendWhere("(dtstart <= ");
1566         qb.appendWhere(endString);
1567         qb.appendWhere(" AND ");
1568         qb.appendWhere("(lastDate IS NULL OR lastDate >= ");
1569         qb.appendWhere(beginString);
1570         qb.appendWhere(")) OR (");
1571         // grab recurrence exceptions that fall outside our expansion window but modify
1572         // recurrences that do fall within our window.  we won't insert these into the output
1573         // set of instances, but instead will just add them to our cancellations list, so we
1574         // can cancel the correct recurrence expansion instances.
1575         qb.appendWhere("originalInstanceTime IS NOT NULL ");
1576         qb.appendWhere("AND originalInstanceTime <= ");
1577         qb.appendWhere(endString);
1578         qb.appendWhere(" AND ");
1579         // we don't have originalInstanceDuration or end time.  for now, assume the original
1580         // instance lasts no longer than 1 week.
1581         // TODO: compute the originalInstanceEndTime or get this from the server.
1582         qb.appendWhere("originalInstanceTime >= ");
1583         qb.appendWhere(String.valueOf(begin - MAX_ASSUMED_DURATION));
1584         qb.appendWhere(")");
1585 
1586         if (Log.isLoggable(TAG, Log.VERBOSE)) {
1587             Log.v(TAG, "Retrieving events to expand: " + qb.toString());
1588         }
1589 
1590         return qb.query(db, EXPAND_COLUMNS, null, null, null, null, null);
1591     }
1592 
1593     /**
1594      * Perform instance expansion on the given entries.
1595      * @param begin Window start (ms).
1596      * @param end Window end (ms).
1597      * @param localTimezone
1598      * @param entries The entries to process.
1599      */
performInstanceExpansion(long begin, long end, String localTimezone, Cursor entries)1600     private void performInstanceExpansion(long begin, long end, String localTimezone, Cursor entries) {
1601         RecurrenceProcessor rp = new RecurrenceProcessor();
1602 
1603         int statusColumn = entries.getColumnIndex(Events.STATUS);
1604         int dtstartColumn = entries.getColumnIndex(Events.DTSTART);
1605         int dtendColumn = entries.getColumnIndex(Events.DTEND);
1606         int eventTimezoneColumn = entries.getColumnIndex(Events.EVENT_TIMEZONE);
1607         int durationColumn = entries.getColumnIndex(Events.DURATION);
1608         int rruleColumn = entries.getColumnIndex(Events.RRULE);
1609         int rdateColumn = entries.getColumnIndex(Events.RDATE);
1610         int exruleColumn = entries.getColumnIndex(Events.EXRULE);
1611         int exdateColumn = entries.getColumnIndex(Events.EXDATE);
1612         int allDayColumn = entries.getColumnIndex(Events.ALL_DAY);
1613         int idColumn = entries.getColumnIndex(Events._ID);
1614         int syncIdColumn = entries.getColumnIndex(Events._SYNC_ID);
1615         int originalEventColumn = entries.getColumnIndex(Events.ORIGINAL_EVENT);
1616         int originalInstanceTimeColumn = entries.getColumnIndex(Events.ORIGINAL_INSTANCE_TIME);
1617 
1618         ContentValues initialValues;
1619         EventInstancesMap instancesMap = new EventInstancesMap();
1620 
1621         Duration duration = new Duration();
1622         Time eventTime = new Time();
1623 
1624         // Invariant: entries contains all events that affect the current
1625         // window.  It consists of:
1626         // a) Individual events that fall in the window.  These will be
1627         //    displayed.
1628         // b) Recurrences that included the window.  These will be displayed
1629         //    if not canceled.
1630         // c) Recurrence exceptions that fall in the window.  These will be
1631         //    displayed if not cancellations.
1632         // d) Recurrence exceptions that modify an instance inside the
1633         //    window (subject to 1 week assumption above), but are outside
1634         //    the window.  These will not be displayed.  Cases c and d are
1635         //    distingushed by the start / end time.
1636 
1637         while (entries.moveToNext()) {
1638             try {
1639                 initialValues = null;
1640 
1641                 boolean allDay = entries.getInt(allDayColumn) != 0;
1642 
1643                 String eventTimezone = entries.getString(eventTimezoneColumn);
1644                 if (allDay || TextUtils.isEmpty(eventTimezone)) {
1645                     // in the events table, allDay events start at midnight.
1646                     // this forces them to stay at midnight for all day events
1647                     // TODO: check that this actually does the right thing.
1648                     eventTimezone = Time.TIMEZONE_UTC;
1649                 }
1650 
1651                 long dtstartMillis = entries.getLong(dtstartColumn);
1652                 Long eventId = Long.valueOf(entries.getLong(idColumn));
1653 
1654                 String durationStr = entries.getString(durationColumn);
1655                 if (durationStr != null) {
1656                     try {
1657                         duration.parse(durationStr);
1658                     }
1659                     catch (DateException e) {
1660                         Log.w(TAG, "error parsing duration for event "
1661                                 + eventId + "'" + durationStr + "'", e);
1662                         duration.sign = 1;
1663                         duration.weeks = 0;
1664                         duration.days = 0;
1665                         duration.hours = 0;
1666                         duration.minutes = 0;
1667                         duration.seconds = 0;
1668                         durationStr = "+P0S";
1669                     }
1670                 }
1671 
1672                 String syncId = entries.getString(syncIdColumn);
1673                 String originalEvent = entries.getString(originalEventColumn);
1674 
1675                 long originalInstanceTimeMillis = -1;
1676                 if (!entries.isNull(originalInstanceTimeColumn)) {
1677                     originalInstanceTimeMillis= entries.getLong(originalInstanceTimeColumn);
1678                 }
1679                 int status = entries.getInt(statusColumn);
1680 
1681                 String rruleStr = entries.getString(rruleColumn);
1682                 String rdateStr = entries.getString(rdateColumn);
1683                 String exruleStr = entries.getString(exruleColumn);
1684                 String exdateStr = entries.getString(exdateColumn);
1685 
1686                 RecurrenceSet recur = new RecurrenceSet(rruleStr, rdateStr, exruleStr, exdateStr);
1687 
1688                 if (recur.hasRecurrence()) {
1689                     // the event is repeating
1690 
1691                     if (status == Events.STATUS_CANCELED) {
1692                         // should not happen!
1693                         Log.e(TAG, "Found canceled recurring event in "
1694                                 + "Events table.  Ignoring.");
1695                         continue;
1696                     }
1697 
1698                     // need to parse the event into a local calendar.
1699                     eventTime.timezone = eventTimezone;
1700                     eventTime.set(dtstartMillis);
1701                     eventTime.allDay = allDay;
1702 
1703                     if (durationStr == null) {
1704                         // should not happen.
1705                         Log.e(TAG, "Repeating event has no duration -- "
1706                                 + "should not happen.");
1707                         if (allDay) {
1708                             // set to one day.
1709                             duration.sign = 1;
1710                             duration.weeks = 0;
1711                             duration.days = 1;
1712                             duration.hours = 0;
1713                             duration.minutes = 0;
1714                             duration.seconds = 0;
1715                             durationStr = "+P1D";
1716                         } else {
1717                             // compute the duration from dtend, if we can.
1718                             // otherwise, use 0s.
1719                             duration.sign = 1;
1720                             duration.weeks = 0;
1721                             duration.days = 0;
1722                             duration.hours = 0;
1723                             duration.minutes = 0;
1724                             if (!entries.isNull(dtendColumn)) {
1725                                 long dtendMillis = entries.getLong(dtendColumn);
1726                                 duration.seconds = (int) ((dtendMillis - dtstartMillis) / 1000);
1727                                 durationStr = "+P" + duration.seconds + "S";
1728                             } else {
1729                                 duration.seconds = 0;
1730                                 durationStr = "+P0S";
1731                             }
1732                         }
1733                     }
1734 
1735                     long[] dates;
1736                     dates = rp.expand(eventTime, recur, begin, end);
1737 
1738                     // Initialize the "eventTime" timezone outside the loop.
1739                     // This is used in computeTimezoneDependentFields().
1740                     if (allDay) {
1741                         eventTime.timezone = Time.TIMEZONE_UTC;
1742                     } else {
1743                         eventTime.timezone = localTimezone;
1744                     }
1745 
1746                     long durationMillis = duration.getMillis();
1747                     for (long date : dates) {
1748                         initialValues = new ContentValues();
1749                         initialValues.put(Instances.EVENT_ID, eventId);
1750 
1751                         initialValues.put(Instances.BEGIN, date);
1752                         long dtendMillis = date + durationMillis;
1753                         initialValues.put(Instances.END, dtendMillis);
1754 
1755                         computeTimezoneDependentFields(date, dtendMillis,
1756                                 eventTime, initialValues);
1757                         instancesMap.add(syncId, initialValues);
1758                     }
1759                 } else {
1760                     // the event is not repeating
1761                     initialValues = new ContentValues();
1762 
1763                     // if this event has an "original" field, then record
1764                     // that we need to cancel the original event (we can't
1765                     // do that here because the order of this loop isn't
1766                     // defined)
1767                     if (originalEvent != null && originalInstanceTimeMillis != -1) {
1768                         initialValues.put(Events.ORIGINAL_EVENT, originalEvent);
1769                         initialValues.put(Events.ORIGINAL_INSTANCE_TIME,
1770                                 originalInstanceTimeMillis);
1771                         initialValues.put(Events.STATUS, status);
1772                     }
1773 
1774                     long dtendMillis = dtstartMillis;
1775                     if (durationStr == null) {
1776                         if (!entries.isNull(dtendColumn)) {
1777                             dtendMillis = entries.getLong(dtendColumn);
1778                         }
1779                     } else {
1780                         dtendMillis = duration.addTo(dtstartMillis);
1781                     }
1782 
1783                     // this non-recurring event might be a recurrence exception that doesn't
1784                     // actually fall within our expansion window, but instead was selected
1785                     // so we can correctly cancel expanded recurrence instances below.  do not
1786                     // add events to the instances map if they don't actually fall within our
1787                     // expansion window.
1788                     if ((dtendMillis < begin) || (dtstartMillis > end)) {
1789                         if (originalEvent != null && originalInstanceTimeMillis != -1) {
1790                             initialValues.put(Events.STATUS, Events.STATUS_CANCELED);
1791                         } else {
1792                             Log.w(TAG, "Unexpected event outside window: " + syncId);
1793                             continue;
1794                         }
1795                     }
1796 
1797                     initialValues.put(Instances.EVENT_ID, eventId);
1798                     initialValues.put(Instances.BEGIN, dtstartMillis);
1799 
1800                     initialValues.put(Instances.END, dtendMillis);
1801 
1802                     if (allDay) {
1803                         eventTime.timezone = Time.TIMEZONE_UTC;
1804                     } else {
1805                         eventTime.timezone = localTimezone;
1806                     }
1807                     computeTimezoneDependentFields(dtstartMillis, dtendMillis,
1808                             eventTime, initialValues);
1809 
1810                     instancesMap.add(syncId, initialValues);
1811                 }
1812             } catch (DateException e) {
1813                 Log.w(TAG, "RecurrenceProcessor error ", e);
1814             } catch (TimeFormatException e) {
1815                 Log.w(TAG, "RecurrenceProcessor error ", e);
1816             }
1817         }
1818 
1819         // Invariant: instancesMap contains all instances that affect the
1820         // window, indexed by original sync id.  It consists of:
1821         // a) Individual events that fall in the window.  They have:
1822         //   EVENT_ID, BEGIN, END
1823         // b) Instances of recurrences that fall in the window.  They may
1824         //   be subject to exceptions.  They have:
1825         //   EVENT_ID, BEGIN, END
1826         // c) Exceptions that fall in the window.  They have:
1827         //   ORIGINAL_EVENT, ORIGINAL_INSTANCE_TIME, STATUS (since they can
1828         //   be a modification or cancellation), EVENT_ID, BEGIN, END
1829         // d) Recurrence exceptions that modify an instance inside the
1830         //   window but fall outside the window.  They have:
1831         //   ORIGINAL_EVENT, ORIGINAL_INSTANCE_TIME, STATUS =
1832         //   STATUS_CANCELED, EVENT_ID, BEGIN, END
1833 
1834         // First, delete the original instances corresponding to recurrence
1835         // exceptions.  We do this by iterating over the list and for each
1836         // recurrence exception, we search the list for an instance with a
1837         // matching "original instance time".  If we find such an instance,
1838         // we remove it from the list.  If we don't find such an instance
1839         // then we cancel the recurrence exception.
1840         Set<String> keys = instancesMap.keySet();
1841         for (String syncId : keys) {
1842             InstancesList list = instancesMap.get(syncId);
1843             for (ContentValues values : list) {
1844 
1845                 // If this instance is not a recurrence exception, then
1846                 // skip it.
1847                 if (!values.containsKey(Events.ORIGINAL_EVENT)) {
1848                     continue;
1849                 }
1850 
1851                 String originalEvent = values.getAsString(Events.ORIGINAL_EVENT);
1852                 long originalTime = values.getAsLong(Events.ORIGINAL_INSTANCE_TIME);
1853                 InstancesList originalList = instancesMap.get(originalEvent);
1854                 if (originalList == null) {
1855                     // The original recurrence is not present, so don't try canceling it.
1856                     continue;
1857                 }
1858 
1859                 // Search the original event for a matching original
1860                 // instance time.  If there is a matching one, then remove
1861                 // the original one.  We do this both for exceptions that
1862                 // change the original instance as well as for exceptions
1863                 // that delete the original instance.
1864                 for (int num = originalList.size() - 1; num >= 0; num--) {
1865                     ContentValues originalValues = originalList.get(num);
1866                     long beginTime = originalValues.getAsLong(Instances.BEGIN);
1867                     if (beginTime == originalTime) {
1868                         // We found the original instance, so remove it.
1869                         originalList.remove(num);
1870                     }
1871                 }
1872             }
1873         }
1874 
1875         // Invariant: instancesMap contains filtered instances.
1876         // It consists of:
1877         // a) Individual events that fall in the window.
1878         // b) Instances of recurrences that fall in the window and have not
1879         //   been subject to exceptions.
1880         // c) Exceptions that fall in the window.  They will have
1881         //   STATUS_CANCELED if they are cancellations.
1882         // d) Recurrence exceptions that modify an instance inside the
1883         //   window but fall outside the window.  These are STATUS_CANCELED.
1884 
1885         // Now do the inserts.  Since the db lock is held when this method is executed,
1886         // this will be done in a transaction.
1887         // NOTE: if there is lock contention (e.g., a sync is trying to merge into the db
1888         // while the calendar app is trying to query the db (expanding instances)), we will
1889         // not be "polite" and yield the lock until we're done.  This will favor local query
1890         // operations over sync/write operations.
1891         for (String syncId : keys) {
1892             InstancesList list = instancesMap.get(syncId);
1893             for (ContentValues values : list) {
1894 
1895                 // If this instance was cancelled then don't create a new
1896                 // instance.
1897                 Integer status = values.getAsInteger(Events.STATUS);
1898                 if (status != null && status == Events.STATUS_CANCELED) {
1899                     continue;
1900                 }
1901 
1902                 // Remove these fields before inserting a new instance
1903                 values.remove(Events.ORIGINAL_EVENT);
1904                 values.remove(Events.ORIGINAL_INSTANCE_TIME);
1905                 values.remove(Events.STATUS);
1906 
1907                 mInstancesInserter.replace(values);
1908             }
1909         }
1910     }
1911 
1912     /**
1913      * Computes the timezone-dependent fields of an instance of an event and
1914      * updates the "values" map to contain those fields.
1915      *
1916      * @param begin the start time of the instance (in UTC milliseconds)
1917      * @param end the end time of the instance (in UTC milliseconds)
1918      * @param local a Time object with the timezone set to the local timezone
1919      * @param values a map that will contain the timezone-dependent fields
1920      */
computeTimezoneDependentFields(long begin, long end, Time local, ContentValues values)1921     private void computeTimezoneDependentFields(long begin, long end,
1922             Time local, ContentValues values) {
1923         local.set(begin);
1924         int startDay = Time.getJulianDay(begin, local.gmtoff);
1925         int startMinute = local.hour * 60 + local.minute;
1926 
1927         local.set(end);
1928         int endDay = Time.getJulianDay(end, local.gmtoff);
1929         int endMinute = local.hour * 60 + local.minute;
1930 
1931         // Special case for midnight, which has endMinute == 0.  Change
1932         // that to +24 hours on the previous day to make everything simpler.
1933         // Exception: if start and end minute are both 0 on the same day,
1934         // then leave endMinute alone.
1935         if (endMinute == 0 && endDay > startDay) {
1936             endMinute = 24 * 60;
1937             endDay -= 1;
1938         }
1939 
1940         values.put(Instances.START_DAY, startDay);
1941         values.put(Instances.END_DAY, endDay);
1942         values.put(Instances.START_MINUTE, startMinute);
1943         values.put(Instances.END_MINUTE, endMinute);
1944     }
1945 
fillBusyBits(int minDay, int startDay, int endDay, int startMinute, int endMinute, boolean allDay, int[] busybits, int[] allDayCounts)1946     private void fillBusyBits(int minDay, int startDay, int endDay, int startMinute,
1947             int endMinute, boolean allDay, int[] busybits, int[] allDayCounts) {
1948 
1949         // The startDay can be less than the minDay if we have an event
1950         // that starts earlier than the time range we are interested in.
1951         // In that case, we ignore the time range that falls outside the
1952         // the range we are interested in.
1953         if (startDay < minDay) {
1954             startDay = minDay;
1955             startMinute = 0;
1956         }
1957 
1958         // Likewise, truncate the event's end day so that it doesn't go past
1959         // the expected range.
1960         int numDays = busybits.length;
1961         int stopDay = endDay;
1962         if (stopDay > minDay + numDays - 1) {
1963             stopDay = minDay + numDays - 1;
1964         }
1965         int dayIndex = startDay - minDay;
1966 
1967         if (allDay) {
1968             for (int day = startDay; day <= stopDay; day++, dayIndex++) {
1969                 allDayCounts[dayIndex] += 1;
1970             }
1971             return;
1972         }
1973 
1974         for (int day = startDay; day <= stopDay; day++, dayIndex++) {
1975             int endTime = endMinute;
1976             // If the event ends on a future day, then show it extending to
1977             // the end of this day.
1978             if (endDay > day) {
1979                 endTime = 24 * 60;
1980             }
1981 
1982             int startBit = startMinute / BUSYBIT_INTERVAL ;
1983             int endBit = (endTime + BUSYBIT_INTERVAL - 1) / BUSYBIT_INTERVAL;
1984             int len = endBit - startBit;
1985             if (len == 0) {
1986                 len = 1;
1987             }
1988             if (len < 0 || len > 24) {
1989                 Log.e("Cal", "fillBusyBits() error: len " + len
1990                         + " startMinute,endTime " + startMinute + " , " + endTime
1991                         + " startDay,endDay " + startDay + " , " + endDay);
1992             } else {
1993                 int oneBits = BIT_MASKS[len];
1994                 busybits[dayIndex] |= oneBits << startBit;
1995             }
1996 
1997             // Set the start minute to the beginning of the day, in
1998             // case this event spans multiple days.
1999             startMinute = 0;
2000         }
2001     }
2002 
mergeBusyBits(int startDay, int endDay, int[] busybits, int[] allDayCounts)2003     private void mergeBusyBits(int startDay, int endDay, int[] busybits, int[] allDayCounts) {
2004         mDb.beginTransaction();
2005         try {
2006             mergeBusyBitsLocked(startDay, endDay, busybits, allDayCounts);
2007             mDb.setTransactionSuccessful();
2008         } finally {
2009             mDb.endTransaction();
2010         }
2011     }
2012 
mergeBusyBitsLocked(int startDay, int endDay, int[] busybits, int[] allDayCounts)2013     private void mergeBusyBitsLocked(int startDay, int endDay, int[] busybits,
2014             int[] allDayCounts) {
2015         final SQLiteDatabase db = getDatabase();
2016         Cursor cursor = null;
2017         try {
2018             String selection = "day>=" + startDay + " AND day<=" + endDay;
2019             cursor = db.query("BusyBits", sBusyBitProjection, selection, null, null, null, null);
2020             if (cursor == null) {
2021                 return;
2022             }
2023             while (cursor.moveToNext()) {
2024                 int day = cursor.getInt(BUSYBIT_INDEX_DAY);
2025                 int busy = cursor.getInt(BUSYBIT_INDEX_BUSYBITS);
2026                 int allDayCount = cursor.getInt(BUSYBIT_INDEX_ALL_DAY_COUNT);
2027 
2028                 int dayIndex = day - startDay;
2029                 busybits[dayIndex] |= busy;
2030                 allDayCounts[dayIndex] += allDayCount;
2031             }
2032         } finally {
2033             if (cursor != null) {
2034                 cursor.close();
2035             }
2036         }
2037 
2038         // Allocate a map that we can reuse
2039         ContentValues values = new ContentValues();
2040 
2041         // Write the busy bits to the database
2042         int len = busybits.length;
2043         for (int dayIndex = 0; dayIndex < len; dayIndex++) {
2044             int busy = busybits[dayIndex];
2045             int allDayCount = allDayCounts[dayIndex];
2046             if (busy == 0 && allDayCount == 0) {
2047                 continue;
2048             }
2049             int day = startDay + dayIndex;
2050 
2051             values.clear();
2052             values.put(BusyBits.DAY, day);
2053             values.put(BusyBits.BUSYBITS, busy);
2054             values.put(BusyBits.ALL_DAY_COUNT, allDayCount);
2055             db.replace("BusyBits", null, values);
2056         }
2057     }
2058 
2059     /**
2060      * Updates the BusyBit table when a new event is inserted into the Events
2061      * table.  This is called after the event has been entered into the Events
2062      * table.  If the event time is not within the date range of the current
2063      * BusyBits table, then the busy bits are not updated.  The BusyBits
2064      * table is not automatically expanded to include this event.
2065      *
2066      * @param eventId the id of the newly created event
2067      * @param values the ContentValues for the new event
2068      */
insertBusyBitsLocked(long eventId, ContentValues values)2069     private void insertBusyBitsLocked(long eventId, ContentValues values) {
2070         MetaData.Fields fields = mMetaData.getFieldsLocked();
2071         if (fields.maxBusyBit == 0) {
2072             return;
2073         }
2074 
2075         // If this is a recurrence event, then the expanded Instances range
2076         // should be 0 because this is called after updateInstancesLocked().
2077         // But for now check this condition and report an error if it occurs.
2078         // In the future, we could even support recurring events by
2079         // expanding them here and updating the busy bits for each instance.
2080         if (isRecurrenceEvent(values))  {
2081             Log.e(TAG, "insertBusyBitsLocked(): unexpected recurrence event\n");
2082             return;
2083         }
2084 
2085         long dtstartMillis = values.getAsLong(Events.DTSTART);
2086         Long dtendMillis = values.getAsLong(Events.DTEND);
2087         if (dtendMillis == null) {
2088             dtendMillis = dtstartMillis;
2089         }
2090 
2091         boolean allDay = false;
2092         Integer allDayInteger = values.getAsInteger(Events.ALL_DAY);
2093         if (allDayInteger != null) {
2094             allDay = allDayInteger != 0;
2095         }
2096 
2097         Time time = new Time();
2098         if (allDay) {
2099             time.timezone = Time.TIMEZONE_UTC;
2100         }
2101 
2102         ContentValues busyValues = new ContentValues();
2103         computeTimezoneDependentFields(dtstartMillis, dtendMillis, time, busyValues);
2104 
2105         int startDay = busyValues.getAsInteger(Instances.START_DAY);
2106         int endDay = busyValues.getAsInteger(Instances.END_DAY);
2107 
2108         // If the event time is not in the expanded BusyBits range,
2109         // then return.
2110         if (startDay > fields.maxBusyBit || endDay < fields.minBusyBit) {
2111             return;
2112         }
2113 
2114         // Allocate space for the busy bits, one 32-bit integer for each day,
2115         // plus 24 bytes for the count of events that occur in each time slot.
2116         int numDays = endDay - startDay + 1;
2117         int[] busybits = new int[numDays];
2118         int[] allDayCounts = new int[numDays];
2119 
2120         int startMinute = busyValues.getAsInteger(Instances.START_MINUTE);
2121         int endMinute = busyValues.getAsInteger(Instances.END_MINUTE);
2122         fillBusyBits(startDay, startDay, endDay, startMinute, endMinute,
2123                 allDay, busybits, allDayCounts);
2124         mergeBusyBits(startDay, endDay, busybits, allDayCounts);
2125     }
2126 
2127     /**
2128      * Updates the busy bits for an event that is being updated.  This is
2129      * called before the event is updated in the Events table because we need
2130      * to know the time of the event before it was changed.
2131      *
2132      * @param eventId the id of the event being updated
2133      * @param values the ContentValues for the updated event
2134      */
updateBusyBitsLocked(long eventId, ContentValues values)2135     private void updateBusyBitsLocked(long eventId, ContentValues values) {
2136         MetaData.Fields fields = mMetaData.getFieldsLocked();
2137         if (fields.maxBusyBit == 0) {
2138             return;
2139         }
2140 
2141         // If this is a recurring event, then clear the BusyBits table.
2142         if (isRecurrenceEvent(values))  {
2143             mMetaData.writeLocked(fields.timezone, fields.minInstance, fields.maxInstance,
2144                     0 /* startDay */, 0 /* endDay */);
2145             return;
2146         }
2147 
2148         // If the event fields being updated don't contain the start or end
2149         // time, then we don't need to bother updating the BusyBits table.
2150         Long dtstartLong = values.getAsLong(Events.DTSTART);
2151         Long dtendLong = values.getAsLong(Events.DTEND);
2152         if (dtstartLong == null && dtendLong == null) {
2153             return;
2154         }
2155 
2156         // If the timezone has changed, then clear the busy bits table
2157         // and return.
2158         String dbTimezone = fields.timezone;
2159         String localTimezone = TimeZone.getDefault().getID();
2160         boolean timezoneChanged = (dbTimezone == null) || !dbTimezone.equals(localTimezone);
2161         if (timezoneChanged) {
2162             mMetaData.writeLocked(fields.timezone, fields.minInstance, fields.maxInstance,
2163                     0 /* startDay */, 0 /* endDay */);
2164             return;
2165         }
2166 
2167         // Read the existing event start and end times from the Events table.
2168         TimeRange eventRange = readEventStartEnd(eventId);
2169 
2170         // Fill in the new start time (if missing) or the new end time (if
2171         // missing) from the existing event start and end times.
2172         long dtstartMillis;
2173         if (dtstartLong != null) {
2174             dtstartMillis = dtstartLong;
2175         } else {
2176             dtstartMillis = eventRange.begin;
2177         }
2178 
2179         long dtendMillis;
2180         if (dtendLong != null) {
2181             dtendMillis = dtendLong;
2182         } else {
2183             dtendMillis = eventRange.end;
2184         }
2185 
2186         // Compute the start and end Julian days for the event.
2187         Time time = new Time();
2188         if (eventRange.allDay) {
2189             time.timezone = Time.TIMEZONE_UTC;
2190         }
2191         ContentValues busyValues = new ContentValues();
2192         computeTimezoneDependentFields(eventRange.begin, eventRange.end, time, busyValues);
2193         int oldStartDay = busyValues.getAsInteger(Instances.START_DAY);
2194         int oldEndDay = busyValues.getAsInteger(Instances.END_DAY);
2195 
2196         boolean allDay = false;
2197         Integer allDayInteger = values.getAsInteger(Events.ALL_DAY);
2198         if (allDayInteger != null) {
2199             allDay = allDayInteger != 0;
2200         }
2201 
2202         if (allDay) {
2203             time.timezone = Time.TIMEZONE_UTC;
2204         } else {
2205             time.timezone = TimeZone.getDefault().getID();
2206         }
2207 
2208         computeTimezoneDependentFields(dtstartMillis, dtendMillis, time, busyValues);
2209         int newStartDay = busyValues.getAsInteger(Instances.START_DAY);
2210         int newEndDay = busyValues.getAsInteger(Instances.END_DAY);
2211 
2212         // If both the old and new event times are outside the expanded
2213         // BusyBits table, then return.
2214         if ((oldStartDay > fields.maxBusyBit || oldEndDay < fields.minBusyBit)
2215                 && (newStartDay > fields.maxBusyBit || newEndDay < fields.minBusyBit)) {
2216             return;
2217         }
2218 
2219         // If the old event time is within the expanded Instances range,
2220         // then clear the BusyBits table and return.
2221         if (oldStartDay <= fields.maxBusyBit && oldEndDay >= fields.minBusyBit) {
2222             // We could recompute the busy bits for the days containing the
2223             // old event time.  For now, just clear the BusyBits table.
2224             mMetaData.writeLocked(fields.timezone, fields.minInstance, fields.maxInstance,
2225                     0 /* startDay */, 0 /* endDay */);
2226             return;
2227         }
2228 
2229         // The new event time is within the expanded Instances range.
2230         // So insert the busy bits for that day (or days).
2231 
2232         // Allocate space for the busy bits, one 32-bit integer for each day,
2233         // plus 24 bytes for the count of events that occur in each time slot.
2234         int numDays = newEndDay - newStartDay + 1;
2235         int[] busybits = new int[numDays];
2236         int[] allDayCounts = new int[numDays];
2237 
2238         int startMinute = busyValues.getAsInteger(Instances.START_MINUTE);
2239         int endMinute = busyValues.getAsInteger(Instances.END_MINUTE);
2240         fillBusyBits(newStartDay, newStartDay, newEndDay, startMinute, endMinute,
2241                 allDay, busybits, allDayCounts);
2242         mergeBusyBits(newStartDay, newEndDay, busybits, allDayCounts);
2243     }
2244 
2245     /**
2246      * This method is called just before an event is deleted.
2247      *
2248      * @param eventId
2249      */
deleteBusyBitsLocked(long eventId)2250     private void deleteBusyBitsLocked(long eventId) {
2251         MetaData.Fields fields = mMetaData.getFieldsLocked();
2252         if (fields.maxBusyBit == 0) {
2253             return;
2254         }
2255 
2256         // TODO: if the event being deleted is not a recurring event and the
2257         // start and end time are outside the BusyBit range, then we could
2258         // avoid clearing the BusyBits table.  For now, always clear the
2259         // BusyBits table because deleting events is relatively rare.
2260         mMetaData.writeLocked(fields.timezone, fields.minInstance, fields.maxInstance,
2261                 0 /* startDay */, 0 /* endDay */);
2262     }
2263 
2264     // Read the start and end time for an event from the Events table.
2265     // Also read the "all-day" indicator.
readEventStartEnd(long eventId)2266     private TimeRange readEventStartEnd(long eventId) {
2267         Cursor cursor = null;
2268         TimeRange range = new TimeRange();
2269         try {
2270             cursor = query(ContentUris.withAppendedId(Events.CONTENT_URI, eventId),
2271                     new String[] { Events.DTSTART, Events.DTEND, Events.ALL_DAY },
2272                     null /* selection */,
2273                     null /* selectionArgs */,
2274                     null /* sort */);
2275             if (cursor == null || !cursor.moveToFirst()) {
2276                 Log.d(TAG, "Couldn't find " + eventId + " in Events table");
2277                 return null;
2278             }
2279             range.begin = cursor.getLong(0);
2280             range.end = cursor.getLong(1);
2281             range.allDay = cursor.getInt(2) != 0;
2282         } finally {
2283             if (cursor != null) {
2284                 cursor.close();
2285             }
2286         }
2287         return range;
2288     }
2289 
2290     @Override
getType(Uri url)2291     public String getType(Uri url) {
2292         int match = sURLMatcher.match(url);
2293         switch (match) {
2294             case EVENTS:
2295                 return "vnd.android.cursor.dir/event";
2296             case EVENTS_ID:
2297                 return "vnd.android.cursor.item/event";
2298             case REMINDERS:
2299                 return "vnd.android.cursor.dir/reminder";
2300             case REMINDERS_ID:
2301                 return "vnd.android.cursor.item/reminder";
2302             case CALENDAR_ALERTS:
2303                 return "vnd.android.cursor.dir/calendar-alert";
2304             case CALENDAR_ALERTS_BY_INSTANCE:
2305                 return "vnd.android.cursor.dir/calendar-alert-by-instance";
2306             case CALENDAR_ALERTS_ID:
2307                 return "vnd.android.cursor.item/calendar-alert";
2308             case INSTANCES:
2309             case INSTANCES_BY_DAY:
2310                 return "vnd.android.cursor.dir/event-instance";
2311             case BUSYBITS:
2312                 return "vnd.android.cursor.dir/busybits";
2313             default:
2314                 throw new IllegalArgumentException("Unknown URL " + url);
2315         }
2316     }
2317 
isRecurrenceEvent(ContentValues values)2318     public static boolean isRecurrenceEvent(ContentValues values) {
2319         return (!TextUtils.isEmpty(values.getAsString(Events.RRULE))||
2320                 !TextUtils.isEmpty(values.getAsString(Events.RDATE))||
2321                 !TextUtils.isEmpty(values.getAsString(Events.ORIGINAL_EVENT)));
2322     }
2323 
2324     @Override
insertInternal(Uri url, ContentValues initialValues)2325     public Uri insertInternal(Uri url, ContentValues initialValues) {
2326         final SQLiteDatabase db = getDatabase();
2327         long rowID;
2328 
2329         int match = sURLMatcher.match(url);
2330         switch (match) {
2331             case EVENTS:
2332                 if (!isTemporary()) {
2333                     initialValues.put(Events._SYNC_DIRTY, 1);
2334                     if (!initialValues.containsKey(Events.DTSTART)) {
2335                         throw new RuntimeException("DTSTART field missing from event");
2336                     }
2337                 }
2338                 // TODO: avoid the call to updateBundleFromEvent if this is just finding local
2339                 // changes.  or avoid for temp providers altogether, if we can compute this
2340                 // during a merge.
2341                 // TODO: do we really need to make a copy?
2342                 ContentValues updatedValues = updateContentValuesFromEvent(initialValues);
2343                 if (updatedValues == null) {
2344                     throw new RuntimeException("Could not insert event.");
2345                     // return null;
2346                 }
2347                 String owner = null;
2348                 if (updatedValues.containsKey(Events.CALENDAR_ID) &&
2349                         !updatedValues.containsKey(Events.ORGANIZER)) {
2350                     owner = getOwner(updatedValues.getAsLong(Events.CALENDAR_ID));
2351                     // TODO: This isn't entirely correct.  If a guest is adding a recurrence
2352                     // exception to an event, the organizer should stay the original organizer.
2353                     // This value doesn't go to the server and it will get fixed on sync,
2354                     // so it shouldn't really matter.
2355                     if (owner != null) {
2356                         updatedValues.put(Events.ORGANIZER, owner);
2357                     }
2358                 }
2359 
2360                 long rowId = mEventsInserter.insert(updatedValues);
2361                 Uri uri = Uri.parse("content://" + url.getAuthority() + "/events/" + rowId);
2362                 if (!isTemporary() && rowId != -1) {
2363                     updateEventRawTimesLocked(rowId, updatedValues);
2364                     updateInstancesLocked(updatedValues, rowId, true /* new event */, db);
2365                     insertBusyBitsLocked(rowId, updatedValues);
2366 
2367                     // If we inserted a new event that specified the self-attendee
2368                     // status, then we need to add an entry to the attendees table.
2369                     if (initialValues.containsKey(Events.SELF_ATTENDEE_STATUS)) {
2370                         int status = initialValues.getAsInteger(Events.SELF_ATTENDEE_STATUS);
2371                         if (owner == null) {
2372                             owner = getOwner(updatedValues.getAsLong(Events.CALENDAR_ID));
2373                         }
2374                         createAttendeeEntry(rowId, status, owner);
2375                     }
2376                     triggerAppWidgetUpdate(rowId);
2377                 }
2378 
2379                 return uri;
2380             case CALENDARS:
2381                 if (!isTemporary()) {
2382                     Integer syncEvents = initialValues.getAsInteger(Calendars.SYNC_EVENTS);
2383                     if (syncEvents != null && syncEvents == 1) {
2384                         String accountName = initialValues.getAsString(Calendars._SYNC_ACCOUNT);
2385                         String accountType = initialValues.getAsString(
2386                                 Calendars._SYNC_ACCOUNT_TYPE);
2387                         final Account account = new Account(accountName, accountType);
2388                         String calendarUrl = initialValues.getAsString(Calendars.URL);
2389                         scheduleSync(account, false /* two-way sync */, calendarUrl);
2390                     }
2391                 }
2392                 rowID = mCalendarsInserter.insert(initialValues);
2393                 return ContentUris.withAppendedId(Calendars.CONTENT_URI, rowID);
2394             case ATTENDEES:
2395                 if (!initialValues.containsKey(Attendees.EVENT_ID)) {
2396                     throw new IllegalArgumentException("Attendees values must "
2397                             + "contain an event_id");
2398                 }
2399                 rowID = mAttendeesInserter.insert(initialValues);
2400 
2401                 // Copy the attendee status value to the Events table.
2402                 updateEventAttendeeStatus(db, initialValues);
2403 
2404                 return ContentUris.withAppendedId(Calendar.Attendees.CONTENT_URI, rowID);
2405             case REMINDERS:
2406                 if (!initialValues.containsKey(Reminders.EVENT_ID)) {
2407                     throw new IllegalArgumentException("Reminders values must "
2408                             + "contain an event_id");
2409                 }
2410                 rowID = mRemindersInserter.insert(initialValues);
2411 
2412                 if (!isTemporary()) {
2413                     // Schedule another event alarm, if necessary
2414                     if (Log.isLoggable(TAG, Log.DEBUG)) {
2415                         Log.d(TAG, "insertInternal() changing reminder");
2416                     }
2417                     scheduleNextAlarm(false /* do not remove alarms */);
2418                 }
2419                 return ContentUris.withAppendedId(Calendar.Reminders.CONTENT_URI, rowID);
2420             case CALENDAR_ALERTS:
2421                 if (!initialValues.containsKey(CalendarAlerts.EVENT_ID)) {
2422                     throw new IllegalArgumentException("CalendarAlerts values must "
2423                             + "contain an event_id");
2424                 }
2425                 rowID = mCalendarAlertsInserter.insert(initialValues);
2426 
2427                 return Uri.parse(CalendarAlerts.CONTENT_URI + "/" + rowID);
2428             case EXTENDED_PROPERTIES:
2429                 if (!initialValues.containsKey(Calendar.ExtendedProperties.EVENT_ID)) {
2430                     throw new IllegalArgumentException("ExtendedProperties values must "
2431                             + "contain an event_id");
2432                 }
2433                 rowID = mExtendedPropertiesInserter.insert(initialValues);
2434 
2435                 return ContentUris.withAppendedId(ExtendedProperties.CONTENT_URI, rowID);
2436             case DELETED_EVENTS:
2437                 if (isTemporary()) {
2438                     rowID = mDeletedEventsInserter.insert(initialValues);
2439                     return ContentUris.withAppendedId(Calendar.Events.DELETED_CONTENT_URI, rowID);
2440                 }
2441                 // fallthrough
2442             case EVENTS_ID:
2443             case REMINDERS_ID:
2444             case CALENDAR_ALERTS_ID:
2445             case EXTENDED_PROPERTIES_ID:
2446             case INSTANCES:
2447             case INSTANCES_BY_DAY:
2448                 throw new UnsupportedOperationException("Cannot insert into that URL");
2449             default:
2450                 throw new IllegalArgumentException("Unknown URL " + url);
2451         }
2452     }
2453 
2454     /**
2455      * Gets the calendar's owner for an event.
2456      * @param calId
2457      * @return email of owner or null
2458      */
getOwner(long calId)2459     private String getOwner(long calId) {
2460         // Get the email address of this user from this Calendar
2461         String emailAddress = null;
2462         Cursor cursor = null;
2463         try {
2464             cursor = query(ContentUris.withAppendedId(Calendars.CONTENT_URI, calId),
2465                     new String[] { Calendars.OWNER_ACCOUNT },
2466                     null /* selection */,
2467                     null /* selectionArgs */,
2468                     null /* sort */);
2469             if (cursor == null || !cursor.moveToFirst()) {
2470                 Log.d(TAG, "Couldn't find " + calId + " in Calendars table");
2471                 return null;
2472             }
2473             emailAddress = cursor.getString(0);
2474         } finally {
2475             if (cursor != null) {
2476                 cursor.close();
2477             }
2478         }
2479         return emailAddress;
2480     }
2481 
2482     /**
2483      * Creates an entry in the Attendees table that refers to the given event
2484      * and that has the given response status.
2485      *
2486      * @param eventId the event id that the new entry in the Attendees table
2487      * should refer to
2488      * @param status the response status
2489      * @param emailAddress the email of the attendee
2490      */
createAttendeeEntry(long eventId, int status, String emailAddress)2491     private void createAttendeeEntry(long eventId, int status, String emailAddress) {
2492         ContentValues values = new ContentValues();
2493         values.put(Attendees.EVENT_ID, eventId);
2494         values.put(Attendees.ATTENDEE_STATUS, status);
2495         values.put(Attendees.ATTENDEE_TYPE, Attendees.TYPE_NONE);
2496         // TODO: The relationship could actually be ORGANIZER, but it will get straightened out
2497         // on sync.
2498         values.put(Attendees.ATTENDEE_RELATIONSHIP,
2499                 Attendees.RELATIONSHIP_ATTENDEE);
2500         values.put(Attendees.ATTENDEE_EMAIL, emailAddress);
2501 
2502         // We don't know the ATTENDEE_NAME but that will be filled in by the
2503         // server and sent back to us.
2504         mAttendeesInserter.insert(values);
2505     }
2506 
2507     /**
2508      * Updates the attendee status in the Events table to be consistent with
2509      * the value in the Attendees table.
2510      *
2511      * @param db the database
2512      * @param attendeeValues the column values for one row in the Attendees
2513      * table.
2514      */
updateEventAttendeeStatus(SQLiteDatabase db, ContentValues attendeeValues)2515     private void updateEventAttendeeStatus(SQLiteDatabase db, ContentValues attendeeValues) {
2516         // Get the event id for this attendee
2517         long eventId = attendeeValues.getAsLong(Attendees.EVENT_ID);
2518 
2519         if (MULTIPLE_ATTENDEES_PER_EVENT) {
2520             // Get the calendar id for this event
2521             Cursor cursor = null;
2522             long calId;
2523             try {
2524                 cursor = query(ContentUris.withAppendedId(Events.CONTENT_URI, eventId),
2525                         new String[] { Events.CALENDAR_ID },
2526                         null /* selection */,
2527                         null /* selectionArgs */,
2528                         null /* sort */);
2529                 if (cursor == null || !cursor.moveToFirst()) {
2530                     Log.d(TAG, "Couldn't find " + eventId + " in Events table");
2531                     return;
2532                 }
2533                 calId = cursor.getLong(0);
2534             } finally {
2535                 if (cursor != null) {
2536                     cursor.close();
2537                 }
2538             }
2539 
2540             // Get the owner email for this Calendar
2541             String calendarEmail = null;
2542             cursor = null;
2543             try {
2544                 cursor = query(ContentUris.withAppendedId(Calendars.CONTENT_URI, calId),
2545                         new String[] { Calendars.OWNER_ACCOUNT },
2546                         null /* selection */,
2547                         null /* selectionArgs */,
2548                         null /* sort */);
2549                 if (cursor == null || !cursor.moveToFirst()) {
2550                     Log.d(TAG, "Couldn't find " + calId + " in Calendars table");
2551                     return;
2552                 }
2553                 calendarEmail = cursor.getString(0);
2554             } finally {
2555                 if (cursor != null) {
2556                     cursor.close();
2557                 }
2558             }
2559 
2560             if (calendarEmail == null) {
2561                 return;
2562             }
2563 
2564             // Get the email address for this attendee
2565             String attendeeEmail = null;
2566             if (attendeeValues.containsKey(Attendees.ATTENDEE_EMAIL)) {
2567                 attendeeEmail = attendeeValues.getAsString(Attendees.ATTENDEE_EMAIL);
2568             }
2569 
2570             // If the attendee email does not match the calendar email, then this
2571             // attendee is not the owner of this calendar so we don't update the
2572             // selfAttendeeStatus in the event.
2573             if (!calendarEmail.equals(attendeeEmail)) {
2574                 return;
2575             }
2576         }
2577 
2578         int status = Attendees.ATTENDEE_STATUS_NONE;
2579         if (attendeeValues.containsKey(Attendees.ATTENDEE_RELATIONSHIP)) {
2580             int rel = attendeeValues.getAsInteger(Attendees.ATTENDEE_RELATIONSHIP);
2581             if (rel == Attendees.RELATIONSHIP_ORGANIZER) {
2582                 status = Attendees.ATTENDEE_STATUS_ACCEPTED;
2583             }
2584         }
2585 
2586         if (attendeeValues.containsKey(Attendees.ATTENDEE_STATUS)) {
2587             status = attendeeValues.getAsInteger(Attendees.ATTENDEE_STATUS);
2588         }
2589 
2590         ContentValues values = new ContentValues();
2591         values.put(Events.SELF_ATTENDEE_STATUS, status);
2592         db.update("Events", values, "_id="+eventId, null);
2593     }
2594 
2595     /**
2596      * Updates the instances table when an event is added or updated.
2597      * @param values The new values of the event.
2598      * @param rowId The database row id of the event.
2599      * @param newEvent true if the event is new.
2600      * @param db The database
2601      */
updateInstancesLocked(ContentValues values, long rowId, boolean newEvent, SQLiteDatabase db)2602     private void updateInstancesLocked(ContentValues values,
2603             long rowId,
2604             boolean newEvent,
2605             SQLiteDatabase db) {
2606 
2607         // If there are no expanded Instances, then return.
2608         MetaData.Fields fields = mMetaData.getFieldsLocked();
2609         if (fields.maxInstance == 0) {
2610             return;
2611         }
2612 
2613         Long dtstartMillis = values.getAsLong(Events.DTSTART);
2614         if (dtstartMillis == null) {
2615             if (newEvent) {
2616                 // must be present for a new event.
2617                 throw new RuntimeException("DTSTART missing.");
2618             }
2619             if (Config.LOGV) Log.v(TAG, "Missing DTSTART.  "
2620                     + "No need to update instance.");
2621             return;
2622         }
2623 
2624         Long lastDateMillis = values.getAsLong(Events.LAST_DATE);
2625         Long originalInstanceTime = values.getAsLong(Events.ORIGINAL_INSTANCE_TIME);
2626 
2627         if (!newEvent) {
2628             // Want to do this for regular event, recurrence, or exception.
2629             // For recurrence or exception, more deletion may happen below if we
2630             // do an instance expansion.  This deletion will suffice if the exception
2631             // is moved outside the window, for instance.
2632             db.delete("Instances", "event_id=" + rowId, null /* selectionArgs */);
2633         }
2634 
2635         if (isRecurrenceEvent(values))  {
2636             // The recurrence or exception needs to be (re-)expanded if:
2637             // a) Exception or recurrence that falls inside window
2638             boolean insideWindow = dtstartMillis <= fields.maxInstance &&
2639                     (lastDateMillis == null || lastDateMillis >= fields.minInstance);
2640             // b) Exception that affects instance inside window
2641             // These conditions match the query in getEntries
2642             //  See getEntries comment for explanation of subtracting 1 week.
2643             boolean affectsWindow = originalInstanceTime != null &&
2644                     originalInstanceTime <= fields.maxInstance &&
2645                     originalInstanceTime >= fields.minInstance - MAX_ASSUMED_DURATION;
2646             if (insideWindow || affectsWindow) {
2647                 updateRecurrenceInstancesLocked(values, rowId, db);
2648             }
2649             // TODO: an exception creation or update could be optimized by
2650             // updating just the affected instances, instead of regenerating
2651             // the recurrence.
2652             return;
2653         }
2654 
2655         Long dtendMillis = values.getAsLong(Events.DTEND);
2656         if (dtendMillis == null) {
2657             dtendMillis = dtstartMillis;
2658         }
2659 
2660         // if the event is in the expanded range, insert
2661         // into the instances table.
2662         // TODO: deal with durations.  currently, durations are only used in
2663         // recurrences.
2664 
2665         if (dtstartMillis <= fields.maxInstance && dtendMillis >= fields.minInstance) {
2666             ContentValues instanceValues = new ContentValues();
2667             instanceValues.put(Instances.EVENT_ID, rowId);
2668             instanceValues.put(Instances.BEGIN, dtstartMillis);
2669             instanceValues.put(Instances.END, dtendMillis);
2670 
2671             boolean allDay = false;
2672             Integer allDayInteger = values.getAsInteger(Events.ALL_DAY);
2673             if (allDayInteger != null) {
2674                 allDay = allDayInteger != 0;
2675             }
2676 
2677             // Update the timezone-dependent fields.
2678             Time local = new Time();
2679             if (allDay) {
2680                 local.timezone = Time.TIMEZONE_UTC;
2681             } else {
2682                 local.timezone = fields.timezone;
2683             }
2684 
2685             computeTimezoneDependentFields(dtstartMillis, dtendMillis, local, instanceValues);
2686             mInstancesInserter.insert(instanceValues);
2687         }
2688     }
2689 
2690     /**
2691      * Determines the recurrence entries associated with a particular recurrence.
2692      * This set is the base recurrence and any exception.
2693      *
2694      * Normally the entries are indicated by the sync id of the base recurrence
2695      * (which is the originalEvent in the exceptions).
2696      * However, a complication is that a recurrence may not yet have a sync id.
2697      * In that case, the recurrence is specified by the rowId.
2698      *
2699      * @param recurrenceSyncId The sync id of the base recurrence, or null.
2700      * @param rowId The row id of the base recurrence.
2701      * @return the relevant entries.
2702      */
getRelevantRecurrenceEntries(String recurrenceSyncId, long rowId)2703     private Cursor getRelevantRecurrenceEntries(String recurrenceSyncId, long rowId) {
2704         final SQLiteDatabase db = getDatabase();
2705         SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
2706 
2707         qb.setTables("Events INNER JOIN Calendars ON (calendar_id = Calendars._id)");
2708         qb.setProjectionMap(sEventsProjectionMap);
2709         if (recurrenceSyncId == null) {
2710             String where = "Events._id = " + rowId;
2711             qb.appendWhere(where);
2712         } else {
2713             String where = "Events._sync_id = \"" + recurrenceSyncId + "\""
2714                     + " OR Events.originalEvent = \"" + recurrenceSyncId + "\"";
2715             qb.appendWhere(where);
2716         }
2717         if (Log.isLoggable(TAG, Log.VERBOSE)) {
2718             Log.v(TAG, "Retrieving events to expand: " + qb.toString());
2719         }
2720 
2721         return qb.query(db, EXPAND_COLUMNS, null /* selection */, null /* selectionArgs */, null /* groupBy */, null /* having */, null /* sortOrder */);
2722     }
2723 
2724     /**
2725      * Do incremental Instances update of a recurrence or recurrence exception.
2726      *
2727      * This method does performInstanceExpansion on just the modified recurrence,
2728      * to avoid the overhead of recomputing the entire instance table.
2729      *
2730      * @param values The new values of the event.
2731      * @param rowId The database row id of the event.
2732      * @param db The database
2733      */
updateRecurrenceInstancesLocked(ContentValues values, long rowId, SQLiteDatabase db)2734     private void updateRecurrenceInstancesLocked(ContentValues values,
2735             long rowId,
2736             SQLiteDatabase db) {
2737         MetaData.Fields fields = mMetaData.getFieldsLocked();
2738         String originalEvent = values.getAsString(Events.ORIGINAL_EVENT);
2739         String recurrenceSyncId = null;
2740         if (originalEvent != null) {
2741             recurrenceSyncId = originalEvent;
2742         } else {
2743             // Get the recurrence's sync id from the database
2744             recurrenceSyncId = DatabaseUtils.stringForQuery(db, "SELECT _sync_id FROM Events"
2745                     + " WHERE _id = " + rowId, null /* selection args */);
2746         }
2747         // recurrenceSyncId is the _sync_id of the underlying recurrence
2748         // If the recurrence hasn't gone to the server, it will be null.
2749 
2750         // Need to clear out old instances
2751         if (recurrenceSyncId == null) {
2752             // Creating updating a recurrence that hasn't gone to the server.
2753             // Need to delete based on row id
2754             String where = "_id IN (SELECT Instances._id as _id"
2755                     + " FROM Instances INNER JOIN Events"
2756                     + " ON (Events._id = Instances.event_id)"
2757                     + " WHERE Events._id =?)";
2758             db.delete("Instances", where, new String[]{"" + rowId});
2759         } else {
2760             // Creating or modifying a recurrence or exception.
2761             // Delete instances for recurrence (_sync_id = recurrenceSyncId)
2762             // and all exceptions (originalEvent = recurrenceSyncId)
2763             String where = "_id IN (SELECT Instances._id as _id"
2764                     + " FROM Instances INNER JOIN Events"
2765                     + " ON (Events._id = Instances.event_id)"
2766                     + " WHERE Events._sync_id =?"
2767                     + " OR Events.originalEvent =?)";
2768             db.delete("Instances", where, new String[]{recurrenceSyncId, recurrenceSyncId});
2769         }
2770 
2771         // Now do instance expansion
2772         Cursor entries = getRelevantRecurrenceEntries(recurrenceSyncId, rowId);
2773         try {
2774             performInstanceExpansion(fields.minInstance, fields.maxInstance, fields.timezone, entries);
2775         } finally {
2776             if (entries != null) {
2777                 entries.close();
2778             }
2779         }
2780 
2781         // Clear busy bits
2782         mMetaData.writeLocked(fields.timezone, fields.minInstance, fields.maxInstance,
2783                 0 /* startDay */, 0 /* endDay */);
2784     }
2785 
calculateLastDate(ContentValues values)2786     long calculateLastDate(ContentValues values)
2787             throws DateException {
2788         // Allow updates to some event fields like the title or hasAlarm
2789         // without requiring DTSTART.
2790         if (!values.containsKey(Events.DTSTART)) {
2791             if (values.containsKey(Events.DTEND) || values.containsKey(Events.RRULE)
2792                     || values.containsKey(Events.DURATION)
2793                     || values.containsKey(Events.EVENT_TIMEZONE)
2794                     || values.containsKey(Events.RDATE)
2795                     || values.containsKey(Events.EXRULE)
2796                     || values.containsKey(Events.EXDATE)) {
2797                 throw new RuntimeException("DTSTART field missing from event");
2798             }
2799             return -1;
2800         }
2801         long dtstartMillis = values.getAsLong(Events.DTSTART);
2802         long lastMillis = -1;
2803 
2804         // Can we use dtend with a repeating event?  What does that even
2805         // mean?
2806         // NOTE: if the repeating event has a dtend, we convert it to a
2807         // duration during event processing, so this situation should not
2808         // occur.
2809         Long dtEnd = values.getAsLong(Events.DTEND);
2810         if (dtEnd != null) {
2811             lastMillis = dtEnd;
2812         } else {
2813             // find out how long it is
2814             Duration duration = new Duration();
2815             String durationStr = values.getAsString(Events.DURATION);
2816             if (durationStr != null) {
2817                 duration.parse(durationStr);
2818             }
2819 
2820             RecurrenceSet recur = new RecurrenceSet(values);
2821 
2822             if (recur.hasRecurrence()) {
2823                 // the event is repeating, so find the last date it
2824                 // could appear on
2825 
2826                 String tz = values.getAsString(Events.EVENT_TIMEZONE);
2827 
2828                 if (TextUtils.isEmpty(tz)) {
2829                     // floating timezone
2830                     tz = Time.TIMEZONE_UTC;
2831                 }
2832                 Time dtstartLocal = new Time(tz);
2833 
2834                 dtstartLocal.set(dtstartMillis);
2835 
2836                 RecurrenceProcessor rp = new RecurrenceProcessor();
2837                 lastMillis = rp.getLastOccurence(dtstartLocal, recur);
2838                 if (lastMillis == -1) {
2839                     return lastMillis;  // -1
2840                 }
2841             } else {
2842                 // the event is not repeating, just use dtstartMillis
2843                 lastMillis = dtstartMillis;
2844             }
2845 
2846             // that was the beginning of the event.  this is the end.
2847             lastMillis = duration.addTo(lastMillis);
2848         }
2849         return lastMillis;
2850     }
2851 
updateContentValuesFromEvent(ContentValues initialValues)2852     private ContentValues updateContentValuesFromEvent(ContentValues initialValues) {
2853         try {
2854             ContentValues values = new ContentValues(initialValues);
2855 
2856             long last = calculateLastDate(values);
2857             if (last != -1) {
2858                 values.put(Events.LAST_DATE, last);
2859             }
2860 
2861             return values;
2862         } catch (DateException e) {
2863             // don't add it if there was an error
2864             Log.w(TAG, "Could not calculate last date.", e);
2865             return null;
2866         }
2867     }
2868 
updateEventRawTimesLocked(long eventId, ContentValues values)2869     private void updateEventRawTimesLocked(long eventId, ContentValues values) {
2870         ContentValues rawValues = new ContentValues();
2871 
2872         rawValues.put("event_id", eventId);
2873 
2874         String timezone = values.getAsString(Events.EVENT_TIMEZONE);
2875 
2876         boolean allDay = false;
2877         Integer allDayInteger = values.getAsInteger(Events.ALL_DAY);
2878         if (allDayInteger != null) {
2879             allDay = allDayInteger != 0;
2880         }
2881 
2882         if (allDay || TextUtils.isEmpty(timezone)) {
2883             // floating timezone
2884             timezone = Time.TIMEZONE_UTC;
2885         }
2886 
2887         Time time = new Time(timezone);
2888         time.allDay = allDay;
2889         Long dtstartMillis = values.getAsLong(Events.DTSTART);
2890         if (dtstartMillis != null) {
2891             time.set(dtstartMillis);
2892             rawValues.put("dtstart2445", time.format2445());
2893         }
2894 
2895         Long dtendMillis = values.getAsLong(Events.DTEND);
2896         if (dtendMillis != null) {
2897             time.set(dtendMillis);
2898             rawValues.put("dtend2445", time.format2445());
2899         }
2900 
2901         Long originalInstanceMillis = values.getAsLong(Events.ORIGINAL_INSTANCE_TIME);
2902         if (originalInstanceMillis != null) {
2903             // This is a recurrence exception so we need to get the all-day
2904             // status of the original recurring event in order to format the
2905             // date correctly.
2906             allDayInteger = values.getAsInteger(Events.ORIGINAL_ALL_DAY);
2907             if (allDayInteger != null) {
2908                 time.allDay = allDayInteger != 0;
2909             }
2910             time.set(originalInstanceMillis);
2911             rawValues.put("originalInstanceTime2445", time.format2445());
2912         }
2913 
2914         Long lastDateMillis = values.getAsLong(Events.LAST_DATE);
2915         if (lastDateMillis != null) {
2916             time.allDay = allDay;
2917             time.set(lastDateMillis);
2918             rawValues.put("lastDate2445", time.format2445());
2919         }
2920 
2921         mEventsRawTimesInserter.replace(rawValues);
2922     }
2923 
2924     @Override
deleteInternal(Uri url, String where, String[] whereArgs)2925     public int deleteInternal(Uri url, String where, String[] whereArgs) {
2926         final SQLiteDatabase db = getDatabase();
2927         int match = sURLMatcher.match(url);
2928         switch (match)
2929         {
2930             case EVENTS_ID:
2931             {
2932                 String id = url.getLastPathSegment();
2933                 if (where != null) {
2934                     throw new UnsupportedOperationException("CalendarProvider "
2935                             + "doesn't support where based deletion for type "
2936                             + match);
2937                 }
2938                 if (!isTemporary()) {
2939                     deleteBusyBitsLocked(Integer.parseInt(id));
2940 
2941                     // Query this event to get the fields needed for inserting
2942                     // a new row in the DeletedEvents table.
2943                     Cursor cursor = db.query("Events", EVENTS_PROJECTION,
2944                             "_id=" + id, null, null, null, null);
2945                     try {
2946                         if (cursor.moveToNext()) {
2947                             String syncId = cursor.getString(EVENTS_SYNC_ID_INDEX);
2948                             if (!TextUtils.isEmpty(syncId)) {
2949                                 String syncVersion = cursor.getString(EVENTS_SYNC_VERSION_INDEX);
2950                                 String syncAccountName =
2951                                         cursor.getString(EVENTS_SYNC_ACCOUNT_NAME_INDEX);
2952                                 String syncAccountType =
2953                                         cursor.getString(EVENTS_SYNC_ACCOUNT_TYPE_INDEX);
2954                                 Long calId = cursor.getLong(EVENTS_CALENDAR_ID_INDEX);
2955 
2956                                 ContentValues values = new ContentValues();
2957                                 values.put(Events._SYNC_ID, syncId);
2958                                 values.put(Events._SYNC_VERSION, syncVersion);
2959                                 values.put(Events._SYNC_ACCOUNT, syncAccountName);
2960                                 values.put(Events._SYNC_ACCOUNT_TYPE, syncAccountType);
2961                                 values.put(Events.CALENDAR_ID, calId);
2962                                 mDeletedEventsInserter.insert(values);
2963 
2964                                 // TODO: we may also want to delete exception
2965                                 // events for this event (in case this was a
2966                                 // recurring event).  We can do that with the
2967                                 // following code:
2968                                 // db.delete("Events", "originalEvent=?", new String[] {syncId});
2969                             }
2970 
2971                             // If this was a recurring event or a recurrence
2972                             // exception, then force a recalculation of the
2973                             // instances.
2974                             String rrule = cursor.getString(EVENTS_RRULE_INDEX);
2975                             String rdate = cursor.getString(EVENTS_RDATE_INDEX);
2976                             String origEvent = cursor.getString(EVENTS_ORIGINAL_EVENT_INDEX);
2977                             if (!TextUtils.isEmpty(rrule) || !TextUtils.isEmpty(rdate)
2978                                     || !TextUtils.isEmpty(origEvent)) {
2979                                 mMetaData.clearInstanceRange();
2980                             }
2981                         }
2982                     } finally {
2983                         cursor.close();
2984                         cursor = null;
2985                     }
2986                     triggerAppWidgetUpdate(-1);
2987                 }
2988 
2989                 // There is a delete trigger that will cause all instances
2990                 // matching this event id to get deleted as well.  In fact, all
2991                 // of the following tables will remove entries matching this
2992                 // event id: Instances, EventsRawTimes, Attendees, Reminders,
2993                 // CalendarAlerts, and ExtendedProperties.
2994                 int result = db.delete("Events", "_id=" + id, null);
2995                 return result;
2996             }
2997             case ATTENDEES:
2998             {
2999                 int result = db.delete("Attendees", where, whereArgs);
3000                 return result;
3001             }
3002             case ATTENDEES_ID:
3003             {
3004                 // we currently don't support deletions to the attendees list.
3005                 // TODO: remove this restriction when we handle the full attendees
3006                 // feed.  we'll need to put in some logic to check that the
3007                 // modification will be allowed by the server.
3008                 throw new IllegalArgumentException("Cannot delete attendees.");
3009                 //                String id = url.getPathSegments().get(1);
3010                 //                int result = db.delete("Attendees", "_id="+id, null);
3011                 //                return result;
3012             }
3013             case REMINDERS:
3014             {
3015                 int result = db.delete("Reminders", where, whereArgs);
3016                 return result;
3017             }
3018             case REMINDERS_ID:
3019             {
3020                 String id = url.getLastPathSegment();
3021                 int result = db.delete("Reminders", "_id="+id, null);
3022                 return result;
3023             }
3024             case CALENDAR_ALERTS:
3025             {
3026                 int result = db.delete("CalendarAlerts", where, whereArgs);
3027                 return result;
3028             }
3029             case CALENDAR_ALERTS_ID:
3030             {
3031                 String id = url.getLastPathSegment();
3032                 int result = db.delete("CalendarAlerts", "_id="+id, null);
3033                 return result;
3034             }
3035             case DELETED_EVENTS:
3036             case EVENTS:
3037                 throw new UnsupportedOperationException("Cannot delete that URL");
3038             case CALENDARS_ID:
3039                 StringBuilder whereSb = new StringBuilder("_id=");
3040                 whereSb.append(url.getPathSegments().get(1));
3041                 if (!TextUtils.isEmpty(where)) {
3042                     whereSb.append(" AND (");
3043                     whereSb.append(where);
3044                     whereSb.append(')');
3045                 }
3046                 where = whereSb.toString();
3047                 // fall through to CALENDARS for the actual delete
3048             case CALENDARS:
3049                 return deleteMatchingCalendars(where);
3050             case INSTANCES:
3051             case INSTANCES_BY_DAY:
3052                 throw new UnsupportedOperationException("Cannot delete that URL");
3053             default:
3054                 throw new IllegalArgumentException("Unknown URL " + url);
3055         }
3056     }
3057 
deleteMatchingCalendars(String where)3058     private int deleteMatchingCalendars(String where) {
3059         // query to find all the calendars that match, for each
3060         // - delete calendar subscription
3061         // - delete calendar
3062 
3063         int numDeleted = 0;
3064         final SQLiteDatabase db = getDatabase();
3065         Cursor c = db.query("Calendars", sCalendarsIdProjection, where, null,
3066                 null, null, null);
3067         if (c == null) {
3068             return 0;
3069         }
3070         try {
3071             while (c.moveToNext()) {
3072                 long id = c.getLong(CALENDARS_INDEX_ID);
3073                 if (!isTemporary()) {
3074                     modifyCalendarSubscription(id, false /* not selected */);
3075                 }
3076                 c.deleteRow();
3077                 numDeleted++;
3078             }
3079         } finally {
3080             c.close();
3081         }
3082         return numDeleted;
3083     }
3084 
3085     // TODO: call calculateLastDate()!
3086     @Override
updateInternal(Uri url, ContentValues values, String where, String[] selectionArgs)3087     public int updateInternal(Uri url, ContentValues values,
3088             String where, String[] selectionArgs) {
3089         int match = sURLMatcher.match(url);
3090 
3091         // TODO: remove this restriction
3092         if (!TextUtils.isEmpty(where) && match != CALENDAR_ALERTS) {
3093             throw new IllegalArgumentException(
3094                     "WHERE based updates not supported");
3095         }
3096         final SQLiteDatabase db = getDatabase();
3097 
3098         switch (match) {
3099             case CALENDARS_ID:
3100             {
3101                 long id = ContentUris.parseId(url);
3102                 Integer syncEvents = values.getAsInteger(Calendars.SYNC_EVENTS);
3103                 if (syncEvents != null && !isTemporary()) {
3104                     modifyCalendarSubscription(id, syncEvents == 1);
3105                 }
3106 
3107                 int result = db.update("Calendars", values, "_id="+ id, null);
3108                 if (!isTemporary()) {
3109                     // When we change the display status of a Calendar
3110                     // we need to update the busy bits.
3111                     if (values.containsKey(Calendars.SELECTED) || (syncEvents != null)) {
3112                         // Clear the BusyBits table.
3113                         mMetaData.clearBusyBitRange();
3114                     }
3115                 }
3116 
3117                 return result;
3118             }
3119             case EVENTS_ID:
3120             {
3121                 long id = ContentUris.parseId(url);
3122                 if (!isTemporary()) {
3123                     values.put(Events._SYNC_DIRTY, 1);
3124 
3125                     // Disallow updating the attendee status in the Events
3126                     // table.  In the future, we could support this but we
3127                     // would have to query and update the attendees table
3128                     // to keep the values consistent.
3129                     if (values.containsKey(Events.SELF_ATTENDEE_STATUS)) {
3130                         throw new IllegalArgumentException("Updating "
3131                                 + Events.SELF_ATTENDEE_STATUS
3132                                 + " in Events table is not allowed.");
3133                     }
3134 
3135                     if (values.containsKey(Events.HTML_URI)) {
3136                         throw new IllegalArgumentException("Updating "
3137                                 + Events.HTML_URI
3138                                 + " in Events table is not allowed.");
3139                     }
3140 
3141                     updateBusyBitsLocked(id, values);
3142                 }
3143 
3144                 ContentValues updatedValues = updateContentValuesFromEvent(values);
3145                 if (updatedValues == null) {
3146                     Log.w(TAG, "Could not update event.");
3147                     return 0;
3148                 }
3149 
3150                 int result = db.update("Events", updatedValues, "_id="+id, null);
3151                 if (!isTemporary()) {
3152                     if (result > 0) {
3153                         updateEventRawTimesLocked(id, updatedValues);
3154                         updateInstancesLocked(updatedValues, id, false /* not a new event */, db);
3155 
3156                         if (values.containsKey(Events.DTSTART)) {
3157                             // The start time of the event changed, so run the
3158                             // event alarm scheduler.
3159                             if (Log.isLoggable(TAG, Log.DEBUG)) {
3160                                 Log.d(TAG, "updateInternal() changing event");
3161                             }
3162                             scheduleNextAlarm(false /* do not remove alarms */);
3163                             triggerAppWidgetUpdate(id);
3164                         }
3165                     }
3166                 }
3167                 return result;
3168             }
3169             case ATTENDEES_ID:
3170             {
3171                 // Copy the attendee status value to the Events table.
3172                 updateEventAttendeeStatus(db, values);
3173 
3174                 long id = ContentUris.parseId(url);
3175                 return db.update("Attendees", values, "_id="+id, null);
3176             }
3177             case CALENDAR_ALERTS_ID:
3178             {
3179                 long id = ContentUris.parseId(url);
3180                 return db.update("CalendarAlerts", values, "_id="+id, null);
3181             }
3182             case CALENDAR_ALERTS:
3183             {
3184                 return db.update("CalendarAlerts", values, where, null);
3185             }
3186             case REMINDERS_ID:
3187             {
3188                 long id = ContentUris.parseId(url);
3189                 int result = db.update("Reminders", values, "_id="+id, null);
3190                 if (!isTemporary()) {
3191                     // Reschedule the event alarms because the
3192                     // "minutes" field may have changed.
3193                     if (Log.isLoggable(TAG, Log.DEBUG)) {
3194                         Log.d(TAG, "updateInternal() changing reminder");
3195                     }
3196                     scheduleNextAlarm(false /* do not remove alarms */);
3197                 }
3198                 return result;
3199             }
3200             case EXTENDED_PROPERTIES_ID:
3201             {
3202                 long id = ContentUris.parseId(url);
3203                 return db.update("ExtendedProperties", values, "_id="+id, null);
3204             }
3205             default:
3206                 throw new IllegalArgumentException("Unknown URL " + url);
3207         }
3208     }
3209 
3210     /**
3211      * Schedule a calendar sync for the account.
3212      * @param account the account for which to schedule a sync
3213      * @param uploadChangesOnly if set, specify that the sync should only send
3214      *   up local changes
3215      * @param url the url feed for the calendar to sync (may be null)
3216      */
scheduleSync(Account account, boolean uploadChangesOnly, String url)3217     private void scheduleSync(Account account, boolean uploadChangesOnly, String url) {
3218         Bundle extras = new Bundle();
3219         extras.putBoolean(ContentResolver.SYNC_EXTRAS_UPLOAD, uploadChangesOnly);
3220         if (url != null) {
3221             extras.putString("feed", url);
3222             extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true);
3223         }
3224         ContentResolver.requestSync(account, Calendars.CONTENT_URI.getAuthority(), extras);
3225     }
3226 
modifyCalendarSubscription(long id, boolean syncEvents)3227     private void modifyCalendarSubscription(long id, boolean syncEvents) {
3228         // get the account, url, and current selected state
3229         // for this calendar.
3230         Cursor cursor = query(ContentUris.withAppendedId(Calendars.CONTENT_URI, id),
3231                 new String[] {Calendars._SYNC_ACCOUNT, Calendars._SYNC_ACCOUNT_TYPE,
3232                         Calendars.URL, Calendars.SYNC_EVENTS},
3233                 null /* selection */,
3234                 null /* selectionArgs */,
3235                 null /* sort */);
3236 
3237         Account account = null;
3238         String calendarUrl = null;
3239         boolean oldSyncEvents = false;
3240         if (cursor != null && cursor.moveToFirst()) {
3241             try {
3242                 final String accountName = cursor.getString(0);
3243                 final String accountType = cursor.getString(1);
3244                 account = new Account(accountName, accountType);
3245                 calendarUrl = cursor.getString(2);
3246                 oldSyncEvents = (cursor.getInt(3) != 0);
3247             } finally {
3248                 cursor.close();
3249             }
3250         }
3251 
3252         if (account == null || TextUtils.isEmpty(calendarUrl)) {
3253             // should not happen?
3254             Log.w(TAG, "Cannot update subscription because account "
3255                     + "or calendar url empty -- should not happen.");
3256             return;
3257         }
3258 
3259         if (oldSyncEvents == syncEvents) {
3260             // nothing to do
3261             return;
3262         }
3263 
3264         // If we are no longer syncing a calendar then make sure that the
3265         // old calendar sync data is cleared.  Then if we later add this
3266         // calendar back, we will sync all the events.
3267         if (!syncEvents) {
3268             byte[] data = readSyncDataBytes(account);
3269             GDataSyncData syncData = AbstractGDataSyncAdapter.newGDataSyncDataFromBytes(data);
3270             if (syncData != null) {
3271                 syncData.feedData.remove(calendarUrl);
3272                 data = AbstractGDataSyncAdapter.newBytesFromGDataSyncData(syncData);
3273                 writeSyncDataBytes(account, data);
3274             }
3275 
3276             // Delete all of the events in this calendar to save space.
3277             // This is the closest we can come to deleting a calendar.
3278             // Clients should never actually delete a calendar.  That won't
3279             // work.  We need to keep the calendar entry in the Calendars table
3280             // in order to know not to sync the events for that calendar from
3281             // the server.
3282             final SQLiteDatabase db = getDatabase();
3283             String[] args = new String[] {Long.toString(id)};
3284             db.delete("Events", CALENDAR_ID_SELECTION, args);
3285             // Note that we do not delete the matching entries
3286             // in the DeletedEvents table.  We will let those
3287             // deleted events propagate to the server.
3288 
3289             // TODO: cancel any pending/ongoing syncs for this calendar.
3290 
3291             // TODO: there is a corner case to deal with here: namely, if
3292             // we edit or delete an event on the phone and then remove
3293             // (that is, stop syncing) a calendar, and if we also make a
3294             // change on the server to that event at about the same time,
3295             // then we will never propagate the changes from the phone to
3296             // the server.
3297         }
3298 
3299         // If the calendar is not selected for syncing, then don't download
3300         // events.
3301         scheduleSync(account, !syncEvents, calendarUrl);
3302     }
3303 
3304     @Override
onSyncStop(SyncContext context, boolean success)3305     public void onSyncStop(SyncContext context, boolean success) {
3306         super.onSyncStop(context, success);
3307         if (Log.isLoggable(TAG, Log.DEBUG)) {
3308             Log.d(TAG, "onSyncStop() success: " + success);
3309         }
3310         scheduleNextAlarm(false /* do not remove alarms */);
3311         triggerAppWidgetUpdate(-1);
3312     }
3313 
3314     @Override
getMergers()3315     protected Iterable<EventMerger> getMergers() {
3316         return Collections.singletonList(new EventMerger());
3317     }
3318 
3319     /**
3320      * Update any existing widgets with the changed events.
3321      *
3322      * @param changedEventId Specific event known to be changed, otherwise -1.
3323      *            If present, we use it to decide if an update is necessary.
3324      */
triggerAppWidgetUpdate(long changedEventId)3325     private synchronized void triggerAppWidgetUpdate(long changedEventId) {
3326         Context context = getContext();
3327         if (context != null) {
3328             mAppWidgetProvider.providerUpdated(context, changedEventId);
3329         }
3330     }
3331 
bootCompleted()3332     void bootCompleted() {
3333         // Remove alarms from the CalendarAlerts table that have been marked
3334         // as "scheduled" but not fired yet.  We do this because the
3335         // AlarmManagerService loses all information about alarms when the
3336         // power turns off but we store the information in a database table
3337         // that persists across reboots. See the documentation for
3338         // scheduleNextAlarmLocked() for more information.
3339         scheduleNextAlarm(true /* remove alarms */);
3340     }
3341 
3342     /* Retrieve and cache the alarm manager */
getAlarmManager()3343     private AlarmManager getAlarmManager() {
3344         synchronized(mAlarmLock) {
3345             if (mAlarmManager == null) {
3346                 Context context = getContext();
3347                 if (context == null) {
3348                     Log.e(TAG, "getAlarmManager() cannot get Context");
3349                     return null;
3350                 }
3351                 Object service = context.getSystemService(Context.ALARM_SERVICE);
3352                 mAlarmManager = (AlarmManager) service;
3353             }
3354             return mAlarmManager;
3355         }
3356     }
3357 
scheduleNextAlarmCheck(long triggerTime)3358     void scheduleNextAlarmCheck(long triggerTime) {
3359         AlarmManager manager = getAlarmManager();
3360         if (manager == null) {
3361             Log.e(TAG, "scheduleNextAlarmCheck() cannot get AlarmManager");
3362             return;
3363         }
3364         Context context = getContext();
3365         Intent intent = new Intent(CalendarReceiver.SCHEDULE);
3366         intent.setClass(context, CalendarReceiver.class);
3367         PendingIntent pending = PendingIntent.getBroadcast(context,
3368                 0, intent, PendingIntent.FLAG_NO_CREATE);
3369         if (pending != null) {
3370             // Cancel any previous alarms that do the same thing.
3371             manager.cancel(pending);
3372         }
3373         pending = PendingIntent.getBroadcast(context,
3374                 0, intent, PendingIntent.FLAG_CANCEL_CURRENT);
3375 
3376         if (Log.isLoggable(TAG, Log.DEBUG)) {
3377             Time time = new Time();
3378             time.set(triggerTime);
3379             String timeStr = time.format(" %a, %b %d, %Y %I:%M%P");
3380             Log.d(TAG, "scheduleNextAlarmCheck at: " + triggerTime + timeStr);
3381         }
3382 
3383         manager.set(AlarmManager.RTC_WAKEUP, triggerTime, pending);
3384     }
3385 
3386     /*
3387      * This method runs the alarm scheduler in a background thread.
3388      */
scheduleNextAlarm(boolean removeAlarms)3389     void scheduleNextAlarm(boolean removeAlarms) {
3390         Thread thread = new AlarmScheduler(removeAlarms);
3391         thread.start();
3392     }
3393 
3394     /**
3395      * This method runs in a background thread and schedules an alarm for
3396      * the next calendar event, if necessary.
3397      */
runScheduleNextAlarm(boolean removeAlarms)3398     private void runScheduleNextAlarm(boolean removeAlarms) {
3399         // Do not schedule any alarms if this is a temporary database.
3400         if (isTemporary()) {
3401             return;
3402         }
3403 
3404         final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
3405         db.beginTransaction();
3406         try {
3407             if (removeAlarms) {
3408                 removeScheduledAlarmsLocked(db);
3409             }
3410             scheduleNextAlarmLocked(db);
3411             db.setTransactionSuccessful();
3412         } finally {
3413             db.endTransaction();
3414         }
3415     }
3416 
3417     /**
3418      * This method looks at the 24-hour window from now for any events that it
3419      * needs to schedule.  This method runs within a database transaction. It
3420      * also runs in a background thread.
3421      *
3422      * The CalendarProvider keeps track of which alarms it has already scheduled
3423      * to avoid scheduling them more than once and for debugging problems with
3424      * alarms.  It stores this knowledge in a database table called CalendarAlerts
3425      * which persists across reboots.  But the actual alarm list is in memory
3426      * and disappears if the phone loses power.  To avoid missing an alarm, we
3427      * clear the entries in the CalendarAlerts table when we start up the
3428      * CalendarProvider.
3429      *
3430      * Scheduling an alarm multiple times is not tragic -- we filter out the
3431      * extra ones when we receive them. But we still need to keep track of the
3432      * scheduled alarms. The main reason is that we need to prevent multiple
3433      * notifications for the same alarm (on the receive side) in case we
3434      * accidentally schedule the same alarm multiple times.  We don't have
3435      * visibility into the system's alarm list so we can never know for sure if
3436      * we have already scheduled an alarm and it's better to err on scheduling
3437      * an alarm twice rather than missing an alarm.  Another reason we keep
3438      * track of scheduled alarms in a database table is that it makes it easy to
3439      * run an SQL query to find the next reminder that we haven't scheduled.
3440      *
3441      * @param db the database
3442      */
scheduleNextAlarmLocked(SQLiteDatabase db)3443     private void scheduleNextAlarmLocked(SQLiteDatabase db) {
3444         AlarmManager alarmManager = getAlarmManager();
3445         if (alarmManager == null) {
3446             Log.e(TAG, "Failed to find the AlarmManager. Could not schedule the next alarm!");
3447             return;
3448         }
3449 
3450         final long currentMillis = System.currentTimeMillis();
3451         final long start = currentMillis - SCHEDULE_ALARM_SLACK;
3452         final long end = start + (24 * 60 * 60 * 1000);
3453         ContentResolver cr = getContext().getContentResolver();
3454         if (Log.isLoggable(TAG, Log.DEBUG)) {
3455             Time time = new Time();
3456             time.set(start);
3457             String startTimeStr = time.format(" %a, %b %d, %Y %I:%M%P");
3458             Log.d(TAG, "runScheduleNextAlarm() start search: " + startTimeStr);
3459         }
3460 
3461         // Clear old alarms but keep alarms around for a while to prevent
3462         // multiple alerts for the same reminder.  The "clearUpToTime'
3463         // should be further in the past than the point in time where
3464         // we start searching for events (the "start" variable defined above).
3465         long clearUpToTime = currentMillis - CLEAR_OLD_ALARM_THRESHOLD;
3466         db.delete("CalendarAlerts", CalendarAlerts.ALARM_TIME + "<" + clearUpToTime, null);
3467 
3468         long nextAlarmTime = end;
3469         long alarmTime = CalendarAlerts.findNextAlarmTime(cr, currentMillis);
3470         if (alarmTime != -1 && alarmTime < nextAlarmTime) {
3471             nextAlarmTime = alarmTime;
3472         }
3473 
3474         // Extract events from the database sorted by alarm time.  The
3475         // alarm times are computed from Instances.begin (whose units
3476         // are milliseconds) and Reminders.minutes (whose units are
3477         // minutes).
3478         //
3479         // Also, ignore events whose end time is already in the past.
3480         // Also, ignore events alarms that we have already scheduled.
3481         //
3482         // Note 1: we can add support for the case where Reminders.minutes
3483         // equals -1 to mean use Calendars.minutes by adding a UNION for
3484         // that case where the two halves restrict the WHERE clause on
3485         // Reminders.minutes != -1 and Reminders.minutes = 1, respectively.
3486         //
3487         // Note 2: we have to name "myAlarmTime" different from the
3488         // "alarmTime" column in CalendarAlerts because otherwise the
3489         // query won't find multiple alarms for the same event.
3490         String query = "SELECT begin-(minutes*60000) AS myAlarmTime,"
3491                 + " Instances.event_id AS eventId, begin, end,"
3492                 + " title, allDay, method, minutes"
3493                 + " FROM Instances INNER JOIN Events"
3494                 + " ON (Events._id = Instances.event_id)"
3495                 + " INNER JOIN Reminders"
3496                 + " ON (Instances.event_id = Reminders.event_id)"
3497                 + " WHERE method=" + Reminders.METHOD_ALERT
3498                 + " AND myAlarmTime>=" + start
3499                 + " AND myAlarmTime<=" + nextAlarmTime
3500                 + " AND end>=" + currentMillis
3501                 + " AND 0=(SELECT count(*) from CalendarAlerts CA"
3502                 + " where CA.event_id=Instances.event_id AND CA.begin=Instances.begin"
3503                 + " AND CA.alarmTime=myAlarmTime)"
3504                 + " ORDER BY myAlarmTime,begin,title";
3505 
3506         acquireInstanceRangeLocked(start, end, false /* don't use minimum expansion windows */);
3507         Cursor cursor = null;
3508         try {
3509             cursor = db.rawQuery(query, null);
3510 
3511             int beginIndex = cursor.getColumnIndex(Instances.BEGIN);
3512             int endIndex = cursor.getColumnIndex(Instances.END);
3513             int eventIdIndex = cursor.getColumnIndex("eventId");
3514             int alarmTimeIndex = cursor.getColumnIndex("myAlarmTime");
3515             int minutesIndex = cursor.getColumnIndex(Reminders.MINUTES);
3516 
3517             if (Log.isLoggable(TAG, Log.DEBUG)) {
3518                 Time time = new Time();
3519                 time.set(nextAlarmTime);
3520                 String alarmTimeStr = time.format(" %a, %b %d, %Y %I:%M%P");
3521                 Log.d(TAG, "nextAlarmTime: " + alarmTimeStr
3522                         + " cursor results: " + cursor.getCount()
3523                         + " query: " + query);
3524             }
3525 
3526             while (cursor.moveToNext()) {
3527                 // Schedule all alarms whose alarm time is as early as any
3528                 // scheduled alarm.  For example, if the earliest alarm is at
3529                 // 1pm, then we will schedule all alarms that occur at 1pm
3530                 // but no alarms that occur later than 1pm.
3531                 // Actually, we allow alarms up to a minute later to also
3532                 // be scheduled so that we don't have to check immediately
3533                 // again after an event alarm goes off.
3534                 alarmTime = cursor.getLong(alarmTimeIndex);
3535                 long eventId = cursor.getLong(eventIdIndex);
3536                 int minutes = cursor.getInt(minutesIndex);
3537                 long startTime = cursor.getLong(beginIndex);
3538 
3539                 if (Log.isLoggable(TAG, Log.DEBUG)) {
3540                     int titleIndex = cursor.getColumnIndex(Events.TITLE);
3541                     String title = cursor.getString(titleIndex);
3542                     Time time = new Time();
3543                     time.set(alarmTime);
3544                     String schedTime = time.format(" %a, %b %d, %Y %I:%M%P");
3545                     time.set(startTime);
3546                     String startTimeStr = time.format(" %a, %b %d, %Y %I:%M%P");
3547                     long endTime = cursor.getLong(endIndex);
3548                     time.set(endTime);
3549                     String endTimeStr = time.format(" - %a, %b %d, %Y %I:%M%P");
3550                     time.set(currentMillis);
3551                     String currentTimeStr = time.format(" %a, %b %d, %Y %I:%M%P");
3552                     Log.d(TAG, "  looking at id: " + eventId + " " + title
3553                             + " " + startTime
3554                             + startTimeStr + endTimeStr + " alarm: "
3555                             + alarmTime + schedTime
3556                             + " currentTime: " + currentTimeStr);
3557                 }
3558 
3559                 if (alarmTime < nextAlarmTime) {
3560                     nextAlarmTime = alarmTime;
3561                 } else if (alarmTime > nextAlarmTime + android.text.format.DateUtils.MINUTE_IN_MILLIS) {
3562                     // This event alarm (and all later ones) will be scheduled
3563                     // later.
3564                     break;
3565                 }
3566 
3567                 // Avoid an SQLiteContraintException by checking if this alarm
3568                 // already exists in the table.
3569                 if (CalendarAlerts.alarmExists(cr, eventId, startTime, alarmTime)) {
3570                     if (Log.isLoggable(TAG, Log.DEBUG)) {
3571                         int titleIndex = cursor.getColumnIndex(Events.TITLE);
3572                         String title = cursor.getString(titleIndex);
3573                         Log.d(TAG, "  alarm exists for id: " + eventId + " " + title);
3574                     }
3575                     continue;
3576                 }
3577 
3578                 // Insert this alarm into the CalendarAlerts table
3579                 long endTime = cursor.getLong(endIndex);
3580                 Uri uri = CalendarAlerts.insert(cr, eventId, startTime,
3581                         endTime, alarmTime, minutes);
3582                 if (uri == null) {
3583                     Log.e(TAG, "runScheduleNextAlarm() insert into CalendarAlerts table failed");
3584                     continue;
3585                 }
3586 
3587                 Intent intent = new Intent(android.provider.Calendar.EVENT_REMINDER_ACTION);
3588                 intent.setData(uri);
3589 
3590                 // Also include the begin and end time of this event, because
3591                 // we cannot determine that from the Events database table.
3592                 intent.putExtra(android.provider.Calendar.EVENT_BEGIN_TIME, startTime);
3593                 intent.putExtra(android.provider.Calendar.EVENT_END_TIME, endTime);
3594                 if (Log.isLoggable(TAG, Log.DEBUG)) {
3595                     int titleIndex = cursor.getColumnIndex(Events.TITLE);
3596                     String title = cursor.getString(titleIndex);
3597                     Time time = new Time();
3598                     time.set(alarmTime);
3599                     String schedTime = time.format(" %a, %b %d, %Y %I:%M%P");
3600                     time.set(startTime);
3601                     String startTimeStr = time.format(" %a, %b %d, %Y %I:%M%P");
3602                     time.set(endTime);
3603                     String endTimeStr = time.format(" %a, %b %d, %Y %I:%M%P");
3604                     time.set(currentMillis);
3605                     String currentTimeStr = time.format(" %a, %b %d, %Y %I:%M%P");
3606                     Log.d(TAG, "  scheduling " + title
3607                             + startTimeStr  + " - " + endTimeStr + " alarm: " + schedTime
3608                             + " currentTime: " + currentTimeStr
3609                             + " uri: " + uri);
3610                 }
3611                 PendingIntent sender = PendingIntent.getBroadcast(getContext(),
3612                         0, intent, PendingIntent.FLAG_CANCEL_CURRENT);
3613                 alarmManager.set(AlarmManager.RTC_WAKEUP, alarmTime, sender);
3614             }
3615         } finally {
3616             if (cursor != null) {
3617                 cursor.close();
3618             }
3619         }
3620 
3621         // If we scheduled an event alarm, then schedule the next alarm check
3622         // for one minute past that alarm.  Otherwise, if there were no
3623         // event alarms scheduled, then check again in 24 hours.  If a new
3624         // event is inserted before the next alarm check, then this method
3625         // will be run again when the new event is inserted.
3626         if (nextAlarmTime != Long.MAX_VALUE) {
3627             scheduleNextAlarmCheck(nextAlarmTime + android.text.format.DateUtils.MINUTE_IN_MILLIS);
3628         } else {
3629             scheduleNextAlarmCheck(currentMillis + android.text.format.DateUtils.DAY_IN_MILLIS);
3630         }
3631     }
3632 
3633     /**
3634      * Removes the entries in the CalendarAlerts table for alarms that we have
3635      * scheduled but that have not fired yet. We do this to ensure that we
3636      * don't miss an alarm.  The CalendarAlerts table keeps track of the
3637      * alarms that we have scheduled but the actual alarm list is in memory
3638      * and will be cleared if the phone reboots.
3639      *
3640      * We don't need to remove entries that have already fired, and in fact
3641      * we should not remove them because we need to display the notifications
3642      * until the user dismisses them.
3643      *
3644      * We could remove entries that have fired and been dismissed, but we leave
3645      * them around for a while because it makes it easier to debug problems.
3646      * Entries that are old enough will be cleaned up later when we schedule
3647      * new alarms.
3648      */
removeScheduledAlarmsLocked(SQLiteDatabase db)3649     private void removeScheduledAlarmsLocked(SQLiteDatabase db) {
3650         if (Log.isLoggable(TAG, Log.DEBUG)) {
3651             Log.d(TAG, "removing scheduled alarms");
3652         }
3653         db.delete(CalendarAlerts.TABLE_NAME,
3654                 CalendarAlerts.STATE + "=" + CalendarAlerts.SCHEDULED, null /* whereArgs */);
3655     }
3656 
3657     private static String sEventsTable = "Events";
3658     private static String sDeletedEventsTable = "DeletedEvents";
3659     private static String sAttendeesTable = "Attendees";
3660     private static String sRemindersTable = "Reminders";
3661     private static String sCalendarAlertsTable = "CalendarAlerts";
3662     private static String sExtendedPropertiesTable = "ExtendedProperties";
3663 
3664     private class EventMerger extends AbstractTableMerger {
3665 
3666         private ContentValues mValues = new ContentValues();
EventMerger()3667         EventMerger() {
3668             super(getDatabase(), sEventsTable, Calendar.Events.CONTENT_URI,
3669                     sDeletedEventsTable, Calendar.Events.DELETED_CONTENT_URI);
3670         }
3671 
3672         @Override
notifyChanges()3673         protected void notifyChanges() {
3674             getContext().getContentResolver().notifyChange(Events.CONTENT_URI,
3675                     null /* observer */, false /* do not sync to network */);
3676         }
3677 
3678         @Override
cursorRowToContentValues(Cursor cursor, ContentValues map)3679         protected void cursorRowToContentValues(Cursor cursor, ContentValues map) {
3680             rowToContentValues(cursor, map);
3681         }
3682 
3683         @Override
insertRow(ContentProvider diffs, Cursor diffsCursor)3684         public void insertRow(ContentProvider diffs, Cursor diffsCursor) {
3685             rowToContentValues(diffsCursor, mValues);
3686             final SQLiteDatabase db = getDatabase();
3687             long rowId = mEventsInserter.insert(mValues);
3688             if (rowId <= 0) {
3689                 Log.e(TAG, "Unable to insert values into calendar db: " + mValues);
3690                 return;
3691             }
3692 
3693             long diffsRowId = diffsCursor.getLong(
3694                     diffsCursor.getColumnIndex(Events._ID));
3695 
3696             insertAttendees(diffs, diffsRowId, rowId, db);
3697             insertRemindersIfNecessary(diffs, diffsRowId, rowId, db);
3698             insertExtendedPropertiesIfNecessary(diffs, diffsRowId, rowId, db);
3699             updateEventRawTimesLocked(rowId, mValues);
3700             updateInstancesLocked(mValues, rowId, true /* new event */, db);
3701             insertBusyBitsLocked(rowId, mValues);
3702 
3703             // Update the _SYNC_DIRTY flag of the event. We have to do this
3704             // after inserting since the update of the reminders and extended properties
3705             // methods will fire a sql trigger that will cause this flag to
3706             // be set.
3707             clearSyncDirtyFlag(db, rowId);
3708         }
3709 
clearSyncDirtyFlag(SQLiteDatabase db, long rowId)3710         private void clearSyncDirtyFlag(SQLiteDatabase db, long rowId) {
3711             mValues.clear();
3712             mValues.put(Events._SYNC_DIRTY, 0);
3713             db.update(mTable, mValues, Events._ID + '=' + rowId, null);
3714         }
3715 
insertAttendees(ContentProvider diffs, long diffsRowId, long rowId, SQLiteDatabase db)3716         private void insertAttendees(ContentProvider diffs,
3717                 long diffsRowId,
3718                 long rowId,
3719                 SQLiteDatabase db) {
3720             // query attendees in diffs
3721             Cursor attendeesCursor =
3722                     diffs.query(Attendees.CONTENT_URI, null,
3723                             "event_id=" + diffsRowId, null, null);
3724             ContentValues attendeesValues = new ContentValues();
3725             try {
3726                 while (attendeesCursor.moveToNext()) {
3727                     attendeesValues.clear();
3728                     DatabaseUtils.cursorStringToContentValues(attendeesCursor,
3729                             Attendees.ATTENDEE_NAME,
3730                             attendeesValues);
3731                     DatabaseUtils.cursorStringToContentValues(attendeesCursor,
3732                             Attendees.ATTENDEE_EMAIL,
3733                             attendeesValues);
3734                     DatabaseUtils.cursorIntToContentValues(attendeesCursor,
3735                             Attendees.ATTENDEE_STATUS,
3736                             attendeesValues);
3737                     DatabaseUtils.cursorIntToContentValues(attendeesCursor,
3738                             Attendees.ATTENDEE_TYPE,
3739                             attendeesValues);
3740                     DatabaseUtils.cursorIntToContentValues(attendeesCursor,
3741                             Attendees.ATTENDEE_RELATIONSHIP,
3742                             attendeesValues);
3743                     attendeesValues.put(Attendees.EVENT_ID, rowId);
3744                     mAttendeesInserter.insert(attendeesValues);
3745                 }
3746             } finally {
3747                 if (attendeesCursor != null) {
3748                     attendeesCursor.close();
3749                 }
3750             }
3751         }
3752 
insertRemindersIfNecessary(ContentProvider diffs, long diffsRowId, long rowId, SQLiteDatabase db)3753         private void insertRemindersIfNecessary(ContentProvider diffs,
3754                 long diffsRowId,
3755                 long rowId,
3756                 SQLiteDatabase db) {
3757             // insert reminders, if necessary.
3758             Integer hasAlarm = mValues.getAsInteger(Events.HAS_ALARM);
3759             if (hasAlarm != null && hasAlarm.intValue() == 1) {
3760                 // query reminders in diffs
3761                 Cursor reminderCursor =
3762                         diffs.query(Reminders.CONTENT_URI, null,
3763                                 "event_id=" + diffsRowId, null, null);
3764                 ContentValues reminderValues = new ContentValues();
3765                 try {
3766                     while (reminderCursor.moveToNext()) {
3767                         reminderValues.clear();
3768                         DatabaseUtils.cursorIntToContentValues(reminderCursor,
3769                                 Reminders.METHOD,
3770                                 reminderValues);
3771                         DatabaseUtils.cursorIntToContentValues(reminderCursor,
3772                                 Reminders.MINUTES,
3773                                 reminderValues);
3774                         reminderValues.put(Reminders.EVENT_ID, rowId);
3775                         mRemindersInserter.insert(reminderValues);
3776                     }
3777                 } finally {
3778                     if (reminderCursor != null) {
3779                         reminderCursor.close();
3780                     }
3781                 }
3782             }
3783         }
3784 
insertExtendedPropertiesIfNecessary(ContentProvider diffs, long diffsRowId, long rowId, SQLiteDatabase db)3785         private void insertExtendedPropertiesIfNecessary(ContentProvider diffs,
3786                 long diffsRowId,
3787                 long rowId,
3788                 SQLiteDatabase db) {
3789             // insert extended properties, if necessary.
3790             Integer hasExtendedProperties = mValues.getAsInteger(Events.HAS_EXTENDED_PROPERTIES);
3791             if (hasExtendedProperties != null && hasExtendedProperties.intValue() != 0) {
3792                 // query reminders in diffs
3793                 Cursor extendedPropertiesCursor =
3794                         diffs.query(Calendar.ExtendedProperties.CONTENT_URI, null,
3795                                 "event_id=" + diffsRowId, null, null);
3796                 ContentValues extendedPropertiesValues = new ContentValues();
3797                 try {
3798                     while (extendedPropertiesCursor.moveToNext()) {
3799                         extendedPropertiesValues.clear();
3800                         DatabaseUtils.cursorStringToContentValues(extendedPropertiesCursor,
3801                                 Calendar.ExtendedProperties.NAME, extendedPropertiesValues);
3802                         DatabaseUtils.cursorStringToContentValues(extendedPropertiesCursor,
3803                                 Calendar.ExtendedProperties.VALUE, extendedPropertiesValues);
3804                         extendedPropertiesValues.put(ExtendedProperties.EVENT_ID, rowId);
3805                         mExtendedPropertiesInserter.insert(extendedPropertiesValues);
3806                     }
3807                 } finally {
3808                     if (extendedPropertiesCursor != null) {
3809                         extendedPropertiesCursor.close();
3810                     }
3811                 }
3812             }
3813         }
3814 
3815         @Override
updateRow(long localId, ContentProvider diffs, Cursor diffsCursor)3816         public void updateRow(long localId, ContentProvider diffs,
3817                 Cursor diffsCursor) {
3818             rowToContentValues(diffsCursor, mValues);
3819             final SQLiteDatabase db = getDatabase();
3820             updateBusyBitsLocked(localId, mValues);
3821             int numRows = db.update(mTable, mValues, "_id=" + localId, null /* selectionArgs */);
3822 
3823             if (numRows <= 0) {
3824                 Log.e(TAG, "Unable to update calendar db: " + mValues);
3825                 return;
3826             }
3827 
3828             long diffsRowId = diffsCursor.getLong(
3829                     diffsCursor.getColumnIndex(Events._ID));
3830             // TODO: only update the attendees, reminders, and extended properties if they have
3831             // changed?
3832             // delete the existing attendees, reminders, and extended properties
3833             db.delete(sAttendeesTable, "event_id=" + localId, null /* selectionArgs */);
3834             db.delete(sRemindersTable, "event_id=" + localId, null /* selectionArgs */);
3835             db.delete(sExtendedPropertiesTable, "event_id=" + localId,
3836                     null /* selectionArgs */);
3837 
3838             // process attendees sent by the server.
3839             insertAttendees(diffs, diffsRowId, localId, db);
3840             // process reminders sent by the server.
3841             insertRemindersIfNecessary(diffs, diffsRowId, localId, db);
3842 
3843             // process extended properties sent by the server.
3844             insertExtendedPropertiesIfNecessary(diffs, diffsRowId, localId, db);
3845 
3846             updateEventRawTimesLocked(localId, mValues);
3847             updateInstancesLocked(mValues, localId, false /* not a new event */, db);
3848 
3849             // Update the _SYNC_DIRTY flag of the event. We have to do this
3850             // after updating since the update of the reminders and extended properties
3851             // methods will fire a sql trigger that will cause this flag to
3852             // be set.
3853             clearSyncDirtyFlag(db, localId);
3854         }
3855 
3856         @Override
resolveRow(long localId, String syncId, ContentProvider diffs, Cursor diffsCursor)3857         public void resolveRow(long localId, String syncId,
3858                 ContentProvider diffs, Cursor diffsCursor) {
3859             // server wins
3860             updateRow(localId, diffs, diffsCursor);
3861         }
3862 
3863         @Override
deleteRow(Cursor localCursor)3864         public void deleteRow(Cursor localCursor) {
3865             long localId = localCursor.getLong(localCursor.getColumnIndexOrThrow(Events._ID));
3866             deleteBusyBitsLocked(localId);
3867 
3868             // we have to read this row from the DB since the projection that is used
3869             // by cursor doesn't necessarily contain the columns we need
3870             Cursor c = getDatabase().query(sEventsTable,
3871                     new String[]{Events.RRULE, Events.RDATE, Events.ORIGINAL_EVENT},
3872                     "_id=" + localId, null, null, null, null);
3873             try {
3874                 c.moveToNext();
3875                 // If this was a recurring event or a recurrence exception, then
3876                 // force a recalculation of the instances.
3877                 // We can get a tombstoned recurrence exception
3878                 // that doesn't have a rrule, rdate, or originalEvent, and the
3879                 // check below wouldn't catch that.  However, in practice we also
3880                 // get a different event with a rrule in that case, so the
3881                 // instances get cleared by that rule.
3882                 // This should be re-evaluated when calendar supports gd:deleted.
3883                 String rrule = c.getString(c.getColumnIndexOrThrow(Events.RRULE));
3884                 String rdate = c.getString(c.getColumnIndexOrThrow(Events.RDATE));
3885                 String origEvent = c.getString(c.getColumnIndexOrThrow(Events.ORIGINAL_EVENT));
3886                 if (!TextUtils.isEmpty(rrule) || !TextUtils.isEmpty(rdate)
3887                         || !TextUtils.isEmpty(origEvent)) {
3888                     mMetaData.clearInstanceRange();
3889                 }
3890             } finally {
3891                 c.close();
3892             }
3893             super.deleteRow(localCursor);
3894         }
3895 
rowToContentValues(Cursor diffsCursor, ContentValues values)3896         private void rowToContentValues(Cursor diffsCursor, ContentValues values) {
3897             values.clear();
3898 
3899             DatabaseUtils.cursorStringToContentValues(diffsCursor, Events._SYNC_ID, values);
3900             DatabaseUtils.cursorStringToContentValues(diffsCursor, Events._SYNC_TIME, values);
3901             DatabaseUtils.cursorStringToContentValues(diffsCursor, Events._SYNC_VERSION, values);
3902             DatabaseUtils.cursorStringToContentValues(diffsCursor, Events._SYNC_DIRTY, values);
3903             DatabaseUtils.cursorStringToContentValues(diffsCursor, Events._SYNC_ACCOUNT, values);
3904             DatabaseUtils.cursorStringToContentValues(diffsCursor,
3905                     Events._SYNC_ACCOUNT_TYPE, values);
3906             DatabaseUtils.cursorStringToContentValues(diffsCursor, Events.HTML_URI, values);
3907             DatabaseUtils.cursorStringToContentValues(diffsCursor, Events.TITLE, values);
3908             DatabaseUtils.cursorStringToContentValues(diffsCursor, Events.EVENT_LOCATION, values);
3909             DatabaseUtils.cursorStringToContentValues(diffsCursor, Events.DESCRIPTION, values);
3910             DatabaseUtils.cursorLongToContentValues(diffsCursor, Events.STATUS, values);
3911             DatabaseUtils.cursorIntToContentValues(diffsCursor, Events.SELF_ATTENDEE_STATUS,
3912                     values);
3913             DatabaseUtils.cursorStringToContentValues(diffsCursor, Events.COMMENTS_URI, values);
3914             DatabaseUtils.cursorLongToContentValues(diffsCursor, Events.DTSTART, values);
3915             DatabaseUtils.cursorLongToContentValues(diffsCursor, Events.DTEND, values);
3916             DatabaseUtils.cursorStringToContentValues(diffsCursor, Events.EVENT_TIMEZONE, values);
3917             DatabaseUtils.cursorStringToContentValues(diffsCursor, Events.DURATION, values);
3918             DatabaseUtils.cursorIntToContentValues(diffsCursor, Events.ALL_DAY, values);
3919             DatabaseUtils.cursorIntToContentValues(diffsCursor, Events.VISIBILITY, values);
3920             DatabaseUtils.cursorIntToContentValues(diffsCursor, Events.TRANSPARENCY, values);
3921             DatabaseUtils.cursorIntToContentValues(diffsCursor, Events.HAS_ALARM, values);
3922             DatabaseUtils.cursorIntToContentValues(diffsCursor, Events.HAS_EXTENDED_PROPERTIES,
3923                     values);
3924             DatabaseUtils.cursorStringToContentValues(diffsCursor, Events.RRULE, values);
3925             DatabaseUtils.cursorStringToContentValues(diffsCursor, Events.ORIGINAL_EVENT, values);
3926             DatabaseUtils.cursorLongToContentValues(diffsCursor, Events.ORIGINAL_INSTANCE_TIME,
3927                     values);
3928             DatabaseUtils.cursorStringToContentValues(diffsCursor, Events.ORIGINAL_ALL_DAY,
3929                     values);
3930             DatabaseUtils.cursorLongToContentValues(diffsCursor, Events.LAST_DATE, values);
3931             DatabaseUtils.cursorIntToContentValues(diffsCursor, Events.HAS_ATTENDEE_DATA, values);
3932             DatabaseUtils.cursorLongToContentValues(diffsCursor, Events.CALENDAR_ID, values);
3933             DatabaseUtils.cursorIntToContentValues(diffsCursor, Events.GUESTS_CAN_INVITE_OTHERS,
3934                     values);
3935             DatabaseUtils.cursorIntToContentValues(diffsCursor, Events.GUESTS_CAN_MODIFY, values);
3936             DatabaseUtils.cursorIntToContentValues(diffsCursor, Events.GUESTS_CAN_SEE_GUESTS,
3937                     values);
3938             DatabaseUtils.cursorStringToContentValues(diffsCursor, Events.ORGANIZER, values);
3939         }
3940     }
3941 
3942     private static final int EVENTS = 1;
3943     private static final int EVENTS_ID = 2;
3944     private static final int INSTANCES = 3;
3945     private static final int DELETED_EVENTS = 4;
3946     private static final int CALENDARS = 5;
3947     private static final int CALENDARS_ID = 6;
3948     private static final int ATTENDEES = 7;
3949     private static final int ATTENDEES_ID = 8;
3950     private static final int REMINDERS = 9;
3951     private static final int REMINDERS_ID = 10;
3952     private static final int EXTENDED_PROPERTIES = 11;
3953     private static final int EXTENDED_PROPERTIES_ID = 12;
3954     private static final int CALENDAR_ALERTS = 13;
3955     private static final int CALENDAR_ALERTS_ID = 14;
3956     private static final int CALENDAR_ALERTS_BY_INSTANCE = 15;
3957     private static final int BUSYBITS = 16;
3958     private static final int INSTANCES_BY_DAY = 17;
3959 
3960     private static final UriMatcher sURLMatcher = new UriMatcher(UriMatcher.NO_MATCH);
3961     private static final HashMap<String, String> sInstancesProjectionMap;
3962     private static final HashMap<String, String> sEventsProjectionMap;
3963     private static final HashMap<String, String> sAttendeesProjectionMap;
3964     private static final HashMap<String, String> sRemindersProjectionMap;
3965     private static final HashMap<String, String> sCalendarAlertsProjectionMap;
3966     private static final HashMap<String, String> sBusyBitsProjectionMap;
3967 
3968     static {
3969         sURLMatcher.addURI("calendar", "instances/when/*/*", INSTANCES);
3970         sURLMatcher.addURI("calendar", "instances/whenbyday/*/*", INSTANCES_BY_DAY);
3971         sURLMatcher.addURI("calendar", "events", EVENTS);
3972         sURLMatcher.addURI("calendar", "events/#", EVENTS_ID);
3973         sURLMatcher.addURI("calendar", "calendars", CALENDARS);
3974         sURLMatcher.addURI("calendar", "calendars/#", CALENDARS_ID);
3975         sURLMatcher.addURI("calendar", "deleted_events", DELETED_EVENTS);
3976         sURLMatcher.addURI("calendar", "attendees", ATTENDEES);
3977         sURLMatcher.addURI("calendar", "attendees/#", ATTENDEES_ID);
3978         sURLMatcher.addURI("calendar", "reminders", REMINDERS);
3979         sURLMatcher.addURI("calendar", "reminders/#", REMINDERS_ID);
3980         sURLMatcher.addURI("calendar", "extendedproperties", EXTENDED_PROPERTIES);
3981         sURLMatcher.addURI("calendar", "extendedproperties/#", EXTENDED_PROPERTIES_ID);
3982         sURLMatcher.addURI("calendar", "calendar_alerts", CALENDAR_ALERTS);
3983         sURLMatcher.addURI("calendar", "calendar_alerts/#", CALENDAR_ALERTS_ID);
3984         sURLMatcher.addURI("calendar", "calendar_alerts/by_instance", CALENDAR_ALERTS_BY_INSTANCE);
3985         sURLMatcher.addURI("calendar", "busybits/when/*/*", BUSYBITS);
3986 
3987 
3988         sEventsProjectionMap = new HashMap<String, String>();
3989         // Events columns
sEventsProjectionMap.put(Events.HTML_URI, "htmlUri")3990         sEventsProjectionMap.put(Events.HTML_URI, "htmlUri");
sEventsProjectionMap.put(Events.TITLE, "title")3991         sEventsProjectionMap.put(Events.TITLE, "title");
sEventsProjectionMap.put(Events.EVENT_LOCATION, "eventLocation")3992         sEventsProjectionMap.put(Events.EVENT_LOCATION, "eventLocation");
sEventsProjectionMap.put(Events.DESCRIPTION, "description")3993         sEventsProjectionMap.put(Events.DESCRIPTION, "description");
sEventsProjectionMap.put(Events.STATUS, "eventStatus")3994         sEventsProjectionMap.put(Events.STATUS, "eventStatus");
sEventsProjectionMap.put(Events.SELF_ATTENDEE_STATUS, "selfAttendeeStatus")3995         sEventsProjectionMap.put(Events.SELF_ATTENDEE_STATUS, "selfAttendeeStatus");
sEventsProjectionMap.put(Events.COMMENTS_URI, "commentsUri")3996         sEventsProjectionMap.put(Events.COMMENTS_URI, "commentsUri");
sEventsProjectionMap.put(Events.DTSTART, "dtstart")3997         sEventsProjectionMap.put(Events.DTSTART, "dtstart");
sEventsProjectionMap.put(Events.DTEND, "dtend")3998         sEventsProjectionMap.put(Events.DTEND, "dtend");
sEventsProjectionMap.put(Events.EVENT_TIMEZONE, "eventTimezone")3999         sEventsProjectionMap.put(Events.EVENT_TIMEZONE, "eventTimezone");
sEventsProjectionMap.put(Events.DURATION, "duration")4000         sEventsProjectionMap.put(Events.DURATION, "duration");
sEventsProjectionMap.put(Events.ALL_DAY, "allDay")4001         sEventsProjectionMap.put(Events.ALL_DAY, "allDay");
sEventsProjectionMap.put(Events.VISIBILITY, "visibility")4002         sEventsProjectionMap.put(Events.VISIBILITY, "visibility");
sEventsProjectionMap.put(Events.TRANSPARENCY, "transparency")4003         sEventsProjectionMap.put(Events.TRANSPARENCY, "transparency");
sEventsProjectionMap.put(Events.HAS_ALARM, "hasAlarm")4004         sEventsProjectionMap.put(Events.HAS_ALARM, "hasAlarm");
sEventsProjectionMap.put(Events.HAS_EXTENDED_PROPERTIES, "hasExtendedProperties")4005         sEventsProjectionMap.put(Events.HAS_EXTENDED_PROPERTIES, "hasExtendedProperties");
sEventsProjectionMap.put(Events.RRULE, "rrule")4006         sEventsProjectionMap.put(Events.RRULE, "rrule");
sEventsProjectionMap.put(Events.RDATE, "rdate")4007         sEventsProjectionMap.put(Events.RDATE, "rdate");
sEventsProjectionMap.put(Events.EXRULE, "exrule")4008         sEventsProjectionMap.put(Events.EXRULE, "exrule");
sEventsProjectionMap.put(Events.EXDATE, "exdate")4009         sEventsProjectionMap.put(Events.EXDATE, "exdate");
sEventsProjectionMap.put(Events.ORIGINAL_EVENT, "originalEvent")4010         sEventsProjectionMap.put(Events.ORIGINAL_EVENT, "originalEvent");
sEventsProjectionMap.put(Events.ORIGINAL_INSTANCE_TIME, "originalInstanceTime")4011         sEventsProjectionMap.put(Events.ORIGINAL_INSTANCE_TIME, "originalInstanceTime");
sEventsProjectionMap.put(Events.ORIGINAL_ALL_DAY, "originalAllDay")4012         sEventsProjectionMap.put(Events.ORIGINAL_ALL_DAY, "originalAllDay");
sEventsProjectionMap.put(Events.LAST_DATE, "lastDate")4013         sEventsProjectionMap.put(Events.LAST_DATE, "lastDate");
sEventsProjectionMap.put(Events.HAS_ATTENDEE_DATA, "hasAttendeeData")4014         sEventsProjectionMap.put(Events.HAS_ATTENDEE_DATA, "hasAttendeeData");
sEventsProjectionMap.put(Events.CALENDAR_ID, "calendar_id")4015         sEventsProjectionMap.put(Events.CALENDAR_ID, "calendar_id");
sEventsProjectionMap.put(Events.GUESTS_CAN_INVITE_OTHERS, "guestsCanInviteOthers")4016         sEventsProjectionMap.put(Events.GUESTS_CAN_INVITE_OTHERS, "guestsCanInviteOthers");
sEventsProjectionMap.put(Events.GUESTS_CAN_MODIFY, "guestsCanModify")4017         sEventsProjectionMap.put(Events.GUESTS_CAN_MODIFY, "guestsCanModify");
sEventsProjectionMap.put(Events.GUESTS_CAN_SEE_GUESTS, "guestsCanSeeGuests")4018         sEventsProjectionMap.put(Events.GUESTS_CAN_SEE_GUESTS, "guestsCanSeeGuests");
sEventsProjectionMap.put(Events.ORGANIZER, "organizer")4019         sEventsProjectionMap.put(Events.ORGANIZER, "organizer");
4020 
4021         // Calendar columns
sEventsProjectionMap.put(Events.COLOR, "color")4022         sEventsProjectionMap.put(Events.COLOR, "color");
sEventsProjectionMap.put(Events.ACCESS_LEVEL, "access_level")4023         sEventsProjectionMap.put(Events.ACCESS_LEVEL, "access_level");
sEventsProjectionMap.put(Events.SELECTED, "selected")4024         sEventsProjectionMap.put(Events.SELECTED, "selected");
sEventsProjectionMap.put(Calendars.URL, "url")4025         sEventsProjectionMap.put(Calendars.URL, "url");
sEventsProjectionMap.put(Calendars.TIMEZONE, "timezone")4026         sEventsProjectionMap.put(Calendars.TIMEZONE, "timezone");
sEventsProjectionMap.put(Calendars.OWNER_ACCOUNT, "ownerAccount")4027         sEventsProjectionMap.put(Calendars.OWNER_ACCOUNT, "ownerAccount");
4028 
4029         // Put the shared items into the Instances projection map
4030         sInstancesProjectionMap = new HashMap<String, String>(sEventsProjectionMap);
4031         sAttendeesProjectionMap = new HashMap<String, String>(sEventsProjectionMap);
4032         sRemindersProjectionMap = new HashMap<String, String>(sEventsProjectionMap);
4033         sCalendarAlertsProjectionMap = new HashMap<String, String>(sEventsProjectionMap);
4034 
sEventsProjectionMap.put(Events._ID, "Events._id AS _id")4035         sEventsProjectionMap.put(Events._ID, "Events._id AS _id");
sEventsProjectionMap.put(Events._SYNC_ID, "Events._sync_id AS _sync_id")4036         sEventsProjectionMap.put(Events._SYNC_ID, "Events._sync_id AS _sync_id");
sEventsProjectionMap.put(Events._SYNC_VERSION, "Events._sync_version AS _sync_version")4037         sEventsProjectionMap.put(Events._SYNC_VERSION, "Events._sync_version AS _sync_version");
sEventsProjectionMap.put(Events._SYNC_TIME, "Events._sync_time AS _sync_time")4038         sEventsProjectionMap.put(Events._SYNC_TIME, "Events._sync_time AS _sync_time");
sEventsProjectionMap.put(Events._SYNC_LOCAL_ID, "Events._sync_local_id AS _sync_local_id")4039         sEventsProjectionMap.put(Events._SYNC_LOCAL_ID, "Events._sync_local_id AS _sync_local_id");
sEventsProjectionMap.put(Events._SYNC_DIRTY, "Events._sync_dirty AS _sync_dirty")4040         sEventsProjectionMap.put(Events._SYNC_DIRTY, "Events._sync_dirty AS _sync_dirty");
sEventsProjectionMap.put(Events._SYNC_ACCOUNT, "Events._sync_account AS _sync_account")4041         sEventsProjectionMap.put(Events._SYNC_ACCOUNT, "Events._sync_account AS _sync_account");
sEventsProjectionMap.put(Events._SYNC_ACCOUNT_TYPE, "Events._sync_account_type AS _sync_account_type")4042         sEventsProjectionMap.put(Events._SYNC_ACCOUNT_TYPE,
4043                 "Events._sync_account_type AS _sync_account_type");
4044 
4045         // Instances columns
sInstancesProjectionMap.put(Instances.BEGIN, "begin")4046         sInstancesProjectionMap.put(Instances.BEGIN, "begin");
sInstancesProjectionMap.put(Instances.END, "end")4047         sInstancesProjectionMap.put(Instances.END, "end");
sInstancesProjectionMap.put(Instances.EVENT_ID, "Instances.event_id AS event_id")4048         sInstancesProjectionMap.put(Instances.EVENT_ID, "Instances.event_id AS event_id");
sInstancesProjectionMap.put(Instances._ID, "Instances._id AS _id")4049         sInstancesProjectionMap.put(Instances._ID, "Instances._id AS _id");
sInstancesProjectionMap.put(Instances.START_DAY, "startDay")4050         sInstancesProjectionMap.put(Instances.START_DAY, "startDay");
sInstancesProjectionMap.put(Instances.END_DAY, "endDay")4051         sInstancesProjectionMap.put(Instances.END_DAY, "endDay");
sInstancesProjectionMap.put(Instances.START_MINUTE, "startMinute")4052         sInstancesProjectionMap.put(Instances.START_MINUTE, "startMinute");
sInstancesProjectionMap.put(Instances.END_MINUTE, "endMinute")4053         sInstancesProjectionMap.put(Instances.END_MINUTE, "endMinute");
4054 
4055         // BusyBits columns
4056         sBusyBitsProjectionMap = new HashMap<String, String>();
sBusyBitsProjectionMap.put(BusyBits.DAY, "day")4057         sBusyBitsProjectionMap.put(BusyBits.DAY, "day");
sBusyBitsProjectionMap.put(BusyBits.BUSYBITS, "busyBits")4058         sBusyBitsProjectionMap.put(BusyBits.BUSYBITS, "busyBits");
sBusyBitsProjectionMap.put(BusyBits.ALL_DAY_COUNT, "allDayCount")4059         sBusyBitsProjectionMap.put(BusyBits.ALL_DAY_COUNT, "allDayCount");
4060 
4061         // Attendees columns
sAttendeesProjectionMap.put(Attendees.EVENT_ID, "event_id")4062         sAttendeesProjectionMap.put(Attendees.EVENT_ID, "event_id");
sAttendeesProjectionMap.put(Attendees._ID, "Attendees._id AS _id")4063         sAttendeesProjectionMap.put(Attendees._ID, "Attendees._id AS _id");
sAttendeesProjectionMap.put(Attendees.ATTENDEE_NAME, "attendeeName")4064         sAttendeesProjectionMap.put(Attendees.ATTENDEE_NAME, "attendeeName");
sAttendeesProjectionMap.put(Attendees.ATTENDEE_EMAIL, "attendeeEmail")4065         sAttendeesProjectionMap.put(Attendees.ATTENDEE_EMAIL, "attendeeEmail");
sAttendeesProjectionMap.put(Attendees.ATTENDEE_STATUS, "attendeeStatus")4066         sAttendeesProjectionMap.put(Attendees.ATTENDEE_STATUS, "attendeeStatus");
sAttendeesProjectionMap.put(Attendees.ATTENDEE_RELATIONSHIP, "attendeeRelationship")4067         sAttendeesProjectionMap.put(Attendees.ATTENDEE_RELATIONSHIP, "attendeeRelationship");
sAttendeesProjectionMap.put(Attendees.ATTENDEE_TYPE, "attendeeType")4068         sAttendeesProjectionMap.put(Attendees.ATTENDEE_TYPE, "attendeeType");
4069 
4070         // Reminders columns
sRemindersProjectionMap.put(Reminders.EVENT_ID, "event_id")4071         sRemindersProjectionMap.put(Reminders.EVENT_ID, "event_id");
sRemindersProjectionMap.put(Reminders._ID, "Reminders._id AS _id")4072         sRemindersProjectionMap.put(Reminders._ID, "Reminders._id AS _id");
sRemindersProjectionMap.put(Reminders.MINUTES, "minutes")4073         sRemindersProjectionMap.put(Reminders.MINUTES, "minutes");
sRemindersProjectionMap.put(Reminders.METHOD, "method")4074         sRemindersProjectionMap.put(Reminders.METHOD, "method");
4075 
4076         // CalendarAlerts columns
sCalendarAlertsProjectionMap.put(CalendarAlerts.EVENT_ID, "event_id")4077         sCalendarAlertsProjectionMap.put(CalendarAlerts.EVENT_ID, "event_id");
sCalendarAlertsProjectionMap.put(CalendarAlerts._ID, "CalendarAlerts._id AS _id")4078         sCalendarAlertsProjectionMap.put(CalendarAlerts._ID, "CalendarAlerts._id AS _id");
sCalendarAlertsProjectionMap.put(CalendarAlerts.BEGIN, "begin")4079         sCalendarAlertsProjectionMap.put(CalendarAlerts.BEGIN, "begin");
sCalendarAlertsProjectionMap.put(CalendarAlerts.END, "end")4080         sCalendarAlertsProjectionMap.put(CalendarAlerts.END, "end");
sCalendarAlertsProjectionMap.put(CalendarAlerts.ALARM_TIME, "alarmTime")4081         sCalendarAlertsProjectionMap.put(CalendarAlerts.ALARM_TIME, "alarmTime");
sCalendarAlertsProjectionMap.put(CalendarAlerts.STATE, "state")4082         sCalendarAlertsProjectionMap.put(CalendarAlerts.STATE, "state");
sCalendarAlertsProjectionMap.put(CalendarAlerts.MINUTES, "minutes")4083         sCalendarAlertsProjectionMap.put(CalendarAlerts.MINUTES, "minutes");
4084     }
4085 
4086     /**
4087      * An implementation of EntityIterator that builds the Entity for a calendar event.
4088      */
4089     private static class CalendarEntityIterator implements EntityIterator {
4090         private final Cursor mEntityCursor;
4091         private volatile boolean mIsClosed;
4092         private final SQLiteDatabase mDb;
4093 
4094         private static final String[] EVENTS_PROJECTION = new String[]{
4095                 Calendar.Events._ID,
4096                 Calendar.Events.HTML_URI,
4097                 Calendar.Events.TITLE,
4098                 Calendar.Events.DESCRIPTION,
4099                 Calendar.Events.EVENT_LOCATION,
4100                 Calendar.Events.STATUS,
4101                 Calendar.Events.SELF_ATTENDEE_STATUS,
4102                 Calendar.Events.COMMENTS_URI,
4103                 Calendar.Events.DTSTART,
4104                 Calendar.Events.DTEND,
4105                 Calendar.Events.DURATION,
4106                 Calendar.Events.EVENT_TIMEZONE,
4107                 Calendar.Events.ALL_DAY,
4108                 Calendar.Events.VISIBILITY,
4109                 Calendar.Events.TRANSPARENCY,
4110                 Calendar.Events.HAS_ALARM,
4111                 Calendar.Events.HAS_EXTENDED_PROPERTIES,
4112                 Calendar.Events.RRULE,
4113                 Calendar.Events.RDATE,
4114                 Calendar.Events.EXRULE,
4115                 Calendar.Events.EXDATE,
4116                 Calendar.Events.ORIGINAL_EVENT,
4117                 Calendar.Events.ORIGINAL_INSTANCE_TIME,
4118                 Calendar.Events.ORIGINAL_ALL_DAY,
4119                 Calendar.Events.LAST_DATE,
4120                 Calendar.Events.HAS_ATTENDEE_DATA,
4121                 Calendar.Events.CALENDAR_ID,
4122                 Calendar.Events.GUESTS_CAN_INVITE_OTHERS,
4123                 Calendar.Events.GUESTS_CAN_MODIFY,
4124                 Calendar.Events.GUESTS_CAN_SEE_GUESTS,
4125                 Calendar.Events.ORGANIZER,
4126         };
4127         private static final int COLUMN_ID = 0;
4128         private static final int COLUMN_HTML_URI = 1;
4129         private static final int COLUMN_TITLE = 2;
4130         private static final int COLUMN_DESCRIPTION = 3;
4131         private static final int COLUMN_EVENT_LOCATION = 4;
4132         private static final int COLUMN_STATUS = 5;
4133         private static final int COLUMN_SELF_ATTENDEE_STATUS = 6;
4134         private static final int COLUMN_COMMENTS_URI = 7;
4135         private static final int COLUMN_DTSTART = 8;
4136         private static final int COLUMN_DTEND = 9;
4137         private static final int COLUMN_DURATION = 10;
4138         private static final int COLUMN_EVENT_TIMEZONE = 11;
4139         private static final int COLUMN_ALL_DAY = 12;
4140         private static final int COLUMN_VISIBILITY = 13;
4141         private static final int COLUMN_TRANSPARENCY = 14;
4142         private static final int COLUMN_HAS_ALARM = 15;
4143         private static final int COLUMN_HAS_EXTENDED_PROPERTIES = 16;
4144         private static final int COLUMN_RRULE = 17;
4145         private static final int COLUMN_RDATE = 18;
4146         private static final int COLUMN_EXRULE = 19;
4147         private static final int COLUMN_EXDATE = 20;
4148         private static final int COLUMN_ORIGINAL_EVENT = 21;
4149         private static final int COLUMN_ORIGINAL_INSTANCE_TIME = 22;
4150         private static final int COLUMN_ORIGINAL_ALL_DAY = 23;
4151         private static final int COLUMN_LAST_DATE = 24;
4152         private static final int COLUMN_HAS_ATTENDEE_DATA = 25;
4153         private static final int COLUMN_CALENDAR_ID = 26;
4154         private static final int COLUMN_GUESTS_CAN_INVITE_OTHERS = 27;
4155         private static final int COLUMN_GUESTS_CAN_MODIFY = 28;
4156         private static final int COLUMN_GUESTS_CAN_SEE_GUESTS = 29;
4157         private static final int COLUMN_ORGANIZER = 30;
4158 
4159         private static final String[] REMINDERS_PROJECTION = new String[] {
4160                 Calendar.Reminders.MINUTES,
4161                 Calendar.Reminders.METHOD,
4162         };
4163         private static final int COLUMN_MINUTES = 0;
4164         private static final int COLUMN_METHOD = 1;
4165 
4166         private static final String[] ATTENDEES_PROJECTION = new String[] {
4167                 Calendar.Attendees.ATTENDEE_NAME,
4168                 Calendar.Attendees.ATTENDEE_EMAIL,
4169                 Calendar.Attendees.ATTENDEE_RELATIONSHIP,
4170                 Calendar.Attendees.ATTENDEE_TYPE,
4171                 Calendar.Attendees.ATTENDEE_STATUS,
4172         };
4173         private static final int COLUMN_ATTENDEE_NAME = 0;
4174         private static final int COLUMN_ATTENDEE_EMAIL = 1;
4175         private static final int COLUMN_ATTENDEE_RELATIONSHIP = 2;
4176         private static final int COLUMN_ATTENDEE_TYPE = 3;
4177         private static final int COLUMN_ATTENDEE_STATUS = 4;
4178         private static final String[] EXTENDED_PROJECTION = new String[] {
4179                 Calendar.ExtendedProperties.NAME,
4180                 Calendar.ExtendedProperties.VALUE,
4181         };
4182         private static final int COLUMN_NAME = 0;
4183         private static final int COLUMN_VALUE = 1;
4184 
CalendarEntityIterator(CalendarProvider provider, String eventIdString, Uri uri, String selection, String[] selectionArgs, String sortOrder)4185         public CalendarEntityIterator(CalendarProvider provider, String eventIdString, Uri uri,
4186                 String selection, String[] selectionArgs, String sortOrder) {
4187             mIsClosed = false;
4188             mDb = provider.mOpenHelper.getReadableDatabase();
4189             final SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
4190             qb.setTables(sEventsTable);
4191             if (eventIdString != null) {
4192                 qb.appendWhere(Calendar.Events._ID + "=" + eventIdString);
4193             }
4194             mEntityCursor = qb.query(mDb, EVENTS_PROJECTION, selection, selectionArgs,
4195                     null, null, sortOrder);
4196             mEntityCursor.moveToFirst();
4197         }
4198 
close()4199         public void close() {
4200             if (mIsClosed) {
4201                 throw new IllegalStateException("closing when already closed");
4202             }
4203             mIsClosed = true;
4204             mEntityCursor.close();
4205         }
4206 
hasNext()4207         public boolean hasNext() throws RemoteException {
4208 
4209             if (mIsClosed) {
4210                 throw new IllegalStateException("calling hasNext() when the iterator is closed");
4211             }
4212 
4213             return !mEntityCursor.isAfterLast();
4214         }
4215 
reset()4216         public void reset() throws RemoteException {
4217             if (mIsClosed) {
4218                 throw new IllegalStateException("calling next() when the iterator is closed");
4219             }
4220             mEntityCursor.moveToFirst();
4221         }
4222 
next()4223         public Entity next() throws RemoteException {
4224             if (mIsClosed) {
4225                 throw new IllegalStateException("calling next() when the iterator is closed");
4226             }
4227             if (!hasNext()) {
4228                 throw new IllegalStateException("you may only call next() if hasNext() is true");
4229             }
4230 
4231             final SQLiteCursor c = (SQLiteCursor) mEntityCursor;
4232             final long eventId = c.getLong(COLUMN_ID);
4233 
4234             // we expect the cursor is already at the row we need to read from
4235             ContentValues entityValues = new ContentValues();
4236             entityValues.put(Calendar.Events._ID, eventId);
4237             entityValues.put(Calendar.Events.CALENDAR_ID, c.getInt(COLUMN_CALENDAR_ID));
4238             entityValues.put(Calendar.Events.HTML_URI, c.getString(COLUMN_HTML_URI));
4239             entityValues.put(Calendar.Events.TITLE, c.getString(COLUMN_TITLE));
4240             entityValues.put(Calendar.Events.DESCRIPTION, c.getString(COLUMN_DESCRIPTION));
4241             entityValues.put(Calendar.Events.EVENT_LOCATION, c.getString(COLUMN_EVENT_LOCATION));
4242             entityValues.put(Calendar.Events.STATUS, c.getInt(COLUMN_STATUS));
4243             entityValues.put(Calendar.Events.SELF_ATTENDEE_STATUS,
4244                     c.getInt(COLUMN_SELF_ATTENDEE_STATUS));
4245             entityValues.put(Calendar.Events.COMMENTS_URI, c.getString(COLUMN_COMMENTS_URI));
4246             entityValues.put(Calendar.Events.DTSTART, c.getLong(COLUMN_DTSTART));
4247             entityValues.put(Calendar.Events.DTEND, c.getLong(COLUMN_DTEND));
4248             entityValues.put(Calendar.Events.DURATION, c.getString(COLUMN_DURATION));
4249             entityValues.put(Calendar.Events.EVENT_TIMEZONE, c.getString(COLUMN_EVENT_TIMEZONE));
4250             entityValues.put(Calendar.Events.ALL_DAY, c.getString(COLUMN_ALL_DAY));
4251             entityValues.put(Calendar.Events.VISIBILITY, c.getInt(COLUMN_VISIBILITY));
4252             entityValues.put(Calendar.Events.TRANSPARENCY, c.getInt(COLUMN_TRANSPARENCY));
4253             entityValues.put(Calendar.Events.HAS_ALARM, c.getString(COLUMN_HAS_ALARM));
4254             entityValues.put(Calendar.Events.HAS_EXTENDED_PROPERTIES,
4255                     c.getString(COLUMN_HAS_EXTENDED_PROPERTIES));
4256             entityValues.put(Calendar.Events.RRULE, c.getString(COLUMN_RRULE));
4257             entityValues.put(Calendar.Events.RDATE, c.getString(COLUMN_RDATE));
4258             entityValues.put(Calendar.Events.EXRULE, c.getString(COLUMN_EXRULE));
4259             entityValues.put(Calendar.Events.EXDATE, c.getString(COLUMN_EXDATE));
4260             entityValues.put(Calendar.Events.ORIGINAL_EVENT, c.getString(COLUMN_ORIGINAL_EVENT));
4261             entityValues.put(Calendar.Events.ORIGINAL_INSTANCE_TIME,
4262                     c.getLong(COLUMN_ORIGINAL_INSTANCE_TIME));
4263             entityValues.put(Calendar.Events.ORIGINAL_ALL_DAY, c.getInt(COLUMN_ORIGINAL_ALL_DAY));
4264             entityValues.put(Calendar.Events.LAST_DATE, c.getLong(COLUMN_LAST_DATE));
4265             entityValues.put(Calendar.Events.HAS_ATTENDEE_DATA,
4266                              c.getInt(COLUMN_HAS_ATTENDEE_DATA));
4267             entityValues.put(Calendar.Events.GUESTS_CAN_INVITE_OTHERS,
4268                     c.getInt(COLUMN_GUESTS_CAN_INVITE_OTHERS));
4269             entityValues.put(Calendar.Events.GUESTS_CAN_MODIFY,
4270                     c.getInt(COLUMN_GUESTS_CAN_MODIFY));
4271             entityValues.put(Calendar.Events.GUESTS_CAN_SEE_GUESTS,
4272                     c.getInt(COLUMN_GUESTS_CAN_SEE_GUESTS));
4273             entityValues.put(Calendar.Events.ORGANIZER, c.getString(COLUMN_ORGANIZER));
4274 
4275             Entity entity = new Entity(entityValues);
4276             Cursor cursor = null;
4277             try {
4278                 cursor = mDb.query(sRemindersTable, REMINDERS_PROJECTION, "event_id=" + eventId,
4279                         null, null, null, null);
4280                 while (cursor.moveToNext()) {
4281                     ContentValues reminderValues = new ContentValues();
4282                     reminderValues.put(Calendar.Reminders.MINUTES, cursor.getInt(COLUMN_MINUTES));
4283                     reminderValues.put(Calendar.Reminders.METHOD, cursor.getInt(COLUMN_METHOD));
4284                     entity.addSubValue(Calendar.Reminders.CONTENT_URI, reminderValues);
4285                 }
4286             } finally {
4287                 if (cursor != null) {
4288                     cursor.close();
4289                 }
4290             }
4291 
4292             cursor = null;
4293             try {
4294                 cursor = mDb.query(sAttendeesTable, ATTENDEES_PROJECTION, "event_id=" + eventId,
4295                         null, null, null, null);
4296                 while (cursor.moveToNext()) {
4297                     ContentValues attendeeValues = new ContentValues();
4298                     attendeeValues.put(Calendar.Attendees.ATTENDEE_NAME,
4299                             cursor.getString(COLUMN_ATTENDEE_NAME));
4300                     attendeeValues.put(Calendar.Attendees.ATTENDEE_EMAIL,
4301                             cursor.getString(COLUMN_ATTENDEE_EMAIL));
4302                     attendeeValues.put(Calendar.Attendees.ATTENDEE_RELATIONSHIP,
4303                             cursor.getInt(COLUMN_ATTENDEE_RELATIONSHIP));
4304                     attendeeValues.put(Calendar.Attendees.ATTENDEE_TYPE,
4305                             cursor.getInt(COLUMN_ATTENDEE_TYPE));
4306                     attendeeValues.put(Calendar.Attendees.ATTENDEE_STATUS,
4307                             cursor.getInt(COLUMN_ATTENDEE_STATUS));
4308                     entity.addSubValue(Calendar.Attendees.CONTENT_URI, attendeeValues);
4309                 }
4310             } finally {
4311                 if (cursor != null) {
4312                     cursor.close();
4313                 }
4314             }
4315 
4316             cursor = null;
4317             try {
4318                 cursor = mDb.query(sExtendedPropertiesTable, EXTENDED_PROJECTION,
4319                         "event_id=" + eventId, null, null, null, null);
4320                 while (cursor.moveToNext()) {
4321                     ContentValues extendedValues = new ContentValues();
4322                     extendedValues.put(Calendar.ExtendedProperties.NAME, c.getString(COLUMN_NAME));
4323                     extendedValues.put(Calendar.ExtendedProperties.VALUE,
4324                             c.getString(COLUMN_VALUE));
4325                     entity.addSubValue(Calendar.ExtendedProperties.CONTENT_URI, extendedValues);
4326                 }
4327             } finally {
4328                 if (cursor != null) {
4329                     cursor.close();
4330                 }
4331             }
4332 
4333             mEntityCursor.moveToNext();
4334             // add the data to the contact
4335             return entity;
4336         }
4337     }
4338 
4339     @Override
queryEntities(Uri uri, String selection, String[] selectionArgs, String sortOrder)4340     public EntityIterator queryEntities(Uri uri, String selection, String[] selectionArgs,
4341             String sortOrder) {
4342         final int match = sURLMatcher.match(uri);
4343         switch (match) {
4344             case EVENTS:
4345             case EVENTS_ID:
4346                 String calendarId = null;
4347                 if (match == EVENTS_ID) {
4348                     calendarId = uri.getPathSegments().get(1);
4349                 }
4350 
4351                 return new CalendarEntityIterator(this, calendarId,
4352                         uri, selection, selectionArgs, sortOrder);
4353             default:
4354                 throw new UnsupportedOperationException("Unknown uri: " + uri);
4355         }
4356     }
4357 
4358     @Override
applyBatch(ArrayList<ContentProviderOperation> operations)4359     public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations)
4360             throws OperationApplicationException {
4361 
4362         final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
4363         db.beginTransaction();
4364         try {
4365             ContentProviderResult[] results = super.applyBatch(operations);
4366             db.setTransactionSuccessful();
4367             return results;
4368         } finally {
4369             db.endTransaction();
4370         }
4371     }
4372 }
4373