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