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