Initial Contribution
diff --git a/Android.mk b/Android.mk
new file mode 100644
index 0000000..4a83dbb
--- /dev/null
+++ b/Android.mk
@@ -0,0 +1,20 @@
+LOCAL_PATH:= $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_MODULE_TAGS := user
+
+LOCAL_SRC_FILES := $(call all-subdir-java-files)
+
+# TODO: Remove dependency of application on the test runner (android.test.runner) 
+# library.
+LOCAL_JAVA_LIBRARIES := ext android.test.runner
+
+# We depend on googlelogin-client also, but that is already being included by google-framework
+LOCAL_STATIC_JAVA_LIBRARIES := google-framework
+
+LOCAL_PACKAGE_NAME := CalendarProvider
+
+include $(BUILD_PACKAGE)
+
+# Use the following include to make our test apk.
+include $(call all-makefiles-under,$(LOCAL_PATH))
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
new file mode 100644
index 0000000..5a4b2b3
--- /dev/null
+++ b/AndroidManifest.xml
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2008 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+  
+          http://www.apache.org/licenses/LICENSE-2.0
+  
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+        package="com.android.providers.calendar"
+        android:sharedUserId="android.uid.calendar">
+
+    <uses-permission android:name="android.permission.READ_CALENDAR" />
+    <uses-permission android:name="android.permission.WRITE_CALENDAR" />
+    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
+    <uses-permission android:name="com.google.android.googleapps.permission.GOOGLE_AUTH" />
+    <uses-permission android:name="com.google.android.googleapps.permission.GOOGLE_AUTH.cl" />
+    <uses-permission android:name="android.permission.GET_ACCOUNTS" />
+    <uses-permission android:name="android.permission.READ_SYNC_STATS" />
+    <uses-permission android:name="android.permission.INTERNET" />
+    <uses-permission android:name="android.permission.SUBSCRIBED_FEEDS_READ" />
+    <uses-permission android:name="android.permission.SUBSCRIBED_FEEDS_WRITE" />
+
+    <application android:process="com.android.calendar"
+                 android:label="Calendar Storage"
+                 android:icon="@drawable/app_icon">
+        <!-- TODO: Remove dependency of application on the test runner
+             (android.test) library. -->
+        <uses-library android:name="android.test.runner" />
+        <provider android:name="CalendarProvider" android:authorities="calendar"
+                android:syncable="true" android:multiprocess="false"
+                android:readPermission="android.permission.READ_CALENDAR"
+                android:writePermission="android.permission.WRITE_CALENDAR" />
+        <activity android:name="CalendarContentProviderTests" android:label="Calendar Content Provider">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.UNIT_TEST" />
+            </intent-filter>
+        </activity>
+        <receiver android:name="CalendarReceiver">
+            <intent-filter>
+                <action android:name="android.intent.action.BOOT_COMPLETED" />
+            </intent-filter>
+        </receiver>
+    </application>
+</manifest>
diff --git a/MODULE_LICENSE_APACHE2 b/MODULE_LICENSE_APACHE2
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/MODULE_LICENSE_APACHE2
diff --git a/NOTICE b/NOTICE
new file mode 100644
index 0000000..c5b1efa
--- /dev/null
+++ b/NOTICE
@@ -0,0 +1,190 @@
+
+   Copyright (c) 2005-2008, The Android Open Source Project
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
diff --git a/maketests.py b/maketests.py
new file mode 100644
index 0000000..f5f5cb1
--- /dev/null
+++ b/maketests.py
@@ -0,0 +1,127 @@
+#

+# Usage: Fill in the configuration variables.  It will download the feed

+# for it, parse it, and print out test cases to add to the unit test.

+#

+

+EMAIL = "onoratoj@gmail.com"

+PRIVATE_COOKIE = "432802670aefa458daf036597ec8136b"

+START_DATE = ("2006","01","01")

+END_DATE = ("2009","01","01")

+

+

+

+import sys, urllib, re

+from xml.dom import minidom

+

+def fmt(n):

+    if n < 10:

+        return "0" + str(n)

+    else:

+        return str(n)

+

+def makeDate(d):

+    return d[0] + "-" + d[1] + "-" + d[2]

+

+def makeZDate(d):

+    return d[0] + d[1] + d[2] + "T000000Z"

+

+url = "http://www.google.com/calendar/feeds/onoratoj@gmail.com/private-" \

+        + PRIVATE_COOKIE + "/composite?start-min=" + makeDate(START_DATE) \

+        + "&start-max=" + makeDate(END_DATE)

+

+#data = open("out.xml")

+data = urllib.urlopen(url)

+

+DTSTART_TZID = re.compile("DTSTART;TZID=(.*):(.*)")

+DTSTART = re.compile("DTSTART:(.*)")

+DURATION = re.compile("DURATION:(.*)")

+RRULE = re.compile("RRULE:(.*)")

+TIME = re.compile("(....)-(..)-(..)T(..):(..):(..)....([+-])(..):(..)")

+TIMEZ = re.compile("(....)-(..)-(..)T(..):(..):(..)....Z")

+

+def stripTimezone(str):

+    lines = str.split("\n")

+    drop = False

+    result = []

+    for line in lines:

+        if line == "BEGIN:VTIMEZONE":

+            drop = True

+        if not drop:

+            result.append(line)

+        if line == "END:VTIMEZONE":

+            drop = False

+    return result

+

+def fixInstance(s):

+    m = TIME.match(s[0])

+    if m:

+        if m.group(7) == "+":

+            sign = -1

+        else:

+            sign = 1

+        hour = int(m.group(4)) + (sign * int(m.group(8)))

+        return m.group(1) + m.group(2) + m.group(3) + "T" + fmt(hour) \

+                + m.group(5) + m.group(6) + "Z"

+    m = TIMEZ.match(s[0])

+    if m:

+        return m.group(1) + m.group(2) + m.group(3) + "T" + m.group(4) \

+                + m.group(5) + m.group(6) + "Z"

+    return s[0]

+

+dom = minidom.parse(data)

+root = dom.documentElement

+

+entries = root.getElementsByTagName("entry")

+

+for entry in entries:

+    recurrences = entry.getElementsByTagName("gd:recurrence")

+    dtstart = ""

+    tzid = ""

+    duration = ""

+    rrule = ""

+    if len(recurrences) > 0:

+        recurrence = recurrences[0]

+        s = ""

+        for c in recurrence.childNodes:

+            s = s + c.nodeValue

+        lines = stripTimezone(s)

+        for s in lines:

+            re_dtstart = DTSTART_TZID.match(s)

+            if re_dtstart:

+                dtstart = re_dtstart.group(2)

+                tzid = re_dtstart.group(1)

+            re_dtstart = DTSTART.match(s)

+            if re_dtstart:

+                dtstart = re_dtstart.group(1)

+            re_duration = DURATION.match(s)

+            if re_duration:

+                duration = re_duration.group(1)

+            re_rrule = RRULE.match(s)

+            if re_rrule:

+                rrule = re_rrule.group(1)

+    whens = entry.getElementsByTagName("gd:when")

+    instances = []

+    for w in whens:

+        startTime = w.getAttribute("startTime")

+        endTime = w.getAttribute("endTime")

+        instances.append((startTime,endTime))

+

+    instances = map(fixInstance, instances)

+    instances.sort()

+    if dtstart != "":

+        title = ""

+        for c in entry.getElementsByTagName('title')[0].childNodes:

+            title = title + c.nodeValue

+

+        print "            // " + title

+        print "            test(\"" + dtstart + "\","

+        print "                    \"" + rrule + "\","

+        print "                    \"" + makeZDate(START_DATE) \

+                                    + "\", \"" + makeZDate(END_DATE) + "\","

+        print "                    new String[] {"

+        for i in instances:

+            print "                        \"" + i + "\","

+        print "                    });"

+        print

+

+

diff --git a/res/drawable/app_icon.png b/res/drawable/app_icon.png
new file mode 100644
index 0000000..9241090
--- /dev/null
+++ b/res/drawable/app_icon.png
Binary files differ
diff --git a/src/com/android/providers/calendar/CalendarProvider.java b/src/com/android/providers/calendar/CalendarProvider.java
new file mode 100644
index 0000000..7a2a41f
--- /dev/null
+++ b/src/com/android/providers/calendar/CalendarProvider.java
@@ -0,0 +1,3637 @@
+/*
+**
+** Copyright 2006, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+**     http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** See the License for the specific language governing permissions and
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** limitations under the License.
+*/
+
+package com.android.providers.calendar;
+
+import com.google.android.collect.Sets;
+import com.google.android.gdata.client.AndroidGDataClient;
+import com.google.android.gdata.client.AndroidXmlParserFactory;
+import com.google.android.googlelogin.GoogleLoginServiceBlockingHelper;
+import com.google.android.googlelogin.GoogleLoginServiceNotFoundException;
+import com.google.android.providers.AbstractGDataSyncAdapter;
+import com.google.android.providers.AbstractGDataSyncAdapter.GDataSyncData;
+import com.google.wireless.gdata.calendar.client.CalendarClient;
+import com.google.wireless.gdata.calendar.data.CalendarEntry;
+import com.google.wireless.gdata.calendar.data.CalendarsFeed;
+import com.google.wireless.gdata.calendar.parser.xml.XmlCalendarGDataParserFactory;
+import com.google.wireless.gdata.client.AllDeletedUnavailableException;
+import com.google.wireless.gdata.client.AuthenticationException;
+import com.google.wireless.gdata.data.Entry;
+import com.google.wireless.gdata.parser.GDataParser;
+import com.google.wireless.gdata.parser.ParseException;
+
+import android.app.AlarmManager;
+import android.app.PendingIntent;
+import android.content.AbstractTableMerger;
+import android.content.BroadcastReceiver;
+import android.content.ContentProvider;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.SyncAdapter;
+import android.content.SyncContext;
+import android.content.SyncableContentProvider;
+import android.content.UriMatcher;
+import android.database.Cursor;
+import android.database.DatabaseUtils;
+import android.database.SQLException;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteQueryBuilder;
+import android.graphics.Color;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Debug;
+import android.os.Process;
+import android.pim.DateException;
+import android.pim.RecurrenceSet;
+import android.pim.Time;
+import android.provider.Calendar;
+import android.provider.SyncConstValue;
+import android.provider.Calendar.Attendees;
+import android.provider.Calendar.BusyBits;
+import android.provider.Calendar.CalendarAlerts;
+import android.provider.Calendar.Calendars;
+import android.provider.Calendar.Events;
+import android.provider.Calendar.ExtendedProperties;
+import android.provider.Calendar.Instances;
+import android.provider.Calendar.Reminders;
+import android.text.TextUtils;
+import android.util.Config;
+import android.util.Log;
+import android.util.TimeFormatException;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.URLDecoder;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.TimeZone;
+import java.util.TreeSet;
+
+public class CalendarProvider extends SyncableContentProvider {
+
+    private static final boolean PROFILE = false;
+    private static final boolean DEBUG_ALARMS = false;
+    private static final boolean MULTIPLE_ATTENDEES_PER_EVENT = false;
+    private static final String[] ACCOUNTS_PROJECTION = new String[] { Calendars._SYNC_ACCOUNT};
+
+    private static final String[] EVENTS_PROJECTION = new String[] {
+        Events._SYNC_ID,
+        Events._SYNC_VERSION,
+        Events._SYNC_ACCOUNT,
+        Events.CALENDAR_ID };
+    private static final int EVENTS_SYNC_ID_INDEX = 0;
+    private static final int EVENTS_SYNC_VERSION_INDEX = 1;
+    private static final int EVENTS_SYNC_ACCOUNT_INDEX = 2;
+    private static final int EVENTS_CALENDAR_ID_INDEX = 3;
+
+    private DatabaseUtils.InsertHelper mCalendarsInserter;
+    private DatabaseUtils.InsertHelper mEventsInserter;
+    private DatabaseUtils.InsertHelper mEventsRawTimesInserter;
+    private DatabaseUtils.InsertHelper mDeletedEventsInserter;
+    private DatabaseUtils.InsertHelper mInstancesInserter;
+    private DatabaseUtils.InsertHelper mAttendeesInserter;
+    private DatabaseUtils.InsertHelper mRemindersInserter;
+    private DatabaseUtils.InsertHelper mCalendarAlertsInserter;
+    private DatabaseUtils.InsertHelper mExtendedPropertiesInserter;
+
+    /**
+     * The cached copy of the CalendarMetaData database table.
+     * Make this "package private" instead of "private" so that test code
+     * can access it.
+     */
+    MetaData mMetaData;
+
+    // The interval in minutes for calculating busy bits
+    private static final int BUSYBIT_INTERVAL = 60;
+
+    // A lookup table for getting a bit mask of length N, for N <= 32
+    // For example, BIT_MASKS[4] gives 0xf (which has 4 bits set to 1).
+    // We use this for computing the busy bits for events.
+    private static final int[] BIT_MASKS = {
+        0,
+        0x00000001, 0x00000003, 0x00000007, 0x0000000f,
+        0x0000001f, 0x0000003f, 0x0000007f, 0x000000ff,
+        0x000001ff, 0x000003ff, 0x000007ff, 0x00000fff,
+        0x00001fff, 0x00003fff, 0x00007fff, 0x0000ffff,
+        0x0001ffff, 0x0003ffff, 0x0007ffff, 0x000fffff,
+        0x001fffff, 0x003fffff, 0x007fffff, 0x00ffffff,
+        0x01ffffff, 0x03ffffff, 0x07ffffff, 0x0fffffff,
+        0x1fffffff, 0x3fffffff, 0x7fffffff, 0xffffffff,
+    };
+
+    public static final class TimeRange {
+        public long begin;
+        public long end;
+        public boolean allDay;
+    }
+
+    public static final class InstancesRange {
+        public long begin;
+        public long end;
+
+        public InstancesRange(long begin, long end) {
+            this.begin = begin;
+            this.end = end;
+        }
+    }
+
+    public static final class InstancesList
+            extends ArrayList<ContentValues> {
+    }
+
+    public static final class EventInstancesMap
+            extends HashMap<String, InstancesList> {
+        public void add(String syncId, ContentValues values) {
+            InstancesList instances = get(syncId);
+            if (instances == null) {
+                instances = new InstancesList();
+                put(syncId, instances);
+            }
+            instances.add(values);
+        }
+    }
+
+    // A thread that runs in the background and schedules the next
+    // calendar event alarm.
+    private class AlarmScheduler implements Runnable {
+
+        public AlarmScheduler() {
+        }
+
+        public void run() {
+            try {
+                Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
+                runScheduleNextAlarm();
+            } catch (SQLException e) {
+                Log.e(TAG, "runScheduleNextAlarm() failed", e);
+            }
+        }
+    }
+
+    /**
+     * We search backward in time for event reminders that we may have missed
+     * and schedule them if the event has not yet expired.  The amount in
+     * the past to search backwards is controlled by this constant.  It
+     * should be at least a few minutes to allow for an event that was
+     * recently created on the web to make its way to the phone.  Two hours
+     * might seem like overkill, but it is useful in the case where the user
+     * just crossed into a new timezone and might have just missed an alarm.
+     */
+    private static final long SCHEDULE_ALARM_SLACK = 2 * android.pim.DateUtils.HOUR_IN_MILLIS;
+
+    /**
+     * Alarms older than this threshold will be deleted from the CalendarAlerts
+     * table.  This should be about a day because if the timezone is
+     * wrong and the user corrects it we might delete good alarms that
+     * appear to be old because the device time was incorrectly in the future.
+     * This threshold must also be larger than SCHEDULE_ALARM_SLACK.  We add
+     * the SCHEDULE_ALARM_SLACK to ensure this.
+     */
+    private static final long CLEAR_OLD_ALARM_THRESHOLD = android.pim.DateUtils.DAY_IN_MILLIS
+            + SCHEDULE_ALARM_SLACK;
+
+    // A lock for synchronizing access to fields that are shared
+    // with the AlarmScheduler thread.
+    private Object mAlarmLock = new Object();
+
+    private static final String TAG = "CalendarProvider";
+    private static final String DATABASE_NAME = "calendar.db";
+    
+    // Note: if you update the version number, you must also update the code
+    // in upgradeDatabase() to modify the database (gracefully, if possible).
+    private static final int DATABASE_VERSION = 50;
+
+    private static final String EXPECTED_PROJECTION = "/full";
+
+    private static final String DESIRED_PROJECTION = "/full-selfattendance";
+
+    private static final String FEEDS_SUBSTRING = "/feeds/";
+
+    // Make sure we load at least two months worth of data.
+    // Client apps can load more data in a background thread.
+    private static final long MINIMUM_EXPANSION_SPAN =
+            2L * 31 * 24 * 60 * 60 * 1000;
+
+    private static final String[] sCalendarsIdProjection = new String[] { Calendars._ID };
+    private static final int CALENDARS_INDEX_ID = 0;
+    
+    // Allocate the string constant once here instead of on the heap
+    private static final String CALENDAR_ID_SELECTION = "calendar_id=?";
+
+    private static final String[] sInstancesProjection =
+        new String[] { Instances.START_DAY, Instances.END_DAY,
+        Instances.START_MINUTE, Instances.END_MINUTE, Instances.ALL_DAY };
+
+    private static final int INSTANCES_INDEX_START_DAY = 0;
+    private static final int INSTANCES_INDEX_END_DAY = 1;
+    private static final int INSTANCES_INDEX_START_MINUTE = 2;
+    private static final int INSTANCES_INDEX_END_MINUTE = 3;
+    private static final int INSTANCES_INDEX_ALL_DAY = 4;
+
+    private static final String[] sBusyBitProjection = new String[] {
+        BusyBits.DAY, BusyBits.BUSYBITS, BusyBits.ALL_DAY_COUNT };
+
+    private static final int BUSYBIT_INDEX_DAY = 0;
+    private static final int BUSYBIT_INDEX_BUSYBITS= 1;
+    private static final int BUSYBIT_INDEX_ALL_DAY_COUNT = 2;
+
+    private CalendarClient mCalendarClient = null;
+
+    private AlarmManager mAlarmManager;
+
+    private CalendarSyncAdapter mSyncAdapter;
+
+    /**
+     * Listens for timezone changes and disk-no-longer-full events
+     */
+    private BroadcastReceiver mIntentReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            String action = intent.getAction();
+            if (Intent.ACTION_TIMEZONE_CHANGED.equals(action)) {
+                updateTimezoneDependentFields();
+                scheduleNextAlarm();
+            } else if (Intent.ACTION_DEVICE_STORAGE_OK.equals(action)) {
+                // Try to clean up if things were screwy due to a full disk
+                updateTimezoneDependentFields();
+                scheduleNextAlarm();
+            } else if (Intent.ACTION_TIME_CHANGED.equals(action)) {
+                scheduleNextAlarm();
+            }
+        }
+    };
+
+    public CalendarProvider() {
+        super(DATABASE_NAME, DATABASE_VERSION, Calendars.CONTENT_URI);
+    }
+
+    @Override
+    public boolean onCreate() {
+        super.onCreate();
+
+        // Register for Intent broadcasts
+        IntentFilter filter = new IntentFilter();
+
+        filter.addAction(Intent.ACTION_TIMEZONE_CHANGED);
+        filter.addAction(Intent.ACTION_DEVICE_STORAGE_OK);
+        filter.addAction(Intent.ACTION_TIME_CHANGED);
+        final Context c = getContext();
+
+        // We don't ever unregister this because this thread always wants
+        // to receive notifications, even in the background.  And if this
+        // thread is killed then the whole process will be killed and the
+        // memory resources will be reclaimed.
+        c.registerReceiver(mIntentReceiver, filter);
+
+        mMetaData = new MetaData(mOpenHelper);
+        updateTimezoneDependentFields();
+
+        return true;
+    }
+
+    /**
+     * This creates a background thread to check the timezone and update
+     * the timezone dependent fields in the Instances table if the timezone
+     * has changes.
+     */
+    private void updateTimezoneDependentFields() {
+        Thread thread = new TimezoneCheckerThread();
+        thread.start();
+    }
+
+    private class TimezoneCheckerThread extends Thread {
+        @Override
+        public void run() {
+            Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
+            try {
+                doUpdateTimezoneDependentFields();
+            } catch (SQLException e) {
+                Log.e(TAG, "doUpdateTimezoneDependentFields() failed", e);
+                try {
+                    // Clear at least the in-memory data (and if possible the
+                    // database fields) to force a re-computation of Instances.
+                    mMetaData.clearInstanceRange();
+                } catch (SQLException e2) {
+                    Log.e(TAG, "clearInstanceRange() also failed: " + e2);
+                }
+            }
+        }
+    }
+
+    /**
+     * This method runs in a background thread.  If the timezone has changed
+     * then the Instances table will be regenerated.
+     */
+    private void doUpdateTimezoneDependentFields() {
+        MetaData.Fields fields = mMetaData.getFields();
+        String localTimezone = TimeZone.getDefault().getID();
+        if (TextUtils.equals(fields.timezone, localTimezone)) {
+            return;
+        }
+
+        // The database timezone is different from the current timezone.
+        // Regenerate the Instances table for this month.  Include events
+        // starting at the beginning of this month.
+        long now = System.currentTimeMillis();
+        Time time = new Time();
+        time.set(now);
+        time.monthDay = 1;
+        time.hour = 0;
+        time.minute = 0;
+        time.second = 0;
+        long begin = time.normalize(true);
+        long end = begin + MINIMUM_EXPANSION_SPAN;
+        SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
+        handleInstanceQuery(qb, begin, end, new String[] { Instances._ID },
+                null /* selection */, null /* sort */);
+
+        // Also pre-compute the BusyBits table for this month.
+        int startDay = Time.getJulianDay(begin, time.gmtoff);
+        int endDay = startDay + 31;
+        qb = new SQLiteQueryBuilder();
+        handleBusyBitsQuery(qb, startDay, endDay, sBusyBitProjection,
+                null /* selection */, null /* sort */);
+    }
+
+    @Override
+    protected void onDatabaseOpened(SQLiteDatabase db) {
+        db.markTableSyncable("Events", "DeletedEvents");
+
+        if (!isTemporary()) {
+            mCalendarClient = new CalendarClient(
+                new AndroidGDataClient(getContext().getContentResolver()),
+                new XmlCalendarGDataParserFactory(
+                new AndroidXmlParserFactory()));
+        }
+
+        mCalendarsInserter = new DatabaseUtils.InsertHelper(db, "Calendars");
+        mEventsInserter = new DatabaseUtils.InsertHelper(db, "Events");
+        mEventsRawTimesInserter = new DatabaseUtils.InsertHelper(db, "EventsRawTimes");
+        mDeletedEventsInserter = new DatabaseUtils.InsertHelper(db, "DeletedEvents");
+        mInstancesInserter = new DatabaseUtils.InsertHelper(db, "Instances");
+        mAttendeesInserter = new DatabaseUtils.InsertHelper(db, "Attendees");
+        mRemindersInserter = new DatabaseUtils.InsertHelper(db, "Reminders");
+        mCalendarAlertsInserter = new DatabaseUtils.InsertHelper(db, "CalendarAlerts");
+        mExtendedPropertiesInserter =
+            new DatabaseUtils.InsertHelper(db, "ExtendedProperties");
+    }
+
+    @Override
+    protected boolean upgradeDatabase(SQLiteDatabase db, int oldVersion, int newVersion) {
+        Log.i(TAG, "Upgrading DB from version " + oldVersion
+                    + " to " + newVersion);
+        if (oldVersion < 46) {
+            dropTables(db);
+            bootstrapDatabase(db);
+            return false; // this was lossy
+        }
+
+        if (oldVersion == 46) {
+            Log.w(TAG, "Upgrading CalendarAlerts table");
+            db.execSQL("UPDATE CalendarAlerts SET reminder_id=NULL;");
+            db.execSQL("ALTER TABLE CalendarAlerts ADD COLUMN minutes INTEGER DEFAULT 0;");
+            oldVersion += 1;
+        }
+        
+        if (oldVersion == 47) {
+            // Changing to version 48 was intended to force a data wipe
+            dropTables(db);
+            bootstrapDatabase(db);
+            return false; // this was lossy
+        }
+        
+        if (oldVersion == 48) {
+            // Changing to version 49 was intended to force a data wipe
+            dropTables(db);
+            bootstrapDatabase(db);
+            return false; // this was lossy
+        }
+
+        if (oldVersion == 49) {
+            Log.w(TAG, "Upgrading DeletedEvents table");
+
+            // We don't have enough information to fill in the correct
+            // value of the calendar_id for old rows in the DeletedEvents
+            // table, but rows in that table are transient so it is unlikely
+            // that there are any rows.  Plus, the calendar_id is used only
+            // when deleting a calendar, which is a rare event.  All new rows
+            // will have the correct calendar_id.
+            db.execSQL("ALTER TABLE DeletedEvents ADD COLUMN calendar_id INTEGER;");
+
+            // Trigger to remove a calendar's events when we delete the calendar
+            db.execSQL("DROP TRIGGER IF EXISTS calendar_cleanup");
+            db.execSQL("CREATE TRIGGER calendar_cleanup DELETE ON Calendars " +
+                        "BEGIN " +
+                            "DELETE FROM Events WHERE calendar_id = old._id;" +
+                            "DELETE FROM DeletedEvents WHERE calendar_id = old._id;" +
+                        "END");
+            oldVersion += 1;
+        }
+        return true; // this was lossless
+    }
+
+    private void dropTables(SQLiteDatabase db) {
+        db.execSQL("DROP TABLE IF EXISTS Calendars;");
+        db.execSQL("DROP TABLE IF EXISTS Events;");
+        db.execSQL("DROP TABLE IF EXISTS EventsRawTimes;");
+        db.execSQL("DROP TABLE IF EXISTS DeletedEvents;");
+        db.execSQL("DROP TABLE IF EXISTS Instances;");
+        db.execSQL("DROP TABLE IF EXISTS CalendarMetaData;");
+        db.execSQL("DROP TABLE IF EXISTS BusyBits;");
+        db.execSQL("DROP TABLE IF EXISTS Attendees;");
+        db.execSQL("DROP TABLE IF EXISTS Reminders;");
+        db.execSQL("DROP TABLE IF EXISTS CalendarAlerts;");
+        db.execSQL("DROP TABLE IF EXISTS ExtendedProperties;");
+    }
+
+    @Override
+    protected void bootstrapDatabase(SQLiteDatabase db) {
+        super.bootstrapDatabase(db);
+        db.execSQL("CREATE TABLE Calendars (" +
+                        "_id INTEGER PRIMARY KEY," +
+                        "_sync_account TEXT," +
+                        "_sync_id TEXT," +
+                        "_sync_version TEXT," +
+                        "_sync_time TEXT," +            // UTC
+                        "_sync_local_id INTEGER," +
+                        "_sync_dirty INTEGER," +
+                        "_sync_mark INTEGER," + // Used to filter out new rows
+                        "url TEXT," +
+                        "name TEXT," +
+                        "displayName TEXT," +
+                        "hidden INTEGER NOT NULL DEFAULT 0," +
+                        "color INTEGER," +
+                        "access_level INTEGER," +
+                        "selected INTEGER NOT NULL DEFAULT 1," +
+                        "sync_events INTEGER NOT NULL DEFAULT 0," +
+                        "location TEXT," +
+                        "timezone TEXT" +
+                        ");");
+
+        // Trigger to remove a calendar's events when we delete the calendar
+        db.execSQL("CREATE TRIGGER calendar_cleanup DELETE ON Calendars " +
+                    "BEGIN " +
+                        "DELETE FROM Events WHERE calendar_id = old._id;" +
+                        "DELETE FROM DeletedEvents WHERE calendar_id = old._id;" +
+                    "END");
+
+        // TODO: do we need both dtend and duration?
+        db.execSQL("CREATE TABLE Events (" +
+                        "_id INTEGER PRIMARY KEY," +
+                        "_sync_account TEXT," +
+                        "_sync_id TEXT," +
+                        "_sync_version TEXT," +
+                        "_sync_time TEXT," +            // UTC
+                        "_sync_local_id INTEGER," +
+                        "_sync_dirty INTEGER," +
+                        "_sync_mark INTEGER," + // To filter out new rows
+                        "calendar_id INTEGER," +
+                        "htmlUri TEXT," +
+                        "title TEXT," +
+                        "eventLocation TEXT," +
+                        "description TEXT," +
+                        "eventStatus INTEGER," +
+                        "selfAttendeeStatus INTEGER NOT NULL DEFAULT 0," +
+                        "commentsUri TEXT," +
+                        "dtstart INTEGER," +               // millis since epoch
+                        "dtend INTEGER," +                 // millis since epoch
+                        "eventTimezone TEXT," +         // timezone for event
+                        "duration TEXT," +
+                        "allDay INTEGER NOT NULL DEFAULT 0," +
+                        "visibility INTEGER NOT NULL DEFAULT 0," +
+                        "transparency INTEGER NOT NULL DEFAULT 0," +
+                        "hasAlarm INTEGER NOT NULL DEFAULT 0," +
+                        "hasExtendedProperties INTEGER NOT NULL DEFAULT 0," +
+                        "rrule TEXT," +
+                        "rdate TEXT," +
+                        "exrule TEXT," +
+                        "exdate TEXT," +
+                        "originalEvent TEXT," +
+                        "originalInstanceTime INTEGER," +  // millis since epoch
+                        "lastDate INTEGER" +               // millis since epoch
+                    ");");
+
+        db.execSQL("CREATE INDEX eventsCalendarIdIndex ON Events (" +
+                   Events.CALENDAR_ID +
+                   ");");
+
+        db.execSQL("CREATE TABLE EventsRawTimes (" +
+                        "_id INTEGER PRIMARY KEY," +
+                        "event_id INTEGER NOT NULL," +
+                        "dtstart2445 TEXT," +
+                        "dtend2445 TEXT," +
+                        "originalInstanceTime2445 TEXT," +
+                        "lastDate2445 TEXT," +
+                        "UNIQUE (event_id)" +
+                    ");");
+
+        // NOTE: we do not create a trigger to delete an event's instances upon update,
+        // as all rows currently get updated during a merge.
+
+        db.execSQL("CREATE TABLE DeletedEvents (" +
+                        "_sync_id TEXT," +
+                        "_sync_version TEXT," +
+                        "_sync_account TEXT," +
+                        "_sync_mark INTEGER," + // To filter out new rows
+                        "calendar_id INTEGER" +
+                    ");");
+
+        db.execSQL("CREATE TABLE Instances (" +
+                        "_id INTEGER PRIMARY KEY," +
+                        "event_id INTEGER," +
+                        "begin INTEGER," +         // UTC millis
+                        "end INTEGER," +           // UTC millis
+                        "startDay INTEGER," +      // Julian start day
+                        "endDay INTEGER," +        // Julian end day
+                        "startMinute INTEGER," +   // minutes from midnight
+                        "endMinute INTEGER," +     // minutes from midnight
+                        "UNIQUE (event_id, begin, end)" +
+                    ");");
+
+        db.execSQL("CREATE INDEX instancesStartDayIndex ON Instances (" +
+                   Instances.START_DAY +
+                   ");");
+
+        db.execSQL("CREATE TABLE CalendarMetaData (" +
+                        "_id INTEGER PRIMARY KEY," +
+                        "localTimezone TEXT," +
+                        "minInstance INTEGER," +      // UTC millis
+                        "maxInstance INTEGER," +      // UTC millis
+                        "minBusyBits INTEGER," +      // UTC millis
+                        "maxBusyBits INTEGER" +       // UTC millis
+        ");");
+
+        db.execSQL("CREATE TABLE BusyBits(" +
+                        "day INTEGER PRIMARY KEY," +  // the Julian day
+                        "busyBits INTEGER," +         // 24 bits for 60-minute intervals
+                        "allDayCount INTEGER" +       // number of all-day events
+        ");");
+
+        db.execSQL("CREATE TABLE Attendees (" +
+                        "_id INTEGER PRIMARY KEY," +
+                        "event_id INTEGER," +
+                        "attendeeName TEXT," +
+                        "attendeeEmail TEXT," +
+                        "attendeeStatus INTEGER," +
+                        "attendeeRelationship INTEGER," +
+                        "attendeeType INTEGER" +
+                   ");");
+
+        db.execSQL("CREATE INDEX attendeesEventIdIndex ON Attendees (" +
+                   Attendees.EVENT_ID +
+                   ");");
+
+        db.execSQL("CREATE TABLE Reminders (" +
+                        "_id INTEGER PRIMARY KEY," +
+                        "event_id INTEGER," +
+                        "minutes INTEGER," +
+                        "method INTEGER NOT NULL" +
+                        " DEFAULT " + Reminders.METHOD_DEFAULT +
+                   ");");
+
+        db.execSQL("CREATE INDEX remindersEventIdIndex ON Reminders (" +
+                   Reminders.EVENT_ID +
+                   ");");
+
+        // This table stores the Calendar notifications that have gone off.
+        db.execSQL("CREATE TABLE CalendarAlerts (" +
+                        "_id INTEGER PRIMARY KEY," +
+                        "event_id INTEGER," +
+                        "begin INTEGER NOT NULL," +        // UTC millis
+                        "end INTEGER NOT NULL," +          // UTC millis
+                        "alarmTime INTEGER NOT NULL," +    // UTC millis
+                        "state INTEGER NOT NULL," +
+                        "minutes INTEGER," +
+                        "UNIQUE (alarmTime, begin, event_id)" +
+                   ");");
+
+        db.execSQL("CREATE INDEX calendarAlertsEventIdIndex ON CalendarAlerts (" +
+                   CalendarAlerts.EVENT_ID +
+                   ");");
+
+        db.execSQL("CREATE TABLE ExtendedProperties (" +
+                        "_id INTEGER PRIMARY KEY," +
+                        "event_id INTEGER," +
+                        "name TEXT," +
+                        "value TEXT" +
+                   ");");
+
+        db.execSQL("CREATE INDEX extendedPropertiesEventIdIndex ON ExtendedProperties (" +
+                   ExtendedProperties.EVENT_ID +
+                   ");");
+
+        // Trigger to remove data tied to an event when we delete that event.
+        db.execSQL("CREATE TRIGGER events_cleanup_delete DELETE ON Events " +
+                    "BEGIN " +
+                        "DELETE FROM Instances WHERE event_id = old._id;" +
+                        "DELETE FROM EventsRawTimes WHERE event_id = old._id;" +
+                        "DELETE FROM Attendees WHERE event_id = old._id;" +
+                        "DELETE FROM Reminders WHERE event_id = old._id;" +
+                        "DELETE FROM CalendarAlerts WHERE event_id = old._id;" +
+                        "DELETE FROM ExtendedProperties WHERE event_id = old._id;" +
+                    "END");
+
+        // Triggers to set the _sync_dirty flag when an attendee is changed,
+        // inserted or deleted
+        db.execSQL("CREATE TRIGGER attendees_update UPDATE ON Attendees " +
+                    "BEGIN " +
+                        "UPDATE Events SET _sync_dirty=1 WHERE Events._id=old.event_id;" +
+                    "END");
+        db.execSQL("CREATE TRIGGER attendees_insert INSERT ON Attendees " +
+                    "BEGIN " +
+                        "UPDATE Events SET _sync_dirty=1 WHERE Events._id=new.event_id;" +
+                    "END");
+        db.execSQL("CREATE TRIGGER attendees_delete DELETE ON Attendees " +
+                    "BEGIN " +
+                        "UPDATE Events SET _sync_dirty=1 WHERE Events._id=old.event_id;" +
+                    "END");
+
+        // Triggers to set the _sync_dirty flag when a reminder is changed,
+        // inserted or deleted
+        db.execSQL("CREATE TRIGGER reminders_update UPDATE ON Reminders " +
+                    "BEGIN " +
+                        "UPDATE Events SET _sync_dirty=1 WHERE Events._id=old.event_id;" +
+                    "END");
+        db.execSQL("CREATE TRIGGER reminders_insert INSERT ON Reminders " +
+                    "BEGIN " +
+                        "UPDATE Events SET _sync_dirty=1 WHERE Events._id=new.event_id;" +
+                    "END");
+        db.execSQL("CREATE TRIGGER reminders_delete DELETE ON Reminders " +
+                    "BEGIN " +
+                        "UPDATE Events SET _sync_dirty=1 WHERE Events._id=old.event_id;" +
+                    "END");
+        // Triggers to set the _sync_dirty flag when an extended property is changed,
+        // inserted or deleted
+        db.execSQL("CREATE TRIGGER extended_properties_update UPDATE ON ExtendedProperties " +
+                    "BEGIN " +
+                        "UPDATE Events SET _sync_dirty=1 WHERE Events._id=old.event_id;" +
+                    "END");
+        db.execSQL("CREATE TRIGGER extended_properties_insert UPDATE ON ExtendedProperties " +
+                    "BEGIN " +
+                        "UPDATE Events SET _sync_dirty=1 WHERE Events._id=new.event_id;" +
+                    "END");
+        db.execSQL("CREATE TRIGGER extended_properties_delete UPDATE ON ExtendedProperties " +
+                    "BEGIN " +
+                        "UPDATE Events SET _sync_dirty=1 WHERE Events._id=old.event_id;" +
+                    "END");
+    }
+
+    /**
+     * Make sure that there are no entries for accounts that no longer
+     * exist. We are overriding this since we need to delete from the
+     * Calendars table, which is not syncable, which has triggers that
+     * will delete from the Events and DeletedEvents tables, which are
+     * syncable.
+     */
+    @Override
+    protected void onAccountsChanged(String[] accountsArray) {
+        super.onAccountsChanged(accountsArray);
+
+        Map<String, Boolean> accounts = new HashMap<String, Boolean>();
+        for (String account : accountsArray) {
+            accounts.put(account, false);
+        }
+
+        mDb.beginTransaction();
+        try {
+            deleteRowsForRemovedAccounts(accounts, "Calendars",
+                    SyncConstValue._SYNC_ACCOUNT);
+            mDb.setTransactionSuccessful();
+        } finally {
+            mDb.endTransaction();
+        }
+
+        if (mCalendarClient == null) {
+            return;
+        }
+
+        // If we have calendars for unknown accounts, delete them.
+        // If there are no calendars at all for a given account, add the
+        // default calendar.
+
+        for (Map.Entry<String, Boolean> entry : accounts.entrySet()) {
+            entry.setValue(false);
+            // TODO: remove this break when Calendar supports multiple accounts. Until then
+            // pretend that only the first account exists.
+            break;
+        }
+
+        Set<String> handledAccounts = Sets.newHashSet();
+        ContentResolver cr = getContext().getContentResolver();
+        if (Config.LOGV) Log.v(TAG, "querying calendars");
+        Cursor c = cr.query(Calendars.CONTENT_URI, ACCOUNTS_PROJECTION, null, null, null);
+        try {
+            while (c.moveToNext()) {
+                String account = c.getString(0);
+                if (handledAccounts.contains(account)) {
+                    continue;
+                }
+                handledAccounts.add(account);
+                if (accounts.containsKey(account)) {
+                    if (Config.LOGV) {
+                        Log.v(TAG, "calendars for account " + account
+                                + " exist");
+                    }
+                    accounts.put(account, true /* hasCalendar */);
+                }
+            }
+        } finally {
+            c.close();
+            c = null;
+        }
+
+        if (Config.LOGV) {
+            Log.v(TAG, "scanning over " + accounts.size() + " account(s)");
+        }
+        for (Map.Entry<String, Boolean> entry : accounts.entrySet()) {
+            String account = entry.getKey();
+            boolean hasCalendar = entry.getValue();
+            if (hasCalendar) {
+                if (Config.LOGV) {
+                    Log.v(TAG, "ignoring account " + account +
+                         " since it matched an existing calendar");
+                }
+                continue;
+            }
+            String feedUrl = mCalendarClient.getDefaultCalendarUrl(account,
+                    CalendarClient.PROJECTION_PRIVATE_SELF_ATTENDANCE, null/* query params */);
+            feedUrl = CalendarSyncAdapter.rewriteUrlforAccount(account, feedUrl);
+            if (Config.LOGV) {
+                Log.v(TAG, "adding default calendar for account " + account);
+            }
+            ContentValues values = new ContentValues();
+            values.put(Calendars._SYNC_ACCOUNT, account);
+            values.put(Calendars.URL, feedUrl);
+            values.put(Calendars.DISPLAY_NAME, "Default");
+            values.put(Calendars.SYNC_EVENTS, 1);
+            values.put(Calendars.SELECTED, 1);
+            values.put(Calendars.HIDDEN, 0);
+            values.put(Calendars.COLOR, -14069085 /* blue */);
+            // this is just our best guess.  the real value will get updated
+            // when the user does a sync.
+            values.put(Calendars.TIMEZONE, Time.getCurrentTimezone());
+            values.put(Calendars.ACCESS_LEVEL, Calendars.OWNER_ACCESS);
+            cr.insert(Calendars.CONTENT_URI, values);
+
+            scheduleSync(account, false /* do a full sync */, null /* no url */);
+        }
+    }
+
+    @Override
+    public Cursor queryInternal(Uri url, String[] projectionIn,
+            String selection, String[] selectionArgs, String sort) {
+        final SQLiteDatabase db = getDatabase();
+        SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
+
+        Cursor ret;
+
+        // Generate the body of the query
+        int match = sURLMatcher.match(url);
+        switch (match)
+        {
+            case EVENTS:
+                qb.setTables("Events, Calendars");
+                qb.setProjectionMap(sEventsProjectionMap);
+                qb.appendWhere("Events.calendar_id=Calendars._id");
+                break;
+            case EVENTS_ID:
+                qb.setTables("Events, Calendars");
+                qb.setProjectionMap(sEventsProjectionMap);
+                qb.appendWhere("Events.calendar_id=Calendars._id");
+                qb.appendWhere(" AND Events._id=");
+                qb.appendWhere(url.getPathSegments().get(1));
+                break;
+            case DELETED_EVENTS:
+                if (isTemporary()) {
+                    qb.setTables("DeletedEvents");
+                    break;
+                } else {
+                    throw new IllegalArgumentException("Unknown URL " + url);
+                }
+            case CALENDARS:
+                qb.setTables("Calendars");
+                // see if we want to update the list of calendars from the
+                // server.
+                String update = null;
+                update = url.getQueryParameter("update");
+                if ("1".equals(update)) {
+                    fetchCalendarsFromServer();
+                }
+
+                break;
+            case CALENDARS_ID:
+                qb.setTables("Calendars");
+                qb.appendWhere("_id=");
+                qb.appendWhere(url.getPathSegments().get(1));
+                break;
+            case INSTANCES:
+                long begin;
+                long end;
+                try {
+                    begin = Long.valueOf(url.getPathSegments().get(2));
+                } catch (NumberFormatException nfe) {
+                    throw new IllegalArgumentException("Cannot parse begin "
+                            + url.getPathSegments().get(2));
+                }
+                try {
+                    end = Long.valueOf(url.getPathSegments().get(3));
+                } catch (NumberFormatException nfe) {
+                    throw new IllegalArgumentException("Cannot parse end "
+                            + url.getPathSegments().get(3));
+                }
+                return handleInstanceQuery(qb, begin, end, projectionIn,
+                                           selection, sort);
+            case BUSYBITS:
+                int startDay;
+                int endDay;
+                try {
+                    startDay = Integer.valueOf(url.getPathSegments().get(2));
+                } catch (NumberFormatException nfe) {
+                    throw new IllegalArgumentException("Cannot parse start day "
+                            + url.getPathSegments().get(2));
+                }
+                try {
+                    endDay = Integer.valueOf(url.getPathSegments().get(3));
+                } catch (NumberFormatException nfe) {
+                    throw new IllegalArgumentException("Cannot parse end day "
+                            + url.getPathSegments().get(3));
+                }
+                return handleBusyBitsQuery(qb, startDay, endDay, projectionIn,
+                                           selection, sort);
+            case ATTENDEES:
+                qb.setTables("Attendees, Events, Calendars");
+                qb.setProjectionMap(sAttendeesProjectionMap);
+                qb.appendWhere("Events.calendar_id=Calendars._id");
+                qb.appendWhere(" AND Events._id=Attendees.event_id");
+                break;
+            case ATTENDEES_ID:
+                qb.setTables("Attendees, Events, Calendars");
+                qb.setProjectionMap(sAttendeesProjectionMap);
+                qb.appendWhere("Attendees._id=");
+                qb.appendWhere(url.getPathSegments().get(1));
+                qb.appendWhere(" AND Events.calendar_id=Calendars._id");
+                qb.appendWhere(" AND Events._id=Attendees.event_id");
+                break;
+            case REMINDERS:
+                qb.setTables("Reminders");
+                break;
+            case REMINDERS_ID:
+                qb.setTables("Reminders, Events, Calendars");
+                qb.setProjectionMap(sRemindersProjectionMap);
+                qb.appendWhere("Reminders._id=");
+                qb.appendWhere(url.getLastPathSegment());
+                qb.appendWhere(" AND Events.calendar_id=Calendars._id");
+                qb.appendWhere(" AND Events._id=Reminders.event_id");
+                break;
+            case CALENDAR_ALERTS:
+                qb.setTables("CalendarAlerts, Events, Calendars");
+                qb.setProjectionMap(sCalendarAlertsProjectionMap);
+                qb.appendWhere("Events.calendar_id=Calendars._id");
+                qb.appendWhere(" AND Events._id=CalendarAlerts.event_id");
+                break;
+            case CALENDAR_ALERTS_BY_INSTANCE:
+                qb.setTables("CalendarAlerts, Events, Calendars");
+                qb.setProjectionMap(sCalendarAlertsProjectionMap);
+                qb.appendWhere("Events.calendar_id=Calendars._id");
+                qb.appendWhere(" AND Events._id=CalendarAlerts.event_id");
+                String groupBy = CalendarAlerts.EVENT_ID + "," + CalendarAlerts.BEGIN;
+                return qb.query(db, projectionIn, selection, selectionArgs,
+                        groupBy, null, sort);
+            case CALENDAR_ALERTS_ID:
+                qb.setTables("CalendarAlerts, Events, Calendars");
+                qb.setProjectionMap(sCalendarAlertsProjectionMap);
+                qb.appendWhere("CalendarAlerts._id=");
+                qb.appendWhere(url.getLastPathSegment());
+                qb.appendWhere(" AND Events.calendar_id=Calendars._id");
+                qb.appendWhere(" AND Events._id=CalendarAlerts.event_id");
+                break;
+            case EXTENDED_PROPERTIES:
+                qb.setTables("ExtendedProperties");
+                break;
+            case EXTENDED_PROPERTIES_ID:
+                qb.setTables("ExtendedProperties, Events, Calendars");
+                // not sure if we need a projection map or a join.  see what callers want.
+//                qb.setProjectionMap(sExtendedPropertiesProjectionMap);
+                qb.appendWhere("ExtendedProperties._id=");
+                qb.appendWhere(url.getPathSegments().get(1));
+//                qb.appendWhere(" AND Events.calendar_id = Calendars._id");
+//                qb.appendWhere(" AND Events._id=ExtendedProperties.event_id");
+                break;
+
+            default:
+                throw new IllegalArgumentException("Unknown URL " + url);
+        }
+
+        // run the query
+        ret = qb.query(db, projectionIn, selection, selectionArgs, null, null, sort);
+
+        return ret;
+    }
+
+    private void fetchCalendarsFromServer() {
+        if (mCalendarClient == null) {
+            Log.w(TAG, "Cannot fetch calendars -- calendar url defined.");
+            return;
+        }
+
+        GoogleLoginServiceBlockingHelper loginHelper = null;
+        String username = null;
+        String authToken = null;
+
+        try {
+            loginHelper = new GoogleLoginServiceBlockingHelper(getContext());
+
+            // TODO: allow caller to specify which account's feeds should be updated
+            username = loginHelper.getAccount(false);
+            if (TextUtils.isEmpty(username)) {
+                Log.w(TAG, "Unable to update calendars from server -- "
+                      + "no users configured.");
+                return;
+            }
+
+            try {
+                authToken = loginHelper.getAuthToken(username,
+                                                     mCalendarClient.getServiceName());
+            } catch (GoogleLoginServiceBlockingHelper.AuthenticationException e) {
+                Log.w(TAG, "Unable to update calendars from server -- could not "
+                      + "authenticate user " + username, e);
+                return;
+            }
+        } catch (GoogleLoginServiceNotFoundException e) {
+            Log.e(TAG, "Could not find Google login service", e);
+            return;
+        } finally {
+            if (loginHelper != null) {
+                loginHelper.close();
+            }
+        }
+
+        // get the current set of calendars.  we'll need to pay attention to
+        // which calendars we get back from the server, so we can delete
+        // calendars that have been deleted from the server.
+        Set<Long> existingCalendarIds = new HashSet<Long>();
+
+        final SQLiteDatabase db = getDatabase();
+        db.beginTransaction();
+        try {
+            getCurrentCalendars(existingCalendarIds);
+
+            // get and process the calendars meta feed
+            GDataParser parser = null;
+            try {
+                String feedUrl = mCalendarClient.getUserCalendarsUrl(username);
+                feedUrl = CalendarSyncAdapter.rewriteUrlforAccount(username, feedUrl);
+                parser = mCalendarClient.getParserForUserCalendars(feedUrl, authToken);
+                // process the calendars
+                processCalendars(username, parser, existingCalendarIds);
+            } catch (AuthenticationException ae) {
+                Log.w(TAG, "Unable to process calendars from server -- could not "
+                        + "authenticate user.", ae);
+                return;
+            } catch (ParseException pe) {
+                Log.w(TAG, "Unable to process calendars from server -- could not "
+                        + "parse calendar feed.", pe);
+                return;
+            } catch (IOException ioe) {
+                Log.w(TAG, "Unable to process calendars from server -- encountered "
+                        + "i/o error", ioe);
+                return;
+            } catch (AllDeletedUnavailableException e) {
+                Log.w(TAG, "Unable to process calendars from server -- encountered "
+                        + "an AllDeletedUnavailableException, this should never happen", e);
+                return;
+            } finally {
+                if (parser != null) {
+                    parser.close();
+                }
+            }
+
+            // delete calendars that are no longer sent from the server.
+            final Uri calendarContentUri = Calendars.CONTENT_URI;
+            for (long calId : existingCalendarIds) {
+                // NOTE: triggers delete all events, instances for this calendar.
+                delete(ContentUris.withAppendedId(calendarContentUri, calId),
+                        null /* where */, null /* selectionArgs */);
+            }
+            db.setTransactionSuccessful();
+        } finally {
+            db.endTransaction();
+        }
+    }
+
+    private void getCurrentCalendars(Set<Long> calendarIds) {
+        Cursor cursor = query(Calendars.CONTENT_URI,
+                new String[] { Calendars._ID },
+                null /* selection */,
+                null /* selectionArgs */,
+                null /* sort */);
+        if (cursor != null) {
+            try {
+                while (cursor.moveToNext()) {
+                    calendarIds.add(cursor.getLong(0));
+                }
+            } finally {
+                cursor.close();
+            }
+        }
+    }
+
+    private void processCalendars(String username,
+                                  GDataParser parser,
+                                  Set<Long> existingCalendarIds)
+            throws ParseException, IOException {
+        CalendarsFeed feed = (CalendarsFeed) parser.init();
+        Entry entry = null;
+        ContentValues map = new ContentValues();
+        final Uri calendarContentUri = Calendars.CONTENT_URI;
+        while (parser.hasMoreData()) {
+            entry = parser.readNextEntry(entry);
+            if (Config.LOGV) Log.v(TAG, "Read entry: " + entry.toString());
+            CalendarEntry calendarEntry = (CalendarEntry) entry;
+            String feedUrl = calendarEntryToContentValues(username, feed, calendarEntry, map);
+            if (TextUtils.isEmpty(feedUrl)) {
+                continue;
+            }
+            long calId = -1;
+
+            Cursor c = query(calendarContentUri,
+                    new String[] { Calendars._ID },
+                    Calendars.URL + "='"
+                            + feedUrl + '\'' /* selection */,
+                    null /* selectionArgs */,
+                    null /* sort */);
+            if (c != null) {
+                try {
+                    if (c.moveToFirst()) {
+                        calId = c.getLong(0);
+                        existingCalendarIds.remove(calId);
+                    }
+                } finally {
+                    c.close();
+                }
+            }
+
+            if (calId != -1) {
+                if (Config.LOGV) Log.v(TAG, "Updating calendar " + map);
+                // don't override the existing "selected" or "hidden" settings.
+                map.remove(Calendars.SELECTED);
+                map.remove(Calendars.HIDDEN);
+                // write to db directly, so we don't send a notification.
+                updateInternal(ContentUris.withAppendedId(calendarContentUri, calId), map,
+                       null /* where */, null /* selectionArgs */);
+            } else {
+                // Select this calendar for syncing and display if it is
+                // selected and not hidden.
+                int syncAndDisplay = 0;
+                if (calendarEntry.isSelected() && !calendarEntry.isHidden()) {
+                    syncAndDisplay = 1;
+                }
+                map.put(Calendars.SYNC_EVENTS, syncAndDisplay);
+                map.put(Calendars.SELECTED, syncAndDisplay);
+                map.put(Calendars.HIDDEN, 0);
+                map.put(Calendars._SYNC_ACCOUNT, username);
+                if (Config.LOGV) Log.v(TAG, "Adding calendar " + map);
+                // write to db directly, so we don't send a notification.
+                Uri row = insertInternal(calendarContentUri, map);
+            }
+        }
+    }
+
+    // TODO: unit test.
+    protected static final String convertCalendarIdToFeedUrl(String url) {
+        // id: http://www.google.com/calendar/feeds/<username>/<cal id>
+        // desired feed:
+        //   http://www.google.com/calendar/feeds/<cal id>/<projection>
+        int start = url.indexOf(FEEDS_SUBSTRING);
+        if (start != -1) {
+            // strip out the */ in /feeds/*/
+            start += FEEDS_SUBSTRING.length();
+            int end = url.indexOf('/', start);
+            if (end != -1) {
+                url = url.replace(url.substring(start, end + 1), "");
+            }
+            url = url + "/private" + DESIRED_PROJECTION;
+        }
+        return url;
+    }
+
+    /**
+     * Convert the CalenderEntry to a Bundle that can be inserted/updated into the
+     * Calendars table.
+     */
+    private String calendarEntryToContentValues(String account, CalendarsFeed feed,
+            CalendarEntry entry,
+            ContentValues map) {
+        map.clear();
+
+        String url = entry.getAlternateLink();
+
+        // we only want to fetch the full-selfattendance calendar feeds
+        if (!TextUtils.isEmpty(url)) {
+            if (url.endsWith(EXPECTED_PROJECTION)) {
+                url = url.replace(EXPECTED_PROJECTION, DESIRED_PROJECTION);
+            }
+        } else {
+            // yuck.  the alternate link was not available.  we should
+            // reconstruct from the id.
+            url = entry.getId();
+            if (!TextUtils.isEmpty(url)) {
+                url = convertCalendarIdToFeedUrl(url);
+            } else {
+                if (Config.LOGV) {
+                    Log.v(TAG, "Cannot generate url for calendar feed.");
+                }
+                return null;
+            }
+        }
+
+        url = CalendarSyncAdapter.rewriteUrlforAccount(account, url);
+
+        map.put(Calendars.URL, url);
+        map.put(Calendars.NAME, entry.getTitle());
+
+        // TODO:
+        map.put(Calendars.DISPLAY_NAME, entry.getTitle());
+
+        map.put(Calendars.TIMEZONE, entry.getTimezone());
+
+        String colorStr = entry.getColor();
+        if (!TextUtils.isEmpty(colorStr)) {
+            int color = Color.parseColor(colorStr);
+            // Ensure the alpha is set to max
+            color |= 0xff000000;
+            map.put(Calendars.COLOR, color);
+        }
+
+        map.put(Calendars.SELECTED, entry.isSelected() ? 1 : 0);
+
+        map.put(Calendars.HIDDEN, entry.isHidden() ? 1 : 0);
+
+        int accesslevel;
+        switch (entry.getAccessLevel()) {
+        case CalendarEntry.ACCESS_NONE:
+            accesslevel = Calendars.NO_ACCESS;
+            break;
+        case CalendarEntry.ACCESS_READ:
+            accesslevel = Calendars.READ_ACCESS;
+            break;
+        case CalendarEntry.ACCESS_FREEBUSY:
+            accesslevel = Calendars.FREEBUSY_ACCESS;
+            break;
+        case CalendarEntry.ACCESS_CONTRIBUTOR:
+            accesslevel = Calendars.CONTRIBUTOR_ACCESS;
+            break;
+        case CalendarEntry.ACCESS_OWNER:
+            accesslevel = Calendars.OWNER_ACCESS;
+            break;
+        default:
+            accesslevel = Calendars.NO_ACCESS;
+        }
+        map.put(Calendars.ACCESS_LEVEL, accesslevel);
+        // TODO: use the update time, when calendar actually supports this.
+        // right now, calendar modifies the update time frequently.
+        map.put(Calendars._SYNC_TIME, System.currentTimeMillis());
+
+        return url;
+    }
+
+    /*
+     * Fills the Instances table, if necessary, for the given range and then
+     * queries the Instances table.
+     */
+    private Cursor handleInstanceQuery(SQLiteQueryBuilder qb, long rangeBegin,
+                                       long rangeEnd, String[] projectionIn,
+                                       String selection, String sort) {
+        final SQLiteDatabase db = getDatabase();
+        // will lock the database.
+        acquireInstanceRange(rangeBegin, rangeEnd, true /* use minimum expansion window */);
+        qb.setTables("Instances INNER JOIN Events ON (Instances.event_id=Events._id) " +
+                     "INNER JOIN Calendars ON (Events.calendar_id = Calendars._id)");
+        qb.setProjectionMap(sInstancesProjectionMap);
+        qb.appendWhere("begin <= ");
+        qb.appendWhere(String.valueOf(rangeEnd));
+        qb.appendWhere(" AND end >= ");
+        qb.appendWhere(String.valueOf(rangeBegin));
+        return qb.query(db, projectionIn, selection, null, null, null, sort);
+    }
+
+    private Cursor handleBusyBitsQuery(SQLiteQueryBuilder qb, int startDay,
+                                       int endDay, String[] projectionIn,
+                                       String selection, String sort) {
+        final SQLiteDatabase db = getDatabase();
+        acquireBusyBitRange(startDay, endDay);
+        qb.setTables("BusyBits");
+        qb.setProjectionMap(sBusyBitsProjectionMap);
+        qb.appendWhere("day >= ");
+        qb.appendWhere(String.valueOf(startDay));
+        qb.appendWhere(" AND day <= ");
+        qb.appendWhere(String.valueOf(endDay));
+        return qb.query(db, projectionIn, selection, null, null, null, sort);
+    }
+
+    /**
+     * Ensure that the date range given has all elements in the instance
+     * table.  Acquires the database lock and calls {@link #acquireInstanceRangeLocked}.
+     */
+    private void acquireInstanceRange(final long begin,
+                                      final long end,
+                                      final boolean useMinimumExpansionWindow) {
+        mDb.beginTransaction();
+        try {
+            acquireInstanceRangeLocked(begin, end, useMinimumExpansionWindow);
+            mDb.setTransactionSuccessful();
+        } finally {
+            mDb.endTransaction();
+        }
+    }
+
+    /**
+     * Expands the Instances table (if needed) and the BusyBits table.
+     * Acquires the database lock and calls {@link #acquireBusyBitRangeLocked}.
+     */
+    private void acquireBusyBitRange(final int startDay, final int endDay) {
+        mDb.beginTransaction();
+        try {
+            acquireBusyBitRangeLocked(startDay, endDay);
+            mDb.setTransactionSuccessful();
+        } finally {
+            mDb.endTransaction();
+        }
+    }
+
+    /**
+     * Ensure that the date range given has all elements in the instance
+     * table.  The database lock must be held when calling this method.
+     */
+    private void acquireInstanceRangeLocked(long begin, long end,
+                                            boolean useMinimumExpansionWindow) {
+        long expandBegin = begin;
+        long expandEnd = end;
+
+        if (useMinimumExpansionWindow) {
+            // if we end up having to expand events into the instances table, expand
+            // events for a minimal amount of time, so we do not have to perform
+            // expansions frequently.
+            long span = end - begin;
+            if (span < MINIMUM_EXPANSION_SPAN) {
+                long additionalRange = (MINIMUM_EXPANSION_SPAN - span) / 2;
+                expandBegin -= additionalRange;
+                expandEnd += additionalRange;
+            }
+        }
+
+        // Check if the timezone has changed.
+        // We do this check here because the database is locked and we can
+        // safely delete all the entries in the Instances table.
+        MetaData.Fields fields = mMetaData.getFieldsLocked();
+        String dbTimezone = fields.timezone;
+        long maxInstance = fields.maxInstance;
+        long minInstance = fields.minInstance;
+        String localTimezone = TimeZone.getDefault().getID();
+        boolean timezoneChanged = (dbTimezone == null) || !dbTimezone.equals(localTimezone);
+
+        if (maxInstance == 0 || timezoneChanged) {
+            // Empty the Instances table and expand from scratch.
+            mDb.execSQL("DELETE FROM Instances;");
+            mDb.execSQL("DELETE FROM BusyBits;");
+            if (Config.LOGV) {
+                Log.v(TAG, "acquireInstanceRangeLocked() deleted Instances and Busybits,"
+                        + " timezone changed: " + timezoneChanged);
+            }
+            expandInstanceRangeLocked(expandBegin, expandEnd, localTimezone);
+
+            mMetaData.writeLocked(localTimezone, expandBegin, expandEnd,
+                    0 /* startDay */, 0 /* endDay */);
+            return;
+        }
+
+        // If the desired range [begin, end] has already been
+        // expanded, then simply return.  The range is inclusive, that is,
+        // events that touch either endpoint are included in the expansion.
+        // This means that a zero-duration event that starts and ends at
+        // the endpoint will be included.
+        // We use [begin, end] here and not [expandBegin, expandEnd] for
+        // checking the range because a common case is for the client to
+        // request successive days or weeks, for example.  If we checked
+        // that the expanded range [expandBegin, expandEnd] then we would
+        // always be expanding because there would always be one more day
+        // or week that hasn't been expanded.
+        if ((begin >= minInstance) && (end <= maxInstance)) {
+            if (Config.LOGV) {
+                Log.v(TAG, "Canceled instance query (" + expandBegin + ", " + expandEnd
+                        + ") falls within previously expanded range.");
+            }
+            return;
+        }
+
+        // If the requested begin point has not been expanded, then include
+        // more events than requested in the expansion (use "expandBegin").
+        if (begin < minInstance) {
+            expandInstanceRangeLocked(expandBegin, minInstance, localTimezone);
+            minInstance = expandBegin;
+        }
+
+        // If the requested end point has not been expanded, then include
+        // more events than requested in the expansion (use "expandEnd").
+        if (end > maxInstance) {
+            expandInstanceRangeLocked(maxInstance, expandEnd, localTimezone);
+            maxInstance = expandEnd;
+        }
+
+        // Update the bounds on the Instances table.
+        mMetaData.writeLocked(localTimezone, minInstance, maxInstance,
+                fields.minBusyBit, fields.maxBusyBit);
+    }
+
+    private void acquireBusyBitRangeLocked(int firstDay, int lastDay) {
+        if (firstDay > lastDay) {
+            throw new IllegalArgumentException("firstDay must not be greater than lastDay");
+        }
+        String localTimezone = TimeZone.getDefault().getID();
+        MetaData.Fields fields = mMetaData.getFieldsLocked();
+        String dbTimezone = fields.timezone;
+        int minBusyBit = fields.minBusyBit;
+        int maxBusyBit = fields.maxBusyBit;
+        boolean timezoneChanged = (dbTimezone == null) || !dbTimezone.equals(localTimezone);
+        if (firstDay >= minBusyBit && lastDay <= maxBusyBit && !timezoneChanged) {
+            if (Config.LOGV) {
+                Log.v(TAG, "acquireBusyBitRangeLocked() no expansion needed");
+            }
+            return;
+        }
+
+        // Avoid gaps in the BusyBit table and avoid recomputing the busy bits
+        // that are already in the table.  If the busy bit range has been cleared,
+        // don't bother checking.
+        if (maxBusyBit != 0) {
+            if (firstDay > maxBusyBit) {
+                firstDay = maxBusyBit;
+            } else if (lastDay < minBusyBit) {
+                lastDay = minBusyBit;
+            } else if (firstDay < minBusyBit && lastDay <= maxBusyBit) {
+                lastDay = minBusyBit;
+            } else if (lastDay > maxBusyBit && firstDay >= minBusyBit) {
+                firstDay = maxBusyBit;
+            }
+        }
+
+        // Allocate space for the busy bits, one 32-bit integer for each day.
+        int numDays = lastDay - firstDay + 1;
+        int[] busybits = new int[numDays];
+        int[] allDayCounts = new int[numDays];
+
+        // Convert the first and last Julian day range to a range that uses
+        // UTC milliseconds.
+        Time time = new Time();
+        long begin = time.setJulianDay(firstDay);
+
+        // We add one to lastDay because the time is set to 12am on the given
+        // Julian day and we want to include all the events on the last day.
+        long end = time.setJulianDay(lastDay + 1);
+
+        // Make sure the Instances table includes events in the range
+        // [begin, end].
+        acquireInstanceRange(begin, end, true /* use minimum expansion window */);
+
+        SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
+        qb.setTables("Instances INNER JOIN Events ON (Instances.event_id=Events._id) " +
+                "INNER JOIN Calendars ON (Events.calendar_id = Calendars._id)");
+        qb.setProjectionMap(sInstancesProjectionMap);
+        qb.appendWhere("begin <= ");
+        qb.appendWhere(String.valueOf(end));
+        qb.appendWhere(" AND end >= ");
+        qb.appendWhere(String.valueOf(begin));
+        qb.appendWhere(" AND ");
+        qb.appendWhere(Instances.SELECTED);
+        qb.appendWhere("=1");
+
+        final SQLiteDatabase db = getDatabase();
+        // Get all the instances that overlap the range [begin,end]
+        Cursor cursor = qb.query(db, sInstancesProjection, null, null, null, null, null);
+        int count = 0;
+        try {
+            count = cursor.getCount();
+            while (cursor.moveToNext()) {
+                int startDay = cursor.getInt(INSTANCES_INDEX_START_DAY);
+                int endDay = cursor.getInt(INSTANCES_INDEX_END_DAY);
+                int startMinute = cursor.getInt(INSTANCES_INDEX_START_MINUTE);
+                int endMinute = cursor.getInt(INSTANCES_INDEX_END_MINUTE);
+                boolean allDay = cursor.getInt(INSTANCES_INDEX_ALL_DAY) != 0;
+                fillBusyBits(firstDay, startDay, endDay, startMinute, endMinute,
+                        allDay, busybits, allDayCounts);
+            }
+        } finally {
+            if (cursor != null) {
+                cursor.close();
+            }
+        }
+
+        if (count == 0) {
+            return;
+        }
+
+        // Read the busybit range again because that may have changed when we
+        // called acquireInstanceRange().
+        fields = mMetaData.getFieldsLocked();
+        minBusyBit = fields.minBusyBit;
+        maxBusyBit = fields.maxBusyBit;
+
+        // If the busybit range was cleared, then delete all the entries.
+        if (maxBusyBit == 0) {
+            mDb.execSQL("DELETE FROM BusyBits;");
+        }
+
+        // Merge the busy bits with the database.
+        mergeBusyBits(firstDay, lastDay, busybits, allDayCounts);
+        if (maxBusyBit == 0) {
+            minBusyBit = firstDay;
+            maxBusyBit = lastDay;
+        } else {
+            if (firstDay < minBusyBit) {
+                minBusyBit = firstDay;
+            }
+            if (lastDay > maxBusyBit) {
+                maxBusyBit = lastDay;
+            }
+        }
+        // Update the busy bit range
+        mMetaData.writeLocked(fields.timezone, fields.minInstance, fields.maxInstance,
+                minBusyBit, maxBusyBit);
+    }
+
+    private static final String[] EXPAND_COLUMNS = new String[] {
+                    Events._ID,
+                    Events._SYNC_ID,
+                    Events.STATUS,
+                    Events.DTSTART,
+                    Events.DTEND,
+                    Events.EVENT_TIMEZONE,
+                    Events.RRULE,
+                    Events.RDATE,
+                    Events.EXRULE,
+                    Events.EXDATE,
+                    Events.DURATION,
+                    Events.ALL_DAY,
+                    Events.ORIGINAL_EVENT,
+                    Events.ORIGINAL_INSTANCE_TIME
+                };
+
+    private static class CanceledInstance {
+        public final long begin;
+        public final String syncId;
+
+        public CanceledInstance(long b, String id) {
+            begin = b;
+            syncId = id;
+        }
+    };
+
+    /**
+     * Make instances for the given range.
+     */
+    private void expandInstanceRangeLocked(long begin, long end, String localTimezone) {
+
+        if (PROFILE) {
+            Debug.startMethodTracing("expandInstanceRangeLocked");
+        }
+
+        final SQLiteDatabase db = getDatabase();
+        Cursor entries = null;
+
+        if (Config.LOGV) {
+            Log.v(TAG, "Expanding events between " + begin + " and " + end);
+        }
+
+        try {
+            SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
+            qb.setTables("Events INNER JOIN Calendars ON (calendar_id = Calendars._id)");
+            qb.setProjectionMap(sEventsProjectionMap);
+
+            qb.appendWhere("dtstart <= ");
+            qb.appendWhere(String.valueOf(end));
+            qb.appendWhere(" AND ");
+            qb.appendWhere("(lastDate IS NULL OR lastDate >= ");
+            qb.appendWhere(String.valueOf(begin));
+            qb.appendWhere(")");
+
+            entries = qb.query(db, EXPAND_COLUMNS, null, null, null, null, null);
+
+            RecurrenceProcessor rp = new RecurrenceProcessor();
+
+            TreeSet<Long> dates = new TreeSet<Long>();
+
+            int statusColumn = entries.getColumnIndex(Events.STATUS);
+            int dtstartColumn = entries.getColumnIndex(Events.DTSTART);
+            int dtendColumn = entries.getColumnIndex(Events.DTEND);
+            int eventTimezoneColumn = entries.getColumnIndex(Events.EVENT_TIMEZONE);
+            int durationColumn = entries.getColumnIndex(Events.DURATION);
+            int rruleColumn = entries.getColumnIndex(Events.RRULE);
+            int rdateColumn = entries.getColumnIndex(Events.RDATE);
+            int exruleColumn = entries.getColumnIndex(Events.EXRULE);
+            int exdateColumn = entries.getColumnIndex(Events.EXDATE);
+            int allDayColumn = entries.getColumnIndex(Events.ALL_DAY);
+            int idColumn = entries.getColumnIndex(Events._ID);
+            int syncIdColumn = entries.getColumnIndex(Events._SYNC_ID);
+            int originalEventColumn = entries.getColumnIndex(Events.ORIGINAL_EVENT);
+            int originalInstanceTimeColumn = entries.getColumnIndex(Events.ORIGINAL_INSTANCE_TIME);
+
+            ContentValues initialValues;
+            EventInstancesMap instancesMap = new EventInstancesMap();
+            ArrayList<CanceledInstance> canceled = new ArrayList<CanceledInstance>();
+
+            Duration duration = new Duration();
+            Time eventTime = new Time();
+
+            while (entries.moveToNext()) {
+                initialValues = null;
+
+                boolean allDay = entries.getInt(allDayColumn) != 0;
+
+                String eventTimezone = entries.getString(eventTimezoneColumn);
+                if (allDay || TextUtils.isEmpty(eventTimezone)) {
+                  // in the events table, allDay events start at midnight.
+                  // this forces them to stay at midnight for all day events
+                  // TODO: check that this actually does the right thing.
+                  eventTimezone = Time.TIMEZONE_UTC;
+                }
+
+                long dtstartMillis = entries.getLong(dtstartColumn);
+                Long eventId = Long.valueOf(entries.getLong(idColumn));
+
+                String durationStr = entries.getString(durationColumn);
+                if (durationStr != null) {
+                    try {
+                        duration.parse(durationStr);
+                    }
+                    catch (DateException e) {
+                        Log.w(TAG, "error parsing duration for event "
+                                + eventId + "'" + durationStr + "'", e);
+                        duration.sign = 1;
+                        duration.weeks = 0;
+                        duration.days = 0;
+                        duration.hours = 0;
+                        duration.minutes = 0;
+                        duration.seconds = 0;
+                        durationStr = "+P0S";
+                    }
+                }
+
+                String syncId = entries.getString(syncIdColumn);
+                String originalEvent = entries.getString(originalEventColumn);
+                long originalInstanceTimeMillis = -1;
+                if (!entries.isNull(originalInstanceTimeColumn)) {
+                    originalInstanceTimeMillis= entries.getLong(originalInstanceTimeColumn);
+                }
+                int status = entries.getInt(statusColumn);
+
+                String rruleStr = entries.getString(rruleColumn);
+                String rdateStr = entries.getString(rdateColumn);
+                String exruleStr = entries.getString(exruleColumn);
+                String exdateStr = entries.getString(exdateColumn);
+
+                RecurrenceSet recur = new RecurrenceSet(rruleStr, rdateStr, exruleStr, exdateStr);
+
+                if (recur.hasRecurrence()) {
+                    // the event is repeating
+
+                    if (status == Events.STATUS_CANCELED) {
+                        // should not happen!
+                        Log.e(TAG, "Found canceled recurring event in "
+                        + "Events table.  Ignoring.");
+                        continue;
+                    }
+
+                    // need to parse the event into a local calendar.
+                    eventTime.timezone = eventTimezone;
+                    eventTime.set(dtstartMillis);
+                    eventTime.allDay = allDay;
+
+                    if (durationStr == null) {
+                        // should not happen.
+                        Log.e(TAG, "Repeating event has no duration -- "
+                                + "should not happen.");
+                        if (allDay) {
+                            // set to one day.
+                            duration.sign = 1;
+                            duration.weeks = 0;
+                            duration.days = 1;
+                            duration.hours = 0;
+                            duration.minutes = 0;
+                            duration.seconds = 0;
+                            durationStr = "+P1D";
+                        } else {
+                            // compute the duration from dtend, if we can.
+                            // otherwise, use 0s.
+                            duration.sign = 1;
+                            duration.weeks = 0;
+                            duration.days = 0;
+                            duration.hours = 0;
+                            duration.minutes = 0;
+                            if (!entries.isNull(dtendColumn)) {
+                                long dtendMillis = entries.getLong(dtendColumn);
+                                duration.seconds = (int) ((dtendMillis - dtstartMillis) / 1000);
+                                durationStr = "+P" + duration.seconds + "S";
+                            } else {
+                                duration.seconds = 0;
+                                durationStr = "+P0S";
+                            }
+                        }
+                    }
+
+                    try {
+                        rp.expand(eventTime, recur,
+                                  begin /* range start */, end /* range end */, dates);
+                    }
+                    catch (DateException e) {
+                        Log.w(TAG, "RecurrenceProcessor.expand skipping",e);
+                        continue;
+                    }
+
+                    // Initialize the "eventTime" timezone outside the loop.
+                    // This is used in computeTimezoneDependentFields().
+                    if (allDay) {
+                        eventTime.timezone = Time.TIMEZONE_UTC;
+                    } else {
+                        eventTime.timezone = localTimezone;
+                    }
+
+                    for (long date : dates) {
+                        initialValues = new ContentValues();
+                        initialValues.put(Instances.EVENT_ID, eventId);
+
+                        initialValues.put(Instances.BEGIN, date);
+                        long dtendMillis = duration.addTo(date);
+                        initialValues.put(Instances.END, dtendMillis);
+
+                        computeTimezoneDependentFields(date, dtendMillis,
+                                eventTime, initialValues);
+                        instancesMap.add(syncId, initialValues);
+                    }
+                } else {
+                    // the event is not repeating
+
+                    // if this event has an "original" field, then record
+                    // that we need to cancel the original event (we can't
+                    // do that here because the order of this loop isn't
+                    // defined)
+                    if (originalEvent != null && originalInstanceTimeMillis != -1) {
+                        canceled.add(new CanceledInstance(originalInstanceTimeMillis,
+                                     originalEvent));
+                    }
+
+                    // do not create an instance in the Instances table if this
+                    // is a canceled event/exception to a recurrence.
+                    if (status == Events.STATUS_CANCELED) {
+                        continue;
+                    }
+
+                    initialValues = new ContentValues();
+                    initialValues.put(Instances.EVENT_ID, eventId);
+                    initialValues.put(Instances.BEGIN, dtstartMillis);
+
+                    long dtendMillis = dtstartMillis;
+                    if (durationStr == null) {
+                        if (!entries.isNull(dtendColumn)) {
+                            dtendMillis = entries.getLong(dtendColumn);
+                        }
+                    } else {
+                        dtendMillis = duration.addTo(dtstartMillis);
+                    }
+                    initialValues.put(Instances.END, dtendMillis);
+
+                    if (allDay) {
+                        eventTime.timezone = Time.TIMEZONE_UTC;
+                    } else {
+                        eventTime.timezone = localTimezone;
+                    }
+                    computeTimezoneDependentFields(dtstartMillis, dtendMillis,
+                            eventTime, initialValues);
+
+                    instancesMap.add(syncId, initialValues);
+                }
+            }
+
+            // remove the ones that should be canceled
+
+            int numCanceled = canceled.size();
+            for (int i=0; i<numCanceled; i++) {
+                CanceledInstance cancellation = canceled.get(i);
+                if (TextUtils.isEmpty(cancellation.syncId)) {
+                    // should this ever happen?
+                    continue;
+                }
+                InstancesList instancesList = instancesMap.get(cancellation.syncId);
+                if (instancesList == null) {
+                    continue;
+                }
+                int numInstances = instancesList.size();
+                for (int j=numInstances-1; j>=0; j--) {
+                    ContentValues m = instancesList.get(j);
+                    if (m.get(Instances.BEGIN).equals(cancellation.begin)) {
+                        instancesList.remove(j);
+                    }
+                }
+            }
+
+            // Now do the inserts.  Since the db lock is held when this method is executed,
+            // this will be done in a transaction.
+            // NOTE: if there is lock contention (e.g., a sync is trying to merge into the db
+            // while the calendar app is trying to query the db (expanding instances)), we will
+            // not be "polite" and yield the lock until we're done.  This will favor local query
+            // operations over sync/write operations.
+            Collection<InstancesList> lists = instancesMap.values();
+            for (InstancesList list : lists) {
+                for (ContentValues m : list) {
+                    mInstancesInserter.replace(m);
+                    if (false) {
+                        // yield the lock if anyone else is trying to
+                        // perform a db operation here.
+                        db.yieldIfContended();
+                    }
+                }
+            }
+        } catch (TimeFormatException e) {
+            Log.w(TAG, "Exception in instance query preparation", e);
+        }
+        finally {
+            if (entries != null) {
+                entries.close();
+            }
+        }
+        if (PROFILE) {
+            Debug.stopMethodTracing();
+        }
+        //System.out.println("EXIT  insertInstanceRange begin=" + begin + " end=" + end);
+    }
+
+    /**
+     * Computes the timezone-dependent fields of an instance of an event and
+     * updates the "values" map to contain those fields.
+     *
+     * @param begin the start time of the instance (in UTC milliseconds)
+     * @param end the end time of the instance (in UTC milliseconds)
+     * @param local a Time object with the timezone set to the local timezone
+     * @param values a map that will contain the timezone-dependent fields
+     */
+    private void computeTimezoneDependentFields(long begin, long end,
+            Time local, ContentValues values) {
+        local.set(begin);
+        int startDay = Time.getJulianDay(begin, local.gmtoff);
+        int startMinute = local.hour * 60 + local.minute;
+
+        local.set(end);
+        int endDay = Time.getJulianDay(end, local.gmtoff);
+        int endMinute = local.hour * 60 + local.minute;
+
+        // Special case for midnight, which has endMinute == 0.  Change
+        // that to +24 hours on the previous day to make everything simpler.
+        // Exception: if start and end minute are both 0 on the same day,
+        // then leave endMinute alone.
+        if (endMinute == 0 && endDay > startDay) {
+            endMinute = 24 * 60;
+            endDay -= 1;
+        }
+
+        values.put(Instances.START_DAY, startDay);
+        values.put(Instances.END_DAY, endDay);
+        values.put(Instances.START_MINUTE, startMinute);
+        values.put(Instances.END_MINUTE, endMinute);
+    }
+
+    private void fillBusyBits(int minDay, int startDay, int endDay, int startMinute,
+            int endMinute, boolean allDay, int[] busybits, int[] allDayCounts) {
+
+        // The startDay can be less than the minDay if we have an event
+        // that starts earlier than the time range we are interested in.
+        // In that case, we ignore the time range that falls outside the
+        // the range we are interested in.
+        if (startDay < minDay) {
+            startDay = minDay;
+            startMinute = 0;
+        }
+
+        // Likewise, truncate the event's end day so that it doesn't go past
+        // the expected range.
+        int numDays = busybits.length;
+        int stopDay = endDay;
+        if (stopDay > minDay + numDays - 1) {
+            stopDay = minDay + numDays - 1;
+        }
+        int dayIndex = startDay - minDay;
+
+        if (allDay) {
+            for (int day = startDay; day <= stopDay; day++, dayIndex++) {
+                allDayCounts[dayIndex] += 1;
+            }
+            return;
+        }
+
+        for (int day = startDay; day <= stopDay; day++, dayIndex++) {
+            int endTime = endMinute;
+            // If the event ends on a future day, then show it extending to
+            // the end of this day.
+            if (endDay > day) {
+                endTime = 24 * 60;
+            }
+
+            int startBit = startMinute / BUSYBIT_INTERVAL ;
+            int endBit = (endTime + BUSYBIT_INTERVAL - 1) / BUSYBIT_INTERVAL;
+            int len = endBit - startBit;
+            if (len == 0) {
+                len = 1;
+            }
+            if (len < 0 || len > 24) {
+                Log.e("Cal", "fillBusyBits() error: len " + len
+                        + " startMinute,endTime " + startMinute + " , " + endTime
+                        + " startDay,endDay " + startDay + " , " + endDay);
+            } else {
+                int oneBits = BIT_MASKS[len];
+                busybits[dayIndex] |= oneBits << startBit;
+            }
+
+            // Set the start minute to the beginning of the day, in
+            // case this event spans multiple days.
+            startMinute = 0;
+        }
+    }
+
+    private void mergeBusyBits(int startDay, int endDay, int[] busybits, int[] allDayCounts) {
+        mDb.beginTransaction();
+        try {
+            mergeBusyBitsLocked(startDay, endDay, busybits, allDayCounts);
+            mDb.setTransactionSuccessful();
+        } finally {
+            mDb.endTransaction();
+        }
+    }
+
+    private void mergeBusyBitsLocked(int startDay, int endDay, int[] busybits,
+            int[] allDayCounts) {
+        final SQLiteDatabase db = getDatabase();
+        Cursor cursor = null;
+        try {
+            String selection = "day>=" + startDay + " AND day<=" + endDay;
+            cursor = db.query("BusyBits", sBusyBitProjection, selection, null, null, null, null);
+            if (cursor == null) {
+                return;
+            }
+            while (cursor.moveToNext()) {
+                int day = cursor.getInt(BUSYBIT_INDEX_DAY);
+                int busy = cursor.getInt(BUSYBIT_INDEX_BUSYBITS);
+                int allDayCount = cursor.getInt(BUSYBIT_INDEX_ALL_DAY_COUNT);
+
+                int dayIndex = day - startDay;
+                busybits[dayIndex] |= busy;
+                allDayCounts[dayIndex] += allDayCount;
+            }
+        } finally {
+            if (cursor != null) {
+                cursor.close();
+            }
+        }
+
+        // Allocate a map that we can reuse
+        ContentValues values = new ContentValues();
+
+        // Write the busy bits to the database
+        int len = busybits.length;
+        for (int dayIndex = 0; dayIndex < len; dayIndex++) {
+            int busy = busybits[dayIndex];
+            int allDayCount = allDayCounts[dayIndex];
+            if (busy == 0 && allDayCount == 0) {
+                continue;
+            }
+            int day = startDay + dayIndex;
+
+            values.clear();
+            values.put(BusyBits.DAY, day);
+            values.put(BusyBits.BUSYBITS, busy);
+            values.put(BusyBits.ALL_DAY_COUNT, allDayCount);
+            db.replace("BusyBits", null, values);
+        }
+    }
+
+    /**
+     * Updates the BusyBit table when a new event is inserted into the Events
+     * table.  This is called after the event has been entered into the Events
+     * table.  If the event time is not within the date range of the current
+     * BusyBits table, then the busy bits are not updated.  The BusyBits
+     * table is not automatically expanded to include this event.
+     *
+     * @param eventId the id of the newly created event
+     * @param values the ContentValues for the new event
+     */
+    private void insertBusyBitsLocked(long eventId, ContentValues values) {
+        MetaData.Fields fields = mMetaData.getFieldsLocked();
+        if (fields.maxBusyBit == 0) {
+            return;
+        }
+
+        // If this is a recurrence event, then the expanded Instances range
+        // should be 0 because this is called after updateInstancesLocked().
+        // But for now check this condition and report an error if it occurs.
+        // In the future, we could even support recurring events by
+        // expanding them here and updating the busy bits for each instance.
+        if (isRecurrenceEvent(values))  {
+            Log.e(TAG, "insertBusyBitsLocked(): unexpected recurrence event\n");
+            return;
+        }
+
+        long dtstartMillis = values.getAsLong(Events.DTSTART);
+        Long dtendMillis = values.getAsLong(Events.DTEND);
+        if (dtendMillis == null) {
+            dtendMillis = dtstartMillis;
+        }
+
+        boolean allDay = false;
+        Integer allDayInteger = values.getAsInteger(Events.ALL_DAY);
+        if (allDayInteger != null) {
+            allDay = allDayInteger != 0;
+        }
+
+        Time time = new Time();
+        if (allDay) {
+            time.timezone = Time.TIMEZONE_UTC;
+        }
+
+        ContentValues busyValues = new ContentValues();
+        computeTimezoneDependentFields(dtstartMillis, dtendMillis, time, busyValues);
+
+        int startDay = busyValues.getAsInteger(Instances.START_DAY);
+        int endDay = busyValues.getAsInteger(Instances.END_DAY);
+
+        // If the event time is not in the expanded BusyBits range,
+        // then return.
+        if (startDay > fields.maxBusyBit || endDay < fields.minBusyBit) {
+            return;
+        }
+
+        // Allocate space for the busy bits, one 32-bit integer for each day,
+        // plus 24 bytes for the count of events that occur in each time slot.
+        int numDays = endDay - startDay + 1;
+        int[] busybits = new int[numDays];
+        int[] allDayCounts = new int[numDays];
+
+        int startMinute = busyValues.getAsInteger(Instances.START_MINUTE);
+        int endMinute = busyValues.getAsInteger(Instances.END_MINUTE);
+        fillBusyBits(startDay, startDay, endDay, startMinute, endMinute,
+                allDay, busybits, allDayCounts);
+        mergeBusyBits(startDay, endDay, busybits, allDayCounts);
+    }
+
+    /**
+     * Updates the busy bits for an event that is being updated.  This is
+     * called before the event is updated in the Events table because we need
+     * to know the time of the event before it was changed.
+     *
+     * @param eventId the id of the event being updated
+     * @param values the ContentValues for the updated event
+     */
+    private void updateBusyBitsLocked(long eventId, ContentValues values) {
+        MetaData.Fields fields = mMetaData.getFieldsLocked();
+        if (fields.maxBusyBit == 0) {
+            return;
+        }
+
+        // If this is a recurring event, then clear the BusyBits table.
+        if (isRecurrenceEvent(values))  {
+            mMetaData.writeLocked(fields.timezone, fields.minInstance, fields.maxInstance,
+                    0 /* startDay */, 0 /* endDay */);
+            return;
+        }
+
+        // If the event fields being updated don't contain the start or end
+        // time, then we don't need to bother updating the BusyBits table.
+        Long dtstartLong = values.getAsLong(Events.DTSTART);
+        Long dtendLong = values.getAsLong(Events.DTEND);
+        if (dtstartLong == null && dtendLong == null) {
+            return;
+        }
+
+        // If the timezone has changed, then clear the busy bits table
+        // and return.
+        String dbTimezone = fields.timezone;
+        String localTimezone = TimeZone.getDefault().getID();
+        boolean timezoneChanged = (dbTimezone == null) || !dbTimezone.equals(localTimezone);
+        if (timezoneChanged) {
+            mMetaData.writeLocked(fields.timezone, fields.minInstance, fields.maxInstance,
+                    0 /* startDay */, 0 /* endDay */);
+            return;
+        }
+
+        // Read the existing event start and end times from the Events table.
+        TimeRange eventRange = readEventStartEnd(eventId);
+
+        // Fill in the new start time (if missing) or the new end time (if
+        // missing) from the existing event start and end times.
+        long dtstartMillis;
+        if (dtstartLong != null) {
+            dtstartMillis = dtstartLong;
+        } else {
+            dtstartMillis = eventRange.begin;
+        }
+
+        long dtendMillis;
+        if (dtendLong != null) {
+            dtendMillis = dtendLong;
+        } else {
+            dtendMillis = eventRange.end;
+        }
+
+        // Compute the start and end Julian days for the event.
+        Time time = new Time();
+        if (eventRange.allDay) {
+            time.timezone = Time.TIMEZONE_UTC;
+        }
+        ContentValues busyValues = new ContentValues();
+        computeTimezoneDependentFields(eventRange.begin, eventRange.end, time, busyValues);
+        int oldStartDay = busyValues.getAsInteger(Instances.START_DAY);
+        int oldEndDay = busyValues.getAsInteger(Instances.END_DAY);
+
+        boolean allDay = false;
+        Integer allDayInteger = values.getAsInteger(Events.ALL_DAY);
+        if (allDayInteger != null) {
+            allDay = allDayInteger != 0;
+        }
+
+        if (allDay) {
+            time.timezone = Time.TIMEZONE_UTC;
+        } else {
+            time.timezone = TimeZone.getDefault().getID();
+        }
+
+        computeTimezoneDependentFields(dtstartMillis, dtendMillis, time, busyValues);
+        int newStartDay = busyValues.getAsInteger(Instances.START_DAY);
+        int newEndDay = busyValues.getAsInteger(Instances.END_DAY);
+
+        // If both the old and new event times are outside the expanded
+        // BusyBits table, then return.
+        if ((oldStartDay > fields.maxBusyBit || oldEndDay < fields.minBusyBit)
+                && (newStartDay > fields.maxBusyBit || newEndDay < fields.minBusyBit)) {
+            return;
+        }
+
+        // If the old event time is within the expanded Instances range,
+        // then clear the BusyBits table and return.
+        if (oldStartDay <= fields.maxBusyBit && oldEndDay >= fields.minBusyBit) {
+            // We could recompute the busy bits for the days containing the
+            // old event time.  For now, just clear the BusyBits table.
+            mMetaData.writeLocked(fields.timezone, fields.minInstance, fields.maxInstance,
+                    0 /* startDay */, 0 /* endDay */);
+            return;
+        }
+
+        // The new event time is within the expanded Instances range.
+        // So insert the busy bits for that day (or days).
+
+        // Allocate space for the busy bits, one 32-bit integer for each day,
+        // plus 24 bytes for the count of events that occur in each time slot.
+        int numDays = newEndDay - newStartDay + 1;
+        int[] busybits = new int[numDays];
+        int[] allDayCounts = new int[numDays];
+
+        int startMinute = busyValues.getAsInteger(Instances.START_MINUTE);
+        int endMinute = busyValues.getAsInteger(Instances.END_MINUTE);
+        fillBusyBits(newStartDay, newStartDay, newEndDay, startMinute, endMinute,
+                allDay, busybits, allDayCounts);
+        mergeBusyBits(newStartDay, newEndDay, busybits, allDayCounts);
+    }
+
+    /**
+     * This method is called just before an event is deleted.
+     *
+     * @param eventId
+     */
+    private void deleteBusyBitsLocked(long eventId) {
+        MetaData.Fields fields = mMetaData.getFieldsLocked();
+        if (fields.maxBusyBit == 0) {
+            return;
+        }
+
+        // TODO: if the event being deleted is not a recurring event and the
+        // start and end time are outside the BusyBit range, then we could
+        // avoid clearing the BusyBits table.  For now, always clear the
+        // BusyBits table because deleting events is relatively rare.
+        mMetaData.writeLocked(fields.timezone, fields.minInstance, fields.maxInstance,
+                0 /* startDay */, 0 /* endDay */);
+    }
+
+    // Read the start and end time for an event from the Events table.
+    // Also read the "all-day" indicator.
+    private TimeRange readEventStartEnd(long eventId) {
+        Cursor cursor = null;
+        TimeRange range = new TimeRange();
+        try {
+            cursor = query(ContentUris.withAppendedId(Events.CONTENT_URI, eventId),
+                    new String[] { Events.DTSTART, Events.DTEND, Events.ALL_DAY },
+                    null /* selection */,
+                    null /* selectionArgs */,
+                    null /* sort */);
+            if (cursor == null) {
+                return null;
+            }
+            cursor.moveToFirst();
+            range.begin = cursor.getLong(0);
+            range.end = cursor.getLong(1);
+            range.allDay = cursor.getInt(2) != 0;
+        } finally {
+            if (cursor != null) {
+                cursor.close();
+            }
+        }
+        return range;
+    }
+
+    @Override
+    public String getType(Uri url) {
+        int match = sURLMatcher.match(url);
+        switch (match) {
+        case EVENTS:
+            return "vnd.android.cursor.dir/event";
+        case EVENTS_ID:
+            return "vnd.android.cursor.item/event";
+        case REMINDERS:
+            return "vnd.android.cursor.dir/reminder";
+        case REMINDERS_ID:
+            return "vnd.android.cursor.item/reminder";
+        case CALENDAR_ALERTS:
+            return "vnd.android.cursor.dir/calendar-alert";
+        case CALENDAR_ALERTS_BY_INSTANCE:
+            return "vnd.android.cursor.dir/calendar-alert-by-instance";
+        case CALENDAR_ALERTS_ID:
+            return "vnd.android.cursor.item/calendar-alert";
+        case INSTANCES:
+            return "vnd.android.cursor.dir/event-instance";
+        case BUSYBITS:
+            return "vnd.android.cursor.dir/busybits";
+        default:
+            throw new IllegalArgumentException("Unknown URL " + url);
+        }
+    }
+
+    public static boolean isRecurrenceEvent(ContentValues values) {
+        return (!TextUtils.isEmpty(values.getAsString(Events.RRULE))||
+                !TextUtils.isEmpty(values.getAsString(Events.RDATE))||
+                !TextUtils.isEmpty(values.getAsString(Events.ORIGINAL_EVENT)));
+    }
+
+    @Override
+    public Uri insertInternal(Uri url, ContentValues initialValues) {
+        final SQLiteDatabase db = getDatabase();
+        long rowID;
+
+        int match = sURLMatcher.match(url);
+        switch (match) {
+            case EVENTS:
+                if (!isTemporary()) {
+                    initialValues.put(Events._SYNC_DIRTY, 1);
+
+                    // Disallow inserting the attendee status in the Events
+                    // table because that makes it harder to keep the value
+                    // consistent with the corresponding entry in the
+                    // Attendees table.  Note that it's okay (and expected)
+                    // for the temporary table to contain the attendee status
+                    // because that comes from the server sync and the Events
+                    // table is already consistent with the Attendees table.
+                    if (initialValues.containsKey(Events.SELF_ATTENDEE_STATUS)) {
+                        throw new IllegalArgumentException("Inserting "
+                                + Events.SELF_ATTENDEE_STATUS
+                                + " in Events table is not allowed");
+                    }
+
+                    if (!initialValues.containsKey(Events.DTSTART)) {
+                        throw new RuntimeException("DTSTART field missing from event");
+                    }
+                }
+                // TODO: avoid the call to updateBundleFromEvent if this is just finding local
+                // changes.  or avoid for temp providers altogether, if we can compute this
+                // during a merge.
+                // TODO: do we really need to make a copy?
+                ContentValues updatedValues = updateContentValuesFromEvent(initialValues);
+                if (updatedValues == null) {
+                    throw new RuntimeException("Could not insert event.");
+                    // return null;
+                }
+                long rowId = mEventsInserter.insert(updatedValues);
+                Uri uri = Uri.parse("content://" + url.getAuthority() + "/events/" + rowId);
+                if (!isTemporary() && rowId != -1) {
+                    updateEventRawTimesLocked(rowId, updatedValues);
+                    updateInstancesLocked(updatedValues, rowId, true /* new event */, db);
+                    insertBusyBitsLocked(rowId, updatedValues);
+                }
+
+                return uri;
+            case CALENDARS:
+                if (!isTemporary()) {
+                    Integer syncEvents = initialValues.getAsInteger(Calendars.SYNC_EVENTS);
+                    if (syncEvents != null && syncEvents == 1) {
+                        String account = initialValues.getAsString(Calendars._SYNC_ACCOUNT);
+                        String calendarUrl = initialValues.getAsString(Calendars.URL);
+                        scheduleSync(account, false /* two-way sync */, calendarUrl);
+                    }
+                }
+                rowID = mCalendarsInserter.insert(initialValues);
+                return Uri.parse("content://calendar/calendars/" + rowID);
+            case ATTENDEES:
+                // currently, only sync may insert attendees.  we do not support
+                // inserting or removing attendees (which can only be yourself)
+                // from the app.
+                // TODO: remove this restriction when we deal with the full
+                // attendees feed.  we'll also need to put in some protection to
+                // prevent updates to the attendees that the server might reject.
+                if (!isTemporary()) {
+                    throw new IllegalArgumentException("Can only insert attendees into "
+                    + "the temporary provider.");
+                }
+                if (!initialValues.containsKey(Attendees.EVENT_ID)) {
+                    throw new IllegalArgumentException("Attendees values must "
+                        + "contain an event_id");
+                }
+                rowID = mAttendeesInserter.insert(initialValues);
+
+                // Copy the attendee status value to the Events table.
+                updateEventAttendeeStatus(db, initialValues);
+
+                return Uri.parse("content://calendars/attendees/" + rowID);
+            case REMINDERS:
+                if (!initialValues.containsKey(Reminders.EVENT_ID)) {
+                    throw new IllegalArgumentException("Reminders values must "
+                        + "contain an event_id");
+                }
+                rowID = mRemindersInserter.insert(initialValues);
+
+                if (!isTemporary()) {
+                    // Schedule another event alarm, if necessary
+                    scheduleNextAlarm();
+                }
+                return Uri.parse("content://calendars/reminders/" + rowID);
+            case CALENDAR_ALERTS:
+                if (!initialValues.containsKey(CalendarAlerts.EVENT_ID)) {
+                    throw new IllegalArgumentException("CalendarAlerts values must "
+                        + "contain an event_id");
+                }
+                rowID = mCalendarAlertsInserter.insert(initialValues);
+
+                return Uri.parse(CalendarAlerts.CONTENT_URI + "/" + rowID);
+            case EXTENDED_PROPERTIES:
+                if (!initialValues.containsKey(Calendar.ExtendedProperties.EVENT_ID)) {
+                    throw new IllegalArgumentException("ExtendedProperties values must "
+                        + "contain an event_id");
+                }
+                rowID = mExtendedPropertiesInserter.insert(initialValues);
+
+                return Uri.parse("content://calendars/extendedproperties/" + rowID);
+            case DELETED_EVENTS:
+                if (isTemporary()) {
+                    rowID = mDeletedEventsInserter.insert(initialValues);
+                    return Uri.parse("content://calendar/deleted_events/" + rowID);
+                }
+                // fallthrough
+            case EVENTS_ID:
+            case REMINDERS_ID:
+            case CALENDAR_ALERTS_ID:
+            case EXTENDED_PROPERTIES_ID:
+            case INSTANCES:
+                throw new UnsupportedOperationException("Cannot insert into that URL");
+            default:
+                throw new IllegalArgumentException("Unknown URL " + url);
+        }
+    }
+
+    /**
+     * Extracts the calendar email from a calendar feed url.
+     * @param feed the calendar feed url
+     * @return the calendar email that is in the feed url or null if it can't
+     * find the email address.
+     */
+    private String calendarEmailAddressFromFeedUrl(String feed) {
+        // Example feed url:
+        // https://www.google.com/calendar/feeds/foo%40gmail.com/private/full-noattendees
+        String[] pathComponents = feed.split("/");
+        if (pathComponents.length > 5 && "feeds".equals(pathComponents[4])) {
+            try {
+                return URLDecoder.decode(pathComponents[5], "UTF-8");
+            } catch (UnsupportedEncodingException e) {
+                Log.e(TAG, "unable to url decode the email address in calendar " + feed);
+                return null;
+            }
+        }
+
+        Log.e(TAG, "unable to find the email address in calendar " + feed);
+        return null;
+    }
+
+
+    /**
+     * Updates the attendee status in the Events table to be consistent with
+     * the value in the Attendees table.
+     *
+     * @param db the database
+     * @param attendeeValues the column values for one row in the Attendees
+     * table.
+     */
+    private void updateEventAttendeeStatus(SQLiteDatabase db, ContentValues attendeeValues) {
+        // Get the event id for this attendee
+        long eventId = attendeeValues.getAsLong(Attendees.EVENT_ID);
+
+        // Currently, we only fetch the attendee for the owner of the calendar
+        // so all the following expensive code is just wasted overhead.
+        // When we actually support multiple attendees for an event, we will
+        // have to execute this code (and perhaps tune it to make it as
+        // efficient as possible).
+        if (MULTIPLE_ATTENDEES_PER_EVENT) {
+            // Get the calendar id for this event
+            Cursor cursor = null;
+            long calId;
+            try {
+                cursor = query(ContentUris.withAppendedId(Events.CONTENT_URI, eventId),
+                        new String[] { Events.CALENDAR_ID },
+                        null /* selection */,
+                        null /* selectionArgs */,
+                        null /* sort */);
+                if (cursor == null) {
+                    return;
+                }
+                cursor.moveToFirst();
+                calId = cursor.getLong(0);
+            } finally {
+                if (cursor != null) {
+                    cursor.close();
+                }
+            }
+
+            // Get the feed for this Calendar
+            String calendarUrl = null;
+            cursor = null;
+            try {
+                cursor = query(ContentUris.withAppendedId(Calendars.CONTENT_URI, calId),
+                        new String[] { Calendars.URL },
+                        null /* selection */,
+                        null /* selectionArgs */,
+                        null /* sort */);
+                if (cursor == null) {
+                    return;
+                }
+                cursor.moveToFirst();
+                calendarUrl = cursor.getString(0);
+            } finally {
+                if (cursor != null) {
+                    cursor.close();
+                }
+            }
+
+            // Get the email address from the calendar feed
+            String calendarEmail = calendarEmailAddressFromFeedUrl(calendarUrl);
+            if (calendarEmail == null) {
+                return;
+            }
+
+            // Get the email address for this attendee
+            String attendeeEmail = null;
+            if (attendeeValues.containsKey(Attendees.ATTENDEE_EMAIL)) {
+                attendeeEmail = attendeeValues.getAsString(Attendees.ATTENDEE_EMAIL);
+            }
+
+            // If the attendee email does not match the calendar email, then this
+            // attendee is not the owner of this calendar so we don't update the
+            // selfAttendeeStatus in the event.
+            if (!calendarEmail.equals(attendeeEmail)) {
+                return;
+            }
+        }
+
+        int status = Attendees.ATTENDEE_STATUS_NONE;
+        if (attendeeValues.containsKey(Attendees.ATTENDEE_RELATIONSHIP)) {
+            int rel = attendeeValues.getAsInteger(Attendees.ATTENDEE_RELATIONSHIP);
+            if (rel == Attendees.RELATIONSHIP_ORGANIZER) {
+                status = Attendees.ATTENDEE_STATUS_ACCEPTED;
+            }
+        }
+
+        if (attendeeValues.containsKey(Attendees.ATTENDEE_STATUS)) {
+            status = attendeeValues.getAsInteger(Attendees.ATTENDEE_STATUS);
+        }
+
+        ContentValues values = new ContentValues();
+        values.put(Events.SELF_ATTENDEE_STATUS, status);
+        db.update("Events", values, "_id="+eventId, null);
+    }
+
+    private void updateInstancesLocked(ContentValues values,
+                                       long rowId,
+                                       boolean newEvent,
+                                       SQLiteDatabase db) {
+        if (isRecurrenceEvent(values))  {
+            // TODO: insert the new recurrence into the instances table.
+            mMetaData.clearInstanceRange();
+            return;
+        }
+
+        // If there are no expanded Instances, then return.
+        MetaData.Fields fields = mMetaData.getFieldsLocked();
+        if (fields.maxInstance == 0) {
+            return;
+        }
+
+        // if the event is in the expanded range, insert
+        // into the instances table.
+        // TODO: deal with durations.  currently, durations are only used in
+        // recurrences.
+
+        Long dtstartMillis = values.getAsLong(Events.DTSTART);
+
+        if (dtstartMillis == null) {
+            if (newEvent) {
+                // must be present for a new event.
+                throw new RuntimeException("DTSTART missing.");
+            }
+            if (Config.LOGV) Log.v(TAG, "Missing DTSTART.  "
+                    + "No need to update instance.");
+            return;
+        }
+
+        if (!newEvent) {
+            db.delete("Instances", "event_id=" + rowId, null /* selectionArgs */);
+        }
+
+        Long dtendMillis = values.getAsLong(Events.DTEND);
+        if (dtendMillis == null) {
+            dtendMillis = dtstartMillis;
+        }
+
+        if (dtstartMillis <= fields.maxInstance && dtendMillis >= fields.minInstance) {
+            ContentValues instanceValues = new ContentValues();
+            instanceValues.put(Instances.EVENT_ID, rowId);
+            instanceValues.put(Instances.BEGIN, dtstartMillis);
+            instanceValues.put(Instances.END, dtendMillis);
+
+            boolean allDay = false;
+            Integer allDayInteger = values.getAsInteger(Events.ALL_DAY);
+            if (allDayInteger != null) {
+                allDay = allDayInteger != 0;
+            }
+
+            // Update the timezone-dependent fields.
+            Time local = new Time();
+            if (allDay) {
+                local.timezone = Time.TIMEZONE_UTC;
+            } else {
+                local.timezone = fields.timezone;
+            }
+
+            computeTimezoneDependentFields(dtstartMillis, dtendMillis, local, instanceValues);
+            mInstancesInserter.insert(instanceValues);
+        }
+    }
+
+    long calculateLastDate(ContentValues values)
+                    throws DateException {
+        // Allow updates to some event fields like the title or hasAlarm
+        // without requiring DTSTART.
+        if (!values.containsKey(Events.DTSTART)) {
+            if (values.containsKey(Events.DTEND) || values.containsKey(Events.RRULE)
+                    || values.containsKey(Events.DURATION)
+                    || values.containsKey(Events.EVENT_TIMEZONE)
+                    || values.containsKey(Events.RDATE)
+                    || values.containsKey(Events.EXRULE)
+                    || values.containsKey(Events.EXDATE)) {
+                throw new RuntimeException("DTSTART field missing from event");
+            }
+            return -1;
+        }
+        long dtstartMillis = values.getAsLong(Events.DTSTART);
+        long lastMillis = -1;
+
+        // Can we use dtend with a repeating event?  What does that even
+        // mean?
+        // NOTE: if the repeating event has a dtend, we convert it to a
+        // duration during event processing, so this situation should not
+        // occur.
+        Long dtEnd = values.getAsLong(Events.DTEND);
+        if (dtEnd != null) {
+            lastMillis = dtEnd;
+        } else {
+            // find out how long it is
+            Duration duration = new Duration();
+            String durationStr = values.getAsString(Events.DURATION);
+            if (durationStr != null) {
+                duration.parse(durationStr);
+            }
+
+            RecurrenceSet recur = new RecurrenceSet(values);
+
+            if (recur.hasRecurrence()) {
+                // the event is repeating, so find the last date it
+                // could appear on
+
+                String tz = values.getAsString(Events.EVENT_TIMEZONE);
+
+                if (TextUtils.isEmpty(tz)) {
+                    // floating timezone
+                    tz = Time.TIMEZONE_UTC;
+                }
+                Time dtstartLocal = new Time(tz);
+
+                dtstartLocal.set(dtstartMillis);
+
+                RecurrenceProcessor rp = new RecurrenceProcessor();
+                lastMillis = rp.getLastOccurence(dtstartLocal, recur);
+                if (lastMillis == -1) {
+                    return lastMillis;  // -1
+                }
+            } else {
+                // the event is not repeating, just use dtstartMillis
+                lastMillis = dtstartMillis;
+            }
+
+            // that was the beginning of the event.  this is the end.
+            lastMillis = duration.addTo(lastMillis);
+        }
+        return lastMillis;
+    }
+
+    private ContentValues updateContentValuesFromEvent(ContentValues initialValues) {
+        try {
+            ContentValues values = new ContentValues(initialValues);
+
+            long last = calculateLastDate(values);
+            if (last != -1) {
+                values.put(Events.LAST_DATE, last);
+            }
+
+            return values;
+        } catch (DateException e) {
+            // don't add it if there was an error
+            Log.w(TAG, "Could not calculate last date.", e);
+            return null;
+        }
+    }
+
+    private void updateEventRawTimesLocked(long eventId, ContentValues values) {
+        ContentValues rawValues = new ContentValues();
+
+        rawValues.put("event_id", eventId);
+
+        String timezone = values.getAsString(Events.EVENT_TIMEZONE);
+
+        if (TextUtils.isEmpty(timezone)) {
+            // floating timezone
+            timezone = Time.TIMEZONE_UTC;
+        }
+
+        Time time = new Time(timezone);
+        Long dtstartMillis = values.getAsLong(Events.DTSTART);
+        if (dtstartMillis != null) {
+            time.set(dtstartMillis);
+            rawValues.put("dtstart2445", time.format2445());
+        }
+
+        Long dtendMillis = values.getAsLong(Events.DTEND);
+        if (dtendMillis != null) {
+            time.set(dtendMillis);
+            rawValues.put("dtend2445", time.format2445());
+        }
+
+        Long originalInstanceMillis = values.getAsLong(Events.ORIGINAL_INSTANCE_TIME);
+        if (originalInstanceMillis != null) {
+            time.set(originalInstanceMillis);
+            rawValues.put("originalInstanceTime2445", time.format2445());
+        }
+
+        Long lastDateMillis = values.getAsLong(Events.LAST_DATE);
+        if (lastDateMillis != null) {
+            time.set(lastDateMillis);
+            rawValues.put("lastDate2445", time.format2445());
+        }
+
+        mEventsRawTimesInserter.replace(rawValues);
+    }
+
+    @Override
+    public int deleteInternal(Uri url, String where, String[] whereArgs) {
+        final SQLiteDatabase db = getDatabase();
+        int match = sURLMatcher.match(url);
+        switch (match)
+        {
+            case EVENTS_ID:
+            {
+                String id = url.getLastPathSegment();
+                if (where != null) {
+                    throw new UnsupportedOperationException("CalendarProvider "
+                            + "doesn't support where based deletion for type "
+                            + match);
+                }
+                if (!isTemporary()) {
+                    deleteBusyBitsLocked(Integer.parseInt(id));
+
+                    // Query this event to get the fields needed for inserting
+                    // a new row in the DeletedEvents table.
+                    Cursor cursor = db.query("Events", EVENTS_PROJECTION,
+                            "_id=" + id, null, null, null, null);
+                    try {
+                        if (cursor.moveToNext()) {
+                            String syncId = cursor.getString(EVENTS_SYNC_ID_INDEX);
+                            String syncVersion = cursor.getString(EVENTS_SYNC_VERSION_INDEX);
+                            String syncAccount = cursor.getString(EVENTS_SYNC_ACCOUNT_INDEX);
+                            Long calId = cursor.getLong(EVENTS_CALENDAR_ID_INDEX);
+                            
+                            ContentValues values = new ContentValues();
+                            values.put(Events._SYNC_ID, syncId);
+                            values.put(Events._SYNC_VERSION, syncVersion);
+                            values.put(Events._SYNC_ACCOUNT, syncAccount);
+                            values.put(Events.CALENDAR_ID, calId);
+                            mDeletedEventsInserter.insert(values);
+                        }
+                    } finally {
+                        cursor.close();
+                        cursor = null;
+                    }
+                }
+
+                // There is a delete trigger that will cause all instances
+                // matching this event id to get deleted as well.  In fact, all
+                // of the following tables will remove entries matching this
+                // event id: Instances, EventsRawTimes, Attendees, Reminders,
+                // CalendarAlerts, and ExtendedProperties.
+                int result = db.delete("Events", "_id=" + id, null);
+                return result;
+            }
+            case ATTENDEES_ID:
+            {
+              // we currently don't support deletions to the attendees list.
+              // TODO: remove this restriction when we handle the full attendees
+              // feed.  we'll need to put in some logic to check that the
+              // modification will be allowed by the server.
+              throw new IllegalArgumentException("Cannot delete attendees.");
+              //                String id = url.getPathSegments().get(1);
+              //                int result = db.delete("Attendees", "_id="+id, null);
+              //                return result;
+            }
+            case REMINDERS:
+            {
+                int result = db.delete("Reminders", where, whereArgs);
+                return result;
+            }
+            case REMINDERS_ID:
+            {
+                String id = url.getLastPathSegment();
+                int result = db.delete("Reminders", "_id="+id, null);
+                return result;
+            }
+            case CALENDAR_ALERTS:
+            {
+                int result = db.delete("CalendarAlerts", where, whereArgs);
+                return result;
+            }
+            case CALENDAR_ALERTS_ID:
+            {
+                String id = url.getLastPathSegment();
+                int result = db.delete("CalendarAlerts", "_id="+id, null);
+                return result;
+            }
+            case DELETED_EVENTS:
+            case EVENTS:
+                throw new UnsupportedOperationException("Cannot delete that URL");
+            case CALENDARS_ID:
+                StringBuilder whereSb = new StringBuilder("_id=");
+                whereSb.append(url.getPathSegments().get(1));
+                if (!TextUtils.isEmpty(where)) {
+                    whereSb.append(" AND (");
+                    whereSb.append(where);
+                    whereSb.append(')');
+                }
+                where = whereSb.toString();
+                // fall through to CALENDARS for the actual delete
+            case CALENDARS:
+                return deleteMatchingCalendars(where);
+            case INSTANCES:
+                throw new UnsupportedOperationException("Cannot delete that URL");
+            default:
+                throw new IllegalArgumentException("Unknown URL " + url);
+        }
+    }
+
+    private int deleteMatchingCalendars(String where) {
+        // query to find all the calendars that match, for each
+        // - delete calendar subscription
+        // - delete calendar
+
+        int numDeleted = 0;
+        final SQLiteDatabase db = getDatabase();
+        Cursor c = db.query("Calendars", sCalendarsIdProjection, where, null,
+                null, null, null);
+        if (c == null) {
+            return 0;
+        }
+        try {
+            while (c.moveToNext()) {
+                long id = c.getLong(CALENDARS_INDEX_ID);
+                if (!isTemporary()) {
+                    modifyCalendarSubscription(id, false /* not selected */);
+                }
+                c.deleteRow();
+                numDeleted++;
+            }
+        } finally {
+            c.close();
+        }
+        return numDeleted;
+    }
+
+    // TODO: call calculateLastDate()!
+    @Override
+    public int updateInternal(Uri url, ContentValues values,
+            String where, String[] selectionArgs) {
+        // TODO: remove this restriction
+        if (!TextUtils.isEmpty(where)) {
+            throw new IllegalArgumentException(
+                    "WHERE based updates not supported");
+        }
+        final SQLiteDatabase db = getDatabase();
+
+        int match = sURLMatcher.match(url);
+        switch (match) {
+            case CALENDARS_ID:
+            {
+                long id = ContentUris.parseId(url);
+                Integer syncEvents = values.getAsInteger(Calendars.SYNC_EVENTS);
+                if (syncEvents != null && !isTemporary()) {
+                    modifyCalendarSubscription(id, syncEvents == 1);
+                }
+
+                int result = db.update("Calendars", values, "_id="+ id, null);
+                if (!isTemporary()) {
+                    // When we change the display status of a Calendar
+                    // we need to update the busy bits.
+                    if (values.containsKey(Calendars.SELECTED) || (syncEvents != null)) {
+                        // Clear the BusyBits table.
+                        mMetaData.clearBusyBitRange();
+                    }
+                }
+
+                return result;
+            }
+            case EVENTS_ID:
+            {
+                long id = ContentUris.parseId(url);
+                if (!isTemporary()) {
+                    values.put(Events._SYNC_DIRTY, 1);
+
+                    // Disallow updating the attendee status in the Events
+                    // table.  In the future, we could support this but we
+                    // would have to query and update the attendees table
+                    // to keep the values consistent.
+                    if (values.containsKey(Events.SELF_ATTENDEE_STATUS)) {
+                        throw new IllegalArgumentException("Updating "
+                                + Events.SELF_ATTENDEE_STATUS
+                                + " in Events table is not allowed.");
+                    }
+
+                    if (values.containsKey(Events.HTML_URI)) {
+                        throw new IllegalArgumentException("Updating "
+                                + Events.HTML_URI
+                                + " in Events table is not allowed.");
+                    }
+
+                    updateBusyBitsLocked(id, values);
+                }
+
+                ContentValues updatedValues = updateContentValuesFromEvent(values);
+                if (updatedValues == null) {
+                    Log.w(TAG, "Could not update event.");
+                    return 0;
+                }
+
+                int result = db.update("Events", updatedValues, "_id="+id, null);
+                if (!isTemporary()) {
+                    if (result > 0) {
+                        updateEventRawTimesLocked(id, updatedValues);
+                        updateInstancesLocked(updatedValues, id, false /* not a new event */, db);
+
+                        if (values.containsKey(Events.DTSTART)) {
+                            // The start time of the event changed, so run the
+                            // event alarm scheduler.
+                            scheduleNextAlarm();
+                        }
+                    }
+                }
+                return result;
+            }
+            case ATTENDEES_ID:
+            {
+                // Copy the attendee status value to the Events table.
+                updateEventAttendeeStatus(db, values);
+
+                long id = ContentUris.parseId(url);
+                return db.update("Attendees", values, "_id="+id, null);
+            }
+            case CALENDAR_ALERTS_ID:
+            {
+                long id = ContentUris.parseId(url);
+                return db.update("CalendarAlerts", values, "_id="+id, null);
+            }
+            case REMINDERS_ID:
+            {
+                long id = ContentUris.parseId(url);
+                int result = db.update("Reminders", values, "_id="+id, null);
+                if (!isTemporary()) {
+                    // Reschedule the event alarms because the
+                    // "minutes" field may have changed.
+                    scheduleNextAlarm();
+                }
+                return result;
+            }
+            case EXTENDED_PROPERTIES_ID:
+            {
+                long id = ContentUris.parseId(url);
+                return db.update("ExtendedProperties", values, "_id="+id, null);
+            }
+            default:
+                throw new IllegalArgumentException("Unknown URL " + url);
+        }
+    }
+    
+    /**
+     * Schedule a calendar sync for the account.
+     * @param account the account for which to schedule a sync
+     * @param uploadChangesOnly if set, specify that the sync should only send
+     *   up local changes
+     * @param url the url feed for the calendar to sync (may be null)
+     */
+    private void scheduleSync(String account, boolean uploadChangesOnly, String url) {
+        Bundle extras = new Bundle();
+        extras.putString(ContentResolver.SYNC_EXTRAS_ACCOUNT, account);
+        extras.putBoolean(ContentResolver.SYNC_EXTRAS_UPLOAD, uploadChangesOnly);
+        if (url != null) {
+            extras.putString("feed", url);
+            extras.putBoolean(ContentResolver.SYNC_EXTRAS_FORCE, true);
+        }
+        getContext().getContentResolver().startSync(android.provider.Calendar.CONTENT_URI, extras);
+    }
+
+    private void modifyCalendarSubscription(long id, boolean syncEvents) {
+        // get the account, url, and current selected state
+        // for this calendar.
+        Cursor cursor = query(ContentUris.withAppendedId(Calendars.CONTENT_URI, id),
+                new String[] { Calendars._SYNC_ACCOUNT,
+                               Calendars.URL,
+                               Calendars.SYNC_EVENTS},
+                null /* selection */,
+                null /* selectionArgs */,
+                null /* sort */);
+
+        String account = null;
+        String calendarUrl = null;
+        boolean oldSyncEvents = false;
+        if (cursor != null) {
+            try {
+                cursor.moveToFirst();
+                account = cursor.getString(0);
+                calendarUrl = cursor.getString(1);
+                oldSyncEvents = (cursor.getInt(2) != 0);
+            } finally {
+                cursor.close();
+            }
+        }
+
+        if (TextUtils.isEmpty(account) || TextUtils.isEmpty(calendarUrl)) {
+            // should not happen?
+            Log.w(TAG, "Cannot update subscription because account "
+            + "or calendar url empty -- should not happen.");
+            return;
+        }
+
+        if (oldSyncEvents == syncEvents) {
+            // nothing to do
+            return;
+        }
+
+        // If we are no longer syncing a calendar then make sure that the
+        // old calendar sync data is cleared.  Then if we later add this
+        // calendar back, we will sync all the events.
+        if (!syncEvents) {
+            byte[] data = readSyncDataBytes(account);
+            GDataSyncData syncData = AbstractGDataSyncAdapter.newGDataSyncDataFromBytes(data);
+            if (syncData != null) {
+                syncData.feedData.remove(calendarUrl);
+                data = AbstractGDataSyncAdapter.newBytesFromGDataSyncData(syncData);
+                writeSyncDataBytes(account, data);
+            }
+
+            // Delete all of the events in this calendar to save space.
+            // This is the closest we can come to deleting a calendar.
+            // Clients should never actually delete a calendar.  That won't
+            // work.  We need to keep the calendar entry in the Calendars table
+            // in order to know not to sync the events for that calendar from
+            // the server.
+            final SQLiteDatabase db = getDatabase();
+            String[] args = new String[] {Long.toString(id)};
+            db.delete("Events", CALENDAR_ID_SELECTION, args);
+            // Note that we do not delete the matching entries
+            // in the DeletedEvents table.  We will let those
+            // deleted events propagate to the server.
+            
+            // TODO: there is a corner case to deal with here: namely, if
+            // we edit or delete an event on the phone and then remove
+            // (that is, stop syncing) a calendar, and if we also make a
+            // change on the server to that event at about the same time,
+            // then we will never propagate the changes from the phone to
+            // the server.
+        }
+
+        // If the calendar is not selected for syncing, then don't download
+        // events.
+        scheduleSync(account, !syncEvents, calendarUrl);
+    }
+
+    @Override
+    public synchronized SyncAdapter getSyncAdapter() {
+        if (mSyncAdapter == null) {
+            mSyncAdapter = new CalendarSyncAdapter(getContext(), this);
+        }
+        return mSyncAdapter;
+    }
+
+    @Override
+    public void onSyncStop(SyncContext context, boolean success) {
+        super.onSyncStop(context, success);
+        scheduleNextAlarm();
+    }
+
+    @Override
+    protected Iterable<EventMerger> getMergers() {
+        return Collections.singletonList(new EventMerger());
+    }
+
+    /* Retrieve and cache the alarm manager */
+    private AlarmManager getAlarmManager() {
+        synchronized(mAlarmLock) {
+            if (mAlarmManager == null) {
+                Context context = getContext();
+                if (context == null) {
+                    Log.e(TAG, "getAlarmManager() cannot get Context");
+                    return null;
+                }
+                Object service = context.getSystemService(Context.ALARM_SERVICE);
+                mAlarmManager = (AlarmManager) service;
+            }
+            return mAlarmManager;
+        }
+    }
+
+    void scheduleNextAlarmCheck(long triggerTime) {
+        AlarmManager manager = getAlarmManager();
+        if (manager == null) {
+            Log.e(TAG, "scheduleNextAlarmCheck() cannot get AlarmManager");
+            return;
+        }
+        Context context = getContext();
+        Intent intent = new Intent(CalendarReceiver.SCHEDULE);
+        intent.setClass(context, CalendarReceiver.class);
+        PendingIntent pending = PendingIntent.getBroadcast(context,
+                0, intent, PendingIntent.FLAG_NO_CREATE);
+        if (pending != null) {
+            if (DEBUG_ALARMS) {
+                Log.i(TAG, "cancelling pending alarm " + pending);
+            }
+            // Cancel any previous alarms that do the same thing.
+            manager.cancel(pending);
+        }
+        pending = PendingIntent.getBroadcast(context,
+                0, intent, PendingIntent.FLAG_CANCEL_CURRENT);
+
+        if (DEBUG_ALARMS) {
+            Time time = new Time();
+            time.set(triggerTime);
+            String timeStr = time.format(" %a, %b %d, %Y %I:%M%P");
+            Log.i(TAG, "scheduleNextAlarmCheck at: " + triggerTime + timeStr);
+        }
+
+        manager.set(AlarmManager.RTC_WAKEUP, triggerTime, pending);
+    }
+
+    /*
+     * This method runs the alarm scheduler in a background thread.
+     */
+    void scheduleNextAlarm() {
+        Thread thread = new Thread(new AlarmScheduler());
+        thread.start();
+    }
+
+    /**
+     * This method runs in a background thread and schedules an alarm for
+     * the next calendar event, if necessary.
+     */
+    private void runScheduleNextAlarm() {
+        // Do not schedule any events while syncing or if this is a temporary
+        // database.
+        if (isTemporary())
+            return;
+
+        final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+        db.beginTransaction();
+        try {
+            scheduleNextAlarmLocked(db);
+            db.setTransactionSuccessful();
+        } finally {
+            db.endTransaction();
+        }
+    }
+
+    /**
+     * This method looks at the 24-hour window from now for any events that it
+     * needs to schedule.  This method runs within a database transaction.
+     * 
+     * @param db the database
+     */
+    private void scheduleNextAlarmLocked(SQLiteDatabase db) {
+        AlarmManager alarmManager = getAlarmManager();
+        if (alarmManager == null) {
+            Log.e(TAG, "Failed to find the AlarmManager. Could not schedule the next alarm!");
+            return;
+        }
+
+        final long currentMillis = System.currentTimeMillis();
+        final long start = currentMillis - SCHEDULE_ALARM_SLACK;
+        final long end = start + (24 * 60 * 60 * 1000);
+        ContentResolver cr = getContext().getContentResolver();
+        if (DEBUG_ALARMS) {
+            Time time = new Time();
+            time.set(start);
+            String startTimeStr = time.format(" %a, %b %d, %Y %I:%M%P");
+            Log.i(TAG, "runScheduleNextAlarm() start search: " + startTimeStr);
+        }
+
+        // Clear old alarms but keep alarms around for a while to prevent
+        // multiple alerts for the same reminder.  The "clearUpToTime'
+        // should be further in the past than the point in time where
+        // we start searching for events (the "start" variable defined above).
+        long clearUpToTime = currentMillis - CLEAR_OLD_ALARM_THRESHOLD;
+        db.delete("CalendarAlerts", CalendarAlerts.ALARM_TIME + "<" + clearUpToTime, null);
+
+        long nextAlarmTime = end;
+        long alarmTime = CalendarAlerts.findNextAlarmTime(cr, currentMillis);
+        if (alarmTime != -1 && alarmTime < nextAlarmTime) {
+            nextAlarmTime = alarmTime;
+        }
+
+        // Extract events from the database sorted by alarm time.  The
+        // alarm times are computed from Instances.begin (whose units
+        // are milliseconds) and Reminders.minutes (whose units are
+        // minutes).
+        //
+        // Also, ignore events whose end time is already in the past.
+        // Also, ignore events alarms that we have already scheduled.
+        //
+        // Note 1: we can add support for the case where Reminders.minutes
+        // equals -1 to mean use Calendars.minutes by adding a UNION for
+        // that case where the two halves restrict the WHERE clause on
+        // Reminders.minutes != -1 and Reminders.minutes = 1, respectively.
+        //
+        // Note 2: we have to name "myAlarmTime" different from the
+        // "alarmTime" column in CalendarAlerts because otherwise the
+        // query won't find multiple alarms for the same event.
+        String query = "SELECT begin-(minutes*60000) AS myAlarmTime,"
+            + " Instances.event_id AS eventId, begin, end,"
+            + " title, allDay, method, minutes"
+            + " FROM Instances INNER JOIN Events"
+            + " ON (Events._id = Instances.event_id)"
+            + " INNER JOIN Reminders"
+            + " ON (Instances.event_id = Reminders.event_id)"
+            + " WHERE method=" + Reminders.METHOD_ALERT
+            + " AND myAlarmTime>=" + start
+            + " AND myAlarmTime<=" + nextAlarmTime
+            + " AND end>=" + currentMillis
+            + " AND 0=(SELECT count(*) from CalendarAlerts CA"
+            + " where CA.event_id=Instances.event_id AND CA.begin=Instances.begin"
+            + " AND CA.alarmTime=myAlarmTime)"
+            + " ORDER BY myAlarmTime,begin,title";
+
+        acquireInstanceRange(start, end, false /* don't use minimum expansion windows */);
+        Cursor cursor = null;
+        try {
+            cursor = db.rawQuery(query, null);
+
+            int beginIndex = cursor.getColumnIndex(Instances.BEGIN);
+            int endIndex = cursor.getColumnIndex(Instances.END);
+            int eventIdIndex = cursor.getColumnIndex("eventId");
+            int alarmTimeIndex = cursor.getColumnIndex("myAlarmTime");
+            int minutesIndex = cursor.getColumnIndex(Reminders.MINUTES);
+
+            if (DEBUG_ALARMS) {
+                Time time = new Time();
+                time.set(nextAlarmTime);
+                String alarmTimeStr = time.format(" %a, %b %d, %Y %I:%M%P");
+                Log.i(TAG, "nextAlarmTime: " + alarmTimeStr
+                        + " query: " + query
+                        + " cursor results: " + cursor.getCount());
+            }
+
+            while (cursor.moveToNext()) {
+                // Schedule all alarms whose alarm time is as early as any
+                // scheduled alarm.  For example, if the earliest alarm is at
+                // 1pm, then we will schedule all alarms that occur at 1pm
+                // but no alarms that occur later than 1pm.
+                // Actually, we allow alarms up to a minute later to also
+                // be scheduled so that we don't have to check immediately
+                // again after an event alarm goes off.
+                alarmTime = cursor.getLong(alarmTimeIndex);
+                long eventId = cursor.getLong(eventIdIndex);
+                int minutes = cursor.getInt(minutesIndex);
+                long startTime = cursor.getLong(beginIndex);
+
+                if (DEBUG_ALARMS) {
+                    int titleIndex = cursor.getColumnIndex(Events.TITLE);
+                    String title = cursor.getString(titleIndex);
+                    Time time = new Time();
+                    time.set(alarmTime);
+                    String schedTime = time.format(" %a, %b %d, %Y %I:%M%P");
+                    time.set(startTime);
+                    String startTimeStr = time.format(" %a, %b %d, %Y %I:%M%P");
+                    long endTime = cursor.getLong(endIndex);
+                    time.set(endTime);
+                    String endTimeStr = time.format(" - %a, %b %d, %Y %I:%M%P");
+                    time.set(currentMillis);
+                    String currentTimeStr = time.format(" %a, %b %d, %Y %I:%M%P");
+                    Log.i(TAG, "  looking at id: " + eventId + " " + title
+                            + " " + startTime
+                            + startTimeStr + endTimeStr + " alarm: "
+                            + alarmTime + schedTime
+                            + " currentTime: " + currentTimeStr);
+                }
+
+                if (alarmTime < nextAlarmTime) {
+                    nextAlarmTime = alarmTime;
+                } else if (alarmTime > nextAlarmTime + android.pim.DateUtils.MINUTE_IN_MILLIS) {
+                    // This event alarm (and all later ones) will be scheduled
+                    // later.
+                    break;
+                }
+                
+                // Avoid an SQLiteContraintException by checking if this alarm
+                // already exists in the table.
+                if (CalendarAlerts.alarmExists(cr, eventId, startTime, alarmTime)) {
+                    continue;
+                }
+
+                // Insert this alarm into the CalendarAlerts table
+                long endTime = cursor.getLong(endIndex);
+                Uri uri = CalendarAlerts.insert(cr, eventId, startTime,
+                        endTime, alarmTime, minutes);
+                if (uri == null) {
+                    Log.e(TAG, "runScheduleNextAlarm() insert into CalendarAlerts table failed");
+                    continue;
+                }
+
+                Intent intent = new Intent(android.provider.Calendar.EVENT_REMINDER_ACTION);
+                intent.setData(uri);
+
+                // Also include the begin and end time of this event, because
+                // we cannot determine that from the Events database table.
+                intent.putExtra(android.provider.Calendar.EVENT_BEGIN_TIME, startTime);
+                intent.putExtra(android.provider.Calendar.EVENT_END_TIME, endTime);
+                if (DEBUG_ALARMS) {
+                    int titleIndex = cursor.getColumnIndex(Events.TITLE);
+                    String title = cursor.getString(titleIndex);
+                    Time time = new Time();
+                    time.set(alarmTime);
+                    String schedTime = time.format(" %a, %b %d, %Y %I:%M%P");
+                    time.set(startTime);
+                    String startTimeStr = time.format(" %a, %b %d, %Y %I:%M%P");
+                    time.set(endTime);
+                    String endTimeStr = time.format(" %a, %b %d, %Y %I:%M%P");
+                    time.set(currentMillis);
+                    String currentTimeStr = time.format(" %a, %b %d, %Y %I:%M%P");
+                    Log.i(TAG, "  scheduling " + title
+                            + startTimeStr  + " - " + endTimeStr + " alarm: " + schedTime
+                            + " currentTime: " + currentTimeStr
+                            + " uri: " + uri);
+                }
+                PendingIntent sender = PendingIntent.getBroadcast(getContext(),
+                        0, intent, PendingIntent.FLAG_CANCEL_CURRENT);
+                alarmManager.set(AlarmManager.RTC_WAKEUP, alarmTime, sender);
+            }
+        } finally {
+            if (cursor != null) {
+                cursor.close();
+            }
+        }
+
+        // If we scheduled an event alarm, then schedule the next alarm check
+        // for one minute past that alarm.  Otherwise, if there were no
+        // event alarms scheduled, then check again in 24 hours.  If a new
+        // event is inserted before the next alarm check, then this method
+        // will be run again when the new event is inserted.
+        if (nextAlarmTime != Long.MAX_VALUE) {
+            scheduleNextAlarmCheck(nextAlarmTime + android.pim.DateUtils.MINUTE_IN_MILLIS);
+        } else {
+            scheduleNextAlarmCheck(currentMillis + android.pim.DateUtils.DAY_IN_MILLIS);
+        }
+    }
+
+    private static String sEventsTable = "Events";
+    private static Uri sEventsURL =
+            Uri.parse("content://calendar/events/");
+    private static String sDeletedEventsTable = "DeletedEvents";
+    private static Uri sDeletedEventsURL =
+            Uri.parse("content://calendar/deleted_events/");
+    private static String sAttendeesTable = "Attendees";
+    private static String sRemindersTable = "Reminders";
+    private static String sCalendarAlertsTable = "CalendarAlerts";
+    private static String sExtendedPropertiesTable = "ExtendedProperties";
+
+    private class EventMerger extends AbstractTableMerger {
+
+        private ContentValues mValues = new ContentValues();
+        EventMerger() {
+            super(getDatabase(), sEventsTable, sEventsURL, sDeletedEventsTable, sDeletedEventsURL);
+        }
+
+        @Override
+        protected void notifyChanges() {
+            getContext().getContentResolver().notifyChange(Events.CONTENT_URI,
+                    null /* observer */, false /* do not sync to network */);
+        }
+
+        @Override
+        protected void cursorRowToContentValues(Cursor cursor, ContentValues map) {
+            rowToContentValues(cursor, map);
+        }
+
+        @Override
+        public void insertRow(ContentProvider diffs, Cursor diffsCursor) {
+            rowToContentValues(diffsCursor, mValues);
+            final SQLiteDatabase db = getDatabase();
+            long rowId = mEventsInserter.insert(mValues);
+            if (rowId <= 0) {
+                Log.e(TAG, "Unable to insert values into calendar db: " + mValues);
+                return;
+            }
+
+            long diffsRowId = diffsCursor.getLong(
+                diffsCursor.getColumnIndex(Events._ID));
+
+            insertAttendees(diffs, diffsRowId, rowId, db);
+            insertRemindersIfNecessary(diffs, diffsRowId, rowId, db);
+            insertExtendedPropertiesIfNecessary(diffs, diffsRowId, rowId, db);
+            updateEventRawTimesLocked(rowId, mValues);
+            updateInstancesLocked(mValues, rowId, true /* new event */, db);
+            insertBusyBitsLocked(rowId, mValues);
+
+            // Update the _SYNC_DIRTY flag of the event. We have to do this
+            // after inserting since the update of the reminders and extended properties
+            // methods will fire a sql trigger that will cause this flag to
+            // be set.
+            clearSyncDirtyFlag(db, rowId);
+        }
+
+        private void clearSyncDirtyFlag(SQLiteDatabase db, long rowId) {
+            mValues.clear();
+            mValues.put(Events._SYNC_DIRTY, 0);
+            db.update(mTable, mValues, Events._ID + '=' + rowId, null);
+        }
+
+        private void insertAttendees(ContentProvider diffs,
+                long diffsRowId,
+                long rowId,
+                SQLiteDatabase db) {
+            // query attendees in diffs
+            Cursor attendeesCursor =
+                    diffs.query(Attendees.CONTENT_URI, null,
+                            "event_id=" + diffsRowId, null, null);
+            ContentValues attendeesValues = new ContentValues();
+            try {
+                while (attendeesCursor.moveToNext()) {
+                    attendeesValues.clear();
+                    DatabaseUtils.cursorStringToContentValues(attendeesCursor,
+                            Attendees.ATTENDEE_NAME,
+                            attendeesValues);
+                    DatabaseUtils.cursorStringToContentValues(attendeesCursor,
+                            Attendees.ATTENDEE_EMAIL,
+                            attendeesValues);
+                    DatabaseUtils.cursorIntToContentValues(attendeesCursor,
+                            Attendees.ATTENDEE_STATUS,
+                            attendeesValues);
+                    DatabaseUtils.cursorIntToContentValues(attendeesCursor,
+                            Attendees.ATTENDEE_TYPE,
+                            attendeesValues);
+                    DatabaseUtils.cursorIntToContentValues(attendeesCursor,
+                            Attendees.ATTENDEE_RELATIONSHIP,
+                            attendeesValues);
+                    attendeesValues.put(Attendees.EVENT_ID, rowId);
+                    mAttendeesInserter.insert(attendeesValues);
+                }
+            } finally {
+                if (attendeesCursor != null) {
+                    attendeesCursor.close();
+                }
+            }
+        }
+
+        private void insertRemindersIfNecessary(ContentProvider diffs,
+                                                long diffsRowId,
+                                                long rowId,
+                                                SQLiteDatabase db) {
+            // insert reminders, if necessary.
+            Integer hasAlarm = mValues.getAsInteger(Events.HAS_ALARM);
+            if (hasAlarm != null && hasAlarm.intValue() == 1) {
+                // query reminders in diffs
+                Cursor reminderCursor =
+                        diffs.query(Reminders.CONTENT_URI, null,
+                                "event_id=" + diffsRowId, null, null);
+                ContentValues reminderValues = new ContentValues();
+                try {
+                    while (reminderCursor.moveToNext()) {
+                        reminderValues.clear();
+                        DatabaseUtils.cursorIntToContentValues(reminderCursor,
+                                Reminders.METHOD,
+                                reminderValues);
+                        DatabaseUtils.cursorIntToContentValues(reminderCursor,
+                                Reminders.MINUTES,
+                                reminderValues);
+                        reminderValues.put(Reminders.EVENT_ID, rowId);
+                        mRemindersInserter.insert(reminderValues);
+                    }
+                } finally {
+                    if (reminderCursor != null) {
+                        reminderCursor.close();
+                    }
+                }
+            }
+        }
+
+        private void insertExtendedPropertiesIfNecessary(ContentProvider diffs,
+                                                         long diffsRowId,
+                                                         long rowId,
+                                                         SQLiteDatabase db) {
+            // insert extended properties, if necessary.
+            Integer hasExtendedProperties = mValues.getAsInteger(Events.HAS_EXTENDED_PROPERTIES);
+            if (hasExtendedProperties != null && hasExtendedProperties.intValue() != 0) {
+                // query reminders in diffs
+                Cursor extendedPropertiesCursor =
+                        diffs.query(Calendar.ExtendedProperties.CONTENT_URI, null,
+                                "event_id=" + diffsRowId, null, null);
+                ContentValues extendedPropertiesValues = new ContentValues();
+                try {
+                    while (extendedPropertiesCursor.moveToNext()) {
+                        extendedPropertiesValues.clear();
+                        DatabaseUtils.cursorStringToContentValues(extendedPropertiesCursor,
+                                Calendar.ExtendedProperties.NAME, extendedPropertiesValues);
+                        DatabaseUtils.cursorStringToContentValues(extendedPropertiesCursor,
+                                Calendar.ExtendedProperties.VALUE, extendedPropertiesValues);
+                        mExtendedPropertiesInserter.insert(extendedPropertiesValues);
+                    }
+                } finally {
+                    if (extendedPropertiesCursor != null) {
+                        extendedPropertiesCursor.close();
+                    }
+                }
+            }
+        }
+
+        @Override
+        public void updateRow(long localId, ContentProvider diffs,
+                                 Cursor diffsCursor) {
+            rowToContentValues(diffsCursor, mValues);
+            final SQLiteDatabase db = getDatabase();
+            updateBusyBitsLocked(localId, mValues);
+            int numRows = db.update(mTable, mValues, "_id=" + localId, null /* selectionArgs */);
+
+            if (numRows <= 0) {
+                Log.e(TAG, "Unable to update calendar db: " + mValues);
+                return;
+            }
+
+            long diffsRowId = diffsCursor.getLong(
+                diffsCursor.getColumnIndex(Events._ID));
+            // TODO: only update the attendees, reminders, and extended properties if they have
+            // changed?
+            // delete the existing attendees, reminders, and extended properties
+            db.delete(sAttendeesTable, "event_id=" + localId, null /* selectionArgs */);
+            db.delete(sRemindersTable, "event_id=" + localId, null /* selectionArgs */);
+            db.delete(sExtendedPropertiesTable, "event_id=" + localId,
+                    null /* selectionArgs */);
+
+            // process attendees sent by the server.
+            insertAttendees(diffs, diffsRowId, localId, db);
+            // process reminders sent by the server.
+            insertRemindersIfNecessary(diffs, diffsRowId, localId, db);
+
+            // process extended properties sent by the server.
+            insertExtendedPropertiesIfNecessary(diffs, diffsRowId, localId, db);
+
+            updateEventRawTimesLocked(localId, mValues);
+            updateInstancesLocked(mValues, localId, false /* not a new event */, db);
+
+            // Update the _SYNC_DIRTY flag of the event. We have to do this
+            // after updating since the update of the reminders and extended properties
+            // methods will fire a sql trigger that will cause this flag to
+            // be set.
+            clearSyncDirtyFlag(db, localId);
+        }
+
+        @Override
+        public void resolveRow(long localId, String syncId,
+                                  ContentProvider diffs, Cursor diffsCursor) {
+            // server wins
+            updateRow(localId, diffs, diffsCursor);
+        }
+
+        @Override
+        public void deleteRow(Cursor localCursor) {
+            int idIndex = localCursor.getColumnIndexOrThrow(Events._ID);
+            long localId = localCursor.getLong(idIndex);
+            deleteBusyBitsLocked(localId);
+            super.deleteRow(localCursor);
+        }
+
+        private void rowToContentValues(Cursor diffsCursor, ContentValues values) {
+            values.clear();
+
+            DatabaseUtils.cursorStringToContentValues(diffsCursor, Events._SYNC_ID, values);
+            DatabaseUtils.cursorStringToContentValues(diffsCursor, Events._SYNC_TIME, values);
+            DatabaseUtils.cursorStringToContentValues(diffsCursor, Events._SYNC_VERSION, values);
+            DatabaseUtils.cursorStringToContentValues(diffsCursor, Events._SYNC_DIRTY, values);
+            DatabaseUtils.cursorStringToContentValues(diffsCursor, Events._SYNC_ACCOUNT, values);
+            DatabaseUtils.cursorStringToContentValues(diffsCursor, Events.HTML_URI, values);
+            DatabaseUtils.cursorStringToContentValues(diffsCursor, Events.TITLE, values);
+            DatabaseUtils.cursorStringToContentValues(diffsCursor, Events.EVENT_LOCATION, values);
+            DatabaseUtils.cursorStringToContentValues(diffsCursor, Events.DESCRIPTION, values);
+            DatabaseUtils.cursorLongToContentValues(diffsCursor, Events.STATUS, values);
+            DatabaseUtils.cursorIntToContentValues(diffsCursor, Events.SELF_ATTENDEE_STATUS,
+                    values);
+            DatabaseUtils.cursorStringToContentValues(diffsCursor, Events.COMMENTS_URI, values);
+            DatabaseUtils.cursorLongToContentValues(diffsCursor, Events.DTSTART, values);
+            DatabaseUtils.cursorLongToContentValues(diffsCursor, Events.DTEND, values);
+            DatabaseUtils.cursorStringToContentValues(diffsCursor, Events.EVENT_TIMEZONE, values);
+            DatabaseUtils.cursorStringToContentValues(diffsCursor, Events.DURATION, values);
+            DatabaseUtils.cursorIntToContentValues(diffsCursor, Events.ALL_DAY, values);
+            DatabaseUtils.cursorIntToContentValues(diffsCursor, Events.VISIBILITY, values);
+            DatabaseUtils.cursorIntToContentValues(diffsCursor, Events.TRANSPARENCY, values);
+            DatabaseUtils.cursorIntToContentValues(diffsCursor, Events.HAS_ALARM, values);
+            DatabaseUtils.cursorIntToContentValues(diffsCursor, Events.HAS_EXTENDED_PROPERTIES,
+                    values);
+            DatabaseUtils.cursorStringToContentValues(diffsCursor, Events.RRULE, values);
+            DatabaseUtils.cursorStringToContentValues(diffsCursor, Events.ORIGINAL_EVENT, values);
+            DatabaseUtils.cursorLongToContentValues(diffsCursor, Events.ORIGINAL_INSTANCE_TIME,
+                    values);
+            DatabaseUtils.cursorLongToContentValues(diffsCursor, Events.LAST_DATE, values);
+            DatabaseUtils.cursorLongToContentValues(diffsCursor, Events.CALENDAR_ID, values);
+        }
+    }
+
+    private static final int EVENTS = 1;
+    private static final int EVENTS_ID = 2;
+    private static final int INSTANCES = 3;
+    private static final int DELETED_EVENTS = 4;
+    private static final int CALENDARS = 5;
+    private static final int CALENDARS_ID = 6;
+    private static final int ATTENDEES = 7;
+    private static final int ATTENDEES_ID = 8;
+    private static final int REMINDERS = 9;
+    private static final int REMINDERS_ID = 10;
+    private static final int EXTENDED_PROPERTIES = 11;
+    private static final int EXTENDED_PROPERTIES_ID = 12;
+    private static final int CALENDAR_ALERTS = 13;
+    private static final int CALENDAR_ALERTS_ID = 14;
+    private static final int CALENDAR_ALERTS_BY_INSTANCE = 15;
+    private static final int BUSYBITS = 16;
+
+    private static final UriMatcher sURLMatcher = new UriMatcher(UriMatcher.NO_MATCH);
+    private static final HashMap<String, String> sInstancesProjectionMap;
+    private static final HashMap<String, String> sEventsProjectionMap;
+    private static final HashMap<String, String> sAttendeesProjectionMap;
+    private static final HashMap<String, String> sRemindersProjectionMap;
+    private static final HashMap<String, String> sCalendarAlertsProjectionMap;
+    private static final HashMap<String, String> sBusyBitsProjectionMap;
+
+    static {
+        sURLMatcher.addURI("calendar", "instances/when/*/*", INSTANCES);
+        sURLMatcher.addURI("calendar", "events", EVENTS);
+        sURLMatcher.addURI("calendar", "events/#", EVENTS_ID);
+        sURLMatcher.addURI("calendar", "calendars", CALENDARS);
+        sURLMatcher.addURI("calendar", "calendars/#", CALENDARS_ID);
+        sURLMatcher.addURI("calendar", "deleted_events", DELETED_EVENTS);
+        sURLMatcher.addURI("calendar", "attendees", ATTENDEES);
+        sURLMatcher.addURI("calendar", "attendees/#", ATTENDEES_ID);
+        sURLMatcher.addURI("calendar", "reminders", REMINDERS);
+        sURLMatcher.addURI("calendar", "reminders/#", REMINDERS_ID);
+        sURLMatcher.addURI("calendar", "extendedproperties", EXTENDED_PROPERTIES);
+        sURLMatcher.addURI("calendar", "extendedproperties/#", EXTENDED_PROPERTIES_ID);
+        sURLMatcher.addURI("calendar", "calendar_alerts", CALENDAR_ALERTS);
+        sURLMatcher.addURI("calendar", "calendar_alerts/#", CALENDAR_ALERTS_ID);
+        sURLMatcher.addURI("calendar", "calendar_alerts/by_instance", CALENDAR_ALERTS_BY_INSTANCE);
+        sURLMatcher.addURI("calendar", "busybits/when/*/*", BUSYBITS);
+
+
+        sEventsProjectionMap = new HashMap<String, String>();
+        // Events columns
+        sEventsProjectionMap.put(Events.HTML_URI, "htmlUri");
+        sEventsProjectionMap.put(Events.TITLE, "title");
+        sEventsProjectionMap.put(Events.EVENT_LOCATION, "eventLocation");
+        sEventsProjectionMap.put(Events.DESCRIPTION, "description");
+        sEventsProjectionMap.put(Events.STATUS, "eventStatus");
+        sEventsProjectionMap.put(Events.SELF_ATTENDEE_STATUS, "selfAttendeeStatus");
+        sEventsProjectionMap.put(Events.COMMENTS_URI, "commentsUri");
+        sEventsProjectionMap.put(Events.DTSTART, "dtstart");
+        sEventsProjectionMap.put(Events.DTEND, "dtend");
+        sEventsProjectionMap.put(Events.EVENT_TIMEZONE, "eventTimezone");
+        sEventsProjectionMap.put(Events.DURATION, "duration");
+        sEventsProjectionMap.put(Events.ALL_DAY, "allDay");
+        sEventsProjectionMap.put(Events.VISIBILITY, "visibility");
+        sEventsProjectionMap.put(Events.TRANSPARENCY, "transparency");
+        sEventsProjectionMap.put(Events.HAS_ALARM, "hasAlarm");
+        sEventsProjectionMap.put(Events.HAS_EXTENDED_PROPERTIES, "hasExtendedProperties");
+        sEventsProjectionMap.put(Events.RRULE, "rrule");
+        sEventsProjectionMap.put(Events.RDATE, "rdate");
+        sEventsProjectionMap.put(Events.EXRULE, "exrule");
+        sEventsProjectionMap.put(Events.EXDATE, "exdate");
+        sEventsProjectionMap.put(Events.ORIGINAL_EVENT, "originalEvent");
+        sEventsProjectionMap.put(Events.ORIGINAL_INSTANCE_TIME, "originalInstanceTime");
+        sEventsProjectionMap.put(Events.LAST_DATE, "lastDate");
+        sEventsProjectionMap.put(Events.CALENDAR_ID, "calendar_id");
+        // Calendar columns
+        sEventsProjectionMap.put(Events.COLOR, "color");
+        sEventsProjectionMap.put(Events.ACCESS_LEVEL, "access_level");
+        sEventsProjectionMap.put(Events.SELECTED, "selected");
+        sEventsProjectionMap.put(Calendars.URL, "url");
+        sEventsProjectionMap.put(Calendars.TIMEZONE, "timezone");
+
+        // Put the shared items into the Instances projection map
+        sInstancesProjectionMap = new HashMap<String, String>(sEventsProjectionMap);
+        sAttendeesProjectionMap = new HashMap<String, String>(sEventsProjectionMap);
+        sRemindersProjectionMap = new HashMap<String, String>(sEventsProjectionMap);
+        sCalendarAlertsProjectionMap = new HashMap<String, String>(sEventsProjectionMap);
+
+        sEventsProjectionMap.put(Events._ID, "Events._id AS _id");
+        sEventsProjectionMap.put(Events._SYNC_ID, "Events._sync_id AS _sync_id");
+        sEventsProjectionMap.put(Events._SYNC_VERSION, "Events._sync_version AS _sync_version");
+        sEventsProjectionMap.put(Events._SYNC_TIME, "Events._sync_time AS _sync_time");
+        sEventsProjectionMap.put(Events._SYNC_LOCAL_ID, "Events._sync_local_id AS _sync_local_id");
+        sEventsProjectionMap.put(Events._SYNC_DIRTY, "Events._sync_dirty AS _sync_dirty");
+        sEventsProjectionMap.put(Events._SYNC_ACCOUNT, "Events._sync_account AS _sync_account");
+
+        // Instances columns
+        sInstancesProjectionMap.put(Instances.BEGIN, "begin");
+        sInstancesProjectionMap.put(Instances.END, "end");
+        sInstancesProjectionMap.put(Instances.EVENT_ID, "Instances.event_id AS event_id");
+        sInstancesProjectionMap.put(Instances._ID, "Instances._id AS _id");
+        sInstancesProjectionMap.put(Instances.START_DAY, "startDay");
+        sInstancesProjectionMap.put(Instances.END_DAY, "endDay");
+        sInstancesProjectionMap.put(Instances.START_MINUTE, "startMinute");
+        sInstancesProjectionMap.put(Instances.END_MINUTE, "endMinute");
+
+        // BusyBits columns
+        sBusyBitsProjectionMap = new HashMap<String, String>();
+        sBusyBitsProjectionMap.put(BusyBits.DAY, "day");
+        sBusyBitsProjectionMap.put(BusyBits.BUSYBITS, "busyBits");
+        sBusyBitsProjectionMap.put(BusyBits.ALL_DAY_COUNT, "allDayCount");
+
+        // Attendees columns
+        sAttendeesProjectionMap.put(Attendees.EVENT_ID, "event_id");
+        sAttendeesProjectionMap.put(Attendees._ID, "Attendees._id AS _id");
+        sAttendeesProjectionMap.put(Attendees.ATTENDEE_NAME, "attendeeName");
+        sAttendeesProjectionMap.put(Attendees.ATTENDEE_EMAIL, "attendeeEmail");
+        sAttendeesProjectionMap.put(Attendees.ATTENDEE_STATUS, "attendeeStatus");
+        sAttendeesProjectionMap.put(Attendees.ATTENDEE_RELATIONSHIP, "attendeeRelationship");
+        sAttendeesProjectionMap.put(Attendees.ATTENDEE_TYPE, "attendeeType");
+
+        // Reminders columns
+        sRemindersProjectionMap.put(Reminders.EVENT_ID, "event_id");
+        sRemindersProjectionMap.put(Reminders._ID, "Reminders._id AS _id");
+        sRemindersProjectionMap.put(Reminders.MINUTES, "minutes");
+        sRemindersProjectionMap.put(Reminders.METHOD, "method");
+
+        // CalendarAlerts columns
+        sCalendarAlertsProjectionMap.put(CalendarAlerts.EVENT_ID, "event_id");
+        sCalendarAlertsProjectionMap.put(CalendarAlerts._ID, "CalendarAlerts._id AS _id");
+        sCalendarAlertsProjectionMap.put(CalendarAlerts.BEGIN, "begin");
+        sCalendarAlertsProjectionMap.put(CalendarAlerts.END, "end");
+        sCalendarAlertsProjectionMap.put(CalendarAlerts.ALARM_TIME, "alarmTime");
+        sCalendarAlertsProjectionMap.put(CalendarAlerts.STATE, "state");
+        sCalendarAlertsProjectionMap.put(CalendarAlerts.MINUTES, "minutes");
+    }
+}
diff --git a/src/com/android/providers/calendar/CalendarReceiver.java b/src/com/android/providers/calendar/CalendarReceiver.java
new file mode 100644
index 0000000..f3bef8f
--- /dev/null
+++ b/src/com/android/providers/calendar/CalendarReceiver.java
@@ -0,0 +1,52 @@
+/*
+** Copyright 2006, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+**     http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** See the License for the specific language governing permissions and
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** limitations under the License.
+*/
+
+package com.android.providers.calendar;
+
+import android.content.ContentProvider;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.IContentProvider;
+import android.content.Intent;
+import android.content.BroadcastReceiver;
+
+/**
+ * This IntentReceiver executes when the boot completes and ensures that
+ * the Calendar provider has started and then initializes the alarm
+ * scheduler for the Calendar provider.  This needs to be done after
+ * the boot completes because the alarm manager may not have been started
+ * yet.
+ */
+public class CalendarReceiver extends BroadcastReceiver {
+    
+    static final String SCHEDULE = "com.android.providers.calendar.SCHEDULE_ALARM";
+
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        String action = intent.getAction();
+        ContentResolver cr = context.getContentResolver();
+        CalendarProvider provider;
+        IContentProvider icp = cr.acquireProvider("calendar");
+        provider = (CalendarProvider) ContentProvider.
+                coerceToLocalContentProvider(icp);
+        if (action.equals(SCHEDULE)) {
+            provider.scheduleNextAlarm();
+        } else if (action.equals(Intent.ACTION_BOOT_COMPLETED)) {
+            provider.scheduleNextAlarm();
+        }
+        cr.releaseProvider(icp);
+    }
+}
diff --git a/src/com/android/providers/calendar/CalendarSyncAdapter.java b/src/com/android/providers/calendar/CalendarSyncAdapter.java
new file mode 100644
index 0000000..e368915
--- /dev/null
+++ b/src/com/android/providers/calendar/CalendarSyncAdapter.java
@@ -0,0 +1,1252 @@
+/*
+**
+** Copyright 2006, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+**     http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** See the License for the specific language governing permissions and
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** limitations under the License.
+*/
+
+package com.android.providers.calendar;
+
+import com.google.android.gdata.client.AndroidXmlParserFactory;
+import com.google.android.gdata.client.AndroidGDataClient;
+import com.google.android.providers.AbstractGDataSyncAdapter;
+import com.google.wireless.gdata.calendar.client.CalendarClient;
+import com.google.wireless.gdata.calendar.data.EventEntry;
+import com.google.wireless.gdata.calendar.data.EventsFeed;
+import com.google.wireless.gdata.calendar.data.Reminder;
+import com.google.wireless.gdata.calendar.data.When;
+import com.google.wireless.gdata.calendar.data.Who;
+import com.google.wireless.gdata.calendar.parser.xml.XmlCalendarGDataParserFactory;
+import com.google.wireless.gdata.client.GDataServiceClient;
+import com.google.wireless.gdata.client.QueryParams;
+import com.google.wireless.gdata.data.Entry;
+import com.google.wireless.gdata.data.Feed;
+import com.google.wireless.gdata.data.StringUtils;
+import com.google.wireless.gdata.parser.ParseException;
+
+import android.content.ContentProvider;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.SyncContext;
+import android.content.SyncableContentProvider;
+import android.content.SyncResult;
+import android.database.Cursor;
+import android.database.CursorJoiner;
+import android.net.Uri;
+import android.os.SystemProperties;
+import android.os.Bundle;
+import android.pim.ICalendar;
+import android.pim.RecurrenceSet;
+import android.pim.Time;
+import android.provider.Calendar;
+import android.provider.Calendar.Calendars;
+import android.provider.Calendar.Events;
+import android.provider.SubscribedFeeds;
+import android.provider.SyncConstValue;
+import android.text.TextUtils;
+import android.util.Config;
+import android.util.Log;
+
+import java.util.Enumeration;
+import java.util.Hashtable;
+import java.util.List;
+import java.util.Vector;
+
+/**
+ * SyncAdapter for Google Calendar.  Fetches the list of the user's calendars,
+ * and for each calendar that is marked as &quot;selected&quot; in the web
+ * interface, syncs that calendar.
+ */
+public final class CalendarSyncAdapter extends AbstractGDataSyncAdapter {
+
+    private static final String SELECT_BY_ACCOUNT = Calendars._SYNC_ACCOUNT + "=?";
+    private static final String SELECT_BY_ACCOUNT_AND_FEED =
+            SELECT_BY_ACCOUNT + " AND " + Calendars.URL + "=?";
+
+    private static final String[] CALENDAR_KEY_COLUMNS =
+            new String[]{Calendars._SYNC_ACCOUNT, Calendars.URL};
+    private static final String CALENDAR_KEY_SORT_ORDER =
+            Calendars._SYNC_ACCOUNT + "," + Calendars.URL;
+    private static final String[] FEEDS_KEY_COLUMNS =
+            new String[]{SubscribedFeeds.Feeds._SYNC_ACCOUNT, SubscribedFeeds.Feeds.FEED};
+    private static final String FEEDS_KEY_SORT_ORDER =
+            SubscribedFeeds.Feeds._SYNC_ACCOUNT + ", " + SubscribedFeeds.Feeds.FEED;
+
+    public static class SyncInfo {
+        // public String feedUrl;
+        public long calendarId;
+        public String calendarTimezone;
+    }
+
+    private static final String TAG = "Sync";
+    private static final Integer sTentativeStatus = Events.STATUS_TENTATIVE;
+    private static final Integer sConfirmedStatus = Events.STATUS_CONFIRMED;
+    private static final Integer sCanceledStatus = Events.STATUS_CANCELED;
+
+    private final CalendarClient mCalendarClient;
+
+    private ContentResolver mContentResolver;
+
+    private static final String[] CALENDARS_PROJECTION = new String[] {
+            Calendars._ID,            // 0
+            Calendars.SELECTED,       // 1
+            Calendars._SYNC_TIME,     // 2
+            Calendars.URL,            // 3
+            Calendars.DISPLAY_NAME,   // 4
+            Calendars.TIMEZONE,       // 5
+            Calendars.SYNC_EVENTS     // 6
+    };
+
+    // Counters for sync event logging
+    private static int mServerDiffs;
+    private static int mRefresh;
+
+    protected CalendarSyncAdapter(Context context, SyncableContentProvider provider) {
+        super(context, provider);
+        mCalendarClient = new CalendarClient(
+                new AndroidGDataClient(context.getContentResolver()),
+                new XmlCalendarGDataParserFactory(new AndroidXmlParserFactory()));
+    }
+
+    @Override
+    protected Object createSyncInfo() {
+        return new SyncInfo();
+    }
+
+    @Override
+    protected Entry newEntry() {
+        return new EventEntry();
+    }
+
+    @Override
+    protected Cursor getCursorForTable(ContentProvider cp, Class entryClass) {
+        if (entryClass != EventEntry.class) {
+            throw new IllegalArgumentException("unexpected entry class, " + entryClass.getName());
+        }
+        return cp.query(Calendar.Events.CONTENT_URI, null, null, null, null);
+    }
+
+    @Override
+    protected Cursor getCursorForDeletedTable(ContentProvider cp, Class entryClass) {
+        if (entryClass != EventEntry.class) {
+            throw new IllegalArgumentException("unexpected entry class, " + entryClass.getName());
+        }
+        return cp.query(Calendar.Events.DELETED_CONTENT_URI, null, null, null, null);
+    }
+
+    @Override
+    protected String cursorToEntry(SyncContext context, Cursor c, Entry entry,
+            Object info) throws ParseException {
+        EventEntry event = (EventEntry) entry;
+        SyncInfo syncInfo = (SyncInfo) info;
+
+        String feedUrl = c.getString(c.getColumnIndex(Calendars.URL));
+
+        // update the sync info.  this will be used later when we update the
+        // provider with the results of sending this entry to the calendar
+        // server.
+        syncInfo.calendarId = c.getLong(c.getColumnIndex(Events.CALENDAR_ID));
+        syncInfo.calendarTimezone =
+                c.getString(c.getColumnIndex(Events.EVENT_TIMEZONE));
+        if (TextUtils.isEmpty(syncInfo.calendarTimezone)) {
+            // if the event timezone is not set -- e.g., when we're creating an
+            // event on the device -- we will use the timezone for the calendar.
+            syncInfo.calendarTimezone =
+                    c.getString(c.getColumnIndex(Events.TIMEZONE));
+        }
+
+        // id
+        event.setId(c.getString(c.getColumnIndex(Events._SYNC_ID)));
+        event.setEditUri(c.getString(c.getColumnIndex(Events._SYNC_VERSION)));
+
+        // status
+        byte status;
+        int localStatus = c.getInt(c.getColumnIndex(Events.STATUS));
+        switch (localStatus) {
+            case Events.STATUS_CANCELED:
+                status = EventEntry.STATUS_CANCELED;
+                break;
+            case Events.STATUS_CONFIRMED:
+                status = EventEntry.STATUS_CONFIRMED;
+                break;
+            case Events.STATUS_TENTATIVE:
+                status = EventEntry.STATUS_TENTATIVE;
+                break;
+            default:
+                // should not happen
+                status = EventEntry.STATUS_TENTATIVE;
+                break;
+        }
+        event.setStatus(status);
+
+        // visibility
+        byte visibility;
+        int localVisibility = c.getInt(c.getColumnIndex(Events.VISIBILITY));
+        switch (localVisibility) {
+            case Events.VISIBILITY_DEFAULT:
+                visibility = EventEntry.VISIBILITY_DEFAULT;
+                break;
+            case Events.VISIBILITY_CONFIDENTIAL:
+                visibility = EventEntry.VISIBILITY_CONFIDENTIAL;
+                break;
+            case Events.VISIBILITY_PRIVATE:
+                visibility = EventEntry.VISIBILITY_PRIVATE;
+                break;
+            case Events.VISIBILITY_PUBLIC:
+                visibility = EventEntry.VISIBILITY_PUBLIC;
+                break;
+            default:
+                // should not happen
+                Log.e(TAG, "Unexpected value for visibility: " + localVisibility
+                        + "; using default visibility.");
+                visibility = EventEntry.VISIBILITY_DEFAULT;
+                break;
+        }
+        event.setVisibility(visibility);
+
+        byte transparency;
+        int localTransparency = c.getInt(c.getColumnIndex(Events.TRANSPARENCY));
+        switch (localTransparency) {
+            case Events.TRANSPARENCY_OPAQUE:
+                transparency = EventEntry.TRANSPARENCY_OPAQUE;
+                break;
+            case Events.TRANSPARENCY_TRANSPARENT:
+                transparency = EventEntry.TRANSPARENCY_TRANSPARENT;
+                break;
+            default:
+                // should not happen
+                Log.e(TAG, "Unexpected value for transparency: " + localTransparency
+                        + "; using opaque transparency.");
+                transparency = EventEntry.TRANSPARENCY_OPAQUE;
+                break;
+        }
+        event.setTransparency(transparency);
+
+        // could set the html uri, but there's no need to, since it should not be edited.
+
+        // title
+        event.setTitle(c.getString(c.getColumnIndex(Events.TITLE)));
+
+        // description
+        event.setContent(c.getString(c.getColumnIndex(Events.DESCRIPTION)));
+
+        // where
+        event.setWhere(c.getString(c.getColumnIndex(Events.EVENT_LOCATION)));
+
+        // attendees
+        long eventId = c.getInt(c.getColumnIndex(Events._SYNC_LOCAL_ID));
+        addAttendeesToEntry(eventId, event);
+
+        // comment uri
+        event.setCommentsUri(c.getString(c.getColumnIndexOrThrow(Events.COMMENTS_URI)));
+
+        Time utc = new Time(Time.TIMEZONE_UTC);
+
+        boolean allDay = c.getInt(c.getColumnIndex(Events.ALL_DAY)) != 0;
+
+        String startTime = null;
+        String endTime = null;
+        // start time
+        int dtstartColumn = c.getColumnIndex(Events.DTSTART);
+        if (!c.isNull(dtstartColumn)) {
+            long dtstart = c.getLong(dtstartColumn);
+            utc.set(dtstart);
+            startTime = utc.format3339(allDay);
+        }
+
+        // end time
+        int dtendColumn = c.getColumnIndex(Events.DTEND);
+        if (!c.isNull(dtendColumn)) {
+            long dtend = c.getLong(dtendColumn);
+            utc.set(dtend);
+            endTime = utc.format3339(allDay);
+        }
+
+        When when = new When(startTime, endTime);
+        event.addWhen(when);
+
+        // reminders
+        Integer hasReminder = c.getInt(c.getColumnIndex(Events.HAS_ALARM));
+        if (hasReminder != null && hasReminder.intValue() != 0) {
+            addRemindersToEntry(eventId, event);
+        }
+
+        // extendedProperties
+        Integer hasExtendedProperties = c.getInt(c.getColumnIndex(Events.HAS_EXTENDED_PROPERTIES));
+        if (hasExtendedProperties != null && hasExtendedProperties.intValue() != 0) {
+            addExtendedPropertiesToEntry(eventId, event);
+        }
+
+        long originalStartTime = -1;
+        String originalId = c.getString(c.getColumnIndex(Events.ORIGINAL_EVENT));
+        int originalStartTimeIndex = c.getColumnIndex(Events.ORIGINAL_INSTANCE_TIME);
+        if (!c.isNull(originalStartTimeIndex)) {
+            originalStartTime = c.getLong(originalStartTimeIndex);
+        }
+        if ((originalStartTime != -1) &&
+                !TextUtils.isEmpty(originalId)) {
+
+            Time originalTime = new Time(c.getString(c.getColumnIndex(Events.EVENT_TIMEZONE)));
+            originalTime.set(originalStartTime);
+
+            utc.set(originalStartTime);
+            event.setOriginalEventStartTime(utc.format3339(allDay));
+            event.setOriginalEventId(originalId);
+        }
+
+        // recurrences.
+        ICalendar.Component component = new ICalendar.Component("DUMMY",
+                null /* parent */);
+        if (RecurrenceSet.populateComponent(c, component)) {
+            addRecurrenceToEntry(component, event);
+        }
+
+        // if this is a new entry, return the feed url.  otherwise, return null; the edit url is
+        // already in the entry.
+        if (event.getEditUri() == null) {
+            return feedUrl;
+        } else {
+            return null;
+        }
+    }
+
+    private void addAttendeesToEntry(long eventId, EventEntry event)
+            throws ParseException {
+        Cursor c = getContext().getContentResolver().query(
+                Calendar.Attendees.CONTENT_URI, null, "event_id=" + eventId, null, null);
+
+        try {
+            int nameIndex = c.getColumnIndexOrThrow(Calendar.Attendees.ATTENDEE_NAME);
+            int emailIndex = c.getColumnIndexOrThrow(Calendar.Attendees.ATTENDEE_EMAIL);
+            int statusIndex = c.getColumnIndexOrThrow(Calendar.Attendees.ATTENDEE_STATUS);
+            int typeIndex = c.getColumnIndexOrThrow(Calendar.Attendees.ATTENDEE_TYPE);
+            int relIndex = c.getColumnIndexOrThrow(Calendar.Attendees.ATTENDEE_RELATIONSHIP);
+
+
+
+            while (c.moveToNext()) {
+                Who who = new Who();
+                who.setValue(c.getString(nameIndex));
+                who.setEmail(c.getString(emailIndex));
+                int status = c.getInt(statusIndex);
+                switch (status) {
+                    case Calendar.Attendees.ATTENDEE_STATUS_NONE:
+                        who.setStatus(Who.STATUS_NONE);
+                        break;
+                    case Calendar.Attendees.ATTENDEE_STATUS_ACCEPTED:
+                        who.setStatus(Who.STATUS_ACCEPTED);
+                        break;
+                    case Calendar.Attendees.ATTENDEE_STATUS_DECLINED:
+                        who.setStatus(Who.STATUS_DECLINED);
+                        break;
+                    case Calendar.Attendees.ATTENDEE_STATUS_INVITED:
+                        who.setStatus(Who.STATUS_INVITED);
+                        break;
+                    case Calendar.Attendees.ATTENDEE_STATUS_TENTATIVE:
+                        who.setStatus(Who.STATUS_TENTATIVE);
+                        break;
+                    default:
+                        Log.e(TAG, "Unknown attendee status: " + status);
+                        who.setStatus(Who.STATUS_NONE);
+                        break;
+                }
+                int type = c.getInt(typeIndex);
+                switch (type) {
+                    case Calendar.Attendees.TYPE_NONE:
+                        who.setType(Who.TYPE_NONE);
+                        break;
+                    case Calendar.Attendees.TYPE_REQUIRED:
+                        who.setType(Who.TYPE_REQUIRED);
+                        break;
+                    case Calendar.Attendees.TYPE_OPTIONAL:
+                        who.setType(Who.TYPE_OPTIONAL);
+                        break;
+                    default:
+                        Log.e(TAG, "Unknown attendee type: " + type);
+                        who.setType(Who.TYPE_NONE);
+                        break;
+                }
+                int rel = c.getInt(relIndex);
+                switch (rel) {
+                    case Calendar.Attendees.RELATIONSHIP_NONE:
+                        who.setRelationship(Who.RELATIONSHIP_NONE);
+                        break;
+                    case Calendar.Attendees.RELATIONSHIP_ATTENDEE:
+                        who.setRelationship(Who.RELATIONSHIP_ATTENDEE);
+                        break;
+                    case Calendar.Attendees.RELATIONSHIP_ORGANIZER:
+                        who.setRelationship(Who.RELATIONSHIP_ORGANIZER);
+                        break;
+                    case Calendar.Attendees.RELATIONSHIP_SPEAKER:
+                        who.setRelationship(Who.RELATIONSHIP_SPEAKER);
+                        break;
+                    case Calendar.Attendees.RELATIONSHIP_PERFORMER:
+                        who.setRelationship(Who.RELATIONSHIP_PERFORMER);
+                        break;
+                    default:
+                        Log.e(TAG, "Unknown attendee relationship: " + rel);
+                        who.setRelationship(Who.RELATIONSHIP_NONE);
+                        break;
+                }
+                event.addAttendee(who);
+            }
+        } finally {
+            c.close();
+        }
+    }
+
+    private void addRemindersToEntry(long eventId, EventEntry event)
+            throws ParseException {
+        Cursor c = getContext().getContentResolver().query(
+                Calendar.Reminders.CONTENT_URI, null,
+                "event_id=" + eventId, null, null);
+
+        try {
+            int methodIndex = c.getColumnIndex(Calendar.Reminders.METHOD);
+            int minutesIndex = c.getColumnIndex(Calendar.Reminders.MINUTES);
+
+            while (c.moveToNext()) {
+                Reminder reminder = new Reminder();
+                reminder.setMinutes(c.getInt(minutesIndex));
+                int method = c.getInt(methodIndex);
+                switch(method) {
+                    case Calendar.Reminders.METHOD_DEFAULT:
+                        reminder.setMethod(Reminder.METHOD_DEFAULT);
+                        break;
+                    case Calendar.Reminders.METHOD_ALERT:
+                        reminder.setMethod(Reminder.METHOD_ALERT);
+                        break;
+                    case Calendar.Reminders.METHOD_EMAIL:
+                        reminder.setMethod(Reminder.METHOD_EMAIL);
+                        break;
+                    case Calendar.Reminders.METHOD_SMS:
+                        reminder.setMethod(Reminder.METHOD_SMS);
+                        break;
+                    default:
+                        throw new ParseException("illegal method, " + method);
+                }
+                event.addReminder(reminder);
+            }
+        } finally {
+            c.close();
+        }
+    }
+
+    private void addExtendedPropertiesToEntry(long eventId, EventEntry event)
+            throws ParseException {
+        Cursor c = getContext().getContentResolver().query(
+                Calendar.ExtendedProperties.CONTENT_URI, null,
+                "event_id=" + eventId, null, null);
+
+        try {
+            int nameIndex = c.getColumnIndex(Calendar.ExtendedProperties.NAME);
+            int valueIndex = c.getColumnIndex(Calendar.ExtendedProperties.VALUE);
+
+            while (c.moveToNext()) {
+                String name = c.getString(nameIndex);
+                String value = c.getString(valueIndex);
+                event.addExtendedProperty(name, value);
+            }
+        } finally {
+            c.close();
+        }
+    }
+
+    private void addRecurrenceToEntry(ICalendar.Component component,
+            EventEntry event) {
+        // serialize the component into a Google Calendar recurrence string
+        // we don't serialize the entire component, since we have a dummy
+        // wrapper (BEGIN:DUMMY, END:DUMMY).
+        StringBuilder sb = new StringBuilder();
+
+        // append the properties
+        boolean first = true;
+        for (String propertyName : component.getPropertyNames()) {
+            for (ICalendar.Property property :
+                    component.getProperties(propertyName)) {
+                if (first) {
+                    first = false;
+                } else {
+                    sb.append("\n");
+                }
+                property.toString(sb);
+            }
+        }
+
+        // append the sub-components
+        List<ICalendar.Component> children = component.getComponents();
+        if (children != null) {
+            for (ICalendar.Component child : children) {
+                if (first) {
+                    first = false;
+                } else {
+                    sb.append("\n");
+                }
+                child.toString(sb);
+            }
+        }
+        event.setRecurrence(sb.toString());
+    }
+
+    @Override
+    protected void deletedCursorToEntry(SyncContext context, Cursor c, Entry entry) {
+        EventEntry event = (EventEntry) entry;
+        event.setId(c.getString(c.getColumnIndex(Events._SYNC_ID)));
+        event.setEditUri(c.getString(c.getColumnIndex(Events._SYNC_VERSION)));
+        event.setStatus(EventEntry.STATUS_CANCELED);
+    }
+
+    protected boolean handleAllDeletedUnavailable(GDataSyncData syncData, String feed) {
+        syncData.feedData.remove(feed);
+        getContext().getContentResolver().delete(Calendar.Calendars.CONTENT_URI,
+                Calendar.Calendars._SYNC_ACCOUNT + "=? AND " + Calendar.Calendars.URL + "=?",
+                new String[]{getAccount(), feed});
+        return true;
+    }
+
+    @Override
+    public void onSyncStarting(SyncContext context, String account, boolean forced,
+            SyncResult result) {
+        mContentResolver = getContext().getContentResolver();
+        mServerDiffs = 0;
+        mRefresh = 0;
+        super.onSyncStarting(context, account, forced, result);
+    }
+
+    private void deletedEntryToContentValues(EventEntry event, ContentValues values) {
+        // see #deletedCursorToEntry.  this deletion cannot be an exception to a recurrence (e.g.,
+        // deleting an instance of a repeating event) -- new recurrence exceptions would be
+        // insertions.
+        values.clear();
+
+        // Base sync info
+        values.put(Events._SYNC_ID, event.getId());
+        values.put(Events._SYNC_VERSION, event.getEditUri());
+    }
+
+    /**
+     * Clear out the map and stuff an Entry into it in a format that can
+     * be inserted into a content provider.
+     *
+     * @return ENTRY_OK, ENTRY_DELETED, or ENTRY_INVALID
+     */
+    private int entryToContentValues(EventEntry event, long syncLocalId,
+            ContentValues map, Object info) {
+        SyncInfo syncInfo = (SyncInfo) info;
+
+        // There are 3 cases for parsing a date-time string:
+        //
+        // 1. The date-time string specifies a date and time with a time offset.
+        //    (The "normal" format.)
+        // 2. The date-time string is just a date, used for all-day events,
+        //    with no time or time offset fields. (The "all-day" format.)
+        // 3. The date-time string specifies a date and time, but no time
+        //    offset.  (The "floating" format, not supported yet.)
+        //
+        // Case 1: Time.parse3339() converts the date-time string to UTC and
+        // sets the Time.timezone to UTC.  It does not matter what the initial
+        // Time.timezone field was set to.  The initial timezone is ignored.
+        //
+        // Case 2: The date-time string doesn't specify the time.
+        // Time.parse3339() just sets the date but not the time (hour, minute,
+        // second) fields.  (The time fields should be zero, meaning midnight.)
+        // This code then sets the timezone to UTC (because this is an all-day
+        // event).  It does not matter in this case either what the initial
+        // Time.timezone field was set to.
+        //
+        // Case 3: This is a "floating time" (which we do not support yet).
+        // In this case, it will matter what the initial Time.timezone is set
+        // to.  It should use UTC.  If I specify a floating time of 1pm then I
+        // want that event displayed at 1pm in every timezone.  The easiest way
+        // to support this would be store it as 1pm in UTC and mark the event
+        // as "isFloating" (with a new database column).  Then when displaying
+        // the event, the code checks "isFloating" and just leaves the time at
+        // 1pm without doing any conversion to the local timezone.
+        //
+        // So in all cases, it is correct to set the Time.timezone to UTC.
+        Time time = new Time(Time.TIMEZONE_UTC);
+
+        map.clear();
+
+        // Base sync info
+        map.put(Events._SYNC_ID, event.getId());
+        String version = event.getEditUri();
+        if (!StringUtils.isEmpty(version)) {
+            // Always rewrite the edit URL to https for dasher account to avoid
+            // redirection.
+            map.put(Events._SYNC_VERSION, rewriteUrlforAccount(getAccount(), version));
+        }
+
+        // see if this is an exception to an existing event/recurrence.
+        String originalId = event.getOriginalEventId();
+        String originalStartTime = event.getOriginalEventStartTime();
+        boolean isRecurrenceException = false;
+        if (!StringUtils.isEmpty(originalId) &&
+                !StringUtils.isEmpty(originalStartTime)) {
+            isRecurrenceException = true;
+            time.parse3339(originalStartTime);
+            map.put(Events.ORIGINAL_EVENT, originalId);
+            map.put(Events.ORIGINAL_INSTANCE_TIME, time.toMillis(false /* use isDst */));
+        }
+
+        // Event status
+        byte status = event.getStatus();
+        switch (status) {
+            case EventEntry.STATUS_CANCELED:
+                if (!isRecurrenceException) {
+                    return ENTRY_DELETED;
+                }
+                map.put(Events.STATUS, sCanceledStatus);
+                break;
+            case EventEntry.STATUS_TENTATIVE:
+                map.put(Events.STATUS, sTentativeStatus);
+                break;
+            case EventEntry.STATUS_CONFIRMED:
+                map.put(Events.STATUS, sConfirmedStatus);
+                break;
+            default:
+                // should not happen
+                return ENTRY_INVALID;
+        }
+
+        map.put(Events._SYNC_LOCAL_ID, syncLocalId);
+
+        // Updated time, only needed for non-deleted items
+        String updated = event.getUpdateDate();
+        map.put(Events._SYNC_TIME, updated);
+        map.put(Events._SYNC_DIRTY, 0);
+
+        // visibility
+        switch (event.getVisibility()) {
+            case EventEntry.VISIBILITY_DEFAULT:
+                map.put(Events.VISIBILITY,  Events.VISIBILITY_DEFAULT);
+                break;
+            case EventEntry.VISIBILITY_CONFIDENTIAL:
+                map.put(Events.VISIBILITY,  Events.VISIBILITY_CONFIDENTIAL);
+                break;
+            case EventEntry.VISIBILITY_PRIVATE:
+                map.put(Events.VISIBILITY,  Events.VISIBILITY_PRIVATE);
+                break;
+            case EventEntry.VISIBILITY_PUBLIC:
+                map.put(Events.VISIBILITY,  Events.VISIBILITY_PUBLIC);
+                break;
+            default:
+                // should not happen
+                Log.e(TAG, "Unexpected visibility " + event.getVisibility());
+                return ENTRY_INVALID;
+        }
+
+        // transparency
+        switch (event.getTransparency()) {
+            case EventEntry.TRANSPARENCY_OPAQUE:
+                map.put(Events.TRANSPARENCY,  Events.TRANSPARENCY_OPAQUE);
+                break;
+            case EventEntry.TRANSPARENCY_TRANSPARENT:
+                map.put(Events.TRANSPARENCY,  Events.TRANSPARENCY_TRANSPARENT);
+                break;
+            default:
+                // should not happen
+                Log.e(TAG, "Unexpected transparency " + event.getTransparency());
+                return ENTRY_INVALID;
+        }
+
+        // html uri
+        String htmlUri = event.getHtmlUri();
+        if (!StringUtils.isEmpty(htmlUri)) {
+            // TODO: convert this desktop url into a mobile one?
+            // htmlUri = htmlUri.replace("/event?", "/mevent?"); // but a little more robust
+            map.put(Events.HTML_URI, htmlUri);
+        }
+
+        // title
+        String title = event.getTitle();
+        if (!StringUtils.isEmpty(title)) {
+            map.put(Events.TITLE, title);
+        }
+
+        // content
+        String content = event.getContent();
+        if (!StringUtils.isEmpty(content)) {
+            map.put(Events.DESCRIPTION, content);
+        }
+
+        // where
+        String where = event.getWhere();
+        if (!StringUtils.isEmpty(where)) {
+            map.put(Events.EVENT_LOCATION, where);
+        }
+
+        // Calendar ID
+        map.put(Events.CALENDAR_ID, syncInfo.calendarId);
+
+        // comments uri
+        String commentsUri = event.getCommentsUri();
+        if (commentsUri != null) {
+            map.put(Events.COMMENTS_URI, commentsUri);
+        }
+
+        boolean timesSet = false;
+
+        // see if there are any reminders for this event
+        if (event.getReminders() != null) {
+            // just store that we have reminders.  the caller will have
+            // to update the reminders table separately.
+            map.put(Events.HAS_ALARM, 1);
+        }
+
+        // see if there are any extended properties for this event
+        if (event.getExtendedProperties() != null) {
+            // just store that we have extended properties.  the caller will have
+            // to update the extendedproperties table separately.
+            map.put(Events.HAS_EXTENDED_PROPERTIES, 1);
+        }
+
+        // dtstart & dtend
+        When when = event.getFirstWhen();
+        if (when != null) {
+            String startTime = when.getStartTime();
+            if (!StringUtils.isEmpty(startTime)) {
+                time.parse3339(startTime);
+
+                // we also stash away the event's timezone.
+                // this timezone might get overwritten below, if this event is
+                // a recurrence (recurrences are defined in terms of the
+                // timezone of the creator of the event).
+                // note that we treat all day events as occurring in the UTC timezone, so
+                // an event on 05/08/2007 occurs on 05/08/2007, no matter what timezone the device
+                // is in.
+                // TODO: handle the "floating" timezone.
+                if (time.allDay) {
+                    map.put(Events.ALL_DAY, 1);
+                    map.put(Events.EVENT_TIMEZONE, Time.TIMEZONE_UTC);
+                } else {
+                    map.put(Events.EVENT_TIMEZONE, syncInfo.calendarTimezone);
+                }
+
+                map.put(Events.DTSTART, time.toMillis(false /* use isDst */));
+
+                timesSet = true;
+            }
+
+            String endTime = when.getEndTime();
+            if (!StringUtils.isEmpty(endTime)) {
+                time.parse3339(endTime);
+                map.put(Events.DTEND, time.toMillis(false /* use isDst */));
+            }
+        }
+
+        // rrule
+        String recurrence = event.getRecurrence();
+        if (!TextUtils.isEmpty(recurrence)) {
+            ICalendar.Component recurrenceComponent =
+                    new ICalendar.Component("DUMMY", null /* parent */);
+            ICalendar ical = null;
+            try {
+                ICalendar.parseComponent(recurrenceComponent, recurrence);
+            } catch (ICalendar.FormatException fe) {
+                if (Config.LOGD) {
+                    Log.d(TAG, "Unable to parse recurrence: " + recurrence);
+                }
+                return ENTRY_INVALID;
+            }
+
+            if (!RecurrenceSet.populateContentValues(recurrenceComponent, map)) {
+                return ENTRY_INVALID;
+            }
+
+            timesSet = true;
+        }
+
+        if (!timesSet) {
+            return ENTRY_INVALID;
+        }
+
+        map.put(SyncConstValue._SYNC_ACCOUNT, getAccount());
+        return ENTRY_OK;
+    }
+
+    @Override
+    public void updateProvider(Feed feed,
+            long syncLocalId, boolean forceDelete, Entry entry,
+            ContentProvider provider, Object info) throws ParseException {
+        SyncInfo syncInfo = (SyncInfo) info;
+        EventEntry event = (EventEntry) entry;
+
+        ContentValues map = new ContentValues();
+
+        // use the calendar's timezone, if provided in the feed.
+        // this overwrites whatever was in the db.
+        if ((feed != null) && (feed instanceof EventsFeed)) {
+            EventsFeed eventsFeed = (EventsFeed) feed;
+            syncInfo.calendarTimezone = eventsFeed.getTimezone();
+        }
+
+        if (forceDelete) {
+            deletedEntryToContentValues(event, map);
+            if (Config.LOGV) {
+                Log.v(TAG, "Deleting entry: " + map);
+            }
+            provider.insert(Events.DELETED_CONTENT_URI, map);
+            return;
+        }
+
+        int entryState = entryToContentValues(event, syncLocalId, map, syncInfo);
+
+        if (entryState == ENTRY_DELETED) {
+            if (Config.LOGV) {
+                Log.v(TAG, "Got deleted entry from server: "
+                        + map);
+            }
+            provider.insert(Events.DELETED_CONTENT_URI, map);
+        } else if (entryState == ENTRY_OK) {
+            if (Config.LOGV) {
+                Log.v(TAG, "Got entry from server: " + map);
+            }
+            Uri result = provider.insert(Events.CONTENT_URI, map);
+            long rowId = ContentUris.parseId(result);
+            // handle the reminders for the event
+            Integer hasAlarm = map.getAsInteger(Events.HAS_ALARM);
+            if (hasAlarm != null && hasAlarm == 1) {
+                // reminders should not be null
+                Vector alarms = event.getReminders();
+                if (alarms == null) {
+                    Log.e(TAG, "Have an alarm but do not have any reminders "
+                            + "-- should not happen.");
+                    throw new IllegalStateException("Have an alarm but do not have any reminders");
+                }
+                Enumeration reminders = alarms.elements();
+                while (reminders.hasMoreElements()) {
+                    ContentValues reminderValues = new ContentValues();
+                    reminderValues.put(Calendar.Reminders.EVENT_ID, rowId);
+
+                    Reminder reminder = (Reminder) reminders.nextElement();
+                    byte method = reminder.getMethod();
+                    switch (method) {
+                        case Reminder.METHOD_DEFAULT:
+                            reminderValues.put(Calendar.Reminders.METHOD,
+                                    Calendar.Reminders.METHOD_DEFAULT);
+                            break;
+                        case Reminder.METHOD_ALERT:
+                            reminderValues.put(Calendar.Reminders.METHOD,
+                                    Calendar.Reminders.METHOD_ALERT);
+                            break;
+                        case Reminder.METHOD_EMAIL:
+                            reminderValues.put(Calendar.Reminders.METHOD,
+                                    Calendar.Reminders.METHOD_EMAIL);
+                            break;
+                        case Reminder.METHOD_SMS:
+                            reminderValues.put(Calendar.Reminders.METHOD,
+                                    Calendar.Reminders.METHOD_SMS);
+                            break;
+                        default:
+                            // should not happen.  return false?  we'd have to
+                            // roll back the event.
+                            Log.e(TAG, "Unknown reminder method: " + method
+                                    + " should not happen!");
+                    }
+
+                    int minutes = reminder.getMinutes();
+                    reminderValues.put(Calendar.Reminders.MINUTES,
+                            minutes == Reminder.MINUTES_DEFAULT ?
+                                    Calendar.Reminders.MINUTES_DEFAULT :
+                                    minutes);
+
+                    if (provider.insert(Calendar.Reminders.CONTENT_URI,
+                            reminderValues) == null) {
+                        throw new ParseException("Unable to insert reminders.");
+                    }
+                }
+            }
+
+            // handle attendees for the event
+            Vector attendees = event.getAttendees();
+            Enumeration attendeesEnum = attendees.elements();
+            while (attendeesEnum.hasMoreElements()) {
+                Who who = (Who) attendeesEnum.nextElement();
+                ContentValues attendeesValues = new ContentValues();
+                attendeesValues.put(Calendar.Attendees.EVENT_ID, rowId);
+                attendeesValues.put(Calendar.Attendees.ATTENDEE_NAME, who.getValue());
+                attendeesValues.put(Calendar.Attendees.ATTENDEE_EMAIL, who.getEmail());
+
+                byte status;
+                switch (who.getStatus()) {
+                    case Who.STATUS_NONE:
+                        status = Calendar.Attendees.ATTENDEE_STATUS_NONE;
+                        break;
+                    case Who.STATUS_INVITED:
+                        status = Calendar.Attendees.ATTENDEE_STATUS_INVITED;
+                        break;
+                    case Who.STATUS_ACCEPTED:
+                        status = Calendar.Attendees.ATTENDEE_STATUS_ACCEPTED;
+                        break;
+                    case Who.STATUS_TENTATIVE:
+                        status = Calendar.Attendees.ATTENDEE_STATUS_TENTATIVE;
+                        break;
+                    case Who.STATUS_DECLINED:
+                        status = Calendar.Attendees.ATTENDEE_STATUS_DECLINED;
+                        break;
+                    default:
+                        Log.w(TAG, "Unknown attendee status " + who.getStatus());
+                        status = Calendar.Attendees.ATTENDEE_STATUS_NONE;
+                }
+                attendeesValues.put(Calendar.Attendees.ATTENDEE_STATUS, status);
+                byte rel;
+                switch (who.getRelationship()) {
+                    case Who.RELATIONSHIP_NONE:
+                        rel = Calendar.Attendees.RELATIONSHIP_NONE;
+                        break;
+                    case Who.RELATIONSHIP_ORGANIZER:
+                        rel = Calendar.Attendees.RELATIONSHIP_ORGANIZER;
+                        break;
+                    case Who.RELATIONSHIP_ATTENDEE:
+                        rel = Calendar.Attendees.RELATIONSHIP_ATTENDEE;
+                        break;
+                    case Who.RELATIONSHIP_PERFORMER:
+                        rel = Calendar.Attendees.RELATIONSHIP_PERFORMER;
+                        break;
+                    case Who.RELATIONSHIP_SPEAKER:
+                        rel = Calendar.Attendees.RELATIONSHIP_SPEAKER;
+                        break;
+                    default:
+                        Log.w(TAG, "Unknown attendee relationship " + who.getRelationship());
+                        rel = Calendar.Attendees.RELATIONSHIP_NONE;
+                }
+
+                attendeesValues.put(Calendar.Attendees.ATTENDEE_RELATIONSHIP, rel);
+
+                byte type;
+                switch (who.getType()) {
+                    case Who.TYPE_NONE:
+                        type = Calendar.Attendees.TYPE_NONE;
+                        break;
+                    case Who.TYPE_REQUIRED:
+                        type = Calendar.Attendees.TYPE_REQUIRED;
+                        break;
+                    case Who.TYPE_OPTIONAL:
+                        type = Calendar.Attendees.TYPE_OPTIONAL;
+                        break;
+                    default:
+                        Log.w(TAG, "Unknown attendee type " + who.getType());
+                        type = Calendar.Attendees.TYPE_NONE;
+                }
+                attendeesValues.put(Calendar.Attendees.ATTENDEE_TYPE, type);
+                if (provider.insert(Calendar.Attendees.CONTENT_URI, attendeesValues) == null) {
+                    throw new ParseException("Unable to insert attendees.");
+                }
+            }
+
+            // handle the extended properties for the event
+            Integer hasExtendedProperties = map.getAsInteger(Events.HAS_EXTENDED_PROPERTIES);
+            if (hasExtendedProperties != null && hasExtendedProperties.intValue() != 0) {
+                // extended properties should not be null
+                // TODO: make the extended properties a bit more OO?
+                Hashtable extendedProperties = event.getExtendedProperties();
+                if (extendedProperties == null) {
+                    Log.e(TAG, "Have extendedProperties but do not have any properties"
+                            + "-- should not happen.");
+                    throw new IllegalStateException(
+                            "Have extendedProperties but do not have any properties");
+                }
+                Enumeration propertyNames = extendedProperties.keys();
+                while (propertyNames.hasMoreElements()) {
+                    String propertyName = (String) propertyNames.nextElement();
+                    String propertyValue = (String) extendedProperties.get(propertyName);
+                    ContentValues extendedPropertyValues = new ContentValues();
+                    extendedPropertyValues.put(Calendar.ExtendedProperties.EVENT_ID, rowId);
+                    extendedPropertyValues.put(Calendar.ExtendedProperties.NAME,
+                            propertyName);
+                    extendedPropertyValues.put(Calendar.ExtendedProperties.VALUE,
+                            propertyValue);
+                    if (provider.insert(Calendar.ExtendedProperties.CONTENT_URI,
+                            extendedPropertyValues) == null) {
+                        throw new ParseException("Unable to insert extended properties.");
+                    }
+                }
+            }
+        } else {
+            // If the DTSTART == -1, then the date was out of range.  We don't
+            // need to throw a ParseException because the user can create
+            // dates on the web that we can't handle on the phone.  For
+            // example, events with dates before Dec 13, 1901 can be created
+            // on the web but cannot be handled on the phone.
+            Long dtstart = map.getAsLong(Events.DTSTART);
+            if (dtstart != null && dtstart == -1) {
+                return;
+            }
+
+            if (Config.LOGV) {
+                Log.v(TAG, "Got invalid entry from server: " + map);
+            }
+            throw new ParseException("Got invalid entry from server: " + map);
+        }
+    }
+
+    @Override
+    public void getServerDiffs(SyncContext context,
+            SyncData baseSyncData, SyncableContentProvider tempProvider,
+            Bundle extras, Object baseSyncInfo, SyncResult syncResult) {
+        final ContentResolver cr = getContext().getContentResolver();
+        mServerDiffs++;
+        final boolean syncingSingleFeed = (extras != null) && extras.containsKey("feed");
+        if (syncingSingleFeed) {
+            String feedUrl = extras.getString("feed");
+            getServerDiffsForFeed(context, baseSyncData, tempProvider, feedUrl,
+                    baseSyncInfo, syncResult);
+            return;
+        }
+
+        // select the set of calendars for this account.
+        Cursor cursor = cr.query(Calendar.Calendars.CONTENT_URI,
+                CALENDARS_PROJECTION, SELECT_BY_ACCOUNT,
+                new String[] { getAccount() }, null /* sort order */);
+
+        Bundle syncExtras = new Bundle();
+
+        boolean refreshCalendars = true;
+
+        try {
+            while (cursor.moveToNext()) {
+                boolean syncEvents = (cursor.getInt(6) == 1);
+                String feedUrl = cursor.getString(3);
+
+                if (!syncEvents) {
+                    continue;
+                }
+
+                // since this is a poll (no specific feed selected), refresh the list of calendars.
+                // we can move away from this when we move to the new allcalendars feed, which is
+                // syncable.  until then, we'll rely on the daily poll to keep the list of calendars
+                // up to date.
+                if (refreshCalendars) {
+                    mRefresh++;
+                    context.setStatusText("Fetching list of calendars");
+                    // get rid of the current cursor and fetch from the server.
+                    cursor.close();
+                    final String[] accountSelectionArgs = new String[]{getAccount()};
+                    cursor = cr.query(
+                        Calendar.Calendars.LIVE_CONTENT_URI, CALENDARS_PROJECTION,
+                        SELECT_BY_ACCOUNT, accountSelectionArgs, null /* sort order */);
+                    // start over with the loop
+                    refreshCalendars = false;
+                    continue;
+                }
+
+                // schedule syncs for each of these feeds.
+                syncExtras.clear();
+                syncExtras.putAll(extras);
+                syncExtras.putString("feed", feedUrl);
+                cr.startSync(Calendar.CONTENT_URI, syncExtras);
+            }
+        } finally {
+            cursor.close();
+        }
+    }
+
+
+    private void getServerDiffsForFeed(SyncContext context, SyncData baseSyncData,
+            SyncableContentProvider tempProvider,
+            String feed, Object baseSyncInfo, SyncResult syncResult) {
+        final SyncInfo syncInfo = (SyncInfo) baseSyncInfo;
+        final GDataSyncData syncData = (GDataSyncData) baseSyncData;
+
+        Cursor cursor = getContext().getContentResolver().query(Calendar.Calendars.CONTENT_URI,
+                CALENDARS_PROJECTION, SELECT_BY_ACCOUNT_AND_FEED,
+                new String[] { getAccount(), feed }, null /* sort order */);
+
+        ContentValues map = new ContentValues();
+        int maxResults = getMaxEntriesPerSync();
+
+        try {
+            if (!cursor.moveToFirst()) {
+                return;
+            }
+            // TODO: refactor all of this, so we don't have to rely on
+            // member variables getting updated here in order for the
+            // base class hooks to work.
+
+            syncInfo.calendarId = cursor.getLong(0);
+            boolean syncEvents = (cursor.getInt(6) == 1);
+            long syncTime = cursor.getLong(2);
+            String feedUrl = cursor.getString(3);
+            String name = cursor.getString(4);
+            String origCalendarTimezone =
+                    syncInfo.calendarTimezone = cursor.getString(5);
+
+            if (!syncEvents) {
+                // should not happen.  non-syncable feeds should not be scheduled for syncs nor
+                // should they get tickled.
+                if (Log.isLoggable(TAG, Log.VERBOSE)) {
+                    Log.v(TAG, "Ignoring sync request for non-syncable feed.");
+                }
+                return;
+            }
+
+            context.setStatusText("Syncing " + name);
+
+            // call the superclass implementation to sync the current
+            // calendar from the server.
+            getServerDiffsImpl(context, tempProvider, getFeedEntryClass(), feedUrl, syncInfo,
+                    maxResults, syncData, syncResult);
+            if (mSyncCanceled || syncResult.hasError()) {
+                return;
+            }
+
+            // update the timezone for this calendar if it changed
+            if (!TextUtils.equals(syncInfo.calendarTimezone,
+                    origCalendarTimezone)) {
+                map.clear();
+                map.put(Calendars.TIMEZONE, syncInfo.calendarTimezone);
+                mContentResolver.update(
+                        ContentUris.withAppendedId(Calendars.CONTENT_URI, syncInfo.calendarId),
+                        map, null, null);
+            }
+        } finally {
+            cursor.close();
+        }
+    }
+
+    @Override
+    protected void initTempProvider(SyncableContentProvider cp) {
+        // TODO: don't use the real db's calendar id's.  create new ones locally and translate
+        // during CalendarProvider's merge.
+        
+        // populate temp provider with calendar ids, so joins work.
+        ContentValues map = new ContentValues();
+        Cursor c = getContext().getContentResolver().query(
+                Calendar.Calendars.CONTENT_URI,
+                CALENDARS_PROJECTION,
+                SELECT_BY_ACCOUNT, new String[]{getAccount()}, null /* sort order */);
+        final int idIndex = c.getColumnIndexOrThrow(Calendars._ID);
+        final int urlIndex = c.getColumnIndexOrThrow(Calendars.URL);
+        final int timezoneIndex = c.getColumnIndexOrThrow(Calendars.TIMEZONE);
+        while (c.moveToNext()) {
+            map.clear();
+            map.put(Calendars._ID, c.getLong(idIndex));
+            map.put(Calendars.URL, c.getString(urlIndex));
+            map.put(Calendars.TIMEZONE, c.getString(timezoneIndex));
+            cp.insert(Calendar.Calendars.CONTENT_URI, map);
+        }
+        c.close();
+    }
+
+    public void onAccountsChanged(String[] accountsArray) {
+        if (!"yes".equals(SystemProperties.get("ro.config.sync"))) {
+            return;
+        }
+
+        // - Get a cursor (A) over all selected calendars over all accounts
+        // - Get a cursor (B) over all subscribed feeds for calendar
+        // - If an item is in A but not B then add a subscription
+        // - If an item is in B but not A then remove the subscription
+
+        ContentResolver cr = getContext().getContentResolver();
+        Cursor cursorA = null;
+        Cursor cursorB = null;
+        try {
+            cursorA = Calendar.Calendars.query(cr, null /* projection */,
+                    Calendar.Calendars.SELECTED + "=1", CALENDAR_KEY_SORT_ORDER);
+            int urlIndexA = cursorA.getColumnIndexOrThrow(Calendar.Calendars.URL);
+            int accountIndexA = cursorA.getColumnIndexOrThrow(Calendar.Calendars._SYNC_ACCOUNT);
+            cursorB = SubscribedFeeds.Feeds.query(cr, FEEDS_KEY_COLUMNS,
+                    SubscribedFeeds.Feeds.AUTHORITY + "=?", new String[]{Calendar.AUTHORITY},
+                    FEEDS_KEY_SORT_ORDER);
+            int urlIndexB = cursorB.getColumnIndexOrThrow(SubscribedFeeds.Feeds.FEED);
+            int accountIndexB = cursorB.getColumnIndexOrThrow(SubscribedFeeds.Feeds._SYNC_ACCOUNT);
+            for (CursorJoiner.Result joinerResult :
+                    new CursorJoiner(cursorA, CALENDAR_KEY_COLUMNS, cursorB, FEEDS_KEY_COLUMNS)) {
+                switch (joinerResult) {
+                    case LEFT:
+                        SubscribedFeeds.addFeed(
+                                cr,
+                                cursorA.getString(urlIndexA),
+                                cursorA.getString(accountIndexA),
+                                Calendar.AUTHORITY,
+                                CalendarClient.SERVICE);
+                        break;
+                    case RIGHT:
+                        SubscribedFeeds.deleteFeed(
+                                cr,
+                                cursorB.getString(urlIndexB),
+                                cursorB.getString(accountIndexB),
+                                Calendar.AUTHORITY);
+                        break;
+                    case BOTH:
+                        // do nothing, since the subscription already exists
+                        break;
+                }
+            }
+        } finally {
+            // check for null in case an exception occurred before the cursors got created
+            if (cursorA != null) cursorA.close();
+            if (cursorB != null) cursorB.close();
+        }
+    }
+
+    /**
+     * Should not get called.  The feed url changes depending on which calendar is being sync'd
+     * to/from the device, and thus is determined and passed around as a local variable, where
+     * appropriate.
+     */
+    protected String getFeedUrl(String account) {
+        throw new UnsupportedOperationException("getFeedUrl() should not get called.");
+    }
+
+    protected Class getFeedEntryClass() {
+        return EventEntry.class;
+    }
+
+    @Override
+    protected void updateQueryParameters(QueryParams params) {
+        if (params.getUpdatedMin() == null) {
+            // if this is the first sync, only bother syncing starting from
+            // one month ago.
+            // TODO: remove this restriction -- we may want all of
+            // historical calendar events.
+            Time lastMonth = new Time(Time.TIMEZONE_UTC);
+            lastMonth.setToNow();
+            --lastMonth.month;
+            lastMonth.normalize(true /* ignore isDst */);
+            String startMin = lastMonth.format("%Y-%m-%dT%H:%M:%S.000Z");
+            // TODO: move start-min to CalendarClient?
+            // or create CalendarQueryParams subclass (extra class)?
+            params.setParamValue("start-min", startMin);
+            // HACK: specify that we want to expand recurrences ijn the past,
+            // so the server does not expand any recurrences.  we do this to
+            // avoid a large number of gd:when elements that we do not need,
+            // since we process gd:recurrence elements instead.
+            params.setParamValue("recurrence-expansion-start", "1970-01-01");
+            params.setParamValue("recurrence-expansion-end", "1970-01-01");
+        }
+        // we want to get the events ordered by last modified, so we can
+        // recover in case we cannot process the entire feed.
+        params.setParamValue("orderby", "lastmodified");
+        params.setParamValue("sortorder", "ascending");
+    }
+
+    @Override
+    protected GDataServiceClient getGDataServiceClient() {
+        return mCalendarClient;
+    }
+
+    protected void getStatsString(StringBuffer sb, SyncResult result) {
+        super.getStatsString(sb, result);
+        if (mRefresh > 0) {
+            sb.append("F").append(mRefresh);
+        }
+        if (mServerDiffs > 0) {
+            sb.append("s").append(mServerDiffs);
+        }
+    }
+}
diff --git a/src/com/android/providers/calendar/Duration.java b/src/com/android/providers/calendar/Duration.java
new file mode 100644
index 0000000..b5dad4b
--- /dev/null
+++ b/src/com/android/providers/calendar/Duration.java
@@ -0,0 +1,145 @@
+/* //device/content/providers/pim/Duration.java

+**

+** Copyright 2006, The Android Open Source Project
+**

+** Licensed under the Apache License, Version 2.0 (the "License"); 

+** you may not use this file except in compliance with the License. 

+** You may obtain a copy of the License at 

+**

+**     http://www.apache.org/licenses/LICENSE-2.0 

+**

+** Unless required by applicable law or agreed to in writing, software 

+** distributed under the License is distributed on an "AS IS" BASIS, 

+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 

+** See the License for the specific language governing permissions and 

+** limitations under the License.

+*/

+

+package com.android.providers.calendar;

+

+import android.pim.DateException;

+

+import java.util.Calendar;

+

+/**

+ * According to RFC2445, durations are like this:

+ *       WEEKS

+ *     | DAYS [ HOURS [ MINUTES [ SECONDS ] ] ]

+ *     | HOURS [ MINUTES [ SECONDS ] ]

+ * it doesn't specifically, say, but this sort of implies that you can't have

+ * 70 seconds.

+ */

+public class Duration

+{

+    public int sign; // 1 or -1

+    public int weeks;

+    public int days;

+    public int hours;

+    public int minutes;

+    public int seconds;

+

+    public Duration()

+    {

+        sign = 1;

+    }

+

+    /**

+     * Parse according to RFC2445 ss4.3.6.  (It's actually a little loose with

+     * its parsing, for better or for worse)

+     */

+    public void parse(String str) throws DateException 

+    {

+        sign = 1;

+        weeks = 0;

+        days = 0;

+        hours = 0;

+        minutes = 0;

+        seconds = 0;

+

+        int len = str.length();

+        int index = 0;

+        char c;

+

+        if (len < 1) {

+            return ;

+        }

+

+        c = str.charAt(0);

+        if (c == '-') {

+            sign = -1;

+            index++;

+        }

+        else if (c == '+') {

+            index++;

+        }

+

+        if (len < index) {

+            return ;

+        }

+

+        c = str.charAt(index);

+        if (c != 'P') {

+            throw new DateException (

+                    "Duration.parse(str='" + str + "') expected 'P' at index="

+                    + index);

+        }

+        index++;

+

+        int n = 0;

+        for (; index < len; index++) {

+            c = str.charAt(index);

+            if (c >= '0' && c <= '9') {

+                n *= 10;

+                n += ((int)(c-'0'));

+            }

+            else if (c == 'W') {

+                weeks = n;

+                n = 0;

+            }

+            else if (c == 'H') {

+                hours = n;

+                n = 0;

+            }

+            else if (c == 'M') {

+                minutes = n;

+                n = 0;

+            }

+            else if (c == 'S') {

+                seconds = n;

+                n = 0;

+            }

+            else if (c == 'D') {

+                days = n;

+                n = 0;

+            }

+            else if (c == 'T') {

+            }

+            else {

+                throw new DateException (

+                        "Duration.parse(str='" + str + "') unexpected char '"

+                        + c + "' at index=" + index);

+            }

+        }

+    }

+

+    /**

+     * Add this to the calendar provided, in place, in the calendar.

+     */

+    public void addTo(Calendar cal)

+    {

+        cal.add(Calendar.DAY_OF_MONTH, sign*weeks*7);

+        cal.add(Calendar.DAY_OF_MONTH, sign*days);

+        cal.add(Calendar.HOUR, sign*hours);

+        cal.add(Calendar.MINUTE, sign*minutes);

+        cal.add(Calendar.SECOND, sign*seconds);

+    }

+

+    public long addTo(long dt) {

+        long factor = 1000*sign;

+        return dt + (7*24*60*60*factor*weeks)

+                  + (24*60*60*factor*days)

+                  + (60*60*factor*hours)

+                  + (60*factor*minutes)

+                  + (factor*seconds);

+    }

+}

diff --git a/src/com/android/providers/calendar/MetaData.java b/src/com/android/providers/calendar/MetaData.java
new file mode 100644
index 0000000..ed7f9d9
--- /dev/null
+++ b/src/com/android/providers/calendar/MetaData.java
@@ -0,0 +1,282 @@
+/*
+**
+** Copyright 2008, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+**     http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** See the License for the specific language governing permissions and
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** limitations under the License.
+*/
+
+package com.android.providers.calendar;
+
+
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.provider.Calendar.CalendarMetaData;
+
+/**
+ * The global meta-data used for expanding the Instances table is stored in one
+ * row of the "CalendarMetaData" table.  This class is used for caching those
+ * values to avoid repeatedly banging on the database.  It is also used
+ * for writing the values back to the database, while maintaining the
+ * consistency of the cache.
+ */
+public class MetaData {
+    /**
+     * These fields are updated atomically with the database.
+     * If fields are added or removed from the CalendarMetaData table, those
+     * changes must also be reflected here.
+     */
+    public class Fields {
+        public String timezone;     // local timezone used for Instance expansion
+        public long minInstance;    // UTC millis
+        public long maxInstance;    // UTC millis
+        public int minBusyBit;      // Julian start day
+        public int maxBusyBit;      // Julian end day
+    }
+    
+    /**
+     * The cached copy of the meta-data fields from the database.
+     */
+    private Fields mFields = new Fields();
+    
+    private final SQLiteOpenHelper mOpenHelper;
+    private boolean mInitialized;
+
+    /**
+     * The column names in the CalendarMetaData table.  This projection
+     * must contain all of the columns.
+     */
+    private static final String[] sCalendarMetaDataProjection = {
+        CalendarMetaData.LOCAL_TIMEZONE,
+        CalendarMetaData.MIN_INSTANCE,
+        CalendarMetaData.MAX_INSTANCE,
+        CalendarMetaData.MIN_BUSYBITS,
+        CalendarMetaData.MAX_BUSYBITS };
+    
+    private static final int METADATA_INDEX_LOCAL_TIMEZONE = 0;
+    private static final int METADATA_INDEX_MIN_INSTANCE = 1;
+    private static final int METADATA_INDEX_MAX_INSTANCE = 2;
+    private static final int METADATA_INDEX_MIN_BUSYBIT = 3;
+    private static final int METADATA_INDEX_MAX_BUSYBIT = 4;
+    
+    public MetaData(SQLiteOpenHelper openHelper) {
+        mOpenHelper = openHelper;
+    }
+    
+    /**
+     * Returns a copy of all the MetaData fields.  This method grabs a
+     * database lock to read all the fields atomically.
+     * 
+     * @return a copy of all the MetaData fields.
+     */
+    public Fields getFields() {
+        Fields fields = new Fields();
+        SQLiteDatabase db = mOpenHelper.getReadableDatabase();
+        db.beginTransaction();
+        try {
+            // If the fields have not been initialized from the database,
+            // then read the database.
+            if (!mInitialized) {
+                readLocked(db);
+            }
+            fields.timezone = mFields.timezone;
+            fields.minInstance = mFields.minInstance;
+            fields.maxInstance = mFields.maxInstance;
+            fields.minBusyBit = mFields.minBusyBit;
+            fields.maxBusyBit = mFields.maxBusyBit;
+            db.setTransactionSuccessful();
+        } finally {
+            db.endTransaction();
+        }
+        return fields;
+    }
+
+    /**
+     * This method must be called only while holding a database lock.
+     * 
+     * <p>
+     * Returns a copy of all the MetaData fields.  This method assumes
+     * the database lock has already been acquired.
+     * </p>
+     * 
+     * @return a copy of all the MetaData fields.
+     */
+    public Fields getFieldsLocked() {
+        Fields fields = new Fields();
+
+        // If the fields have not been initialized from the database,
+        // then read the database.
+        if (!mInitialized) {
+            SQLiteDatabase db = mOpenHelper.getReadableDatabase();
+            readLocked(db);
+        }
+        fields.timezone = mFields.timezone;
+        fields.minInstance = mFields.minInstance;
+        fields.maxInstance = mFields.maxInstance;
+        fields.minBusyBit = mFields.minBusyBit;
+        fields.maxBusyBit = mFields.maxBusyBit;
+        return fields;
+    }
+    
+    /**
+     * Reads the meta-data for the CalendarProvider from the database and
+     * updates the member variables.  This method executes while the database
+     * lock is held.  If there were no exceptions reading the database,
+     * mInitialized is set to true.
+     */
+    private void readLocked(SQLiteDatabase db) {
+        String timezone = null;
+        long minInstance = 0, maxInstance = 0;
+        int minBusyBit = 0, maxBusyBit = 0;
+
+        // Read the database directly.  We only do this once to initialize
+        // the members of this class.
+        Cursor cursor = db.query("CalendarMetaData", sCalendarMetaDataProjection,
+                null, null, null, null, null);
+        try {
+            if (cursor.moveToNext()) {
+                timezone = cursor.getString(METADATA_INDEX_LOCAL_TIMEZONE);
+                minInstance = cursor.getLong(METADATA_INDEX_MIN_INSTANCE);
+                maxInstance = cursor.getLong(METADATA_INDEX_MAX_INSTANCE);
+                minBusyBit = cursor.getInt(METADATA_INDEX_MIN_BUSYBIT);
+                maxBusyBit = cursor.getInt(METADATA_INDEX_MAX_BUSYBIT);
+            }
+        } finally {
+            if (cursor != null) {
+                cursor.close();
+            }
+        }
+
+        // Cache the result of reading the database
+        mFields.timezone = timezone;
+        mFields.minInstance = minInstance;
+        mFields.maxInstance = maxInstance;
+        mFields.minBusyBit = minBusyBit;
+        mFields.maxBusyBit = maxBusyBit;
+        
+        // Mark the fields as initialized
+        mInitialized = true;
+    }
+
+    /**
+     * Writes the meta-data for the CalendarProvider.  The values to write are
+     * passed in as parameters.  All of the values are updated atomically,
+     * including the cached copy of the meta-data.
+     * 
+     * @param timezone the local timezone used for Instance expansion
+     * @param begin the start of the Instance expansion in UTC milliseconds
+     * @param end the end of the Instance expansion in UTC milliseconds
+     * @param startDay the start of the BusyBit expansion (the start Julian day)
+     * @param endDay the end of the BusyBit expansion (the end Julian day)
+     */
+    public void write(String timezone, long begin, long end, int startDay, int endDay) {
+        SQLiteDatabase db = mOpenHelper.getReadableDatabase();
+        db.beginTransaction();
+        try {
+            writeLocked(timezone, begin, end, startDay, endDay);
+            db.setTransactionSuccessful();
+        } finally {
+            db.endTransaction();
+        }
+    }
+
+    /**
+     * This method must be called only while holding a database lock.
+     * 
+     * <p>
+     * Writes the meta-data for the CalendarProvider.  The values to write are
+     * passed in as parameters.  All of the values are updated atomically,
+     * including the cached copy of the meta-data.
+     * </p>
+     * 
+     * @param timezone the local timezone used for Instance expansion
+     * @param begin the start of the Instance expansion in UTC milliseconds
+     * @param end the end of the Instance expansion in UTC milliseconds
+     * @param startDay the start of the BusyBit expansion (the start Julian day)
+     * @param endDay the end of the BusyBit expansion (the end Julian day)
+     */
+    public void writeLocked(String timezone, long begin, long end, int startDay, int endDay) {
+        ContentValues values = new ContentValues();
+        values.put("_id", 1);
+        values.put(CalendarMetaData.LOCAL_TIMEZONE, timezone);
+        values.put(CalendarMetaData.MIN_INSTANCE, begin);
+        values.put(CalendarMetaData.MAX_INSTANCE, end);
+        values.put(CalendarMetaData.MIN_BUSYBITS, startDay);
+        values.put(CalendarMetaData.MAX_BUSYBITS, endDay);
+
+        // Atomically update the database and the cached members.
+        try {
+            SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+            db.replace("CalendarMetaData", null, values);
+        } catch (RuntimeException e) {
+            // Failed: zero the in-memory fields to force recomputation.
+            mFields.timezone = null;
+            mFields.minInstance = mFields.maxInstance = 0;
+            mFields.minBusyBit = mFields.maxBusyBit = 0;
+            throw e;
+        }
+
+        // Update the cached members last in case the database update fails
+        mFields.timezone = timezone;
+        mFields.minInstance = begin;
+        mFields.maxInstance = end;
+        mFields.minBusyBit = startDay;
+        mFields.maxBusyBit = endDay;
+    }
+
+    /**
+     * Clears the time range for the Instances table.  The rows in the
+     * Instances table will be deleted (and regenerated) the next time
+     * that the Instances table is queried.
+     * 
+     * Also clears the time range for the BusyBits table because that depends
+     * on the Instances table.
+     */
+    public void clearInstanceRange() {
+        SQLiteDatabase db = mOpenHelper.getReadableDatabase();
+        db.beginTransaction();
+        try {
+            // If the fields have not been initialized from the database,
+            // then read the database.
+            if (!mInitialized) {
+                readLocked(db);
+            }
+            writeLocked(mFields.timezone, 0 /* begin */, 0 /* end */,
+                    0 /* startDay */, 0 /* endDay */);
+            db.setTransactionSuccessful();
+        } finally {
+            db.endTransaction();
+        }
+    }
+
+    /**
+     * Clears the time range for the BusyBits table.
+     */
+    public void clearBusyBitRange() {
+        SQLiteDatabase db = mOpenHelper.getReadableDatabase();
+        db.beginTransaction();
+        try {
+            // If the fields have not been initialized from the database,
+            // then read the database.
+            if (!mInitialized) {
+                readLocked(db);
+            }
+            writeLocked(mFields.timezone, mFields.minInstance, mFields.maxInstance,
+                    0 /* startDay */, 0 /* endDay */);
+            db.setTransactionSuccessful();
+        } finally {
+            db.endTransaction();
+        }
+    }
+}
diff --git a/src/com/android/providers/calendar/RecurrenceProcessor.java b/src/com/android/providers/calendar/RecurrenceProcessor.java
new file mode 100644
index 0000000..c04a538
--- /dev/null
+++ b/src/com/android/providers/calendar/RecurrenceProcessor.java
@@ -0,0 +1,890 @@
+/* //device/content/providers/pim/RecurrenceProcessor.java
+**
+** Copyright 2006, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License"); 
+** you may not use this file except in compliance with the License. 
+** You may obtain a copy of the License at 
+**
+**     http://www.apache.org/licenses/LICENSE-2.0 
+**
+** Unless required by applicable law or agreed to in writing, software 
+** distributed under the License is distributed on an "AS IS" BASIS, 
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
+** See the License for the specific language governing permissions and 
+** limitations under the License.
+*/
+
+package com.android.providers.calendar;
+
+import android.pim.DateException;
+import android.pim.EventRecurrence;
+import android.pim.RecurrenceSet;
+import android.pim.Time;
+import android.util.Log;
+
+import java.util.TreeSet;
+
+public class RecurrenceProcessor
+{
+    // these are created once and reused.
+    private Time mIterator = new Time(Time.TIMEZONE_UTC);
+    private Time mUntil = new Time(Time.TIMEZONE_UTC);
+    private StringBuilder mStringBuilder = new StringBuilder();
+    private Time mGenerated = new Time(Time.TIMEZONE_UTC);
+    private DaySet mDays = new DaySet(false);
+
+    public RecurrenceProcessor()
+    {
+    }
+
+    private static final String TAG = "RecurrenceProcessor";
+
+    private static final boolean SPEW = false;
+
+    /**
+     * Returns the time (millis since epoch) of the last occurrence, -1 if the event repeats
+     * forever.
+     */
+    public long getLastOccurence(Time dtstart,
+                                 RecurrenceSet recur) throws DateException {
+
+        long lastTime = -1;
+        boolean hasCount = false;
+
+        // first see if there are any "until"s specified.  if so, use the latest
+        // until / rdate.
+        if (recur.rrules != null) {
+            for (EventRecurrence rrule : recur.rrules) {
+                if (rrule.count != 0) {
+                    hasCount = true;
+                }
+                    
+                if (rrule.until != null) {
+                    // according to RFC 2445, until must be in UTC.
+                    mIterator.parse2445(rrule.until);
+                    long untilTime = mIterator.toMillis(false /* use isDst */);
+                    if (untilTime > lastTime) {
+                        lastTime = untilTime;
+                    }
+                }
+            }
+            if ((lastTime != -1) && recur.rdates != null) {
+                for (long dt : recur.rdates) {
+                    if (dt > lastTime) {
+                        lastTime = dt;
+                    }
+                }
+            }
+            if (lastTime != -1) {
+                return lastTime;
+            }
+        } else if ((recur.rdates != null) &&
+                   ((recur.exrules == null) && (recur.exdates == null))) {
+            // if there are only rdates, we can just pick the last one.
+            for (long dt : recur.rdates) {
+                if (dt > lastTime) {
+                    lastTime = dt;
+                }
+            }
+            return lastTime;
+        }
+
+        // expand the complete recurrence if there were any counts specified,
+        // or if there were rdates specified.
+        if ((recur.rrules != null && hasCount) || (recur.rdates != null)) {
+            // could return 0 if the recurrence only occurs before dtstart?
+            // i don't think we would have been called in that case.
+            // TODO: check this!
+            return expand(dtstart, recur,
+                    dtstart.toMillis(false /* use isDst */) /* range start */,
+                    -1 /* range end */, null /* output */);
+        }
+        return -1;
+    }
+
+    /**
+     * a -- list of values
+     * N -- number of values to use in a
+     * v -- value to check for
+     */
+    private static boolean listContains(int[] a, int N, int v)
+    {
+        for (int i=0; i<N; i++) {
+            if (a[i] == v) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * a -- list of values
+     * N -- number of values to use in a
+     * v -- value to check for
+     * max -- if a value in a is negative, add that negative value
+     *        to max and compare that instead; this is how we deal with
+     *        negative numbers being offsets from the end value
+     */
+    private static boolean listContains(int[] a, int N, int v, int max)
+    {
+        for (int i=0; i<N; i++) {
+            int w = a[i];
+            if (w > 0) {
+                if (w == v) {
+                    return true;
+                }
+            } else {
+                max += w; // w is negative
+                if (max == v) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Filter out the ones for events whose BYxxx rule is for
+     * a period greater than or equal to the period of the FREQ.
+     *
+     * Returns 0 if the event should not be filtered out
+     * Returns something else (a rule number which is useful for debugging)
+     * if the event should not be returned
+     */
+    private static int filter(EventRecurrence r, Time iterator)
+    {
+        boolean found;
+        int freq = r.freq;
+
+        if (EventRecurrence.MONTHLY >= freq) {
+            // BYMONTH
+            if (r.bymonthCount > 0) {
+                found = listContains(r.bymonth, r.bymonthCount,
+                        iterator.month + 1);
+                if (!found) {
+                    return 1;
+                }
+            }
+        }
+        if (EventRecurrence.WEEKLY >= freq) {
+            // BYWEEK -- this is just a guess.  I wonder how many events
+            // acutally use BYWEEKNO.
+            if (r.byweeknoCount > 0) {
+                found = listContains(r.byweekno, r.byweeknoCount,
+                                iterator.getWeekNumber(),
+                                iterator.getActualMaximum(Time.WEEK_NUM));
+                if (!found) {
+                    return 2;
+                }
+            }
+        }
+        if (EventRecurrence.DAILY >= freq) {
+            // BYYEARDAY
+            if (r.byyeardayCount > 0) {
+                found = listContains(r.byyearday, r.byyeardayCount,
+                                iterator.yearDay, iterator.getActualMaximum(Time.YEAR_DAY));
+                if (!found) {
+                    return 3;
+                }
+            }
+            // BYMONTHDAY
+            if (r.bymonthdayCount > 0 ) {
+                found = listContains(r.bymonthday, r.bymonthdayCount,
+                                iterator.monthDay,
+                                iterator.getActualMaximum(Time.MONTH_DAY));
+                if (!found) {
+                    return 4;
+                }
+            }
+            // BYDAY -- when filtering, we ignore the number field, because it
+            // only is meaningful when creating more events.
+byday:
+            if (r.bydayCount > 0) {
+                int a[] = r.byday;
+                int N = r.bydayCount;
+                int v = EventRecurrence.timeDay2Day(iterator.weekDay);
+                for (int i=0; i<N; i++) {
+                    if (a[i] == v) {
+                        break byday;
+                    }
+                }
+                return 5;
+            }
+        }
+        if (EventRecurrence.HOURLY >= freq) {
+            // BYHOUR
+            found = listContains(r.byhour, r.byhourCount,
+                            iterator.hour,
+                            iterator.getActualMaximum(Time.HOUR));
+            if (!found) {
+                return 6;
+            }
+        }
+        if (EventRecurrence.MINUTELY >= freq) {
+            // BYMINUTE
+            found = listContains(r.byminute, r.byminuteCount,
+                            iterator.minute,
+                            iterator.getActualMaximum(Time.MINUTE));
+            if (!found) {
+                return 7;
+            }
+        }
+        if (EventRecurrence.SECONDLY >= freq) {
+            // BYSECOND
+            found = listContains(r.bysecond, r.bysecondCount,
+                            iterator.second,
+                            iterator.getActualMaximum(Time.SECOND));
+            if (!found) {
+                return 8;
+            }
+        }
+        // BYSETPOS -- we might have to do this by postprocessing
+        // the list
+
+        // if we got to here, we didn't filter it out
+        return 0;
+    }
+
+    private static final int USE_ITERATOR = 0;
+    private static final int USE_BYLIST = 1;
+
+    /**
+     * Return whether we should make this list from the BYxxx list or
+     * from the component of the iterator.
+     */
+    int generateByList(int count, int freq, int byFreq)
+    {
+        if (byFreq >= freq) {
+            return USE_ITERATOR;
+        } else {
+            if (count == 0) {
+                return USE_ITERATOR;
+            } else {
+                return USE_BYLIST;
+            }
+        }
+    }
+
+    private static boolean useBYX(int freq, int freqConstant, int count)
+    {
+        return freq > freqConstant && count > 0;
+    }
+
+    public static class DaySet
+    {
+        public DaySet(boolean zulu)
+        {
+            mTime = new Time(Time.TIMEZONE_UTC);
+        }
+
+        void setRecurrence(EventRecurrence r)
+        {
+            mYear = 0;
+            mMonth = -1;
+            mR = r;
+        }
+
+        boolean get(Time iterator, int day)
+        {
+            int realYear = iterator.year;
+            int realMonth = iterator.month;
+
+            Time t = null;
+
+            if (SPEW) {
+                Log.i(TAG, "get called with iterator=" + iterator
+                        + " " + iterator.month
+                        + "/" + iterator.monthDay
+                        + "/" + iterator.year + " day=" + day);
+            }
+            if (day < 1 || day > 28) {
+                // if might be past the end of the month, we need to normalize it
+                t = mTime;
+                t.set(day, realMonth, realYear);
+                t.normalize(true /* ignore isDst */);
+                realYear = t.year;
+                realMonth = t.month;
+                day = t.monthDay;
+                if (SPEW) {
+                    Log.i(TAG, "normalized t=" + t + " " + t.month
+                            + "/" + t.monthDay
+                            + "/" + t.year);
+                }
+            }
+
+            /*
+            if (true || SPEW) {
+                Log.i(TAG, "set t=" + t + " " + realMonth + "/" + day + "/" + realYear);
+            }
+            */
+            if (realYear != mYear || realMonth != mMonth) {
+                if (t == null) {
+                    t = mTime;
+                    t.set(day, realMonth, realYear);
+                    t.normalize(true /* ignore isDst */);
+                    if (SPEW) {
+                        Log.i(TAG, "set t=" + t + " " + t.month
+                                + "/" + t.monthDay
+                                + "/" + t.year
+                                + " realMonth=" + realMonth + " mMonth=" + mMonth);
+                    }
+                }
+                mYear = realYear;
+                mMonth = realMonth;
+                mDays = generateDaysList(t, mR);
+                if (SPEW) {
+                    Log.i(TAG, "generated days list");
+                }
+            }
+            return (mDays & (1<<day)) != 0;
+        }
+
+        /**
+         * Fill in a bit set containing the days of the month on which this
+         * will occur.
+         *
+         * Only call this if the r.freq > DAILY.  Otherwise, we should be
+         * processing the BYDAY, BYMONTHDAY, etc. as filters instead.
+         *
+         * monthOffset may be -1, 0 or 1
+         */
+        private static int generateDaysList(Time generated, EventRecurrence r)
+        {
+            int days = 0;
+
+            int i, count, v;
+            int[] byday, bydayNum, bymonthday;
+            int j, lastDayThisMonth;
+            int first; // Time.SUNDAY, etc
+            int k;
+
+            lastDayThisMonth = generated.getActualMaximum(Time.MONTH_DAY);
+
+            // BYDAY
+            count = r.bydayCount;
+            if (count > 0) {
+                // calculate the day of week for the first of this month (first)
+                j = generated.monthDay;
+                while (j >= 8) {
+                    j -= 7;
+                }
+                first = generated.weekDay;
+                if (first >= j) {
+                    first = first - j + 1;
+                } else {
+                    first = first - j + 8;
+                }
+
+                // What to do if the event is weekly:
+                // This isn't ideal, but we'll generate a month's worth of events
+                // and the code that calls this will only use the ones that matter
+                // for the current week.
+                byday = r.byday;
+                bydayNum = r.bydayNum;
+                for (i=0; i<count; i++) {
+                    v = bydayNum[i];
+                    j = EventRecurrence.day2TimeDay(byday[i]) - first + 1;
+                    if (j <= 0) {
+                        j += 7;
+                    }
+                    if (v == 0) {
+                        // v is 0, each day in the month/week
+                        for (; j<=lastDayThisMonth; j+=7) {
+                            if (SPEW) Log.i(TAG, "setting " + j + " for rule "
+                                    + v + "/" + EventRecurrence.day2TimeDay(byday[i]));
+                            days |= 1 << j;
+                        }
+                    }
+                    else if (v > 0) {
+                        // v is positive, count from the beginning of the month
+                        // -1 b/c the first one should add 0
+                        j += 7*(v-1);
+                        if (j <= lastDayThisMonth) {
+                            if (SPEW) Log.i(TAG, "setting " + j + " for rule "
+                                    + v + "/" + EventRecurrence.day2TimeDay(byday[i]));
+                            // if it's impossible, we drop it
+                            days |= 1 << j;
+                        }
+                    }
+                    else {
+                        // v is negative, count from the end of the month
+                        // find the last one
+                        for (; j<=lastDayThisMonth; j+=7) {
+                        }
+                        // v is negative
+                        // should do +1 b/c the last one should add 0, but we also
+                        // skipped the j -= 7 b/c the loop to find the last one
+                        // overshot by one week
+                        j += 7*v;
+                        if (j >= 1) {
+                            if (SPEW) Log.i(TAG, "setting " + j + " for rule "
+                                    + v + "/" + EventRecurrence.day2TimeDay(byday[i]));
+                            days |= 1 << j;
+                        }
+                    }
+                }
+            }
+
+            // BYMONTHDAY
+            // Q: What happens if we have BYMONTHDAY and BYDAY?
+            // A: I didn't see it in the spec, so in lieu of that, we'll
+            // intersect the two.  That seems reasonable to me.
+            if (r.freq > EventRecurrence.WEEKLY) {
+                count = r.bymonthdayCount;
+                if (count != 0) {
+                    bymonthday = r.bymonthday;
+                    if (r.bydayCount == 0) {
+                        for (i=0; i<count; i++) {
+                            v = bymonthday[i];
+                            if (v >= 0) {
+                                days |= 1 << v;
+                            } else {
+                                j = lastDayThisMonth + v + 1; // v is negative
+                                if (j >= 1 && j <= lastDayThisMonth) {
+                                    days |= 1 << j;
+                                }
+                            }
+                        }
+                    } else {
+                        // This is O(lastDayThisMonth*count), which is really
+                        // O(count) with a decent sized constant.
+                        for (j=1; j<=lastDayThisMonth; j++) {
+                            next_day : { 
+                                if ((days&(1<<j)) != 0) {
+                                    for (i=0; i<count; i++) {
+                                        if (bymonthday[i] == j) {
+                                            break next_day;
+                                        }
+                                    }
+                                    days &= ~(1<<j);
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+            return days;
+        }
+
+        private EventRecurrence mR;
+        private int mDays;
+        private Time mTime;
+        private int mYear;
+        private int mMonth;
+    }
+
+
+    // TODO: document, clean up these return codes.  currently, the return value
+    // is the last occurrence of the recurrence within the expansion window.
+    // 0 is returned if there are no instances within the expansion window.
+    public long expand(Time dtstart,
+                       RecurrenceSet recur,
+                       long rangeStartMillis,
+                       long rangeEndMillis,
+                       TreeSet<Long> dtSet) throws DateException {
+        if (dtSet != null) {
+            dtSet.clear();
+        } else {
+            // create the set locally, for book-keeping.
+            dtSet = new TreeSet<Long>();
+        }
+
+        if (recur.rrules != null) {
+            for (EventRecurrence rrule : recur.rrules) {
+                expand(dtstart, rrule, rangeStartMillis,
+                       rangeEndMillis, true /* add */, dtSet);
+            }
+        }
+        if (recur.rdates != null) {
+            for (long dt : recur.rdates) {
+                dtSet.add(dt);
+            }
+        }
+        if (recur.exrules != null) {
+            for (EventRecurrence exrule : recur.exrules) {
+                expand(dtstart, exrule, rangeStartMillis,
+                       rangeEndMillis, false /* remove */, dtSet);
+            }
+        }
+        if (recur.exdates != null) {
+            for (long dt : recur.exdates) {
+                dtSet.remove(dt);
+            }
+        }
+        if (dtSet.isEmpty()) {
+            // this can happen if the recurrence does not occur within the
+            // expansion window.
+            return 0;
+        }
+        return dtSet.last();
+    }
+
+    /**
+     * Run the recurrence algorithm.  Processes events defined in the local
+     * timezone of the event.  Return a list of iCalendar DATETIME
+     * strings containing the start date/times of the occurrences; the output
+     * times are defined in the local timezone of the event.
+     *
+     * If you want all of the events, pass null for rangeEnd.  If you pass
+     * null for rangeEnd, and the event doesn't have a COUNT or UNTIL field,
+     * you'll get a DateException.
+     *
+     * @param dtstart the dtstart date as defined in RFC2445.  This
+     * {@link Time} should be in the timezone of the event.
+     * @param r the parsed recurrence, as defiend in RFC2445
+     * @param rangeStartMillis the first date-time you care about, inclusive
+     * @param rangeEndMillis the last date-time you care about, not inclusive (so
+     *                  if you care about everything up through and including
+     *                  Dec 22 1995, set last to Dec 23, 1995 00:00:00
+     * @param add Whether or not we should add to out, or remove from out.
+     * @param out the ArrayList you'd like to fill with the events
+     */
+    public void expand(Time dtstart,
+                       EventRecurrence r,
+                       long rangeStartMillis,
+                       long rangeEndMillis,
+                       boolean add,
+                       TreeSet<Long> out) throws DateException {
+        String timezone = dtstart.timezone;
+        long dtstartMillis = dtstart.toMillis(false /* use isDst */);
+        int count = 0;
+
+        // add the dtstart instance to the recurrence, if within range.
+        if (add) {
+            if (dtstartMillis >= rangeStartMillis) {
+                if ((rangeEndMillis != -1 && (dtstartMillis <= rangeEndMillis))
+                    || r.count > 0) {
+                        out.add(dtstartMillis);
+                        ++count;
+                }
+            }
+        }
+
+        // reset the Time objects to the new timezone.
+        // (we want to avoid creating new objects here)
+        // we do NOT clear the until -- untils *must* be specified in UTC.
+        mIterator.clear(timezone);
+        mGenerated.clear(timezone);
+        
+        Time iterator = mIterator;
+        Time until = mUntil;
+        StringBuilder sb = mStringBuilder;
+        Time generated = mGenerated;
+        DaySet days = mDays;
+
+        try {
+
+            days.setRecurrence(r);
+
+            if (rangeEndMillis == -1 && r.until == null && r.count == 0) {
+                throw new DateException(
+                        "No range end provided for a recurrence that has no UNTIL or COUNT.");
+            }
+
+            // the top-level frequency
+            int freqField;
+            int freqAmount = r.interval;
+            int freq = r.freq;
+            switch (freq)
+            {
+                case EventRecurrence.SECONDLY:
+                    freqField = Time.SECOND;
+                    break;
+                case EventRecurrence.MINUTELY:
+                    freqField = Time.MINUTE;
+                    break;
+                case EventRecurrence.HOURLY:
+                    freqField = Time.HOUR;
+                    break;
+                case EventRecurrence.DAILY:
+                    freqField = Time.MONTH_DAY;
+                    break;
+                case EventRecurrence.WEEKLY:
+                    freqField = Time.MONTH_DAY;
+                    freqAmount = 7 * r.interval;
+                    if (freqAmount <= 0) {
+                        freqAmount = 7;
+                    }
+                    break;
+                case EventRecurrence.MONTHLY:
+                    freqField = Time.MONTH;
+                    break;
+                case EventRecurrence.YEARLY:
+                    freqField = Time.YEAR;
+                    break;
+                default:
+                    throw new DateException("bad freq=" + freq);
+            }
+            if (freqAmount <= 0) {
+                freqAmount = 1;
+            }
+
+            int bymonthCount = r.bymonthCount;
+            boolean usebymonth = useBYX(freq, EventRecurrence.MONTHLY, bymonthCount);
+            boolean useDays = freq >= EventRecurrence.WEEKLY &&
+                                 (r.bydayCount > 0 || r.bymonthdayCount > 0);
+            int byhourCount = r.byhourCount;
+            boolean usebyhour = useBYX(freq, EventRecurrence.HOURLY, byhourCount);
+            int byminuteCount = r.byminuteCount;
+            boolean usebyminute = useBYX(freq, EventRecurrence.MINUTELY, byminuteCount);
+            int bysecondCount = r.bysecondCount;
+            boolean usebysecond = useBYX(freq, EventRecurrence.SECONDLY, bysecondCount);
+
+            // initialize the iterator
+            iterator.set(dtstart);
+            if (freqField == Time.MONTH) {
+                if (useDays) {
+                    // if it's monthly, and we're going to be generating
+                    // days, set the iterator day field to 1 because sometimes
+                    // we'll skip months if it's greater than 28.
+                    // XXX Do we generate days for MONTHLY w/ BYHOUR?  If so,
+                    // we need to do this then too.
+                    iterator.monthDay = 1;
+                }
+            }
+
+            if (r.until != null) {
+                until.parse(r.until);
+            }
+            long untilMillis = until.toMillis(false /* use isDst */);
+
+            sb.ensureCapacity(15);
+            sb.setLength(15); // TODO: pay attention to whether or not the event
+            // is an all-day one.
+
+            if (SPEW) {
+                Log.i(TAG, "expand called w/ rangeStart=" + rangeStartMillis
+                        + " rangeEnd=" + rangeEndMillis);
+            }
+
+            // go until the end of the range or we're done with this event
+            boolean eventEnded = false;
+            int N, i, v;
+            int a[];
+            events: {
+                while (true) {
+                    int monthIndex = 0;
+
+                    iterator.normalize(true /* ignore isDst */);
+
+                    int iteratorYear = iterator.year;
+                    int iteratorMonth = iterator.month + 1;
+                    int iteratorDay = iterator.monthDay;
+                    int iteratorHour = iterator.hour;
+                    int iteratorMinute = iterator.minute;
+                    int iteratorSecond = iterator.second;
+
+                    // year is never expanded -- there is no BYYEAR
+                    generated.set(iterator);
+
+                    if (SPEW) Log.i(TAG, "year=" + generated.year);
+
+                    do { // month
+                        int month = usebymonth
+                                        ? r.bymonth[monthIndex]
+                                        : iteratorMonth;
+                        month--;
+                        if (SPEW) Log.i(TAG, "  month=" + month);
+
+                        int dayIndex = 1;
+                        int lastDayToExamine = 0;
+
+                        // Use this to handle weeks that overlap the end of the month.
+                        // Keep the year and month that days is for, and generate it
+                        // when needed in the loop
+                        if (useDays) {
+                            // Determine where to start and end, don't worry if this happens
+                            // to be before dtstart or after the end, because that will be
+                            // filtered in the inner loop
+                            if (freq == EventRecurrence.WEEKLY) {
+                                int dow = iterator.weekDay;
+                                dayIndex = iterator.monthDay - dow;
+                                lastDayToExamine = dayIndex + 6;
+                            } else {
+                                lastDayToExamine = generated
+                                    .getActualMaximum(Time.MONTH_DAY);
+                            }
+                            if (SPEW) Log.i(TAG, "dayIndex=" + dayIndex
+                                    + " lastDayToExamine=" + lastDayToExamine
+                                    + " days=" + days);
+                        }
+
+                        do { // day
+                            int day;
+                            if (useDays) {
+                                if (!days.get(iterator, dayIndex)) {
+                                    dayIndex++;
+                                    continue;
+                                } else {
+                                    day = dayIndex;
+                                }
+                            } else {
+                                day = iteratorDay;
+                            }
+                            if (SPEW) Log.i(TAG, "    day=" + day);
+
+                            // hour
+                            int hourIndex = 0;
+                            do {
+                                int hour = usebyhour
+                                                ? r.byhour[hourIndex]
+                                                : iteratorHour;
+                                if (SPEW) Log.i(TAG, "      hour=" + hour + " usebyhour=" + usebyhour);
+
+                                // minute
+                                int minuteIndex = 0;
+                                do {
+                                    int minute = usebyminute
+                                                    ? r.byminute[minuteIndex]
+                                                    : iteratorMinute;
+                                    if (SPEW) Log.i(TAG, "        minute=" + minute);
+
+                                    // second
+                                    int secondIndex = 0;
+                                    do {
+                                        int second = usebysecond
+                                                        ? r.bysecond[secondIndex]
+                                                        : iteratorSecond;
+                                        if (SPEW) Log.i(TAG, "          second=" + second);
+
+                                        // we do this here each time, because if we distribute it, we find the
+                                        // month advancing extra times, as we set the month to the 32nd, 33rd, etc.
+                                        // days.
+                                        generated.set(second, minute, hour, day, month, iteratorYear);
+
+                                        long genMillis = generated.normalize(true /* ignore DST */);
+                                        // sometimes events get generated (BYDAY, BYHOUR, etc.) that
+                                        // are before dtstart.  Filter these.  I believe this is correct,
+                                        // but Google Calendar doesn't seem to always do this.
+                                        if (genMillis >= dtstartMillis) {
+                                            // filter and then add
+                                            int filtered = filter(r, generated);
+                                            if (0 == filtered) {
+
+                                                // increase the count as long
+                                                // as this isn't the same
+                                                // as the first instance
+                                                // specified by the DTSTART
+                                                // (for RRULEs -- additive).
+                                                if (!add) {
+                                                    ++count;
+                                                } else if (dtstartMillis !=
+                                                               genMillis) {
+                                                    ++count;
+                                                }
+                                                // one reason we can stop is that we're past the until date
+                                                if (r.until != null &&
+                                                    genMillis > untilMillis) {
+                                                    if (SPEW) {
+                                                        Log.i(TAG, "stopping b/c until="
+                                                            + untilMillis
+                                                            + " generated="
+                                                            + genMillis);
+                                                    }
+                                                    break events;
+                                                }
+                                                // or we're past rangeEnd
+                                                if (rangeEndMillis != -1 &&
+                                                    genMillis >= rangeEndMillis) {
+                                                    if (SPEW) {
+                                                        Log.i(TAG, "stopping b/c rangeEnd="
+                                                                + rangeEndMillis
+                                                                + " generated=" + generated);
+                                                    }
+                                                    break events;
+                                                }
+
+                                                if (out != null &&
+                                                    genMillis >= rangeStartMillis) {
+                                                    if (SPEW) {
+                                                        Log.i(TAG, "adding date=" + generated + " filtered=" + filtered);
+                                                    }
+                                                    if (add) {
+                                                        out.add(genMillis);
+                                                    } else {
+                                                        out.remove(genMillis);
+                                                    }
+                                                }
+                                                // another is that count is high enough
+                                                if (r.count > 0 && r.count == count) {
+                                                    //Log.i(TAG, "stopping b/c count=" + count);
+                                                    break events;
+                                                }
+                                            }
+                                        }
+                                        secondIndex++;
+                                    } while (usebysecond && secondIndex < bysecondCount);
+                                    minuteIndex++;
+                                } while (usebyminute && minuteIndex < byminuteCount);
+                                hourIndex++;
+                            } while (usebyhour && hourIndex < byhourCount);
+                            dayIndex++;
+                        } while (useDays && dayIndex <= lastDayToExamine);
+                        monthIndex++;
+                    } while (usebymonth && monthIndex < bymonthCount);
+
+                    // Add freqAmount to freqField until we get another date that we want.
+                    // We don't want to "generate" dates with the iterator.
+                    // XXX: We do this for days, because there is a varying number of days
+                    // per month
+                    int oldDay = iterator.monthDay;
+                    generated.set(iterator);  // just using generated as a temporary.
+                    int n = 1;
+                    while (true) {
+                        int value = freqAmount * n;
+                        switch (freqField) {
+                            case Time.SECOND:
+                                iterator.second += value;
+                                break;
+                            case Time.MINUTE:
+                                iterator.minute += value;
+                                break;
+                            case Time.HOUR:
+                                iterator.hour += value;
+                                break;
+                            case Time.MONTH_DAY:
+                                iterator.monthDay += value;
+                                break;
+                            case Time.MONTH:
+                                iterator.month += value;
+                                break;
+                            case Time.YEAR:
+                                iterator.year += value;
+                                break;
+                            case Time.WEEK_DAY:
+                                iterator.monthDay += value;
+                                break;
+                            case Time.YEAR_DAY:
+                                iterator.monthDay += value;
+                                break;
+                            default:
+                                throw new RuntimeException("bad field=" + freqField);
+                        }
+
+                        iterator.normalize(true /* ignore isDst */);
+                        if (freqField != Time.YEAR && freqField != Time.MONTH) {
+                            break;
+                        }
+                        if (iterator.monthDay == oldDay) {
+                            break;
+                        }
+                        n++;
+                        iterator.set(generated);
+                    }
+                }
+            }
+        }
+        catch (DateException e) {
+            Log.w(TAG, "DateException with r=" + r + " rangeStart=" + rangeStartMillis
+                    + " rangeEnd=" + rangeEndMillis);
+            throw e;
+        }
+        catch (RuntimeException t) {
+            Log.w(TAG, "RuntimeException with r=" + r + " rangeStart=" + rangeStartMillis
+                    + " rangeEnd=" + rangeEndMillis);
+            throw t;
+        }
+    }
+}
+
diff --git a/src/com/android/providers/calendar/VCal.java b/src/com/android/providers/calendar/VCal.java
new file mode 100644
index 0000000..e49c83a
--- /dev/null
+++ b/src/com/android/providers/calendar/VCal.java
@@ -0,0 +1,275 @@
+/* //device/apps/Calendar/MonthView.java

+**

+** Copyright 2006, The Android Open Source Project
+**

+** Licensed under the Apache License, Version 2.0 (the "License"); 

+** you may not use this file except in compliance with the License. 

+** You may obtain a copy of the License at 

+**

+**     http://www.apache.org/licenses/LICENSE-2.0 

+**

+** Unless required by applicable law or agreed to in writing, software 

+** distributed under the License is distributed on an "AS IS" BASIS, 

+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 

+** See the License for the specific language governing permissions and 

+** limitations under the License.

+*/

+

+package com.android.providers.calendar;

+

+import android.pim.DateException;

+import android.pim.DateUtils;

+import android.util.Log;

+

+import java.util.ArrayList;

+import java.util.Calendar;

+import java.util.GregorianCalendar;

+import java.util.TimeZone;

+import java.util.regex.Pattern;

+

+public class VCal

+{

+    public static final Pattern LINE = Pattern.compile(

+            "([^:;]+)([^:]*):(.*)");

+

+    public ArrayList<Property> properties = new ArrayList<Property>();

+    public String dtstart;

+    public String tzid;

+    public String duration;

+    public String rrule;

+    public boolean allDay;

+

+    public void dump()

+    {

+        System.out.println("-----------------------");

+        dump(properties, "");

+        System.out.println("dtstart='" + this.dtstart + "'");

+        System.out.println("tzid='" + this.tzid + "'");

+        System.out.println("duration='" + this.duration + "'");

+        System.out.println("rrule='" + this.rrule + "'");

+        System.out.println("-----------------------");

+    }

+

+    public static void dump(ArrayList<Property> props, String prefix)

+    {

+        int count = props.size();

+        for (int i=0; i<count; i++) {

+            Property prop = props.get(i);

+            System.out.println(prefix + prop.name);

+            if (prop instanceof Begin) {

+                Begin b = (Begin)prop;

+                dump(b.properties, prefix + "  ");

+            }

+        }

+   }

+

+    public static class Parameter

+    {

+        public String name;

+        public String value;

+    }

+

+    public static class Property

+    {

+        public String name;

+        public Parameter[] parameters;

+        public String value;

+        public String[] values;

+    }

+

+    public static class Begin extends Property

+    {

+        public Begin parent;

+        public ArrayList<Property> properties = new ArrayList<Property>();

+    }

+

+

+    public static Property make(String name)

+    {

+        Property p;

+        if (name.equals("BEGIN")) {

+            p = new Begin();

+        }

+        else {

+            p = new Property();

+        }

+        p.name = name;

+        return p;

+    }

+

+    public static VCal parse(String str)

+    {

+        VCal vc = new VCal();

+

+        int i, j, start;

+        int N, M;

+

+        // first we deal with line folding, by replacing all "\r\n " strings

+        // with nothing

+        str = str.replaceAll("\r\n ", "");

+

+        // it's supposed to be \r\n, but not everyone does that

+        str = str.replaceAll("\r\n", "\n");

+        str = str.replaceAll("\r", "\n");

+

+        ArrayList<Parameter> params = new ArrayList<Parameter>();

+        ArrayList<Property> props = vc.properties;

+

+        // then we split into lines

+        String[] lines = str.split("\n");

+

+        Begin begin = null;

+        //System.out.println("lines.length=" + lines);

+        N = lines.length;

+        for (j=0; j<N; j++) {

+            //System.out.println("===[" + lines[j] + "]===");

+            String line = lines[j];

+            int len = line.length();

+            if (len > 0) {

+                i = 0;

+                char c;

+                do {

+                    c = line.charAt(i);

+                    i++;

+                } while (c != ';' && c != ':');

+

+                String n = line.substring(0, i-1);

+                Property prop = make(n);

+                props.add(prop);

+                if (n.equals("BEGIN")) {

+                    Begin b = (Begin)prop;

+                    b.parent = begin;

+                    begin = b;

+                    props = begin.properties;

+                }

+                else if (n.equals("END")) {

+                    begin = begin.parent;

+                    if (begin != null) {

+                        props = begin.properties;

+                    } else {

+                        props = vc.properties;

+                    }

+                }

+

+                //System.out.println("name=[" + prop.name + "]");

+                params.clear();

+                while (c == ';') {

+                    Parameter param = new Parameter();

+                    start = i;

+                    i++;

+                    // param name

+                    do {

+                        c = line.charAt(i);

+                        i++;

+                    } while (c != '=');

+                    param.name = line.substring(start, i-1);

+                    //System.out.println(" param.name=[" + param.name + "]");

+                    start = i;

+                    if (line.charAt(start) == '"') {

+                        i++;

+                        start++;

+                        do {

+                            c = line.charAt(i);

+                            i++;

+                        } while (c != '"');

+                        param.value = line.substring(start, i-1);

+                        c = line.charAt(i);

+                        i++;

+                        //System.out.println(" param.valueA=[" + param.value

+                        //                              + "]");

+                    } else {

+                        do {

+                            c = line.charAt(i);

+                            i++;

+                        } while (c != ';' && c != ':');

+                        param.value = line.substring(start, i-1);

+                        //System.out.println(" param.valueB=["

+                        //                              + param.value + "]");

+                    }

+                    params.add(param);

+                }

+                Object[] array = params.toArray();

+                prop.parameters = new Parameter[array.length];

+                System.arraycopy(array, 0, prop.parameters, 0, array.length);

+                if (c != ':') {

+                    throw new RuntimeException("error finding ':' c=" + c);

+                }

+                prop.value = line.substring(i);

+                prop.values = line.split(",");

+            }

+        }

+

+        N = vc.properties.size();

+        Calendar calStart = null;

+        for (i=0; i<N; i++) {

+            Property prop = vc.properties.get(i);

+            String n = prop.name;

+            if (n.equals("DTSTART")) {

+                try {

+                    calStart = parseDateTime(prop, vc);

+                    vc.dtstart = prop.value;

+                } catch (DateException de) {

+                    Log.w("CalendarProvider", "Unable to parse DTSTART=" + n, de);

+                    return null;

+                }

+            } else if (n.equals("DTEND")) {

+                // TODO: store the dtend, compute when expanding instances?

+                // will we ever need to deal with seeing the DTEND before the

+                // DTSTART?

+                try {

+                    if (calStart == null) {

+                        vc.duration = "+P0S";

+                    } else {

+                        Calendar calEnd =

+                                parseDateTime(prop, vc);

+                        long durationMillis =

+                                calEnd.getTimeInMillis() -

+                                        calStart.getTimeInMillis();

+                        long durationSeconds = (durationMillis / 1000);

+                        vc.duration = "+P" + durationSeconds + "S";

+                    }

+                } catch (DateException de) {

+                    Log.w("CalendarProvider", "Unable to parse DTEND=" + n, de);

+                    return null;

+                }

+            } else if (n.equals("DURATION")) {

+                vc.duration = prop.value;

+            } else if (n.equals("RRULE")) {

+                vc.rrule = prop.value;

+            }

+        }

+        return vc;

+    }

+

+    private static Calendar parseDateTime(Property prop, VCal vc) throws DateException {

+        int M;

+        int j;

+        String dt = prop.value;

+        M = prop.parameters.length;

+        for (j=0; j<M; j++) {

+            Parameter param = prop.parameters[j];

+            if (param.name.equals("TZID")) {

+                vc.tzid = param.value;

+            }

+        }

+

+        TimeZone tz = TimeZone.getTimeZone(vc.tzid);

+        if (tz == null) {

+            tz = TimeZone.getTimeZone("UTC");

+        }

+        GregorianCalendar somewhere = new GregorianCalendar(tz);

+        DateUtils.parseDateTime(dt, somewhere);

+        if (dt.length() == 8) {

+            // this seems to work.

+            vc.allDay = true;

+        }

+        return somewhere;

+        /*GregorianCalendar zulu = new GregorianCalendar(

+                                       TimeZone.getTimeZone("GMT"));

+        zulu.setTimeInMillis(somewhere.getTimeInMillis());

+        return zulu;*/

+        // System.out.println("DTSTART=" + dtstart

+        //         + " somewhere=" + somewhere

+        //         + " vc.dtstart=" + vc.dtstart);

+    }

+}