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