Merge "Change mdpi-icon for Email Application"
diff --git a/Android.mk b/Android.mk
index 1fd0799..f1e3f85 100644
--- a/Android.mk
+++ b/Android.mk
@@ -18,9 +18,11 @@
 LOCAL_MODULE_TAGS := optional
 
 LOCAL_SRC_FILES := $(call all-java-files-under, src)
+# EXCHANGE-REMOVE-SECTION-START
 LOCAL_SRC_FILES += \
-    src/com/android/exchange/IEmailService.aidl \
-    src/com/android/exchange/IEmailServiceCallback.aidl
+    src/com/android/email/service/IEmailService.aidl \
+    src/com/android/email/service/IEmailServiceCallback.aidl
+# EXCHANGE-REMOVE-SECTION-END
 
 
 LOCAL_PACKAGE_NAME := Email
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 9b376af..76ff915 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -31,6 +31,8 @@
 
     <!--  For EAS purposes; could be removed when EAS has a permanent home -->
     <uses-permission android:name="android.permission.WRITE_CONTACTS"/>
+    <uses-permission android:name="android.permission.WRITE_CALENDAR"/>
+    <uses-permission android:name="android.permission.READ_CALENDAR"/>
 
     <!-- Only required if a store implements push mail and needs to keep network open -->
     <uses-permission android:name="android.permission.WAKE_LOCK"/>
@@ -83,11 +85,13 @@
             android:label="@string/account_setup_outgoing_title"
             >
         </activity>
+        <!--EXCHANGE-REMOVE-SECTION-START-->
         <activity
             android:name=".activity.setup.AccountSetupExchange"
             android:label="@string/account_setup_exchange_title"
             >
         </activity>
+        <!--EXCHANGE-REMOVE-SECTION-END-->
         <activity
             android:name=".activity.setup.AccountSetupOptions"
             android:label="@string/account_setup_options_title"
@@ -171,6 +175,7 @@
                 <category android:name="android.intent.category.DEFAULT" />
             </intent-filter>
         </activity>
+        <!--EXCHANGE-REMOVE-SECTION-START-->
        <receiver android:name="com.android.exchange.EmailSyncAlarmReceiver"/>
        <receiver android:name="com.android.exchange.MailboxAlarmReceiver"/>
        <receiver android:name="com.android.exchange.BootReceiver" android:enabled="true">
@@ -178,6 +183,7 @@
                 <action android:name="android.intent.action.BOOT_COMPLETED" />
             </intent-filter>
        </receiver>
+        <!--EXCHANGE-REMOVE-SECTION-END-->
 
        <receiver android:name=".service.BootReceiver" android:enabled="true">
             <intent-filter>
@@ -191,12 +197,36 @@
             </intent-filter>
         </receiver>
 
+        <!-- Support for DeviceAdmin / DevicePolicyManager.  See SecurityPolicy class for impl. -->
+        <receiver
+            android:name=".SecurityPolicy$PolicyAdmin"
+            android:label="@string/device_admin_label"
+            android:description="@string/device_admin_description"
+            android:permission="android.permission.BIND_DEVICE_ADMIN" >
+            <meta-data
+                android:name="android.app.device_admin"
+                android:resource="@xml/device_admin" />
+            <intent-filter>
+                <action android:name="android.app.action.DEVICE_ADMIN_ENABLED" />
+            </intent-filter>
+        </receiver>
+        
+        <receiver
+            android:name=".OneTimeInitializer"
+            android:enabled="true"
+            >
+            <intent-filter>
+                <action android:name="android.intent.action.BOOT_COMPLETED" />
+            </intent-filter>
+        </receiver>
+
         <service
             android:name=".service.MailService"
             android:enabled="false"
             >
         </service>
         
+        <!--EXCHANGE-REMOVE-SECTION-START-->
         <!--Required stanza to register the ContactsSyncAdapterService with SyncManager -->
         <service 
             android:name="com.android.exchange.ContactsSyncAdapterService" 
@@ -208,6 +238,17 @@
                        android:resource="@xml/syncadapter_contacts" />
         </service>
 
+        <!--Required stanza to register the CalendarSyncAdapterService with SyncManager -->
+        <service
+            android:name="com.android.exchange.CalendarSyncAdapterService"
+            android:exported="true">
+            <intent-filter>
+                <action android:name="android.content.SyncAdapter" />
+            </intent-filter>
+            <meta-data android:name="android.content.SyncAdapter"
+                       android:resource="@xml/syncadapter_calendar" />
+        </service>
+
         <!-- Add android:process=":remote" below to enable SyncManager as a separate process -->
         <service
             android:name="com.android.exchange.SyncManager"
@@ -216,13 +257,37 @@
         </service>
 
         <!--Required stanza to register the EasAuthenticatorService with AccountManager -->
-        <service android:name=".service.EasAuthenticatorService" android:exported="true">
+        <service
+            android:name=".service.EasAuthenticatorService"
+            android:exported="true"
+            android:enabled="true"
+            >
             <intent-filter>
                 <action android:name="android.accounts.AccountAuthenticator" />
             </intent-filter>
-            <meta-data android:name="android.accounts.AccountAuthenticator"
-                       android:resource="@xml/authenticator" />
+            <meta-data
+                android:name="android.accounts.AccountAuthenticator"
+                android:resource="@xml/authenticator"
+                />
         </service>
+        <!--
+            EasAuthenticatorService with the altenative label.  Disabled by default,
+            and OneTimeInitializer enables it if the vendor policy tells so.
+        -->
+        <service
+            android:name=".service.EasAuthenticatorServiceAlternate"
+            android:exported="true"
+            android:enabled="false"
+            >
+            <intent-filter>
+                <action android:name="android.accounts.AccountAuthenticator" />
+            </intent-filter>
+            <meta-data
+                android:name="android.accounts.AccountAuthenticator"
+                android:resource="@xml/authenticator_alternate"
+                />
+        </service>
+        <!--EXCHANGE-REMOVE-SECTION-END-->
 
         <provider
             android:name=".provider.AttachmentProvider"
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 4e2df73..154deee 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -295,12 +295,21 @@
 
     <!-- Title of screen when setting up new email account -->
     <string name="account_setup_basics_title">Set up email</string>
+    <!-- Title of the screen when adding exchange account -->
+    <string name="account_setup_basics_exchange_title">
+        Add an Exchange account</string>
+    <!-- Title of the screen when adding exchange account -->
+    <string name="account_setup_basics_exchange_title_alternate">
+        Add an Exchange ActiveSync account</string>
     <!-- On "Set up email" screen, enthusiastic welcome message. -->
     <string name="accounts_welcome">You can configure Email for most accounts in just a few steps.
         </string>
     <!-- On "Set up email" screen, enthusiastic welcome message (in EAS mode). -->
     <string name="accounts_welcome_exchange">You can configure an Exchange account in just a few
         steps.</string>
+    <!-- On "Set up email" screen, enthusiastic welcome message (in EAS mode). -->
+    <string name="accounts_welcome_exchange_alternate">
+        You can configure an Exchange ActiveSync account in just a few steps.</string>
     <!-- On "Set up email" screen, brief instructions -->
     <!--  DEPRECATED - REMOVE -->
     <string name="account_setup_basics_instructions"></string>
@@ -356,6 +365,10 @@
     <!-- Do Not Translate. "Add new email account" screen, button name in response to what
          type of account this is -->
     <string name="account_setup_account_type_exchange_action">Exchange</string>
+    <!-- Do Not Translate. "Add new email account" screen, button name in
+         response to what type of account this is -->
+    <string name="account_setup_account_type_exchange_action_alternate">
+        Microsoft Exchange ActiveSync</string>
 
     <!-- "Incoming server settings" screen, label for text field -->
     <string name="account_setup_incoming_title">Incoming server settings</string>
@@ -447,7 +460,11 @@
     <!-- In Account setup options & Account Settings screens, check box for new-mail notification -->
     <string name="account_setup_options_notify_label">Notify me when email arrives.</string>
     <!-- In Account setup options screen, optional check box to also sync contacts -->
-    <string name="account_setup_options_sync_contacts_label">Sync contacts from this account.</string>
+    <string name="account_setup_options_sync_contacts_label">Sync contacts from this account.
+    </string>
+    <!-- In Account setup options screen, optional check box to also sync contacts -->
+    <string name="account_setup_options_sync_calendar_label">Sync calendar from this account.
+    </string>
     <!-- Dialog title when "setup" could not finish -->
     <string name="account_setup_failed_dlg_title">Setup could not finish</string>
     <!-- In Account setup options screen, label for email check frequency selector -->
@@ -518,6 +535,10 @@
     <string name="account_settings_description_label">Account name</string>
     <!-- On Settings screen, setting option name -->
     <string name="account_settings_name_label">Your name</string>
+    <!-- On Settings screen, setting option name -->
+    <string name="account_settings_signature_label">Signature</string>
+    <!-- On Settings screen, setting option name -->
+    <string name="account_settings_signature_hint">Append text to messages you send</string>
     <!-- On Settings screen, section heading -->
     <string name="account_settings_notifications">Notification settings</string>
 
@@ -557,7 +578,16 @@
 
     <!-- Name of Microsoft Exchange account type; used by AccountManager -->
     <string name="exchange_name">Corporate</string>
+    <!-- Name of Microsoft Exchange account type; used by AccountManager -->
+    <string name="exchange_name_alternate">Microsoft Exchange ActiveSync</string>
 
 	<!-- Message that appears if the AccountManager cannot create the system Account -->
 	<string name="system_account_create_failed">The AccountManager could not create the Account; please try again.</string>
+
+    <!-- Strings that support the DeviceAdmin / DevicePolicyManager API -->
+    <!-- Name of the DeviceAdmin (seen in settings - anywhere else?) -->
+    <string name="device_admin_label">Email Device Administrator</string>
+    <!-- Long-form description of the DeviceAdmin (seen in settings - anywhere else?) -->
+    <string name="device_admin_description">Email Device Administrator - Long Description</string>
+    
 </resources>
diff --git a/res/xml/syncadapter_calendar.xml b/res/xml/syncadapter_calendar.xml
new file mode 100644
index 0000000..a6c0fc6
--- /dev/null
+++ b/res/xml/syncadapter_calendar.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/**
+ * Copyright (c) 2010, 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.
+ */
+-->
+
+<!-- The attributes in this XML file provide configuration information -->
+<!-- for the SyncAdapter. -->
+
+<sync-adapter xmlns:android="http://schemas.android.com/apk/res/android"
+    android:contentAuthority="com.android.calendar"
+    android:accountType="com.android.exchange"
+/>
diff --git a/src/com/android/exchange/AbstractSyncService.java b/src/com/android/exchange/AbstractSyncService.java
index 425424d..08583ec 100644
--- a/src/com/android/exchange/AbstractSyncService.java
+++ b/src/com/android/exchange/AbstractSyncService.java
@@ -76,8 +76,8 @@
     protected Object mSynchronizer = new Object();
 
     protected volatile long mRequestTime = 0;
-    protected ArrayList<PartRequest> mPartRequests = new ArrayList<PartRequest>();
-    protected PartRequest mPendingPartRequest = null;
+    protected ArrayList<Request> mRequests = new ArrayList<Request>();
+    protected PartRequest mPendingRequest = null;
 
     /**
      * Sent by SyncManager to request that the service stop itself cleanly
@@ -282,54 +282,23 @@
     }
 
     /**
-     * PartRequest handling (common functionality)
-     * Can be overridden if desired, but IMAP/EAS both use the next three methods as-is
+     * Request handling (common functionality)
+     * Can be overridden if desired
      */
 
-    public void addPartRequest(PartRequest req) {
-        synchronized (mPartRequests) {
-            mPartRequests.add(req);
+    public void addRequest(Request req) {
+        synchronized (mRequests) {
+            mRequests.add(req);
             mRequestTime = System.currentTimeMillis();
         }
     }
 
-    public void removePartRequest(PartRequest req) {
-        synchronized (mPartRequests) {
-            mPartRequests.remove(req);
+    public void removeRequest(Request req) {
+        synchronized (mRequests) {
+            mRequests.remove(req);
         }
     }
 
-    public PartRequest hasPartRequest(long emailId, String part) {
-        synchronized (mPartRequests) {
-            for (PartRequest pr : mPartRequests) {
-                if (pr.emailId == emailId && pr.loc.equals(part))
-                    return pr;
-            }
-        }
-        return null;
-    }
-
-    // cancelPartRequest is sent in response to user input to stop an attachment load
-    // that is in progress. This will almost certainly require code overriding the base
-    // functionality, as sockets may need to be closed, etc. and this functionality will be
-    // service dependent. This returns the canceled PartRequest or null
-    public PartRequest cancelPartRequest(long emailId, String part) {
-        synchronized (mPartRequests) {
-            PartRequest p = null;
-            for (PartRequest pr : mPartRequests) {
-                if (pr.emailId == emailId && pr.loc.equals(part)) {
-                    p = pr;
-                    break;
-                }
-            }
-            if (p != null) {
-                mPartRequests.remove(p);
-                return p;
-            }
-        }
-        return null;
-    }
-
     /**
      * Convenience method wrapping calls to retrieve columns from a single row, via EmailProvider.
      * The arguments are exactly the same as to contentResolver.query().  Results are returned in
diff --git a/src/com/android/exchange/BootReceiver.java b/src/com/android/exchange/BootReceiver.java
index 5312bfb..1ebfa7b 100644
--- a/src/com/android/exchange/BootReceiver.java
+++ b/src/com/android/exchange/BootReceiver.java
@@ -16,6 +16,8 @@
 
 package com.android.exchange;
 
+import com.android.email.ExchangeUtils;
+
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
@@ -25,6 +27,6 @@
     @Override
     public void onReceive(Context context, Intent intent) {
         Log.d("Exchange", "BootReceiver onReceive");
-        context.startService(new Intent(context, SyncManager.class));
+        ExchangeUtils.startExchangeService(context);
     }
 }
diff --git a/src/com/android/exchange/CalendarSyncAdapterService.java b/src/com/android/exchange/CalendarSyncAdapterService.java
new file mode 100644
index 0000000..d2f4ecf
--- /dev/null
+++ b/src/com/android/exchange/CalendarSyncAdapterService.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2010 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.exchange;
+
+import com.android.email.provider.EmailContent;
+import com.android.email.provider.EmailContent.AccountColumns;
+import com.android.email.provider.EmailContent.Mailbox;
+import com.android.email.provider.EmailContent.MailboxColumns;
+
+import android.accounts.Account;
+import android.accounts.OperationCanceledException;
+import android.app.Service;
+import android.content.AbstractThreadedSyncAdapter;
+import android.content.ContentProviderClient;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SyncResult;
+import android.database.Cursor;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.provider.Calendar.Events;
+import android.util.Log;
+
+public class CalendarSyncAdapterService extends Service {
+    private static final String TAG = "EAS CalendarSyncAdapterService";
+    private static SyncAdapterImpl sSyncAdapter = null;
+    private static final Object sSyncAdapterLock = new Object();
+
+    private static final String ACCOUNT_AND_TYPE_CALENDAR =
+        MailboxColumns.ACCOUNT_KEY + "=? AND " + MailboxColumns.TYPE + '=' + Mailbox.TYPE_CALENDAR;
+
+    public CalendarSyncAdapterService() {
+        super();
+    }
+
+    private static class SyncAdapterImpl extends AbstractThreadedSyncAdapter {
+        private Context mContext;
+
+        public SyncAdapterImpl(Context context) {
+            super(context, true /* autoInitialize */);
+            mContext = context;
+        }
+
+        @Override
+        public void onPerformSync(Account account, Bundle extras,
+                String authority, ContentProviderClient provider, SyncResult syncResult) {
+            try {
+                CalendarSyncAdapterService.performSync(mContext, account, extras,
+                        authority, provider, syncResult);
+            } catch (OperationCanceledException e) {
+            }
+        }
+    }
+
+    @Override
+    public void onCreate() {
+        super.onCreate();
+        synchronized (sSyncAdapterLock) {
+            if (sSyncAdapter == null) {
+                sSyncAdapter = new SyncAdapterImpl(getApplicationContext());
+            }
+        }
+    }
+
+    @Override
+    public IBinder onBind(Intent intent) {
+        return sSyncAdapter.getSyncAdapterBinder();
+    }
+
+    /**
+     * Partial integration with system SyncManager; we tell our EAS SyncManager to start a calendar
+     * sync when we get the signal from the system SyncManager.
+     * The missing piece at this point is integration with the push/ping mechanism in EAS; this will
+     * be put in place at a later time.
+     */
+    private static void performSync(Context context, Account account, Bundle extras,
+            String authority, ContentProviderClient provider, SyncResult syncResult)
+            throws OperationCanceledException {
+        ContentResolver cr = context.getContentResolver();
+        boolean logging = Eas.USER_LOG;
+        if (logging) {
+            Log.d(TAG, "performSync");
+        }
+        if (extras.getBoolean(ContentResolver.SYNC_EXTRAS_UPLOAD)) {
+            Cursor c = cr.query(Events.CONTENT_URI,
+                    new String[] {Events._ID}, Events._SYNC_DIRTY + "=1", null, null);
+            try {
+                if (!c.moveToFirst()) {
+                    if (logging) {
+                        Log.d(TAG, "Upload sync; no changes");
+                    }
+                    return;
+                }
+            } finally {
+                c.close();
+            }
+        }
+
+        // Find the (EmailProvider) account associated with this email address
+        Cursor accountCursor =
+            cr.query(EmailContent.Account.CONTENT_URI,
+                    EmailContent.ID_PROJECTION, AccountColumns.EMAIL_ADDRESS + "=?",
+                    new String[] {account.name}, null);
+        try {
+            if (accountCursor.moveToFirst()) {
+                long accountId = accountCursor.getLong(0);
+                // Now, find the calendar mailbox associated with the account
+                Cursor mailboxCursor = cr.query(Mailbox.CONTENT_URI, EmailContent.ID_PROJECTION,
+                        ACCOUNT_AND_TYPE_CALENDAR, new String[] {Long.toString(accountId)}, null);
+                try {
+                     if (mailboxCursor.moveToFirst()) {
+                        if (logging) {
+                            Log.d(TAG, "Calendar sync requested for " + account.name);
+                        }
+                        // Ask for a sync from our sync manager
+                        SyncManager.serviceRequest(mailboxCursor.getLong(0),
+                                SyncManager.SYNC_UPSYNC);
+                    }
+                } finally {
+                    mailboxCursor.close();
+                }
+            }
+        } finally {
+            accountCursor.close();
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/exchange/ContactsSyncAdapterService.java b/src/com/android/exchange/ContactsSyncAdapterService.java
index 69fe792..b39d789 100644
--- a/src/com/android/exchange/ContactsSyncAdapterService.java
+++ b/src/com/android/exchange/ContactsSyncAdapterService.java
@@ -16,6 +16,7 @@
 
 package com.android.exchange;
 
+import com.android.email.Email;
 import com.android.email.provider.EmailContent;
 import com.android.email.provider.EmailContent.AccountColumns;
 import com.android.email.provider.EmailContent.Mailbox;
@@ -98,7 +99,7 @@
         if (extras.getBoolean(ContentResolver.SYNC_EXTRAS_UPLOAD)) {
             Uri uri = RawContacts.CONTENT_URI.buildUpon()
                 .appendQueryParameter(RawContacts.ACCOUNT_NAME, account.name)
-                .appendQueryParameter(RawContacts.ACCOUNT_TYPE, Eas.ACCOUNT_MANAGER_TYPE)
+                .appendQueryParameter(RawContacts.ACCOUNT_TYPE, Email.EXCHANGE_ACCOUNT_MANAGER_TYPE)
                 .build();
             Cursor c = cr.query(uri,
                     new String[] {RawContacts._ID}, RawContacts.DIRTY + "=1", null, null);
diff --git a/src/com/android/exchange/Eas.java b/src/com/android/exchange/Eas.java
index c005544..86db9ab 100644
--- a/src/com/android/exchange/Eas.java
+++ b/src/com/android/exchange/Eas.java
@@ -38,7 +38,6 @@
     public static final int DEBUG_FILE_BIT = 4;
 
     public static final String VERSION = "0.3";
-    public static final String ACCOUNT_MANAGER_TYPE = "com.android.exchange";
     public static final String ACCOUNT_MAILBOX = "__eas";
 
     // From EAS spec
diff --git a/src/com/android/exchange/EasAuthenticationException.java b/src/com/android/exchange/EasAuthenticationException.java
new file mode 100644
index 0000000..f5b14b9
--- /dev/null
+++ b/src/com/android/exchange/EasAuthenticationException.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2010 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.exchange;
+
+import java.io.IOException;
+
+/**
+ * Use this to be able to distinguish login (authentication) failures from other I/O
+ * exceptions during a sync, as they are handled very differently.
+ */
+public class EasAuthenticationException extends IOException {
+    private static final long serialVersionUID = 1L;
+
+    EasAuthenticationException() {
+        super();
+    }
+}
diff --git a/src/com/android/exchange/EasOutboxService.java b/src/com/android/exchange/EasOutboxService.java
index 38c9603..d827d51 100644
--- a/src/com/android/exchange/EasOutboxService.java
+++ b/src/com/android/exchange/EasOutboxService.java
@@ -26,6 +26,7 @@
 import com.android.email.provider.EmailContent.Message;
 import com.android.email.provider.EmailContent.MessageColumns;
 import com.android.email.provider.EmailContent.SyncColumns;
+import com.android.email.service.EmailServiceStatus;
 
 import org.apache.http.HttpResponse;
 import org.apache.http.HttpStatus;
diff --git a/src/com/android/exchange/EasSyncService.java b/src/com/android/exchange/EasSyncService.java
index e8a35eb..544a82f 100644
--- a/src/com/android/exchange/EasSyncService.java
+++ b/src/com/android/exchange/EasSyncService.java
@@ -29,11 +29,14 @@
 import com.android.email.provider.EmailContent.MailboxColumns;
 import com.android.email.provider.EmailContent.Message;
 import com.android.email.service.EmailServiceProxy;
+import com.android.email.service.EmailServiceStatus;
 import com.android.exchange.adapter.AbstractSyncAdapter;
 import com.android.exchange.adapter.AccountSyncAdapter;
+import com.android.exchange.adapter.CalendarSyncAdapter;
 import com.android.exchange.adapter.ContactsSyncAdapter;
 import com.android.exchange.adapter.EmailSyncAdapter;
 import com.android.exchange.adapter.FolderSyncParser;
+import com.android.exchange.adapter.MeetingResponseParser;
 import com.android.exchange.adapter.PingParser;
 import com.android.exchange.adapter.Serializer;
 import com.android.exchange.adapter.Tags;
@@ -628,7 +631,7 @@
      * @throws IOException
      */
     protected void getAttachment(PartRequest req) throws IOException {
-        Attachment att = req.att;
+        Attachment att = req.mAttachment;
         Message msg = Message.restoreMessageWithId(mContext, att.mMessageKey);
         doProgressCallback(msg.mId, att.mId, 0);
 
@@ -640,9 +643,9 @@
             HttpEntity e = res.getEntity();
             int len = (int)e.getContentLength();
             InputStream is = res.getEntity().getContent();
-            File f = (req.destination != null)
-                    ? new File(req.destination)
-                    : createUniqueFileInternal(req.destination, att.mFileName);
+            File f = (req.mDestination != null)
+                    ? new File(req.mDestination)
+                    : createUniqueFileInternal(req.mDestination, att.mFileName);
             if (f != null) {
                 // Ensure that the target directory exists
                 File destDir = f.getParentFile();
@@ -654,7 +657,7 @@
                 // len < 0 means "chunked" transfer-encoding
                 if (len != 0) {
                     try {
-                        mPendingPartRequest = req;
+                        mPendingRequest = req;
                         byte[] bytes = new byte[CHUNK_SIZE];
                         int length = len;
                         // Loop terminates 1) when EOF is reached or 2) if an IOException occurs
@@ -689,7 +692,7 @@
                             }
                        }
                     } finally {
-                        mPendingPartRequest = null;
+                        mPendingRequest = null;
                     }
                 }
                 os.flush();
@@ -697,8 +700,8 @@
 
                 // EmailProvider will throw an exception if we try to update an unsaved attachment
                 if (att.isSaved()) {
-                    String contentUriString = (req.contentUriString != null)
-                            ? req.contentUriString
+                    String contentUriString = (req.mContentUriString != null)
+                            ? req.mContentUriString
                             : "file://" + f.getAbsolutePath();
                     ContentValues cv = new ContentValues();
                     cv.put(AttachmentColumns.CONTENT_URI, contentUriString);
@@ -711,6 +714,37 @@
         }
     }
 
+    /**
+     * Responds to a meeting request.  The MeetingResponseRequest is basically our
+     * wrapper for the meetingResponse service call
+     * @param req the request (message id and response code)
+     * @throws IOException
+     */
+    protected void sendMeetingResponse(MeetingResponseRequest req) throws IOException {
+        Message msg = Message.restoreMessageWithId(mContext, req.mMessageId);
+        Serializer s = new Serializer();
+        s.start(Tags.MREQ_MEETING_RESPONSE).start(Tags.MREQ_REQUEST);
+        s.data(Tags.MREQ_USER_RESPONSE, Integer.toString(req.mResponse));
+        s.data(Tags.MREQ_COLLECTION_ID, Long.toString(msg.mMailboxKey));
+        s.data(Tags.MREQ_REQ_ID, msg.mServerId);
+        s.end().end().done();
+        HttpResponse res = sendHttpClientPost("MeetingResponse", s.toByteArray());
+        int status = res.getStatusLine().getStatusCode();
+        if (status == HttpStatus.SC_OK) {
+            HttpEntity e = res.getEntity();
+            int len = (int)e.getContentLength();
+            InputStream is = res.getEntity().getContent();
+            if (len != 0) {
+                new MeetingResponseParser(is, this).parse();
+            }
+        } else if (isAuthError(status)) {
+            throw new EasAuthenticationException();
+        } else {
+            userLog("Meeting response request failed, code: " + status);
+            throw new IOException();
+        }
+    }
+
     @SuppressWarnings("deprecation")
     private String makeUriString(String cmd, String extra) throws IOException {
          // Cache the authentication string and the command string
@@ -1274,7 +1308,7 @@
         return pp.getSyncStatus();
     }
 
-    private String getFilterType() {
+    private String getEmailFilter() {
         String filter = Eas.FILTER_1_WEEK;
         switch (mAccount.mSyncLookback) {
             case com.android.email.Account.SYNC_WINDOW_1_DAY: {
@@ -1323,18 +1357,29 @@
                 return;
             }
 
+            // Now, handle various requests
             while (true) {
-                PartRequest req = null;
-                synchronized (mPartRequests) {
-                    if (mPartRequests.isEmpty()) {
+                Request req = null;
+                synchronized (mRequests) {
+                    if (mRequests.isEmpty()) {
                         break;
                     } else {
-                        req = mPartRequests.get(0);
+                        req = mRequests.get(0);
                     }
                 }
-                getAttachment(req);
-                synchronized(mPartRequests) {
-                    mPartRequests.remove(req);
+
+                // Our two request types are PartRequest (loading attachment) and
+                // MeetingResponseRequest (respond to a meeting request)
+                if (req instanceof PartRequest) {
+                    getAttachment((PartRequest)req);
+                } else if (req instanceof MeetingResponseRequest) {
+                    sendMeetingResponse((MeetingResponseRequest)req);
+                }
+
+                // If there's an exception handling the request, we'll throw it
+                // Otherwise, we remove the request
+                synchronized(mRequests) {
+                    mRequests.remove(req);
                 }
             }
 
@@ -1360,8 +1405,11 @@
             // Handle options
             s.start(Tags.SYNC_OPTIONS);
             // Set the lookback appropriately (EAS calls this a "filter") for all but Contacts
-            if (!className.equals("Contacts")) {
-                s.data(Tags.SYNC_FILTER_TYPE, getFilterType());
+            if (className.equals("Email")) {
+                s.data(Tags.SYNC_FILTER_TYPE, getEmailFilter());
+            } else if (className.equals("Calendar")) {
+                // TODO Force one month for calendar until we can set this!
+                s.data(Tags.SYNC_FILTER_TYPE, Eas.FILTER_1_MONTH);
             }
             // Set the truncation amount for all classes
             if (mProtocolVersionDouble >= 12.0) {
@@ -1452,6 +1500,8 @@
                 AbstractSyncAdapter target;
                 if (mMailbox.mType == Mailbox.TYPE_CONTACTS) {
                     target = new ContactsSyncAdapter(mMailbox, this);
+                } else if (mMailbox.mType == Mailbox.TYPE_CALENDAR) {
+                    target = new CalendarSyncAdapter(mMailbox, this);
                 } else {
                     target = new EmailSyncAdapter(mMailbox, this);
                 }
@@ -1465,6 +1515,9 @@
                     sync(target);
                 } while (mRequestTime != 0);
             }
+        } catch (EasAuthenticationException e) {
+            userLog("Caught authentication error");
+            mExitStatus = EXIT_LOGIN_FAILURE;
         } catch (IOException e) {
             String message = e.getMessage();
             userLog("Caught IOException: ", (message == null) ? "No message" : message);
diff --git a/src/com/android/exchange/EmailServiceStatus.java b/src/com/android/exchange/EmailServiceStatus.java
deleted file mode 100644
index 697548c..0000000
--- a/src/com/android/exchange/EmailServiceStatus.java
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- *  Copyright (C) 2008-2009 Marc Blank
- * Licensed to 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.exchange;
-
-/**
- * Definitions of service status codes returned to IEmailServiceCallback's status method
- */
-public interface EmailServiceStatus {
-    public static final int SUCCESS = 0;
-    public static final int IN_PROGRESS = 1;
-
-    public static final int MESSAGE_NOT_FOUND = 0x10;
-    public static final int ATTACHMENT_NOT_FOUND = 0x11;
-    public static final int FOLDER_NOT_DELETED = 0x12;
-    public static final int FOLDER_NOT_RENAMED = 0x13;
-    public static final int FOLDER_NOT_CREATED = 0x14;
-    public static final int REMOTE_EXCEPTION = 0x15;
-    public static final int LOGIN_FAILED = 0x16;
-
-    // Maybe we should automatically retry these?
-    public static final int CONNECTION_ERROR = 0x20;
-}
diff --git a/src/com/android/exchange/IEmailService.aidl b/src/com/android/exchange/IEmailService.aidl
deleted file mode 100644
index 81181eb..0000000
--- a/src/com/android/exchange/IEmailService.aidl
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * Copyright (C) 2008-2009 Marc Blank
- * Licensed to 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.exchange;
-
-import com.android.exchange.IEmailServiceCallback;
-import com.android.email.provider.EmailContent;
-import android.os.Bundle;
-
-interface IEmailService {
-    int validate(in String protocol, in String host, in String userName, in String password,
-        int port, boolean ssl, boolean trustCertificates) ;
-
-    void startSync(long mailboxId);
-    void stopSync(long mailboxId);
-
-    void loadMore(long messageId);
-    void loadAttachment(long attachmentId, String destinationFile, String contentUriString);
-
-    void updateFolderList(long accountId);
-
-    boolean createFolder(long accountId, String name);
-    boolean deleteFolder(long accountId, String name);
-    boolean renameFolder(long accountId, String oldName, String newName);
-
-    void setCallback(IEmailServiceCallback cb);
-
-    void setLogging(int on);
-
-    void hostChanged(long accountId);
-
-    Bundle autoDiscover(String userName, String password);
-}
\ No newline at end of file
diff --git a/src/com/android/exchange/IEmailServiceCallback.aidl b/src/com/android/exchange/IEmailServiceCallback.aidl
deleted file mode 100644
index a789c4a..0000000
--- a/src/com/android/exchange/IEmailServiceCallback.aidl
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- * Copyright (C) 2008-2009 Marc Blank
- * Licensed to 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.exchange;
-
-oneway interface IEmailServiceCallback {
-    /*
-     * Ordinary results:
-     *   statuscode = 1, progress = 0:      "starting"
-     *   statuscode = 0, progress = n/a:    "finished"
-     *
-     * If there is an error, it must be reported as follows:
-     *   statuscode = err, progress = n/a:  "stopping due to error"
-     *
-     * *Optionally* a callback can also include intermediate values from 1..99 e.g.
-     *   statuscode = 1, progress = 0:      "starting"
-     *   statuscode = 1, progress = 30:     "working"
-     *   statuscode = 1, progress = 60:     "working"
-     *   statuscode = 0, progress = n/a:    "finished"
-     */
-
-    /**
-     * Callback to indicate that an account is being synced (updating folder list)
-     * accountId = the account being synced
-     * statusCode = 0 for OK, 1 for progress, other codes for error
-     * progress = 0 for "start", 1..100 for optional progress reports
-     */
-    void syncMailboxListStatus(long accountId, int statusCode, int progress);
-
-    /**
-     * Callback to indicate that a mailbox is being synced
-     * mailboxId = the mailbox being synced
-     * statusCode = 0 for OK, 1 for progress, other codes for error
-     * progress = 0 for "start", 1..100 for optional progress reports
-     */
-    void syncMailboxStatus(long mailboxId, int statusCode, int progress);
-
-    /**
-     * Callback to indicate that a particular attachment is being synced
-     * messageId = the message that owns the attachment
-     * attachmentId = the attachment being synced
-     * statusCode = 0 for OK, 1 for progress, other codes for error
-     * progress = 0 for "start", 1..100 for optional progress reports
-     */
-    void loadAttachmentStatus(long messageId, long attachmentId, int statusCode, int progress);
-
-    /**
-     * Callback to indicate that a particular message is being sent
-     * messageId = the message being sent
-     * statusCode = 0 for OK, 1 for progress, other codes for error
-     * progress = 0 for "start", 1..100 for optional progress reports
-     */
-    void sendMessageStatus(long messageId, String subject, int statusCode, int progress);
-}
diff --git a/src/com/android/exchange/MeetingResponseRequest.java b/src/com/android/exchange/MeetingResponseRequest.java
new file mode 100644
index 0000000..1d69a6c
--- /dev/null
+++ b/src/com/android/exchange/MeetingResponseRequest.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2010 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.exchange;
+
+/**
+ * MeetingResponseRequest is the EAS wrapper for responding to meeting requests.
+ */
+public class MeetingResponseRequest extends Request {
+    public int mResponse;
+
+    MeetingResponseRequest(long messageId, int response) {
+        mMessageId = messageId;
+        mResponse = response;
+    }
+}
diff --git a/src/com/android/exchange/PartRequest.java b/src/com/android/exchange/PartRequest.java
index 72b79f4..b6b281d 100644
--- a/src/com/android/exchange/PartRequest.java
+++ b/src/com/android/exchange/PartRequest.java
@@ -24,24 +24,21 @@
  * the attachment to be loaded, it also contains the callback to be used for status/progress
  * updates to the UI.
  */
-public class PartRequest {
-    public long timeStamp;
-    public long emailId;
-    public Attachment att;
-    public String destination;
-    public String contentUriString;
-    public String loc;
+public class PartRequest extends Request {
+    public Attachment mAttachment;
+    public String mDestination;
+    public String mContentUriString;
+    public String mLocation;
 
     public PartRequest(Attachment _att) {
-        timeStamp = System.currentTimeMillis();
-        emailId = _att.mMessageKey;
-        att = _att;
-        loc = att.mLocation;
+        mMessageId = _att.mMessageKey;
+        mAttachment = _att;
+        mLocation = mAttachment.mLocation;
     }
 
     public PartRequest(Attachment _att, String _destination, String _contentUriString) {
         this(_att);
-        destination = _destination;
-        contentUriString = _contentUriString;
+        mDestination = _destination;
+        mContentUriString = _contentUriString;
     }
 }
diff --git a/src/com/android/exchange/Request.java b/src/com/android/exchange/Request.java
new file mode 100644
index 0000000..185dd7f
--- /dev/null
+++ b/src/com/android/exchange/Request.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2010 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.exchange;
+
+/**
+ * Requests for mailbox actions are handled by subclasses of this abstract class.
+ * Two subclasses are now defined: PartRequest (attachment load) and MeetingResponseRequest
+ * (respond to a meeting invitation)
+ */
+public abstract class Request {
+    public long mTimeStamp = System.currentTimeMillis();
+    public long mMessageId;
+}
diff --git a/src/com/android/exchange/SyncManager.java b/src/com/android/exchange/SyncManager.java
index 2648b8c..58f976d 100644
--- a/src/com/android/exchange/SyncManager.java
+++ b/src/com/android/exchange/SyncManager.java
@@ -18,6 +18,7 @@
 package com.android.exchange;
 
 import com.android.email.AccountBackupRestore;
+import com.android.email.Email;
 import com.android.email.mail.MessagingException;
 import com.android.email.mail.store.TrustManagerFactory;
 import com.android.email.provider.EmailContent;
@@ -29,6 +30,8 @@
 import com.android.email.provider.EmailContent.MailboxColumns;
 import com.android.email.provider.EmailContent.Message;
 import com.android.email.provider.EmailContent.SyncColumns;
+import com.android.email.service.IEmailService;
+import com.android.email.service.IEmailServiceCallback;
 import com.android.exchange.utility.FileLogger;
 
 import org.apache.http.conn.ClientConnectionManager;
@@ -312,7 +315,7 @@
         public void loadAttachment(long attachmentId, String destinationFile,
                 String contentUriString) throws RemoteException {
             Attachment att = Attachment.restoreAttachmentWithId(SyncManager.this, attachmentId);
-            partRequest(new PartRequest(att, destinationFile, contentUriString));
+            sendMessageRequest(new PartRequest(att, destinationFile, contentUriString));
         }
 
         public void updateFolderList(long accountId) throws RemoteException {
@@ -348,8 +351,11 @@
             Eas.setUserDebug(on);
         }
 
+        public void sendMeetingResponse(long messageId, int response) throws RemoteException {
+            sendMessageRequest(new MeetingResponseRequest(messageId, response));
+        }
+
         public void loadMore(long messageId) throws RemoteException {
-            // TODO Auto-generated method stub
         }
 
         // The following three methods are not implemented in this version
@@ -507,7 +513,7 @@
                         // Delete this from AccountManager...
                         android.accounts.Account acct =
                             new android.accounts.Account(account.mEmailAddress,
-                                    Eas.ACCOUNT_MANAGER_TYPE);
+                                    Email.EXCHANGE_ACCOUNT_MANAGER_TYPE);
                         AccountManager.get(SyncManager.this).removeAccount(acct, null, null);
                         mSyncableEasMailboxSelector = null;
                         mEasAccountSelector = null;
@@ -702,7 +708,8 @@
     public class EasAccountsUpdatedListener implements OnAccountsUpdateListener {
        public void onAccountsUpdated(android.accounts.Account[] accounts) {
            reconcileAccountsWithAccountManager(INSTANCE, getAccountList(),
-                   AccountManager.get(INSTANCE).getAccountsByType(Eas.ACCOUNT_MANAGER_TYPE));
+                   AccountManager.get(INSTANCE).getAccountsByType(
+                           Email.EXCHANGE_ACCOUNT_MANAGER_TYPE));
        }
     }
 
@@ -1195,7 +1202,7 @@
                 // Create an AccountManager style Account
                 android.accounts.Account acct =
                     new android.accounts.Account(easAccount.mEmailAddress,
-                            Eas.ACCOUNT_MANAGER_TYPE);
+                            Email.EXCHANGE_ACCOUNT_MANAGER_TYPE);
                 // Get the Contacts mailbox; this happens rarely so it's ok to get it all
                 Mailbox contacts = Mailbox.restoreMailboxWithId(this, contactsId);
                 int syncInterval = contacts.mSyncInterval;
@@ -1338,7 +1345,7 @@
         }
     }
 
-    private void startService(Mailbox m, int reason, PartRequest req) {
+    private void startService(Mailbox m, int reason, Request req) {
         // Don't sync if there's no connectivity
         if (sConnectivityHold) return;
         synchronized (sSyncToken) {
@@ -1351,7 +1358,7 @@
                     if (!((EasSyncService)service).mIsValid) return;
                     service.mSyncReason = reason;
                     if (req != null) {
-                        service.addPartRequest(req);
+                        service.addRequest(req);
                     }
                     startService(service, m);
                 }
@@ -1607,7 +1614,7 @@
                         if (account != null) {
                             android.accounts.Account a =
                                 new android.accounts.Account(account.mEmailAddress,
-                                        Eas.ACCOUNT_MANAGER_TYPE);
+                                        Email.EXCHANGE_ACCOUNT_MANAGER_TYPE);
                             if (!ContentResolver.getSyncAutomatically(a,
                                     ContactsContract.AUTHORITY)) {
                                 continue;
@@ -1726,9 +1733,9 @@
         }
     }
 
-    static public void partRequest(PartRequest req) {
+    static public void sendMessageRequest(Request req) {
         if (INSTANCE == null) return;
-        Message msg = Message.restoreMessageWithId(INSTANCE, req.emailId);
+        Message msg = Message.restoreMessageWithId(INSTANCE, req.mMessageId);
         if (msg == null) {
             return;
         }
@@ -1739,33 +1746,7 @@
             service = startManualSync(mailboxId, SYNC_SERVICE_PART_REQUEST, req);
             kick("part request");
         } else {
-            service.addPartRequest(req);
-        }
-    }
-
-    static public PartRequest hasPartRequest(long emailId, String part) {
-        if (INSTANCE == null) return null;
-        Message msg = Message.restoreMessageWithId(INSTANCE, emailId);
-        if (msg == null) {
-            return null;
-        }
-        long mailboxId = msg.mMailboxKey;
-        AbstractSyncService service = INSTANCE.mServiceMap.get(mailboxId);
-        if (service != null) {
-            return service.hasPartRequest(emailId, part);
-        }
-        return null;
-    }
-
-    static public void cancelPartRequest(long emailId, String part) {
-        Message msg = Message.restoreMessageWithId(INSTANCE, emailId);
-        if (msg == null) {
-            return;
-        }
-        long mailboxId = msg.mMailboxKey;
-        AbstractSyncService service = INSTANCE.mServiceMap.get(mailboxId);
-        if (service != null) {
-            service.cancelPartRequest(emailId, part);
+            service.addRequest(req);
         }
     }
 
@@ -1793,7 +1774,7 @@
         return PING_STATUS_OK;
     }
 
-    static public AbstractSyncService startManualSync(long mailboxId, int reason, PartRequest req) {
+    static public AbstractSyncService startManualSync(long mailboxId, int reason, Request req) {
         if (INSTANCE == null || INSTANCE.mServiceMap == null) return null;
         synchronized (sSyncToken) {
             if (INSTANCE.mServiceMap.get(mailboxId) == null) {
@@ -1866,13 +1847,14 @@
             int exitStatus = svc.mExitStatus;
             switch (exitStatus) {
                 case AbstractSyncService.EXIT_DONE:
-                    if (!svc.mPartRequests.isEmpty()) {
+                    if (!svc.mRequests.isEmpty()) {
                         // TODO Handle this case
                     }
                     errorMap.remove(mailboxId);
                     break;
                 case AbstractSyncService.EXIT_IO_ERROR:
                     Mailbox m = Mailbox.restoreMailboxWithId(INSTANCE, mailboxId);
+                    if (m == null) return;
                     if (syncError != null) {
                         syncError.escalate();
                         INSTANCE.log(m.mDisplayName + " held for " + syncError.holdDelay + "ms");
diff --git a/src/com/android/exchange/adapter/CalendarSyncAdapter.java b/src/com/android/exchange/adapter/CalendarSyncAdapter.java
index 739c9e2..5a202bb 100644
--- a/src/com/android/exchange/adapter/CalendarSyncAdapter.java
+++ b/src/com/android/exchange/adapter/CalendarSyncAdapter.java
@@ -17,11 +17,44 @@
 
 package com.android.exchange.adapter;
 
+import com.android.email.Email;
 import com.android.email.provider.EmailContent.Mailbox;
+import com.android.exchange.Eas;
 import com.android.exchange.EasSyncService;
+import com.android.exchange.utility.CalendarUtilities;
+import com.android.exchange.utility.Duration;
+
+import android.content.ContentProviderClient;
+import android.content.ContentProviderOperation;
+import android.content.ContentProviderResult;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Entity;
+import android.content.EntityIterator;
+import android.content.OperationApplicationException;
+import android.content.Entity.NamedContentValues;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.RemoteException;
+import android.pim.DateException;
+import android.provider.Calendar;
+import android.provider.SyncStateContract;
+import android.provider.Calendar.Attendees;
+import android.provider.Calendar.Calendars;
+import android.provider.Calendar.Events;
+import android.provider.Calendar.EventsEntity;
+import android.provider.Calendar.ExtendedProperties;
+import android.provider.Calendar.Reminders;
+import android.provider.Calendar.SyncState;
+import android.provider.ContactsContract.RawContacts;
+import android.util.Log;
 
 import java.io.IOException;
 import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.StringTokenizer;
+import java.util.TimeZone;
 
 /**
  * Sync adapter class for EAS calendars
@@ -29,8 +62,56 @@
  */
 public class CalendarSyncAdapter extends AbstractSyncAdapter {
 
+    private static final String TAG = "EasCalendarSyncAdapter";
+    // Since exceptions will have the same _SYNC_ID as the original event we have to check that
+    // there's no original event when finding an item by _SYNC_ID
+    private static final String SERVER_ID = Events._SYNC_ID + "=? AND " +
+        Events.ORIGINAL_EVENT + " ISNULL";
+    private static final String DIRTY_TOP_LEVEL =
+        Events._SYNC_DIRTY + "=1 AND " + Events.ORIGINAL_EVENT + " ISNULL";
+    private static final String DIRTY_EXCEPTION =
+        Events._SYNC_DIRTY + "=1 AND " + Events.ORIGINAL_EVENT + " NOTNULL";
+    private static final String DIRTY_IN_CALENDAR =
+        Events._SYNC_DIRTY + "=1 AND " + Events.CALENDAR_ID + "=?";
+    private static final String CLIENT_ID_SELECTION = Events._SYNC_LOCAL_ID + "=?";
+    private static final String ATTENDEES_EXCEPT_ORGANIZER = Attendees.EVENT_ID + "=? AND " +
+        Attendees.ATTENDEE_RELATIONSHIP + "!=" + Attendees.RELATIONSHIP_ORGANIZER;
+    private static final String[] ID_PROJECTION = new String[] {Events._ID};
+    private static final String[] ORIGINAL_EVENT_PROJECTION = new String[] {Events.ORIGINAL_EVENT};
+
+    private static final String CALENDAR_SELECTION =
+        Calendars._SYNC_ACCOUNT + "=? AND " + Calendars._SYNC_ACCOUNT_TYPE + "=?";
+    private static final int CALENDAR_SELECTION_ID = 0;
+
+    private static final String CATEGORY_TOKENIZER_DELIMITER = "\\";
+
+    private static final ContentProviderOperation PLACEHOLDER_OPERATION =
+        ContentProviderOperation.newInsert(Uri.EMPTY).build();
+
+    private static final Uri sEventsUri = asSyncAdapter(Events.CONTENT_URI);
+    private static final Uri sAttendeesUri = asSyncAdapter(Attendees.CONTENT_URI);
+    private static final Uri sExtendedPropertiesUri = asSyncAdapter(ExtendedProperties.CONTENT_URI);
+    private static final Uri sRemindersUri = asSyncAdapter(Reminders.CONTENT_URI);
+
+    private android.accounts.Account mAccountManagerAccount;
+    private long mCalendarId = -1;
+
+    private ArrayList<Long> mDeletedIdList = new ArrayList<Long>();
+    private ArrayList<Long> mUpdatedIdList = new ArrayList<Long>();
+
     public CalendarSyncAdapter(Mailbox mailbox, EasSyncService service) {
         super(mailbox, service);
+
+        Cursor c = mService.mContentResolver.query(Calendars.CONTENT_URI,
+                new String[] {Calendars._ID}, CALENDAR_SELECTION,
+                new String[] {mAccount.mEmailAddress, Email.EXCHANGE_ACCOUNT_MANAGER_TYPE}, null);
+        try {
+            if (c.moveToFirst()) {
+                mCalendarId = c.getLong(CALENDAR_SELECTION_ID);
+            }
+        } finally {
+            c.close();
+        }
     }
 
     @Override
@@ -39,19 +120,1133 @@
     }
 
     @Override
-    public boolean sendLocalChanges(Serializer s) throws IOException {
-        // TODO Auto-generated method stub
-        return false;
-    }
-
-    @Override
     public void cleanup() {
-        // TODO Auto-generated method stub
     }
 
     @Override
     public boolean parse(InputStream is) throws IOException {
-        // TODO Auto-generated method stub
+        EasCalendarSyncParser p = new EasCalendarSyncParser(is, this);
+        return p.parse();
+    }
+
+    static Uri asSyncAdapter(Uri uri) {
+        return uri.buildUpon().appendQueryParameter(Calendar.CALLER_IS_SYNCADAPTER, "true").build();
+    }
+
+    /**
+     * Generate the uri for the data row associated with this NamedContentValues object
+     * @param ncv the NamedContentValues object
+     * @return a uri that can be used to refer to this row
+     */
+    public Uri dataUriFromNamedContentValues(NamedContentValues ncv) {
+        long id = ncv.values.getAsLong(RawContacts._ID);
+        Uri dataUri = ContentUris.withAppendedId(ncv.uri, id);
+        return dataUri;
+    }
+
+    /**
+     * We get our SyncKey from CalendarProvider.  If there's not one, we set it to "0" (the reset
+     * state) and save that away.
+     */
+    @Override
+    public String getSyncKey() throws IOException {
+        ContentProviderClient client =
+            mService.mContentResolver.acquireContentProviderClient(Calendar.CONTENT_URI);
+        try {
+            byte[] data = SyncStateContract.Helpers.get(client,
+                    asSyncAdapter(Calendar.SyncState.CONTENT_URI), getAccountManagerAccount());
+            if (data == null || data.length == 0) {
+                // Initialize the SyncKey
+                setSyncKey("0", false);
+                return "0";
+            } else {
+                return new String(data);
+            }
+        } catch (RemoteException e) {
+            throw new IOException("Can't get SyncKey from ContactsProvider");
+        }
+    }
+
+    /**
+     * We only need to set this when we're forced to make the SyncKey "0" (a reset).  In all other
+     * cases, the SyncKey is set within Calendar
+     */
+    @Override
+    public void setSyncKey(String syncKey, boolean inCommands) throws IOException {
+        if ("0".equals(syncKey) || !inCommands) {
+            ContentProviderClient client =
+                mService.mContentResolver
+                    .acquireContentProviderClient(Calendar.CONTENT_URI);
+            try {
+                SyncStateContract.Helpers.set(client, asSyncAdapter(Calendar.SyncState.CONTENT_URI),
+                        getAccountManagerAccount(), syncKey.getBytes());
+                userLog("SyncKey set to ", syncKey, " in CalendarProvider");
+           } catch (RemoteException e) {
+                throw new IOException("Can't set SyncKey in CalendarProvider");
+            }
+        }
+        mMailbox.mSyncKey = syncKey;
+    }
+
+    public android.accounts.Account getAccountManagerAccount() {
+        if (mAccountManagerAccount == null) {
+            mAccountManagerAccount =
+                new android.accounts.Account(mAccount.mEmailAddress,
+                        Email.EXCHANGE_ACCOUNT_MANAGER_TYPE);
+        }
+        return mAccountManagerAccount;
+    }
+
+    class EasCalendarSyncParser extends AbstractSyncParser {
+
+        String[] mBindArgument = new String[1];
+        String mMailboxIdAsString;
+        Uri mAccountUri;
+        CalendarOperations mOps = new CalendarOperations();
+
+        public EasCalendarSyncParser(InputStream in, CalendarSyncAdapter adapter)
+                throws IOException {
+            super(in, adapter);
+            setLoggingTag("CalendarParser");
+            mAccountUri = Events.CONTENT_URI;
+        }
+
+        @Override
+        public void wipe() {
+            // Delete the calendar associated with this account
+            // TODO Make sure the Events, etc. are also deleted
+            mContentResolver.delete(Calendars.CONTENT_URI, CALENDAR_SELECTION,
+                    new String[] {mAccount.mEmailAddress, Email.EXCHANGE_ACCOUNT_MANAGER_TYPE});
+        }
+
+        public void addEvent(CalendarOperations ops, String serverId, boolean update)
+                throws IOException {
+            ContentValues cv = new ContentValues();
+            cv.put(Events.CALENDAR_ID, mCalendarId);
+            cv.put(Events._SYNC_ACCOUNT, mAccount.mEmailAddress);
+            cv.put(Events._SYNC_ACCOUNT_TYPE, Email.EXCHANGE_ACCOUNT_MANAGER_TYPE);
+            cv.put(Events._SYNC_ID, serverId);
+
+            int allDayEvent = 0;
+            String organizerName = null;
+            String organizerEmail = null;
+            int eventOffset = -1;
+
+            boolean firstTag = true;
+            long eventId = -1;
+            long startTime = -1;
+            long endTime = -1;
+            while (nextTag(Tags.SYNC_APPLICATION_DATA) != END) {
+                if (update && firstTag) {
+                    // Find the event that's being updated
+                    Cursor c = getServerIdCursor(serverId);
+                    long id = -1;
+                    try {
+                        if (c.moveToFirst()) {
+                            id = c.getLong(0);
+                        }
+                    } finally {
+                        c.close();
+                    }
+                    if (id > 0) {
+                        if (tag == Tags.CALENDAR_ATTENDEES) {
+                            // This is an attendees-only update; just delete/re-add attendees
+                            mBindArgument[0] = Long.toString(id);
+                            ops.add(ContentProviderOperation.newDelete(Attendees.CONTENT_URI)
+                                    .withSelection(ATTENDEES_EXCEPT_ORGANIZER, mBindArgument)
+                                    .build());
+                            eventId = id;
+                        } else {
+                            // Otherwise, delete the original event and recreate it
+                            userLog("Changing (delete/add) event ", serverId);
+                            ops.delete(id);
+                            // Add a placeholder event so that associated tables can reference
+                            // this as a back reference.  We add the event at the end of the method
+                            eventOffset = ops.newEvent(PLACEHOLDER_OPERATION);
+                        }
+                    }
+                } else if (firstTag) {
+                    // Add a placeholder event so that associated tables can reference
+                    // this as a back reference.  We add the event at the end of the method
+                   eventOffset = ops.newEvent(PLACEHOLDER_OPERATION);
+                }
+                firstTag = false;
+                switch (tag) {
+                    case Tags.CALENDAR_ALL_DAY_EVENT:
+                        allDayEvent = getValueInt();
+                        cv.put(Events.ALL_DAY, allDayEvent);
+                        break;
+                    case Tags.CALENDAR_ATTENDEES:
+                        // If eventId >= 0, this is an update; otherwise, a new Event
+                        attendeesParser(ops, organizerName, organizerEmail, eventId);
+                        break;
+                    case Tags.BASE_BODY:
+                        cv.put(Events.DESCRIPTION, bodyParser());
+                        break;
+                    case Tags.CALENDAR_BODY:
+                        cv.put(Events.DESCRIPTION, getValue());
+                        break;
+                    case Tags.CALENDAR_TIME_ZONE:
+                        TimeZone tz = CalendarUtilities.tziStringToTimeZone(getValue());
+                        if (tz != null) {
+                            cv.put(Events.EVENT_TIMEZONE, tz.getID());
+                        } else {
+                            cv.put(Events.EVENT_TIMEZONE, TimeZone.getDefault().getID());
+                        }
+                        break;
+                    case Tags.CALENDAR_START_TIME:
+                        startTime = CalendarUtilities.parseDateTimeToMillis(getValue());
+                        cv.put(Events.DTSTART, startTime);
+                        cv.put(Events.ORIGINAL_INSTANCE_TIME, startTime);
+                        break;
+                    case Tags.CALENDAR_END_TIME:
+                        endTime = CalendarUtilities.parseDateTimeToMillis(getValue());
+                        break;
+                    case Tags.CALENDAR_EXCEPTIONS:
+                        exceptionsParser(ops, cv);
+                        break;
+                    case Tags.CALENDAR_LOCATION:
+                        cv.put(Events.EVENT_LOCATION, getValue());
+                        break;
+                    case Tags.CALENDAR_RECURRENCE:
+                        String rrule = recurrenceParser(ops);
+                        if (rrule != null) {
+                            cv.put(Events.RRULE, rrule);
+                        }
+                        break;
+                    case Tags.CALENDAR_ORGANIZER_EMAIL:
+                        organizerEmail = getValue();
+                        cv.put(Events.ORGANIZER, organizerEmail);
+                        break;
+                    case Tags.CALENDAR_SUBJECT:
+                        cv.put(Events.TITLE, getValue());
+                        break;
+                    case Tags.CALENDAR_SENSITIVITY:
+                        cv.put(Events.VISIBILITY, encodeVisibility(getValueInt()));
+                        break;
+                    case Tags.CALENDAR_ORGANIZER_NAME:
+                        organizerName = getValue();
+                        break;
+                    case Tags.CALENDAR_REMINDER_MINS_BEFORE:
+                        ops.newReminder(getValueInt());
+                        cv.put(Events.HAS_ALARM, 1);
+                        break;
+                    // The following are fields we should save (for changes), though they don't
+                    // relate to data used by CalendarProvider at this point
+                    case Tags.CALENDAR_UID:
+                        ops.newExtendedProperty("uid", getValue());
+                        break;
+                    case Tags.CALENDAR_DTSTAMP:
+                        ops.newExtendedProperty("dtstamp", getValue());
+                        break;
+                    case Tags.CALENDAR_MEETING_STATUS:
+                        ops.newExtendedProperty("meeting_status", getValue());
+                        break;
+                    case Tags.CALENDAR_BUSY_STATUS:
+                        ops.newExtendedProperty("busy_status", getValue());
+                        break;
+                    case Tags.CALENDAR_CATEGORIES:
+                        String categories = categoriesParser(ops);
+                        if (categories.length() > 0) {
+                            ops.newExtendedProperty("categories", categories);
+                        }
+                        break;
+                    default:
+                        skipTag();
+                }
+            }
+
+            // If there's no recurrence, set DTEND to the end time
+            if (!cv.containsKey(Events.RRULE)) {
+                cv.put(Events.DTEND, endTime);
+                cv.put(Events.LAST_DATE, endTime);
+            }
+
+            // Set the DURATION using rfc2445
+            if (allDayEvent != 0) {
+                cv.put(Events.DURATION, "P1D");
+            } else {
+                cv.put(Events.DURATION, "P" + ((endTime - startTime) / MINUTES) + "M");
+            }
+
+            // Put the real event in the proper place in the ops ArrayList
+            if (eventOffset >= 0) {
+                ops.set(eventOffset, ContentProviderOperation
+                        .newInsert(sEventsUri).withValues(cv).build());
+            }
+        }
+
+        private String recurrenceParser(CalendarOperations ops) throws IOException {
+            // Turn this information into an RRULE
+            int type = -1;
+            int occurrences = -1;
+            int interval = -1;
+            int dow = -1;
+            int dom = -1;
+            int wom = -1;
+            int moy = -1;
+            String until = null;
+
+            while (nextTag(Tags.CALENDAR_RECURRENCE) != END) {
+                switch (tag) {
+                    case Tags.CALENDAR_RECURRENCE_TYPE:
+                        type = getValueInt();
+                        break;
+                    case Tags.CALENDAR_RECURRENCE_INTERVAL:
+                        interval = getValueInt();
+                        break;
+                    case Tags.CALENDAR_RECURRENCE_OCCURRENCES:
+                        occurrences = getValueInt();
+                        break;
+                    case Tags.CALENDAR_RECURRENCE_DAYOFWEEK:
+                        dow = getValueInt();
+                        break;
+                    case Tags.CALENDAR_RECURRENCE_DAYOFMONTH:
+                        dom = getValueInt();
+                        break;
+                    case Tags.CALENDAR_RECURRENCE_WEEKOFMONTH:
+                        wom = getValueInt();
+                        break;
+                    case Tags.CALENDAR_RECURRENCE_MONTHOFYEAR:
+                        moy = getValueInt();
+                        break;
+                    case Tags.CALENDAR_RECURRENCE_UNTIL:
+                        until = getValue();
+                        break;
+                    default:
+                       skipTag();
+                }
+            }
+
+            return CalendarUtilities.rruleFromRecurrence(type, occurrences, interval,
+                    dow, dom, wom, moy, until);
+        }
+
+        private void exceptionParser(CalendarOperations ops, ContentValues parentCv)
+                throws IOException {
+            ContentValues cv = new ContentValues();
+            cv.put(Events.CALENDAR_ID, mCalendarId);
+            cv.put(Events._SYNC_ACCOUNT, mAccount.mEmailAddress);
+            cv.put(Events._SYNC_ACCOUNT_TYPE, Email.EXCHANGE_ACCOUNT_MANAGER_TYPE);
+
+            // It appears that these values have to be copied from the parent if they are to appear
+            // Note that they can be overridden below
+            cv.put(Events.ORGANIZER, parentCv.getAsString(Events.ORGANIZER));
+            cv.put(Events.TITLE, parentCv.getAsString(Events.TITLE));
+            cv.put(Events.DESCRIPTION, parentCv.getAsBoolean(Events.DESCRIPTION));
+            cv.put(Events.ORIGINAL_ALL_DAY, parentCv.getAsInteger(Events.ALL_DAY));
+
+            // This column is the key that links the exception to the serverId
+            // TODO Make sure calendar knows this isn't globally unique!!
+            cv.put(Events.ORIGINAL_EVENT, parentCv.getAsString(Events._SYNC_ID));
+
+            while (nextTag(Tags.SYNC_APPLICATION_DATA) != END) {
+                switch (tag) {
+                    case Tags.CALENDAR_EXCEPTION_START_TIME:
+                        cv.put(Events.ORIGINAL_INSTANCE_TIME,
+                                CalendarUtilities.parseDateTimeToMillis(getValue()));
+                        break;
+                    case Tags.CALENDAR_EXCEPTION_IS_DELETED:
+                        if (getValueInt() == 1) {
+                            cv.put(Events.STATUS, Events.STATUS_CANCELED);
+                        }
+                        break;
+                    case Tags.CALENDAR_ALL_DAY_EVENT:
+                        cv.put(Events.ALL_DAY, getValueInt());
+                        break;
+                    case Tags.BASE_BODY:
+                        cv.put(Events.DESCRIPTION, bodyParser());
+                        break;
+                    case Tags.CALENDAR_BODY:
+                        cv.put(Events.DESCRIPTION, getValue());
+                        break;
+                    case Tags.CALENDAR_START_TIME:
+                        cv.put(Events.DTSTART, CalendarUtilities.parseDateTimeToMillis(getValue()));
+                        break;
+                    case Tags.CALENDAR_END_TIME:
+                        cv.put(Events.DTEND, CalendarUtilities.parseDateTimeToMillis(getValue()));
+                        break;
+                    case Tags.CALENDAR_LOCATION:
+                        cv.put(Events.EVENT_LOCATION, getValue());
+                        break;
+                    case Tags.CALENDAR_RECURRENCE:
+                        String rrule = recurrenceParser(ops);
+                        if (rrule != null) {
+                            cv.put(Events.RRULE, rrule);
+                        }
+                        break;
+                    case Tags.CALENDAR_SUBJECT:
+                        cv.put(Events.TITLE, getValue());
+                        break;
+                    case Tags.CALENDAR_SENSITIVITY:
+                        cv.put(Events.VISIBILITY, encodeVisibility(getValueInt()));
+                        break;
+
+                        // TODO How to handle these items that are linked to event id!
+
+//                    case Tags.CALENDAR_DTSTAMP:
+//                        ops.newExtendedProperty("dtstamp", getValue());
+//                        break;
+//                    case Tags.CALENDAR_BUSY_STATUS:
+//                        // TODO Try to fit this into Calendar scheme
+//                        ops.newExtendedProperty("busy_status", getValue());
+//                        break;
+//                    case Tags.CALENDAR_REMINDER_MINS_BEFORE:
+//                        ops.newReminder(getValueInt());
+//                        break;
+
+                        // Not yet handled
+                    default:
+                        skipTag();
+                }
+            }
+
+            if (!cv.containsKey(Events.DTSTART)) {
+                cv.put(Events.DTSTART, parentCv.getAsLong(Events.DTSTART));
+            }
+            if (!cv.containsKey(Events.DTEND)) {
+                cv.put(Events.DTEND, parentCv.getAsLong(Events.DTEND));
+            }
+            // TODO See if this is necessary
+            //cv.put(Events.LAST_DATE, cv.getAsLong(Events.DTEND));
+
+            ops.newException(cv);
+        }
+
+        private int encodeVisibility(int easVisibility) {
+            int visibility = 0;
+            switch(easVisibility) {
+                case 0:
+                    visibility = Events.VISIBILITY_DEFAULT;
+                    break;
+                case 1:
+                    visibility = Events.VISIBILITY_PUBLIC;
+                    break;
+                case 2:
+                    visibility = Events.VISIBILITY_PRIVATE;
+                    break;
+                case 3:
+                    visibility = Events.VISIBILITY_CONFIDENTIAL;
+                    break;
+            }
+            return visibility;
+        }
+
+        private void exceptionsParser(CalendarOperations ops, ContentValues cv)
+                throws IOException {
+            while (nextTag(Tags.CALENDAR_EXCEPTIONS) != END) {
+                switch (tag) {
+                    case Tags.CALENDAR_EXCEPTION:
+                        exceptionParser(ops, cv);
+                        break;
+                    default:
+                        skipTag();
+                }
+            }
+        }
+
+        private String categoriesParser(CalendarOperations ops) throws IOException {
+            StringBuilder categories = new StringBuilder();
+            while (nextTag(Tags.CALENDAR_CATEGORIES) != END) {
+                switch (tag) {
+                    case Tags.CALENDAR_CATEGORY:
+                        // TODO Handle categories (there's no similar concept for gdata AFAIK)
+                        // We need to save them and spit them back when we update the event
+                        categories.append(getValue());
+                        categories.append(CATEGORY_TOKENIZER_DELIMITER);
+                    default:
+                        skipTag();
+                }
+            }
+            return categories.toString();
+        }
+
+        private String attendeesParser(CalendarOperations ops, String organizerName,
+                String organizerEmail, long eventId) throws IOException {
+            String body = null;
+            // First, handle the organizer (who IS an attendee on device, but NOT in EAS)
+            if (organizerName != null || organizerEmail != null) {
+                ContentValues cv = new ContentValues();
+                if (organizerName != null) {
+                    cv.put(Attendees.ATTENDEE_NAME, organizerName);
+                }
+                if (organizerEmail != null) {
+                    cv.put(Attendees.ATTENDEE_EMAIL, organizerEmail);
+                }
+                cv.put(Attendees.ATTENDEE_RELATIONSHIP, Attendees.RELATIONSHIP_ORGANIZER);
+                cv.put(Attendees.ATTENDEE_TYPE, Attendees.TYPE_REQUIRED);
+                if (eventId < 0) {
+                    ops.newAttendee(cv);
+                } else {
+                    ops.updatedAttendee(cv, eventId);
+                }
+            }
+            while (nextTag(Tags.CALENDAR_ATTENDEES) != END) {
+                switch (tag) {
+                    case Tags.CALENDAR_ATTENDEE:
+                        attendeeParser(ops, eventId);
+                        break;
+                    default:
+                        skipTag();
+                }
+            }
+            return body;
+        }
+
+        private void attendeeParser(CalendarOperations ops, long eventId) throws IOException {
+            ContentValues cv = new ContentValues();
+            while (nextTag(Tags.CALENDAR_ATTENDEE) != END) {
+                switch (tag) {
+                    case Tags.CALENDAR_ATTENDEE_EMAIL:
+                        cv.put(Attendees.ATTENDEE_EMAIL, getValue());
+                        break;
+                    case Tags.CALENDAR_ATTENDEE_NAME:
+                        cv.put(Attendees.ATTENDEE_NAME, getValue());
+                        break;
+                    case Tags.CALENDAR_ATTENDEE_STATUS:
+                        int status = getValueInt();
+                        cv.put(Attendees.ATTENDEE_STATUS,
+                                (status == 2) ? Attendees.ATTENDEE_STATUS_TENTATIVE :
+                                (status == 3) ? Attendees.ATTENDEE_STATUS_ACCEPTED :
+                                (status == 4) ? Attendees.ATTENDEE_STATUS_DECLINED :
+                                (status == 5) ? Attendees.ATTENDEE_STATUS_INVITED :
+                                    Attendees.ATTENDEE_STATUS_NONE);
+                        break;
+                    case Tags.CALENDAR_ATTENDEE_TYPE:
+                        int type = Attendees.TYPE_NONE;
+                        // EAS types: 1 = req'd, 2 = opt, 3 = resource
+                        switch (getValueInt()) {
+                            case 1:
+                                type = Attendees.TYPE_REQUIRED;
+                                break;
+                            case 2:
+                                type = Attendees.TYPE_OPTIONAL;
+                                break;
+                        }
+                        cv.put(Attendees.ATTENDEE_TYPE, type);
+                        break;
+                    default:
+                        skipTag();
+                }
+            }
+            cv.put(Attendees.ATTENDEE_RELATIONSHIP, Attendees.RELATIONSHIP_ATTENDEE);
+            if (eventId < 0) {
+                ops.newAttendee(cv);
+            } else {
+                ops.updatedAttendee(cv, eventId);
+            }
+        }
+
+        private String bodyParser() throws IOException {
+            String body = null;
+            while (nextTag(Tags.BASE_BODY) != END) {
+                switch (tag) {
+                    case Tags.BASE_DATA:
+                        body = getValue();
+                        break;
+                    default:
+                        skipTag();
+                }
+            }
+
+            // Handle null data without error
+            if (body == null) return "";
+            // Remove \r's from any body text
+            return body.replace("\r\n", "\n");
+        }
+
+        public void addParser(CalendarOperations ops) throws IOException {
+            String serverId = null;
+            while (nextTag(Tags.SYNC_ADD) != END) {
+                switch (tag) {
+                    case Tags.SYNC_SERVER_ID: // same as
+                        serverId = getValue();
+                        break;
+                    case Tags.SYNC_APPLICATION_DATA:
+                        addEvent(ops, serverId, false);
+                        break;
+                    default:
+                        skipTag();
+                }
+            }
+        }
+
+        private Cursor getServerIdCursor(String serverId) {
+            mBindArgument[0] = serverId;
+            return mContentResolver.query(mAccountUri, ID_PROJECTION, SERVER_ID,
+                    mBindArgument, null);
+        }
+
+        private Cursor getClientIdCursor(String clientId) {
+            mBindArgument[0] = clientId;
+            return mContentResolver.query(mAccountUri, ID_PROJECTION, CLIENT_ID_SELECTION,
+                    mBindArgument, null);
+        }
+
+        public void deleteParser(CalendarOperations ops) throws IOException {
+            while (nextTag(Tags.SYNC_DELETE) != END) {
+                switch (tag) {
+                    case Tags.SYNC_SERVER_ID:
+                        String serverId = getValue();
+                        // Find the event with the given serverId
+                        Cursor c = getServerIdCursor(serverId);
+                        try {
+                            if (c.moveToFirst()) {
+                                userLog("Deleting ", serverId);
+                                ops.delete(c.getLong(0));
+                            }
+                        } finally {
+                            c.close();
+                        }
+                        break;
+                    default:
+                        skipTag();
+                }
+            }
+        }
+
+        /**
+         * A change is handled as a delete (including all exceptions) and an add
+         * This isn't as efficient as attempting to traverse the original and all of its exceptions,
+         * but changes happen infrequently and this code is both simpler and easier to maintain
+         * @param ops the array of pending ContactProviderOperations.
+         * @throws IOException
+         */
+        public void changeParser(CalendarOperations ops) throws IOException {
+            String serverId = null;
+            while (nextTag(Tags.SYNC_CHANGE) != END) {
+                switch (tag) {
+                    case Tags.SYNC_SERVER_ID:
+                        serverId = getValue();
+                        break;
+                    case Tags.SYNC_APPLICATION_DATA:
+                        userLog("Changing " + serverId);
+                        addEvent(ops, serverId, true);
+                        break;
+                    default:
+                        skipTag();
+                }
+            }
+        }
+
+        @Override
+        public void commandsParser() throws IOException {
+            while (nextTag(Tags.SYNC_COMMANDS) != END) {
+                if (tag == Tags.SYNC_ADD) {
+                    addParser(mOps);
+                    incrementChangeCount();
+                } else if (tag == Tags.SYNC_DELETE) {
+                    deleteParser(mOps);
+                    incrementChangeCount();
+                } else if (tag == Tags.SYNC_CHANGE) {
+                    changeParser(mOps);
+                    incrementChangeCount();
+                } else
+                    skipTag();
+            }
+        }
+
+        @Override
+        public void commit() throws IOException {
+            userLog("Calendar SyncKey saved as: ", mMailbox.mSyncKey);
+            // Save the syncKey here, using the Helper provider by Calendar provider
+            mOps.add(SyncStateContract.Helpers.newSetOperation(SyncState.CONTENT_URI,
+                    getAccountManagerAccount(), mMailbox.mSyncKey.getBytes()));
+
+            // Execute these all at once...
+            mOps.execute();
+
+            if (mOps.mResults != null) {
+                // Clear dirty flags for Events sent to server
+                ContentValues cv = new ContentValues();
+                cv.put(Events._SYNC_DIRTY, 0);
+                mContentResolver.update(sEventsUri, cv, DIRTY_IN_CALENDAR,
+                        new String[] {Long.toString(mCalendarId)});
+            }
+        }
+
+        public void addResponsesParser() throws IOException {
+            String serverId = null;
+            String clientId = null;
+            int status = -1;
+            ContentValues cv = new ContentValues();
+            while (nextTag(Tags.SYNC_ADD) != END) {
+                switch (tag) {
+                    case Tags.SYNC_SERVER_ID:
+                        serverId = getValue();
+                        break;
+                    case Tags.SYNC_CLIENT_ID:
+                        clientId = getValue();
+                        break;
+                    case Tags.SYNC_STATUS:
+                        status = getValueInt();
+                        if (status != 1) {
+                            userLog("Attempt to add event failed with status: " + status);
+                        }
+                        break;
+                    default:
+                        skipTag();
+                }
+            }
+
+            if (clientId == null) return;
+            if (serverId == null) {
+                // TODO Reconsider how to handle this
+                serverId = "FAIL:" + status;
+            }
+
+            Cursor c = getClientIdCursor(clientId);
+            try {
+                if (c.moveToFirst()) {
+                    cv.put(Events._SYNC_ID, serverId);
+                    mOps.add(ContentProviderOperation.newUpdate(
+                            ContentUris.withAppendedId(sEventsUri, c.getLong(0)))
+                                    .withValues(cv)
+                                    .build());
+                    userLog("New event " + clientId + " was given serverId: " + serverId);
+                }
+            } finally {
+                c.close();
+            }
+        }
+
+        public void changeResponsesParser() throws IOException {
+            String serverId = null;
+            String status = null;
+            while (nextTag(Tags.SYNC_CHANGE) != END) {
+                switch (tag) {
+                    case Tags.SYNC_SERVER_ID:
+                        serverId = getValue();
+                        break;
+                    case Tags.SYNC_STATUS:
+                        status = getValue();
+                        break;
+                    default:
+                        skipTag();
+                }
+            }
+            if (serverId != null && status != null) {
+                userLog("Changed event " + serverId + " failed with status: " + status);
+            }
+        }
+
+
+        @Override
+        public void responsesParser() throws IOException {
+            // Handle server responses here (for Add and Change)
+            while (nextTag(Tags.SYNC_RESPONSES) != END) {
+                if (tag == Tags.SYNC_ADD) {
+                    addResponsesParser();
+                } else if (tag == Tags.SYNC_CHANGE) {
+                    changeResponsesParser();
+                } else
+                    skipTag();
+            }
+        }
+    }
+
+    private class CalendarOperations extends ArrayList<ContentProviderOperation> {
+        private static final long serialVersionUID = 1L;
+        private int mCount = 0;
+        private ContentProviderResult[] mResults = null;
+        private int mEventStart = 0;
+
+        @Override
+        public boolean add(ContentProviderOperation op) {
+            super.add(op);
+            mCount++;
+            return true;
+        }
+
+        public int newEvent(ContentProviderOperation op) {
+            mEventStart = mCount;
+            add(op);
+            return mEventStart;
+        }
+
+        public void newAttendee(ContentValues cv) {
+            add(ContentProviderOperation
+                    .newInsert(sAttendeesUri)
+                    .withValues(cv)
+                    .withValueBackReference(Attendees.EVENT_ID, mEventStart)
+                    .build());
+        }
+
+        public void updatedAttendee(ContentValues cv, long id) {
+            cv.put(Attendees.EVENT_ID, id);
+            add(ContentProviderOperation.newInsert(sAttendeesUri).withValues(cv).build());
+        }
+
+        public void newException(ContentValues cv) {
+            add(ContentProviderOperation.newInsert(sEventsUri).withValues(cv).build());
+        }
+
+        public void newExtendedProperty(String name, String value) {
+            add(ContentProviderOperation
+                    .newInsert(sExtendedPropertiesUri)
+                    .withValue(ExtendedProperties.NAME, name)
+                    .withValue(ExtendedProperties.VALUE, value)
+                    .withValueBackReference(ExtendedProperties.EVENT_ID, mEventStart)
+                    .build());
+        }
+
+        public void newReminder(int mins) {
+            add(ContentProviderOperation
+                    .newInsert(sRemindersUri)
+                    .withValue(Reminders.MINUTES, mins)
+                    .withValue(Reminders.METHOD, Reminders.METHOD_DEFAULT)
+                    .withValueBackReference(ExtendedProperties.EVENT_ID, mEventStart)
+                    .build());
+        }
+
+        public void delete(long id) {
+            add(ContentProviderOperation
+                    .newDelete(ContentUris.withAppendedId(sEventsUri, id)).build());
+        }
+
+        public void execute() {
+            synchronized (mService.getSynchronizer()) {
+                if (!mService.isStopped()) {
+                    try {
+                        if (!isEmpty()) {
+                            mService.userLog("Executing ", size(), " CPO's");
+                            mResults = mContext.getContentResolver().applyBatch(
+                                    Calendar.AUTHORITY, this);
+                        }
+                    } catch (RemoteException e) {
+                        // There is nothing sensible to be done here
+                        Log.e(TAG, "problem inserting event during server update", e);
+                    } catch (OperationApplicationException e) {
+                        // There is nothing sensible to be done here
+                        Log.e(TAG, "problem inserting event during server update", e);
+                    }
+                }
+            }
+        }
+    }
+
+    private String decodeVisibility(int visibility) {
+        int easVisibility = 0;
+        switch(visibility) {
+            case Events.VISIBILITY_DEFAULT:
+                easVisibility = 0;
+                break;
+            case Events.VISIBILITY_PUBLIC:
+                easVisibility = 1;
+                break;
+            case Events.VISIBILITY_PRIVATE:
+                easVisibility = 2;
+                break;
+            case Events.VISIBILITY_CONFIDENTIAL:
+                easVisibility = 3;
+                break;
+        }
+        return Integer.toString(easVisibility);
+    }
+
+    private void sendEvent(Entity entity, String clientId, Serializer s)
+            throws IOException {
+        // Serialize for EAS here
+        // Set uid with the client id we created
+        // 1) Serialize the top-level event
+        // 2) Serialize attendees and reminders from subvalues
+        // 3) Look for exceptions and serialize with the top-level event
+        ContentValues entityValues = entity.getEntityValues();
+        boolean isException = (clientId == null);
+
+        if (entityValues.containsKey(Events.ALL_DAY)) {
+            s.data(Tags.CALENDAR_ALL_DAY_EVENT,
+                    entityValues.getAsInteger(Events.ALL_DAY).toString());
+        }
+
+        long startTime = entityValues.getAsLong(Events.DTSTART);
+        s.data(Tags.CALENDAR_START_TIME,
+                CalendarUtilities.millisToEasDateTime(startTime));
+
+        if (!entityValues.containsKey(Events.DURATION)) {
+            if (entityValues.containsKey(Events.DTEND)) {
+                s.data(Tags.CALENDAR_END_TIME, CalendarUtilities.millisToEasDateTime(
+                        entityValues.getAsLong(Events.DTEND)));
+            }
+        } else {
+            // Convert this into millis and add it to DTSTART for DTEND
+            // We'll use 1 hour as a default
+            long durationMillis = HOURS;
+            Duration duration = new Duration();
+            try {
+                duration.parse(entityValues.getAsString(Events.DURATION));
+            } catch (DateException e) {
+                // Can't do much about this; use the default (1 hour)
+            }
+            s.data(Tags.CALENDAR_END_TIME,
+                    CalendarUtilities.millisToEasDateTime(startTime + durationMillis));
+        }
+
+        s.data(Tags.CALENDAR_DTSTAMP,
+                CalendarUtilities.millisToEasDateTime(System.currentTimeMillis()));
+
+        if (entityValues.containsKey(Events.EVENT_LOCATION)) {
+            s.data(Tags.CALENDAR_LOCATION,
+                    entityValues.getAsString(Events.EVENT_LOCATION));
+        }
+        if (entityValues.containsKey(Events.TITLE)) {
+            s.data(Tags.CALENDAR_SUBJECT, entityValues.getAsString(Events.TITLE));
+        }
+
+        if (entityValues.containsKey(Events.VISIBILITY)) {
+            s.data(Tags.CALENDAR_SENSITIVITY,
+                    decodeVisibility(entityValues.getAsInteger(Events.VISIBILITY)));
+        } else {
+            // Private if not set
+            s.data(Tags.CALENDAR_SENSITIVITY, "1");
+        }
+
+        if (!isException) {
+            // A time zone is required in all EAS events; we'll use the default if none
+            // is set.
+            String timeZoneName;
+            if (entityValues.containsKey(Events.EVENT_TIMEZONE)) {
+                timeZoneName = entityValues.getAsString(Events.EVENT_TIMEZONE);
+            } else {
+                timeZoneName = TimeZone.getDefault().getID();
+            }
+            String x = CalendarUtilities.timeZoneToTziString(TimeZone.getTimeZone(timeZoneName));
+            s.data(Tags.CALENDAR_TIME_ZONE, x);
+
+            if (entityValues.containsKey(Events.DESCRIPTION)) {
+                String desc = entityValues.getAsString(Events.DESCRIPTION);
+                if (mService.mProtocolVersionDouble >= 12.0) {
+                    s.start(Tags.BASE_BODY);
+                    s.data(Tags.BASE_TYPE, "1");
+                    s.data(Tags.BASE_DATA, desc);
+                    s.end();
+                } else {
+                    s.data(Tags.CALENDAR_BODY, desc);
+                }
+            }
+
+            if (entityValues.containsKey(Events.ORGANIZER)) {
+                s.data(Tags.CALENDAR_ORGANIZER_EMAIL,
+                        entityValues.getAsString(Events.ORGANIZER));
+            }
+
+            if (entityValues.containsKey(Events.RRULE)) {
+                CalendarUtilities.recurrenceFromRrule(
+                        entityValues.getAsString(Events.RRULE), startTime, s);
+            }
+
+            // Handle associated data EXCEPT for attendees, which have to be grouped
+            ArrayList<NamedContentValues> subValues = entity.getSubValues();
+            for (NamedContentValues ncv: subValues) {
+                Uri ncvUri = ncv.uri;
+                ContentValues ncvValues = ncv.values;
+                if (ncvUri.equals(ExtendedProperties.CONTENT_URI)) {
+                    if (ncvValues.containsKey("uid")) {
+                        clientId = ncvValues.getAsString("uid");
+                        s.data(Tags.CALENDAR_UID, clientId);
+                    }
+                    if (ncvValues.containsKey("dtstamp")) {
+                        s.data(Tags.CALENDAR_DTSTAMP, ncvValues.getAsString("dtstamp"));
+                    }
+                    if (ncvValues.containsKey("categories")) {
+                        // Send all the categories back to the server
+                        // We've saved them as a String of delimited tokens
+                        String categories = ncvValues.getAsString("categories");
+                        StringTokenizer st =
+                            new StringTokenizer(categories, CATEGORY_TOKENIZER_DELIMITER);
+                        if (st.countTokens() > 0) {
+                            s.start(Tags.CALENDAR_CATEGORIES);
+                            while (st.hasMoreTokens()) {
+                                String category = st.nextToken();
+                                s.data(Tags.CALENDAR_CATEGORY, category);
+                            }
+                            s.end();
+                        }
+                    }
+                } else if (ncvUri.equals(Reminders.CONTENT_URI)) {
+                    if (ncvValues.containsKey(Reminders.MINUTES)) {
+                        s.data(Tags.CALENDAR_REMINDER_MINS_BEFORE,
+                                ncvValues.getAsString(Reminders.MINUTES));
+                    }
+                }
+            }
+
+            // We've got to send a UID.  If the event is new, we've generated one; if not,
+            // we should have gotten one from extended properties.
+            s.data(Tags.CALENDAR_UID, clientId);
+
+            // Handle attendee data here; keep track of organizer and stream it afterward
+            boolean hasAttendees = false;
+            String organizerName = null;
+            for (NamedContentValues ncv: subValues) {
+                Uri ncvUri = ncv.uri;
+                ContentValues ncvValues = ncv.values;
+                if (ncvUri.equals(Attendees.CONTENT_URI)) {
+                    if (ncvValues.containsKey(Attendees.ATTENDEE_RELATIONSHIP)) {
+                        int relationship =
+                            ncvValues.getAsInteger(Attendees.ATTENDEE_RELATIONSHIP);
+                        // Organizer isn't among attendees in EAS
+                        if (relationship == Attendees.RELATIONSHIP_ORGANIZER) {
+                            if (ncvValues.containsKey(Attendees.ATTENDEE_NAME)) {
+                                // Remember this; we can't insert it into the stream in
+                                // the middle of attendees
+                                organizerName =
+                                    ncvValues.getAsString(Attendees.ATTENDEE_NAME);
+                            }
+                            continue;
+                        }
+                        if (!hasAttendees) {
+                            s.start(Tags.CALENDAR_ATTENDEES);
+                            hasAttendees = true;
+                        }
+                        s.start(Tags.CALENDAR_ATTENDEE);
+                        if (ncvValues.containsKey(Attendees.ATTENDEE_NAME)) {
+                            s.data(Tags.CALENDAR_ATTENDEE_NAME,
+                                    ncvValues.getAsString(Attendees.ATTENDEE_NAME));
+                        }
+                        if (ncvValues.containsKey(Attendees.ATTENDEE_EMAIL)) {
+                            s.data(Tags.CALENDAR_ATTENDEE_EMAIL,
+                                    ncvValues.getAsString(Attendees.ATTENDEE_EMAIL));
+                        }
+                        s.data(Tags.CALENDAR_ATTENDEE_TYPE, "1"); // Required
+                        s.end(); // Attendee
+                    }
+                    // If there's no relationship, we can't create this for EAS
+                }
+            }
+            if (hasAttendees) {
+                s.end();  // Attendees
+            }
+            if (organizerName != null) {
+                s.data(Tags.CALENDAR_ORGANIZER_NAME, organizerName);
+            }
+        } else {
+            // TODO Add reminders to exceptions (allow them to be specified!)
+            if (entityValues.containsKey(Events.ORIGINAL_INSTANCE_TIME)) {
+                s.data(Tags.CALENDAR_EXCEPTION_START_TIME,
+                        CalendarUtilities.millisToEasDateTime(entityValues.getAsLong(
+                                Events.ORIGINAL_INSTANCE_TIME)));
+            } else {
+                // Illegal; what should we do?
+            }
+        }
+    }
+
+    @Override
+    public boolean sendLocalChanges(Serializer s) throws IOException {
+        ContentResolver cr = mService.mContentResolver;
+        Uri uri = Events.CONTENT_URI.buildUpon()
+                .appendQueryParameter(Calendar.CALLER_IS_SYNCADAPTER, "true")
+                .build();
+
+        if (getSyncKey().equals("0")) {
+            return false;
+        }
+
+        try {
+            // We've got to handle exceptions as part of the parent when changes occur, so we need
+            // to find new/changed exceptions and mark the parent dirty
+            Cursor c = cr.query(Events.CONTENT_URI, ORIGINAL_EVENT_PROJECTION, DIRTY_EXCEPTION,
+                    null, null);
+            try {
+                ContentValues cv = new ContentValues();
+                cv.put(Events._SYNC_DIRTY, 1);
+                // Mark the parent dirty in this loop
+                while (c.moveToNext()) {
+                    String serverId = c.getString(0);
+                    cr.update(asSyncAdapter(Events.CONTENT_URI), cv, SERVER_ID,
+                             new String[] {serverId});
+                }
+            } finally {
+                c.close();
+            }
+
+            // Now we can go through dirty top-level events and send them back to the server
+            EntityIterator eventIterator = EventsEntity.newEntityIterator(
+                    cr.query(uri, null, DIRTY_TOP_LEVEL, null, null), cr);
+            ContentValues cidValues = new ContentValues();
+            try {
+                boolean first = true;
+                while (eventIterator.hasNext()) {
+                    Entity entity = eventIterator.next();
+                    String clientId = "uid_" + mMailbox.mId + '_' + System.currentTimeMillis();
+
+                    // For each of these entities, create the change commands
+                    ContentValues entityValues = entity.getEntityValues();
+                    String serverId = entityValues.getAsString(Events._SYNC_ID);
+
+                    // EAS 2.5 needs: BusyStatus DtStamp EndTime Sensitivity StartTime TimeZone UID
+                    // We can generate all but what we're testing for below
+                    if (!entityValues.containsKey(Events.DTSTART)
+                            || (!entityValues.containsKey(Events.DURATION) &&
+                                    !entityValues.containsKey(Events.DTEND))) {
+                        continue;
+                    }
+                    // TODO Handle BusyStatus for EAS 2.5
+                    // What should it be??
+
+                    if (first) {
+                        s.start(Tags.SYNC_COMMANDS);
+                        userLog("Sending Calendar changes to the server");
+                        first = false;
+                    }
+                    if (serverId == null) {
+                        // This is a new event; create a clientId
+                        userLog("Creating new event with clientId: ", clientId);
+                        s.start(Tags.SYNC_ADD).data(Tags.SYNC_CLIENT_ID, clientId);
+                        // And save it in the Event as the local id
+                        cidValues.put(Events._SYNC_LOCAL_ID, clientId);
+                        cr.update(ContentUris.
+                                withAppendedId(uri,
+                                        entityValues.getAsLong(Events._ID)),
+                                        cidValues, null, null);
+                    } else {
+                        if (entityValues.getAsInteger(Events.DELETED) == 1) {
+                            userLog("Deleting event with serverId: ", serverId);
+                            s.start(Tags.SYNC_DELETE).data(Tags.SYNC_SERVER_ID, serverId).end();
+                            mDeletedIdList.add(entityValues.getAsLong(Events._ID));
+                            continue;
+                        }
+                        userLog("Upsync change to event with serverId: " + serverId);
+                        s.start(Tags.SYNC_CHANGE).data(Tags.SYNC_SERVER_ID, serverId);
+                    }
+                    s.start(Tags.SYNC_APPLICATION_DATA);
+                    sendEvent(entity, clientId, s);
+
+                    // Now, the hard part; find exceptions for this event
+                    if (serverId != null) {
+                        EntityIterator exceptionIterator = EventsEntity.newEntityIterator(
+                                cr.query(uri, null, Events.ORIGINAL_EVENT + "=?",
+                                        new String[] {serverId}, null), cr);
+                        boolean exFirst = true;
+                        while (exceptionIterator.hasNext()) {
+                            Entity exceptionEntity = exceptionIterator.next();
+                            if (exFirst) {
+                                s.start(Tags.CALENDAR_EXCEPTIONS);
+                                exFirst = false;
+                            }
+                            s.start(Tags.CALENDAR_EXCEPTION);
+                            sendEvent(exceptionEntity, null, s);
+                            s.end(); // EXCEPTION
+                        }
+                        if (!exFirst) {
+                            s.end(); // EXCEPTIONS
+                        }
+                    }
+
+                    s.end().end(); // ApplicationData & Change
+                    mUpdatedIdList.add(entityValues.getAsLong(Events._ID));
+                }
+                if (!first) {
+                    s.end(); // Commands
+                }
+            } finally {
+                eventIterator.close();
+            }
+        } catch (RemoteException e) {
+            Log.e(TAG, "Could not read dirty events.");
+        }
+
         return false;
     }
 }
diff --git a/src/com/android/exchange/adapter/ContactsSyncAdapter.java b/src/com/android/exchange/adapter/ContactsSyncAdapter.java
index 7211794..b95e475 100644
--- a/src/com/android/exchange/adapter/ContactsSyncAdapter.java
+++ b/src/com/android/exchange/adapter/ContactsSyncAdapter.java
@@ -163,7 +163,7 @@
                 // Make sure ungrouped contacts for Exchange are defaultly visible
                 ContentValues cv = new ContentValues();
                 cv.put(Groups.ACCOUNT_NAME, mAccount.mEmailAddress);
-                cv.put(Groups.ACCOUNT_TYPE, Eas.ACCOUNT_MANAGER_TYPE);
+                cv.put(Groups.ACCOUNT_TYPE, com.android.email.Email.EXCHANGE_ACCOUNT_MANAGER_TYPE);
                 cv.put(Settings.UNGROUPED_VISIBLE, true);
                 client.insert(addCallerIsSyncAdapterParameter(Settings.CONTENT_URI), cv);
                 return "0";
@@ -198,8 +198,8 @@
 
     public android.accounts.Account getAccountManagerAccount() {
         if (mAccountManagerAccount == null) {
-            mAccountManagerAccount =
-                new android.accounts.Account(mAccount.mEmailAddress, Eas.ACCOUNT_MANAGER_TYPE);
+            mAccountManagerAccount = new android.accounts.Account(mAccount.mEmailAddress,
+                    com.android.email.Email.EXCHANGE_ACCOUNT_MANAGER_TYPE);
         }
         return mAccountManagerAccount;
     }
@@ -893,7 +893,8 @@
     private Uri uriWithAccountAndIsSyncAdapter(Uri uri) {
         return uri.buildUpon()
             .appendQueryParameter(RawContacts.ACCOUNT_NAME, mAccount.mEmailAddress)
-            .appendQueryParameter(RawContacts.ACCOUNT_TYPE, Eas.ACCOUNT_MANAGER_TYPE)
+            .appendQueryParameter(RawContacts.ACCOUNT_TYPE,
+                    com.android.email.Email.EXCHANGE_ACCOUNT_MANAGER_TYPE)
             .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
             .build();
     }
@@ -1771,7 +1772,8 @@
         ContentResolver cr = mService.mContentResolver;
         Uri uri = RawContactsEntity.CONTENT_URI.buildUpon()
                 .appendQueryParameter(RawContacts.ACCOUNT_NAME, mAccount.mEmailAddress)
-                .appendQueryParameter(RawContacts.ACCOUNT_TYPE, Eas.ACCOUNT_MANAGER_TYPE)
+                .appendQueryParameter(RawContacts.ACCOUNT_TYPE,
+                        com.android.email.Email.EXCHANGE_ACCOUNT_MANAGER_TYPE)
                 .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
                 .build();
 
@@ -1879,7 +1881,7 @@
                     for (ContentValues cv: emailValues) {
                         sendEmail(s, cv, emailCount++, displayName);
                     }
-                    
+
                     // Now, we'll send up groups, if any
                     if (!groupIds.isEmpty()) {
                         boolean groupFirst = true;
diff --git a/src/com/android/exchange/adapter/EmailSyncAdapter.java b/src/com/android/exchange/adapter/EmailSyncAdapter.java
index 94c5ef0..c835f85 100644
--- a/src/com/android/exchange/adapter/EmailSyncAdapter.java
+++ b/src/com/android/exchange/adapter/EmailSyncAdapter.java
@@ -166,6 +166,14 @@
                         String text = getValue();
                         msg.mText = text;
                         break;
+                    case Tags.EMAIL_MESSAGE_CLASS:
+                        String messageClass = getValue();
+                        if (messageClass.equals("IPM.Schedule.Meeting.Request")) {
+                            msg.mFlags |= Message.FLAG_MEETING_INVITE;
+                        } else if (messageClass.equals("IPM.Schedule.Meeting.Canceled")) {
+                            msg.mFlags |= Message.FLAG_MEETING_CANCEL_NOTICE;
+                        }
+                        break;
                     default:
                         skipTag();
                 }
diff --git a/src/com/android/exchange/adapter/FolderSyncParser.java b/src/com/android/exchange/adapter/FolderSyncParser.java
index 337e329..b760dfd 100644
--- a/src/com/android/exchange/adapter/FolderSyncParser.java
+++ b/src/com/android/exchange/adapter/FolderSyncParser.java
@@ -17,6 +17,7 @@
 
 package com.android.exchange.adapter;
 
+import com.android.email.Email;
 import com.android.email.provider.AttachmentProvider;
 import com.android.email.provider.EmailContent;
 import com.android.email.provider.EmailProvider;
@@ -33,7 +34,10 @@
 import android.content.ContentValues;
 import android.content.OperationApplicationException;
 import android.database.Cursor;
+import android.net.Uri;
 import android.os.RemoteException;
+import android.provider.Calendar.Calendars;
+import android.text.format.Time;
 
 import java.io.IOException;
 import java.io.InputStream;
@@ -239,7 +243,28 @@
                     break;
                 case CALENDAR_TYPE:
                     m.mType = Mailbox.TYPE_CALENDAR;
-                    // For now, no sync, since it's not yet implemented
+                    m.mSyncInterval = mAccount.mSyncInterval;
+
+                    // Create a Calendar object
+                    ContentValues cv = new ContentValues();
+                    // TODO How will this change if the user changes his account display name?
+                    cv.put(Calendars.DISPLAY_NAME, mAccount.mDisplayName);
+                    cv.put(Calendars._SYNC_ACCOUNT, mAccount.mEmailAddress);
+                    cv.put(Calendars._SYNC_ACCOUNT_TYPE, Email.EXCHANGE_ACCOUNT_MANAGER_TYPE);
+                    cv.put(Calendars.SYNC_EVENTS, 1);
+                    cv.put(Calendars.SELECTED, 1);
+                    cv.put(Calendars.HIDDEN, 0);
+                    // TODO Find out how to set color!!
+                    cv.put(Calendars.COLOR, 0xFF228B22 /*green*/);
+                    cv.put(Calendars.TIMEZONE, Time.getCurrentTimezone());
+                    cv.put(Calendars.ACCESS_LEVEL, Calendars.OWNER_ACCESS);
+                    cv.put(Calendars.OWNER_ACCOUNT, mAccount.mEmailAddress);
+
+                    Uri uri = mService.mContentResolver.insert(Calendars.CONTENT_URI, cv);
+                    // We save the id of the calendar into mSyncStatus
+                    if (uri != null) {
+                        m.mSyncStatus = uri.getPathSegments().get(1);
+                    }
                     break;
             }
 
diff --git a/src/com/android/exchange/adapter/MeetingResponseParser.java b/src/com/android/exchange/adapter/MeetingResponseParser.java
new file mode 100644
index 0000000..142c419
--- /dev/null
+++ b/src/com/android/exchange/adapter/MeetingResponseParser.java
@@ -0,0 +1,65 @@
+/* Copyright (C) 2010 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.exchange.adapter;
+
+import com.android.exchange.EasSyncService;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Parse the result of a MeetingRequest command.
+ */
+public class MeetingResponseParser extends Parser {
+    private EasSyncService mService;
+
+    public MeetingResponseParser(InputStream in, EasSyncService service) throws IOException {
+        super(in);
+        mService = service;
+    }
+
+    public void parseResult() throws IOException {
+        while (nextTag(Tags.MREQ_RESULT) != END) {
+            if (tag == Tags.MREQ_STATUS) {
+                int status = getValueInt();
+                if (status != 1) {
+                    mService.userLog("Error in meeting response: " + status);
+                }
+            } else if (tag == Tags.MREQ_CAL_ID) {
+                mService.userLog("Meeting response calendar id: " + getValue());
+            } else {
+                skipTag();
+            }
+        }
+    }
+
+    @Override
+    public boolean parse() throws IOException {
+        boolean res = false;
+        if (nextTag(START_DOCUMENT) != Tags.MREQ_MEETING_RESPONSE) {
+            throw new IOException();
+        }
+        while (nextTag(START_DOCUMENT) != END_DOCUMENT) {
+            if (tag == Tags.MREQ_RESULT) {
+                parseResult();
+            } else {
+                skipTag();
+            }
+        }
+        return res;
+    }
+}
+
diff --git a/src/com/android/exchange/adapter/Parser.java b/src/com/android/exchange/adapter/Parser.java
index 01051c0..57e3f50 100644
--- a/src/com/android/exchange/adapter/Parser.java
+++ b/src/com/android/exchange/adapter/Parser.java
@@ -38,8 +38,6 @@
  */
 public abstract class Parser {
 
-    private static final String TAG = "EasParser";
-
     // The following constants are Wbxml standard
     public static final int START_DOCUMENT = 0;
     public static final int DONE = 1;
@@ -52,6 +50,10 @@
     private static final int EOF_BYTE = -1;
     private boolean logging = false;
     private boolean capture = false;
+    private String logTag = "EAS Parser";
+    
+    // Where tags start in a page
+    private static final int TAG_BASE = 5;
 
     private ArrayList<Integer> captureArray;
 
@@ -158,6 +160,16 @@
     }
 
     /**
+     * Set the tag used for logging.  When debugging is on, every token is logged (Log.v) to
+     * the console.
+     *
+     * @param val the logging tag
+     */
+    public void setLoggingTag(String val) {
+        logTag = val;
+    }
+
+    /**
      * Turns on data capture; this is used to create test streams that represent "live" data and
      * can be used against the various parsers.
      */
@@ -190,6 +202,13 @@
     public String getValue() throws IOException {
         // The false argument tells getNext to return the value as a String
         getNext(false);
+        // This means there was no value given, just <Foo/>; we'll return empty string for now
+        if (type == END) {
+            if (logging) {
+                log("No value for tag: " + tagTable[startTag - TAG_BASE]);
+            }
+            return "";
+        }
         // Save the value
         String val = text;
         // Read the next token; it had better be the end of the current tag
@@ -211,6 +230,9 @@
    public int getValueInt() throws IOException {
         // The true argument to getNext indicates the desire for an integer return value
         getNext(true);
+        if (type == END) {
+            return 0;
+        }
         // Save the value
         int val = num;
         // Read the next token; it had better be the end of the current tag
@@ -313,9 +335,9 @@
         if (cr > 0) {
             str = str.substring(0, cr);
         }
-        Log.v(TAG, str);
+        Log.v(logTag, str);
         if (Eas.FILE_LOG) {
-            FileLogger.log(TAG, str);
+            FileLogger.log(logTag, str);
         }
     }
 
@@ -385,7 +407,7 @@
                     text = readInlineString();
                 }
                 if (logging) {
-                    name = tagTable[startTag - 5];
+                    name = tagTable[startTag - TAG_BASE];
                     log(name + ": " + (asInt ? Integer.toString(num) : text));
                 }
                 break;
@@ -399,7 +421,7 @@
                 noContent = (id & 0x40) == 0;
                 depth++;
                 if (logging) {
-                    name = tagTable[startTag - 5];
+                    name = tagTable[startTag - TAG_BASE];
                     //log('<' + name + '>');
                     nameArray[depth] = name;
                 }
diff --git a/src/com/android/exchange/adapter/Tags.java b/src/com/android/exchange/adapter/Tags.java
index d822181..2c5c971 100644
--- a/src/com/android/exchange/adapter/Tags.java
+++ b/src/com/android/exchange/adapter/Tags.java
@@ -39,6 +39,7 @@
     public static final int MOVE = 0x05;
     public static final int GIE = 0x06;
     public static final int FOLDER = 0x07;
+    public static final int MREQ = 0x08;
     public static final int TASK = 0x09;
     public static final int CONTACTS2 = 0x0C;
     public static final int PING = 0x0D;
@@ -218,6 +219,17 @@
     public static final int FOLDER_COUNT = FOLDER_PAGE + 0x17;
     public static final int FOLDER_VERSION = FOLDER_PAGE + 0x18;
 
+    public static final int MREQ_PAGE = MREQ << PAGE_SHIFT;
+    public static final int MREQ_CAL_ID = MREQ_PAGE + 5;
+    public static final int MREQ_COLLECTION_ID = MREQ_PAGE + 6;
+    public static final int MREQ_MEETING_RESPONSE = MREQ_PAGE + 7;
+    public static final int MREQ_REQ_ID = MREQ_PAGE + 8;
+    public static final int MREQ_REQUEST = MREQ_PAGE + 9;
+    public static final int MREQ_RESULT = MREQ_PAGE + 0xA;
+    public static final int MREQ_STATUS = MREQ_PAGE + 0xB;
+    public static final int MREQ_USER_RESPONSE = MREQ_PAGE + 0xC;
+    public static final int MREQ_VERSION = MREQ_PAGE + 0xD;
+
     public static final int EMAIL_PAGE = EMAIL << PAGE_SHIFT;
     public static final int EMAIL_ATTACHMENT = EMAIL_PAGE + 5;
     public static final int EMAIL_ATTACHMENTS = EMAIL_PAGE + 6;
@@ -441,6 +453,8 @@
         },
         {
             // 0x08 MeetingResponse
+            "CalId", "CollectionId", "MeetingResponse", "ReqId", "Request",
+            "Result", "Status", "UserResponse", "Version"
         },
         {
             // 0x09 Tasks
diff --git a/src/com/android/exchange/utility/CalendarUtilities.java b/src/com/android/exchange/utility/CalendarUtilities.java
new file mode 100644
index 0000000..e9945b7
--- /dev/null
+++ b/src/com/android/exchange/utility/CalendarUtilities.java
@@ -0,0 +1,728 @@
+/*
+ * Copyright (C) 2010 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.exchange.utility;
+
+import com.android.exchange.Eas;
+import com.android.exchange.adapter.Serializer;
+import com.android.exchange.adapter.Tags;
+
+import org.bouncycastle.util.encoders.Base64;
+
+import android.util.Log;
+
+import java.io.IOException;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.GregorianCalendar;
+import java.util.HashMap;
+import java.util.TimeZone;
+
+public class CalendarUtilities {
+    // NOTE: Most definitions in this class are have package visibility for testing purposes
+    private static final String TAG = "CalendarUtility";
+
+    // Time related convenience constants, in milliseconds
+    static final int SECONDS = 1000;
+    static final int MINUTES = SECONDS*60;
+    static final int HOURS = MINUTES*60;
+    static final long DAYS = HOURS*24;
+
+    // NOTE All Microsoft data structures are little endian
+
+    // The following constants relate to standard Microsoft data sizes
+    // For documentation, see http://msdn.microsoft.com/en-us/library/aa505945.aspx
+    static final int MSFT_LONG_SIZE = 4;
+    static final int MSFT_WCHAR_SIZE = 2;
+    static final int MSFT_WORD_SIZE = 2;
+
+    // The following constants relate to Microsoft's SYSTEMTIME structure
+    // For documentation, see: http://msdn.microsoft.com/en-us/library/ms724950(VS.85).aspx?ppud=4
+
+    static final int MSFT_SYSTEMTIME_YEAR = 0 * MSFT_WORD_SIZE;
+    static final int MSFT_SYSTEMTIME_MONTH = 1 * MSFT_WORD_SIZE;
+    static final int MSFT_SYSTEMTIME_DAY_OF_WEEK = 2 * MSFT_WORD_SIZE;
+    static final int MSFT_SYSTEMTIME_DAY = 3 * MSFT_WORD_SIZE;
+    static final int MSFT_SYSTEMTIME_HOUR = 4 * MSFT_WORD_SIZE;
+    static final int MSFT_SYSTEMTIME_MINUTE = 5 * MSFT_WORD_SIZE;
+    //static final int MSFT_SYSTEMTIME_SECONDS = 6 * MSFT_WORD_SIZE;
+    //static final int MSFT_SYSTEMTIME_MILLIS = 7 * MSFT_WORD_SIZE;
+    static final int MSFT_SYSTEMTIME_SIZE = 8*MSFT_WORD_SIZE;
+
+    // The following constants relate to Microsoft's TIME_ZONE_INFORMATION structure
+    // For documentation, see http://msdn.microsoft.com/en-us/library/ms725481(VS.85).aspx
+    static final int MSFT_TIME_ZONE_BIAS_OFFSET = 0;
+    static final int MSFT_TIME_ZONE_STANDARD_NAME_OFFSET =
+        MSFT_TIME_ZONE_BIAS_OFFSET + MSFT_LONG_SIZE;
+    static final int MSFT_TIME_ZONE_STANDARD_DATE_OFFSET =
+        MSFT_TIME_ZONE_STANDARD_NAME_OFFSET + (MSFT_WCHAR_SIZE*32);
+    static final int MSFT_TIME_ZONE_STANDARD_BIAS_OFFSET =
+        MSFT_TIME_ZONE_STANDARD_DATE_OFFSET + MSFT_SYSTEMTIME_SIZE;
+    static final int MSFT_TIME_ZONE_DAYLIGHT_NAME_OFFSET =
+        MSFT_TIME_ZONE_STANDARD_BIAS_OFFSET + MSFT_LONG_SIZE;
+    static final int MSFT_TIME_ZONE_DAYLIGHT_DATE_OFFSET =
+        MSFT_TIME_ZONE_DAYLIGHT_NAME_OFFSET + (MSFT_WCHAR_SIZE*32);
+    static final int MSFT_TIME_ZONE_DAYLIGHT_BIAS_OFFSET =
+        MSFT_TIME_ZONE_DAYLIGHT_DATE_OFFSET + MSFT_SYSTEMTIME_SIZE;
+    static final int MSFT_TIME_ZONE_SIZE =
+        MSFT_TIME_ZONE_DAYLIGHT_BIAS_OFFSET + MSFT_LONG_SIZE;
+
+    // TimeZone cache; we parse/decode as little as possible, because the process is quite slow
+    private static HashMap<String, TimeZone> sTimeZoneCache = new HashMap<String, TimeZone>();
+    // TZI string cache; we keep around our encoded TimeZoneInformation strings
+    private static HashMap<TimeZone, String> sTziStringCache = new HashMap<TimeZone, String>();
+
+    // There is no type 4 (thus, the "")
+    static final String[] sTypeToFreq =
+        new String[] {"DAILY", "WEEKLY", "MONTHLY", "MONTHLY", "", "YEARLY", "YEARLY"};
+
+    static final String[] sDayTokens =
+        new String[] {"SU", "MO", "TU", "WE", "TH", "FR", "SA"};
+
+    static final String[] sTwoCharacterNumbers =
+        new String[] {"00", "01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12"};
+
+    static final int sCurrentYear = new GregorianCalendar().get(Calendar.YEAR);
+    static final TimeZone sGmtTimeZone = TimeZone.getTimeZone("GMT");
+
+    // Return a 4-byte long from a byte array (little endian)
+    static int getLong(byte[] bytes, int offset) {
+        return (bytes[offset++] & 0xFF) | ((bytes[offset++] & 0xFF) << 8) |
+        ((bytes[offset++] & 0xFF) << 16) | ((bytes[offset] & 0xFF) << 24);
+    }
+
+    // Put a 4-byte long into a byte array (little endian)
+    static void setLong(byte[] bytes, int offset, int value) {
+        bytes[offset++] = (byte) (value & 0xFF);
+        bytes[offset++] = (byte) ((value >> 8) & 0xFF);
+        bytes[offset++] = (byte) ((value >> 16) & 0xFF);
+        bytes[offset] = (byte) ((value >> 24) & 0xFF);
+    }
+
+    // Return a 2-byte word from a byte array (little endian)
+    static int getWord(byte[] bytes, int offset) {
+        return (bytes[offset++] & 0xFF) | ((bytes[offset] & 0xFF) << 8);
+    }
+
+    // Put a 2-byte word into a byte array (little endian)
+    static void setWord(byte[] bytes, int offset, int value) {
+        bytes[offset++] = (byte) (value & 0xFF);
+        bytes[offset] = (byte) ((value >> 8) & 0xFF);
+    }
+
+    // Internal structure for storing a time zone date from a SYSTEMTIME structure
+    // This date represents either the start or the end time for DST
+    static class TimeZoneDate {
+        String year;
+        int month;
+        int dayOfWeek;
+        int day;
+        int time;
+        int hour;
+        int minute;
+    }
+
+    // Write SYSTEMTIME data into a byte array (this will either be for the standard or daylight
+    // transition)
+    static void putTimeInMillisIntoSystemTime(byte[] bytes, int offset, long millis) {
+        GregorianCalendar cal = new GregorianCalendar(TimeZone.getDefault());
+        // Round to the next highest minute; we always write seconds as zero
+        cal.setTimeInMillis(millis + 30*SECONDS);
+
+        // MSFT months are 1 based; TimeZone is 0 based
+        setWord(bytes, offset + MSFT_SYSTEMTIME_MONTH, cal.get(Calendar.MONTH) + 1);
+        // MSFT day of week starts w/ Sunday = 0; TimeZone starts w/ Sunday = 1
+        setWord(bytes, offset + MSFT_SYSTEMTIME_DAY_OF_WEEK, cal.get(Calendar.DAY_OF_WEEK) - 1);
+
+        // Get the "day" in TimeZone format
+        int wom = cal.get(Calendar.DAY_OF_WEEK_IN_MONTH);
+        // 5 means "last" in MSFT land; for TimeZone, it's -1
+        setWord(bytes, offset + MSFT_SYSTEMTIME_DAY, wom < 0 ? 5 : wom);
+
+        // Turn hours/minutes into ms from midnight (per TimeZone)
+        setWord(bytes, offset + MSFT_SYSTEMTIME_HOUR, cal.get(Calendar.HOUR));
+        setWord(bytes, offset + MSFT_SYSTEMTIME_MINUTE, cal.get(Calendar.MINUTE));
+     }
+
+    // Build a TimeZoneDate structure from a SYSTEMTIME within a byte array at a given offset
+    static TimeZoneDate getTimeZoneDateFromSystemTime(byte[] bytes, int offset) {
+        TimeZoneDate tzd = new TimeZoneDate();
+
+        // MSFT year is an int; TimeZone is a String
+        int num = getWord(bytes, offset + MSFT_SYSTEMTIME_YEAR);
+        tzd.year = Integer.toString(num);
+
+        // MSFT month = 0 means no daylight time
+        // MSFT months are 1 based; TimeZone is 0 based
+        num = getWord(bytes, offset + MSFT_SYSTEMTIME_MONTH);
+        if (num == 0) {
+            return null;
+        } else {
+            tzd.month = num -1;
+        }
+
+        // MSFT day of week starts w/ Sunday = 0; TimeZone starts w/ Sunday = 1
+        tzd.dayOfWeek = getWord(bytes, offset + MSFT_SYSTEMTIME_DAY_OF_WEEK) + 1;
+
+        // Get the "day" in TimeZone format
+        num = getWord(bytes, offset + MSFT_SYSTEMTIME_DAY);
+        // 5 means "last" in MSFT land; for TimeZone, it's -1
+        if (num == 5) {
+            tzd.day = -1;
+        } else {
+            tzd.day = num;
+        }
+
+        // Turn hours/minutes into ms from midnight (per TimeZone)
+        int hour = getWord(bytes, offset + MSFT_SYSTEMTIME_HOUR);
+        tzd.hour = hour;
+        int minute = getWord(bytes, offset + MSFT_SYSTEMTIME_MINUTE);
+        tzd.minute = minute;
+        tzd.time = (hour*HOURS) + (minute*MINUTES);
+
+        return tzd;
+    }
+
+    // Return a String from within a byte array at the given offset with max characters
+    // Unused for now, but might be helpful for debugging
+    //    String getString(byte[] bytes, int offset, int max) {
+    //    	StringBuilder sb = new StringBuilder();
+    //    	while (max-- > 0) {
+    //    		int b = bytes[offset];
+    //    		if (b == 0) break;
+    //    		sb.append((char)b);
+    //    		offset += 2;
+    //    	}
+    //    	return sb.toString();
+    //    }
+
+    /**
+     * Build a GregorianCalendar, based on a time zone and TimeZoneDate.
+     * @param timeZone the time zone we're checking
+     * @param tzd the TimeZoneDate we're interested in
+     * @return a GregorianCalendar with the given time zone and date
+     */
+    static GregorianCalendar getCheckCalendar(TimeZone timeZone, TimeZoneDate tzd) {
+        GregorianCalendar testCalendar = new GregorianCalendar(timeZone);
+        testCalendar.set(GregorianCalendar.YEAR, sCurrentYear);
+        testCalendar.set(GregorianCalendar.MONTH, tzd.month);
+        testCalendar.set(GregorianCalendar.DAY_OF_WEEK, tzd.dayOfWeek);
+        testCalendar.set(GregorianCalendar.DAY_OF_WEEK_IN_MONTH, tzd.day);
+        testCalendar.set(GregorianCalendar.HOUR_OF_DAY, tzd.hour);
+        testCalendar.set(GregorianCalendar.MINUTE, tzd.minute);
+        return testCalendar;
+    }
+
+    /**
+     * Find a standard/daylight transition between a start time and an end time
+     * @param tz a TimeZone
+     * @param startTime the start time for the test
+     * @param endTime the end time for the test
+     * @param startInDaylightTime whether daylight time is in effect at the startTime
+     * @return the time in millis of the first transition, or 0 if none
+     */
+    static private long findTransition(TimeZone tz, long startTime, long endTime,
+            boolean startInDaylightTime) {
+        long startingEndTime = endTime;
+        Date date = null;
+        while ((endTime - startTime) > MINUTES) {
+            long checkTime = ((startTime + endTime) / 2) + 1;
+            date = new Date(checkTime);
+            if (tz.inDaylightTime(date) != startInDaylightTime) {
+                endTime = checkTime;
+            } else {
+                startTime = checkTime;
+            }
+        }
+        if (endTime == startingEndTime) {
+            // Really, this shouldn't happen
+            return 0;
+        }
+        return startTime;
+    }
+
+    /**
+     * Return a Base64 representation of a MSFT TIME_ZONE_INFORMATION structure from a TimeZone
+     * that might be found in an Event; use cached result, if possible
+     * @param tz the TimeZone
+     * @return the Base64 String representing a Microsoft TIME_ZONE_INFORMATION element
+     */
+    static public String timeZoneToTziString(TimeZone tz) {
+        String tziString = sTziStringCache.get(tz);
+        if (tziString != null) {
+            if (Eas.USER_LOG) {
+                Log.d(TAG, "TZI string for " + tz.getDisplayName() + " found in cache.");
+            }
+            return tziString;
+        }
+        tziString = timeZoneToTziStringImpl(tz);
+        sTziStringCache.put(tz, tziString);
+        return tziString;
+    }
+
+    /**
+     * Calculate the Base64 representation of a MSFT TIME_ZONE_INFORMATION structure from a TimeZone
+     * that might be found in an Event.  Since the internal representation of the TimeZone is hidden
+     * from us we'll find the DST transitions and build the structure from that information
+     * @param tz the TimeZone
+     * @return the Base64 String representing a Microsoft TIME_ZONE_INFORMATION element
+     */
+    static public String timeZoneToTziStringImpl(TimeZone tz) {
+        String tziString;
+        long time = System.currentTimeMillis();
+        byte[] tziBytes = new byte[MSFT_TIME_ZONE_SIZE];
+        int standardBias = - tz.getRawOffset();
+        standardBias /= 60*SECONDS;
+        setLong(tziBytes, MSFT_TIME_ZONE_BIAS_OFFSET, standardBias);
+        // If this time zone has daylight savings time, we need to do a bunch more work
+        if (tz.useDaylightTime()) {
+            long standardTransition = 0;
+            long daylightTransition = 0;
+            GregorianCalendar cal = new GregorianCalendar();
+            cal.set(sCurrentYear, Calendar.JANUARY, 1, 0, 0, 0);
+            cal.setTimeZone(tz);
+            long startTime = cal.getTimeInMillis();
+            // Calculate rough end of year; no need to do the calculation
+            long endOfYearTime = startTime + 365*DAYS;
+            Date date = new Date(startTime);
+            boolean startInDaylightTime = tz.inDaylightTime(date);
+            // Find the first transition, and store
+            startTime = findTransition(tz, startTime, endOfYearTime, startInDaylightTime);
+            if (startInDaylightTime) {
+                standardTransition = startTime;
+            } else {
+                daylightTransition = startTime;
+            }
+            // Find the second transition, and store
+            startTime = findTransition(tz, startTime, endOfYearTime, !startInDaylightTime);
+            if (startInDaylightTime) {
+                daylightTransition = startTime;
+            } else {
+                standardTransition = startTime;
+            }
+            if (standardTransition != 0 && daylightTransition != 0) {
+                putTimeInMillisIntoSystemTime(tziBytes, MSFT_TIME_ZONE_STANDARD_DATE_OFFSET,
+                        standardTransition);
+                putTimeInMillisIntoSystemTime(tziBytes, MSFT_TIME_ZONE_DAYLIGHT_DATE_OFFSET,
+                        daylightTransition);
+                int dstOffset = tz.getDSTSavings();
+                setLong(tziBytes, MSFT_TIME_ZONE_DAYLIGHT_BIAS_OFFSET, - dstOffset / MINUTES);
+            }
+        }
+        // TODO Use a more efficient Base64 API
+        byte[] tziEncodedBytes = Base64.encode(tziBytes);
+        tziString = new String(tziEncodedBytes);
+        if (Eas.USER_LOG) {
+            Log.d(TAG, "Calculated TZI String for " + tz.getDisplayName() + " in " +
+                    (System.currentTimeMillis() - time) + "ms");
+        }
+        return tziString;
+    }
+
+    /**
+     * Given a String as directly read from EAS, returns a TimeZone corresponding to that String
+     * @param timeZoneString the String read from the server
+     * @return the TimeZone, or TimeZone.getDefault() if not found
+     */
+    static public TimeZone tziStringToTimeZone(String timeZoneString) {
+        // If we have this time zone cached, use that value and return
+        TimeZone timeZone = sTimeZoneCache.get(timeZoneString);
+        if (timeZone != null) {
+            if (Eas.USER_LOG) {
+                Log.d(TAG, " Using cached TimeZone " + timeZone.getDisplayName());
+            }
+        } else {
+            timeZone = tziStringToTimeZoneImpl(timeZoneString);
+            if (timeZone == null) {
+                // If we don't find a match, we just return the current TimeZone.  In theory, this
+                // shouldn't be happening...
+                Log.w(TAG, "TimeZone not found using default: " + timeZoneString);
+                timeZone = TimeZone.getDefault();
+            }
+            sTimeZoneCache.put(timeZoneString, timeZone);
+        }
+        return timeZone;
+    }
+
+    /**
+     * Given a String as directly read from EAS, tries to find a TimeZone in the database of all
+     * time zones that corresponds to that String.
+     * @param timeZoneString the String read from the server
+     * @return the TimeZone, or TimeZone.getDefault() if not found
+     */
+    static public TimeZone tziStringToTimeZoneImpl(String timeZoneString) {
+        TimeZone timeZone = null;
+        // TODO Remove after we're comfortable with performance
+        long time = System.currentTimeMillis();
+        // First, we need to decode the base64 string
+        byte[] timeZoneBytes = Base64.decode(timeZoneString);
+
+        // Then, we get the bias (similar to a rawOffset); for TimeZone, we need ms
+        // but EAS gives us minutes, so do the conversion.  Note that EAS is the bias that's added
+        // to the time zone to reach UTC; our library uses the time from UTC to our time zone, so
+        // we need to change the sign
+        int bias = -1 * getLong(timeZoneBytes, MSFT_TIME_ZONE_BIAS_OFFSET) * MINUTES;
+
+        // Get all of the time zones with the bias as a rawOffset; if there aren't any, we return
+        // the default time zone
+        String[] zoneIds = TimeZone.getAvailableIDs(bias);
+        if (zoneIds.length > 0) {
+            // Try to find an existing TimeZone from the data provided by EAS
+            // We start by pulling out the date that standard time begins
+            TimeZoneDate dstEnd =
+                getTimeZoneDateFromSystemTime(timeZoneBytes, MSFT_TIME_ZONE_STANDARD_DATE_OFFSET);
+            if (dstEnd == null) {
+                // In this case, there is no daylight savings time, so the only interesting data
+                // is the offset, and we know that all of the zoneId's match; we'll take the first
+                timeZone = TimeZone.getTimeZone(zoneIds[0]);
+                String dn = timeZone.getDisplayName();
+                sTimeZoneCache.put(timeZoneString, timeZone);
+                if (Eas.USER_LOG) {
+                    Log.d(TAG, "TimeZone without DST found by offset: " + dn);
+                }
+            } else {
+                TimeZoneDate dstStart = getTimeZoneDateFromSystemTime(timeZoneBytes,
+                        MSFT_TIME_ZONE_DAYLIGHT_DATE_OFFSET);
+                // See comment above for bias...
+                long dstSavings =
+                    -1 * getLong(timeZoneBytes, MSFT_TIME_ZONE_DAYLIGHT_BIAS_OFFSET) * MINUTES;
+
+                // We'll go through each time zone to find one with the same DST transitions and
+                // savings length
+                for (String zoneId: zoneIds) {
+                    // Get the TimeZone using the zoneId
+                    timeZone = TimeZone.getTimeZone(zoneId);
+
+                    // Our strategy here is to check just before and just after the transitions
+                    // and see whether the check for daylight time matches the expectation
+                    // If both transitions match, then we have a match for the offset and start/end
+                    // of dst.  That's the best we can do for now, since there's no other info
+                    // provided by EAS (i.e. we can't get dynamic transitions, etc.)
+
+                    int testSavingsMinutes = timeZone.getDSTSavings() / MINUTES;
+                    int errorBoundsMinutes = (testSavingsMinutes * 2) + 1;
+
+                    // Check start DST transition
+                    GregorianCalendar testCalendar = getCheckCalendar(timeZone, dstStart);
+                    testCalendar.add(GregorianCalendar.MINUTE, - errorBoundsMinutes);
+                    Date before = testCalendar.getTime();
+                    testCalendar.add(GregorianCalendar.MINUTE, 2*errorBoundsMinutes);
+                    Date after = testCalendar.getTime();
+                    if (timeZone.inDaylightTime(before)) continue;
+                    if (!timeZone.inDaylightTime(after)) continue;
+
+                    // Check end DST transition
+                    testCalendar = getCheckCalendar(timeZone, dstEnd);
+                    testCalendar.add(GregorianCalendar.MINUTE, - errorBoundsMinutes);
+                    before = testCalendar.getTime();
+                    testCalendar.add(GregorianCalendar.MINUTE, 2*errorBoundsMinutes);
+                    after = testCalendar.getTime();
+                    if (!timeZone.inDaylightTime(before)) continue;
+                    if (timeZone.inDaylightTime(after)) continue;
+
+                    // Check that the savings are the same
+                    if (dstSavings != timeZone.getDSTSavings()) continue;
+
+                    // If we're here, it's the right time zone, modulo dynamic DST
+                    String dn = timeZone.getDisplayName();
+                    // TODO Remove timing when we're comfortable with performance
+                    if (Eas.USER_LOG) {
+                        Log.d(TAG, "TimeZone found by rules: " + dn + " in " +
+                                (System.currentTimeMillis() - time) + "ms");
+                    }
+                    break;
+                }
+            }
+        }
+        return timeZone;
+    }
+
+    /**
+     * Generate a time in milliseconds from a date string that represents a date/time in GMT
+     * @param DateTime string from Exchange server
+     * @return the time in milliseconds (since Jan 1, 1970)
+     */
+    static public long parseDateTimeToMillis(String date) {
+        // Format for calendar date strings is 20090211T180303Z
+        GregorianCalendar cal = new GregorianCalendar(Integer.parseInt(date.substring(0, 4)),
+                Integer.parseInt(date.substring(4, 6)) - 1, Integer.parseInt(date.substring(6, 8)),
+                Integer.parseInt(date.substring(9, 11)), Integer.parseInt(date.substring(11, 13)),
+                Integer.parseInt(date.substring(13, 15)));
+        cal.setTimeZone(TimeZone.getTimeZone("GMT"));
+        return cal.getTimeInMillis();
+    }
+
+    /**
+     * Generate a GregorianCalendar from a date string that represents a date/time in GMT
+     * @param DateTime string from Exchange server
+     * @return the GregorianCalendar
+     */
+    static public GregorianCalendar parseDateTimeToCalendar(String date) {
+        // Format for calendar date strings is 20090211T180303Z
+        GregorianCalendar cal = new GregorianCalendar(Integer.parseInt(date.substring(0, 4)),
+                Integer.parseInt(date.substring(4, 6)) - 1, Integer.parseInt(date.substring(6, 8)),
+                Integer.parseInt(date.substring(9, 11)), Integer.parseInt(date.substring(11, 13)),
+                Integer.parseInt(date.substring(13, 15)));
+        cal.setTimeZone(TimeZone.getTimeZone("GMT"));
+        return cal;
+    }
+
+    static String formatTwo(int num) {
+        if (num <= 12) {
+            return sTwoCharacterNumbers[num];
+        } else
+            return Integer.toString(num);
+    }
+
+    /**
+     * Generate an EAS formatted date/time string based on GMT. See below for details.
+     */
+    static public String millisToEasDateTime(long millis) {
+        return millisToEasDateTime(millis, sGmtTimeZone);
+    }
+
+    /**
+     * Generate an EAS formatted local date/time string from a time and a time zone
+     * @param millis a time in milliseconds
+     * @param tz a time zone
+     * @return an EAS formatted string indicating the date/time in the given time zone
+     */
+    static public String millisToEasDateTime(long millis, TimeZone tz) {
+        StringBuilder sb = new StringBuilder();
+        GregorianCalendar cal = new GregorianCalendar(tz);
+        cal.setTimeInMillis(millis);
+        sb.append(cal.get(Calendar.YEAR));
+        sb.append(formatTwo(cal.get(Calendar.MONTH) + 1));
+        sb.append(formatTwo(cal.get(Calendar.DAY_OF_MONTH)));
+        sb.append('T');
+        sb.append(formatTwo(cal.get(Calendar.HOUR_OF_DAY)));
+        sb.append(formatTwo(cal.get(Calendar.MINUTE)));
+        sb.append(formatTwo(cal.get(Calendar.SECOND)));
+        sb.append('Z');
+        return sb.toString();
+    }
+
+    static void addByDay(StringBuilder rrule, int dow, int wom) {
+        rrule.append(";BYDAY=");
+        boolean addComma = false;
+        for (int i = 0; i < 7; i++) {
+            if ((dow & 1) == 1) {
+                if (addComma) {
+                    rrule.append(',');
+                }
+                if (wom > 0) {
+                    // 5 = last week -> -1
+                    // So -1SU = last sunday
+                    rrule.append(wom == 5 ? -1 : wom);
+                }
+                rrule.append(sDayTokens[i]);
+                addComma = true;
+            }
+            dow >>= 1;
+        }
+    }
+
+    static void addByMonthDay(StringBuilder rrule, int dom) {
+        // 127 means last day of the month
+        if (dom == 127) {
+            dom = -1;
+        }
+        rrule.append(";BYMONTHDAY=" + dom);
+    }
+
+    /**
+     * Generate the String version of the EAS integer for a given BYDAY value in an rrule
+     * @param dow the BYDAY value of the rrule
+     * @return the String version of the EAS value of these days
+     */
+    static String generateEasDayOfWeek(String dow) {
+        int bits = 0;
+        int bit = 1;
+        for (String token: sDayTokens) {
+            // If we can find the day in the dow String, add the bit to our bits value
+            if (dow.indexOf(token) >= 0) {
+                bits |= bit;
+            }
+            bit <<= 1;
+        }
+        return Integer.toString(bits);
+    }
+
+    /**
+     * Extract the value of a token in an RRULE string
+     * @param rrule an RRULE string
+     * @param token a token to look for in the RRULE
+     * @return the value of that token
+     */
+    static String tokenFromRrule(String rrule, String token) {
+        int start = rrule.indexOf(token);
+        if (start < 0) return null;
+        int len = rrule.length();
+        start += token.length();
+        int end = start;
+        char c;
+        do {
+            c = rrule.charAt(end++);
+            if (!Character.isLetterOrDigit(c) || (end == len)) {
+                if (end == len) end++;
+                return rrule.substring(start, end -1);
+            }
+        } while (true);
+    }
+
+    /**
+     * Write recurrence information to EAS based on the RRULE in CalendarProvider
+     * @param rrule the RRULE, from CalendarProvider
+     * @param startTime, the DTSTART of this Event
+     * @param s the Serializer we're using to write WBXML data
+     * @throws IOException
+     */
+    // NOTE: For the moment, we're only parsing recurrence types that are supported by the
+    // Calendar app UI, which is a small subset of possible recurrence types
+    // This code must be updated when the Calendar adds new functionality
+    static public void recurrenceFromRrule(String rrule, long startTime, Serializer s)
+    throws IOException {
+        Log.d("RRULE", "rule: " + rrule);
+        String freq = tokenFromRrule(rrule, "FREQ=");
+        // If there's no FREQ=X, then we don't write a recurrence
+        // Note that we duplicate s.start(Tags.CALENDAR_RECURRENCE); s.end(); to prevent the
+        // possibility of writing out a partial recurrence stanza
+        if (freq != null) {
+            if (freq.equals("DAILY")) {
+                s.start(Tags.CALENDAR_RECURRENCE);
+                s.data(Tags.CALENDAR_RECURRENCE_TYPE, "0");
+                s.data(Tags.CALENDAR_RECURRENCE_INTERVAL, "1");
+                s.end();
+            } else if (freq.equals("WEEKLY")) {
+                s.start(Tags.CALENDAR_RECURRENCE);
+                s.data(Tags.CALENDAR_RECURRENCE_TYPE, "1");
+                s.data(Tags.CALENDAR_RECURRENCE_INTERVAL, "1");
+                // Requires a day of week (whereas RRULE does not)
+                String byDay = tokenFromRrule(rrule, "BYDAY=");
+                if (byDay != null) {
+                    s.data(Tags.CALENDAR_RECURRENCE_DAYOFWEEK, generateEasDayOfWeek(byDay));
+                }
+                s.end();
+            } else if (freq.equals("MONTHLY")) {
+                String byMonthDay = tokenFromRrule(rrule, "BYMONTHDAY=");
+                if (byMonthDay != null) {
+                    // The nth day of the month
+                    s.start(Tags.CALENDAR_RECURRENCE);
+                    s.data(Tags.CALENDAR_RECURRENCE_TYPE, "2");
+                    s.data(Tags.CALENDAR_RECURRENCE_DAYOFMONTH, byMonthDay);
+                    s.end();
+                } else {
+                    String byDay = tokenFromRrule(rrule, "BYDAY=");
+                    String bareByDay;
+                    if (byDay != null) {
+                        // This can be 1WE (1st Wednesday) or -1FR (last Friday)
+                        int wom = byDay.charAt(0);
+                        if (wom == '-') {
+                            // -1 is the only legal case (last week) Use "5" for EAS
+                            wom = 5;
+                            bareByDay = byDay.substring(2);
+                        } else {
+                            wom = wom - '0';
+                            bareByDay = byDay.substring(1);
+                        }
+                        s.start(Tags.CALENDAR_RECURRENCE);
+                        s.data(Tags.CALENDAR_RECURRENCE_TYPE, "3");
+                        s.data(Tags.CALENDAR_RECURRENCE_WEEKOFMONTH, Integer.toString(wom));
+                        s.data(Tags.CALENDAR_RECURRENCE_DAYOFWEEK, generateEasDayOfWeek(bareByDay));
+                        s.end();
+                    }
+                }
+            } else if (freq.equals("YEARLY")) {
+                String byMonth = tokenFromRrule(rrule, "BYMONTH=");
+                String byMonthDay = tokenFromRrule(rrule, "BYMONTHDAY=");
+                if (byMonth == null || byMonthDay == null) {
+                    // Calculate the month and day from the startDate
+                    GregorianCalendar cal = new GregorianCalendar();
+                    cal.setTimeInMillis(startTime);
+                    cal.setTimeZone(TimeZone.getDefault());
+                    byMonth = Integer.toString(cal.get(Calendar.MONTH) + 1);
+                    byMonthDay = Integer.toString(cal.get(Calendar.DAY_OF_MONTH));
+                }
+                s.start(Tags.CALENDAR_RECURRENCE);
+                s.data(Tags.CALENDAR_RECURRENCE_TYPE, "5");
+                s.data(Tags.CALENDAR_RECURRENCE_DAYOFMONTH, byMonthDay);
+                s.data(Tags.CALENDAR_RECURRENCE_MONTHOFYEAR, byMonth);
+                s.end();
+            }
+        }
+    }
+
+    /**
+     * Build an RRULE String from EAS recurrence information
+     * @param type the type of recurrence
+     * @param occurrences how many recurrences (instances)
+     * @param interval the interval between recurrences
+     * @param dow day of the week
+     * @param dom day of the month
+     * @param wom week of the month
+     * @param moy month of the year
+     * @param until the last recurrence time
+     * @return a valid RRULE String
+     */
+    static public String rruleFromRecurrence(int type, int occurrences, int interval, int dow,
+            int dom, int wom, int moy, String until) {
+        StringBuilder rrule = new StringBuilder("FREQ=" + sTypeToFreq[type]);
+
+        // INTERVAL and COUNT
+        if (interval > 0) {
+            rrule.append(";INTERVAL=" + interval);
+        }
+        if (occurrences > 0) {
+            rrule.append(";COUNT=" + occurrences);
+        }
+
+        // Days, weeks, months, etc.
+        switch(type) {
+            case 0: // DAILY
+            case 1: // WEEKLY
+                if (dow > 0) addByDay(rrule, dow, -1);
+                break;
+            case 2: // MONTHLY
+                if (dom > 0) addByMonthDay(rrule, dom);
+                break;
+            case 3: // MONTHLY (on the nth day)
+                if (dow > 0) addByDay(rrule, dow, wom);
+                break;
+            case 5: // YEARLY
+                if (dom > 0) addByMonthDay(rrule, dom);
+                if (moy > 0) {
+                    // TODO MAKE SURE WE'RE 1 BASED
+                    rrule.append(";BYMONTH=" + moy);
+                }
+                break;
+            case 6: // YEARLY (on the nth day)
+                if (dow > 0) addByDay(rrule, dow, wom);
+                if (moy > 0) addByMonthDay(rrule, dow);
+                break;
+            default:
+                break;
+        }
+
+        // UNTIL comes last
+        // TODO Add UNTIL code
+        if (until != null) {
+            // *** until probably needs reformatting
+            //rrule.append(";UNTIL=" + until);
+        }
+
+        return rrule.toString();
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/exchange/utility/Duration.java b/src/com/android/exchange/utility/Duration.java
new file mode 100644
index 0000000..0ec867c
--- /dev/null
+++ b/src/com/android/exchange/utility/Duration.java
@@ -0,0 +1,128 @@
+/* Copyright 2010, 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.exchange.utility;
+
+import android.pim.DateException;
+
+import java.util.Calendar;
+
+/**
+ * Note: This class was simply copied from the class in CalendarProvider, since we don't have access
+ * to it from the Email app.  I reformated some lines, but otherwise haven't altered the code.
+ */
+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 += (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) {
+        return dt + getMillis();
+    }
+
+    public long getMillis() {
+        long factor = 1000 * sign;
+        return factor * ((7*24*60*60*weeks) + (24*60*60*days) + (60*60*hours) + (60*minutes) +
+                seconds);
+    }
+}
diff --git a/tests/src/com/android/exchange/EasSyncServiceTests.java b/tests/src/com/android/exchange/EasSyncServiceTests.java
index 45f4882..d4372a5 100644
--- a/tests/src/com/android/exchange/EasSyncServiceTests.java
+++ b/tests/src/com/android/exchange/EasSyncServiceTests.java
@@ -73,6 +73,4 @@
             }
         }
     }
-
-
 }
diff --git a/tests/src/com/android/exchange/SyncManagerAccountTests.java b/tests/src/com/android/exchange/SyncManagerAccountTests.java
index 9a1f24b..66c20e9 100644
--- a/tests/src/com/android/exchange/SyncManagerAccountTests.java
+++ b/tests/src/com/android/exchange/SyncManagerAccountTests.java
@@ -17,6 +17,7 @@
 
 package com.android.exchange;
 
+import com.android.email.Email;
 import com.android.email.provider.EmailContent;
 import com.android.email.provider.EmailProvider;
 import com.android.email.provider.ProviderTestUtils;
@@ -63,7 +64,7 @@
     }
 
     private android.accounts.Account makeAccountManagerAccount(String username) {
-        return new android.accounts.Account(username, Eas.ACCOUNT_MANAGER_TYPE);
+        return new android.accounts.Account(username, Email.EXCHANGE_ACCOUNT_MANAGER_TYPE);
     }
 
     private void createAccountManagerAccount(String username) {
@@ -105,7 +106,7 @@
 
     private void deleteTemporaryAccountManagerAccounts(Context context) {
         android.accounts.Account[] accountManagerAccounts =
-            AccountManager.get(context).getAccountsByType(Eas.ACCOUNT_MANAGER_TYPE);
+                AccountManager.get(context).getAccountsByType(Email.EXCHANGE_ACCOUNT_MANAGER_TYPE);
         for (android.accounts.Account accountManagerAccount: accountManagerAccounts) {
             if (accountManagerAccount.name.startsWith(TEST_ACCOUNT_PREFIX) &&
                     accountManagerAccount.name.endsWith(TEST_ACCOUNT_SUFFIX)) {
@@ -135,7 +136,7 @@
         // Check that they're set up properly
         assertEquals(3, EmailContent.count(mMockContext, Account.CONTENT_URI, null, null));
         android.accounts.Account[] accountManagerAccounts =
-            AccountManager.get(context).getAccountsByType(Eas.ACCOUNT_MANAGER_TYPE);
+                AccountManager.get(context).getAccountsByType(Email.EXCHANGE_ACCOUNT_MANAGER_TYPE);
         assertEquals(3, accountManagerAccounts.length);
 
         // Delete account "2" from AccountManager
@@ -145,7 +146,7 @@
 
         // Confirm it's deleted
         accountManagerAccounts =
-            AccountManager.get(context).getAccountsByType(Eas.ACCOUNT_MANAGER_TYPE);
+                AccountManager.get(context).getAccountsByType(Email.EXCHANGE_ACCOUNT_MANAGER_TYPE);
         assertEquals(2, accountManagerAccounts.length);
 
         // Run the reconciler
@@ -169,8 +170,8 @@
                 makeSyncManagerAccountList(), accountManagerAccounts);
 
         // There should now be only one AccountManager account
-        accountManagerAccounts =
-            AccountManager.get(getContext()).getAccountsByType(Eas.ACCOUNT_MANAGER_TYPE);
+        accountManagerAccounts = AccountManager.get(getContext()).getAccountsByType(
+                Email.EXCHANGE_ACCOUNT_MANAGER_TYPE);
         assertEquals(1, accountManagerAccounts.length);
         // ... and it should be account "3"
         assertEquals(getTestAccountEmailAddress("3"), accountManagerAccounts[0].name);
diff --git a/tests/src/com/android/exchange/adapter/CalendarSyncAdapterTests.java b/tests/src/com/android/exchange/adapter/CalendarSyncAdapterTests.java
new file mode 100644
index 0000000..7f380d5
--- /dev/null
+++ b/tests/src/com/android/exchange/adapter/CalendarSyncAdapterTests.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2010 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.exchange.adapter;
+
+public class CalendarSyncAdapterTests extends SyncAdapterTestCase {
+
+    public CalendarSyncAdapterTests() {
+        super();
+    }
+}
diff --git a/tests/src/com/android/exchange/adapter/EmailSyncAdapterTests.java b/tests/src/com/android/exchange/adapter/EmailSyncAdapterTests.java
index e378b03..5f363b8 100644
--- a/tests/src/com/android/exchange/adapter/EmailSyncAdapterTests.java
+++ b/tests/src/com/android/exchange/adapter/EmailSyncAdapterTests.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2009 The Android Open Source Project
+ * Copyright (C) 2010 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.
@@ -32,16 +32,14 @@
 import android.content.ContentUris;
 import android.content.ContentValues;
 import android.content.Context;
-import android.test.ProviderTestCase2;
 
 import java.io.ByteArrayInputStream;
 import java.io.IOException;
-import java.io.InputStream;
 import java.util.ArrayList;
 import java.util.GregorianCalendar;
 import java.util.TimeZone;
 
-public class EmailSyncAdapterTests extends ProviderTestCase2<EmailProvider> {
+public class EmailSyncAdapterTests extends SyncAdapterTestCase {
 
     EmailProvider mProvider;
     Context mMockContext;
@@ -52,54 +50,7 @@
     EasEmailSyncParser mSyncParser;
 
     public EmailSyncAdapterTests() {
-        super(EmailProvider.class, EmailProvider.EMAIL_AUTHORITY);
-    }
-
-    @Override
-    public void setUp() throws Exception {
-        super.setUp();
-        mMockContext = getMockContext();
-        mMockResolver = mMockContext.getContentResolver();
-    }
-
-    @Override
-    public void tearDown() throws Exception {
-        super.tearDown();
-    }
-
-    /**
-     * Create and return a short, simple InputStream that has at least four bytes, which is all
-     * that's required to initialize an EasParser (the parent class of EasEmailSyncParser)
-     * @return the InputStream
-     */
-    public InputStream getTestInputStream() {
-        return new ByteArrayInputStream(new byte[] {0, 0, 0, 0, 0});
-    }
-
-    EasSyncService getTestService() {
-        Account account = new Account();
-        account.mId = -1;
-        Mailbox mailbox = new Mailbox();
-        mailbox.mId = -1;
-        EasSyncService service = new EasSyncService();
-        service.mContext = mMockContext;
-        service.mMailbox = mailbox;
-        service.mAccount = account;
-        return service;
-    }
-
-    EasSyncService getTestService(Account account, Mailbox mailbox) {
-        EasSyncService service = new EasSyncService();
-        service.mContext = mMockContext;
-        service.mMailbox = mailbox;
-        service.mAccount = account;
-        return service;
-    }
-
-    EmailSyncAdapter getTestSyncAdapter() {
-        EasSyncService service = getTestService();
-        EmailSyncAdapter adapter = new EmailSyncAdapter(service.mMailbox, service);
-        return adapter;
+        super();
     }
 
     /**
diff --git a/tests/src/com/android/exchange/adapter/SyncAdapterTestCase.java b/tests/src/com/android/exchange/adapter/SyncAdapterTestCase.java
new file mode 100644
index 0000000..311b550
--- /dev/null
+++ b/tests/src/com/android/exchange/adapter/SyncAdapterTestCase.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2010 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.exchange.adapter;
+
+import com.android.email.provider.EmailProvider;
+import com.android.email.provider.EmailContent.Account;
+import com.android.email.provider.EmailContent.Mailbox;
+import com.android.exchange.EasSyncService;
+import com.android.exchange.adapter.EmailSyncAdapter.EasEmailSyncParser;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.test.ProviderTestCase2;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+
+public class SyncAdapterTestCase extends ProviderTestCase2<EmailProvider> {
+
+    EmailProvider mProvider;
+    Context mMockContext;
+    ContentResolver mMockResolver;
+    Mailbox mMailbox;
+    Account mAccount;
+    EmailSyncAdapter mSyncAdapter;
+    EasEmailSyncParser mSyncParser;
+
+    public SyncAdapterTestCase() {
+        super(EmailProvider.class, EmailProvider.EMAIL_AUTHORITY);
+    }
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        mMockContext = getMockContext();
+        mMockResolver = mMockContext.getContentResolver();
+    }
+
+    @Override
+    public void tearDown() throws Exception {
+        super.tearDown();
+    }
+
+    /**
+     * Create and return a short, simple InputStream that has at least four bytes, which is all
+     * that's required to initialize an EasParser (the parent class of EasEmailSyncParser)
+     * @return the InputStream
+     */
+    public InputStream getTestInputStream() {
+        return new ByteArrayInputStream(new byte[] {0, 0, 0, 0, 0});
+    }
+
+    EasSyncService getTestService() {
+        Account account = new Account();
+        account.mId = -1;
+        Mailbox mailbox = new Mailbox();
+        mailbox.mId = -1;
+        EasSyncService service = new EasSyncService();
+        service.mContext = mMockContext;
+        service.mMailbox = mailbox;
+        service.mAccount = account;
+        return service;
+    }
+
+    EasSyncService getTestService(Account account, Mailbox mailbox) {
+        EasSyncService service = new EasSyncService();
+        service.mContext = mMockContext;
+        service.mMailbox = mailbox;
+        service.mAccount = account;
+        return service;
+    }
+
+    EmailSyncAdapter getTestSyncAdapter() {
+        EasSyncService service = getTestService();
+        EmailSyncAdapter adapter = new EmailSyncAdapter(service.mMailbox, service);
+        return adapter;
+    }
+
+}
diff --git a/tests/src/com/android/exchange/utility/CalendarUtilitiesTests.java b/tests/src/com/android/exchange/utility/CalendarUtilitiesTests.java
new file mode 100644
index 0000000..cb97e86
--- /dev/null
+++ b/tests/src/com/android/exchange/utility/CalendarUtilitiesTests.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2010 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.exchange.utility;
+
+import android.test.AndroidTestCase;
+
+import java.util.TimeZone;
+
+/**
+ * Tests of EAS Calendar Utilities
+ * You can run this entire test case with:
+ *   runtest -c com.android.exchange.utility.CalendarUtilitiesTests email
+ *
+ * Please see RFC2445 for RRULE definition
+ * http://www.ietf.org/rfc/rfc2445.txt
+ */
+
+public class CalendarUtilitiesTests extends AndroidTestCase {
+
+    // Some prebuilt time zones, Base64 encoded (as they arrive from EAS)
+    // More time zones to be added over time
+
+    // Not all time zones are appropriate for testing.  For example, ISRAEL_STANDARD_TIME cannot be
+    // used because DST is determined from year to year in a non-standard way (related to the lunar
+    // calendar); therefore, the test would only work during the year in which it was created
+    private static final String INDIA_STANDARD_TIME =
+        "tv7//0kAbgBkAGkAYQAgAFMAdABhAG4AZABhAHIAZAAgAFQAaQBtAGUAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" +
+        "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEkAbgBkAGkAYQAgAEQAYQB5AGwAaQBnAGgAdAAgAFQAaQBtAGUA" +
+        "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==";
+    private static final String PACIFIC_STANDARD_TIME =
+        "4AEAAFAAYQBjAGkAZgBpAGMAIABTAHQAYQBuAGQAYQByAGQAIABUAGkAbQBlAAAAAAAAAAAAAAAAAAAAAAAA" +
+        "AAAAAAAAAAsAAAABAAIAAAAAAAAAAAAAAFAAYQBjAGkAZgBpAGMAIABEAGEAeQBsAGkAZwBoAHQAIABUAGkA" +
+        "bQBlAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAACAAIAAAAAAAAAxP///w==";
+
+    public void testGetSet() {
+        byte[] bytes = new byte[] {0, 1, 2, 3, 4, 5, 6, 7};
+
+        // First, check that getWord/Long are properly little endian
+        assertEquals(0x0100, CalendarUtilities.getWord(bytes, 0));
+        assertEquals(0x03020100, CalendarUtilities.getLong(bytes, 0));
+        assertEquals(0x07060504, CalendarUtilities.getLong(bytes, 4));
+
+        // Set some words and longs
+        CalendarUtilities.setWord(bytes, 0, 0xDEAD);
+        CalendarUtilities.setLong(bytes, 2, 0xBEEFBEEF);
+        CalendarUtilities.setWord(bytes, 6, 0xCEDE);
+
+        // Retrieve them
+        assertEquals(0xDEAD, CalendarUtilities.getWord(bytes, 0));
+        assertEquals(0xBEEFBEEF, CalendarUtilities.getLong(bytes, 2));
+        assertEquals(0xCEDE, CalendarUtilities.getWord(bytes, 6));
+    }
+
+    public void testParseTimeZoneEndToEnd() {
+        TimeZone tz = CalendarUtilities.tziStringToTimeZone(PACIFIC_STANDARD_TIME);
+        assertEquals("Pacific Standard Time", tz.getDisplayName());
+        tz = CalendarUtilities.tziStringToTimeZone(INDIA_STANDARD_TIME);
+        assertEquals("India Standard Time", tz.getDisplayName());
+    }
+
+    public void testGenerateEasDayOfWeek() {
+        String byDay = "TU;WE;SA";
+        // TU = 4, WE = 8; SA = 64;
+        assertEquals("76", CalendarUtilities.generateEasDayOfWeek(byDay));
+        // MO = 2, TU = 4; WE = 8; TH = 16; FR = 32
+        byDay = "MO;TU;WE;TH;FR";
+        assertEquals("62", CalendarUtilities.generateEasDayOfWeek(byDay));
+        // SU = 1
+        byDay = "SU";
+        assertEquals("1", CalendarUtilities.generateEasDayOfWeek(byDay));
+    }
+
+    public void testTokenFromRrule() {
+        String rrule = "FREQ=DAILY;INTERVAL=1;BYDAY=WE,TH,SA;BYMONTHDAY=17";
+        assertEquals("DAILY", CalendarUtilities.tokenFromRrule(rrule, "FREQ="));
+        assertEquals("1", CalendarUtilities.tokenFromRrule(rrule, "INTERVAL="));
+        assertEquals("17", CalendarUtilities.tokenFromRrule(rrule, "BYMONTHDAY="));
+        assertNull(CalendarUtilities.tokenFromRrule(rrule, "UNTIL="));
+    }
+
+    // Tests in progress...
+
+//    public void testTimeZoneToTziString() {
+//        for (String timeZoneId: TimeZone.getAvailableIDs()) {
+//            TimeZone timeZone = TimeZone.getTimeZone(timeZoneId);
+//            if (timeZone != null) {
+//                String tzs = CalendarUtilities.timeZoneToTziString(timeZone);
+//                TimeZone newTimeZone = CalendarUtilities.tziStringToTimeZone(tzs);
+//                System.err.println("In: " + timeZone.getDisplayName() + ", Out: " + newTimeZone.getDisplayName());
+//            }
+//        }
+//     }
+//    public void testParseTimeZone() {
+//        GregorianCalendar cal = getTestCalendar(parsedTimeZone, dstStart);
+//        cal.add(GregorianCalendar.MINUTE, -1);
+//        Date b = cal.getTime();
+//        cal.add(GregorianCalendar.MINUTE, 2);
+//        Date a = cal.getTime();
+//        if (parsedTimeZone.inDaylightTime(b) || !parsedTimeZone.inDaylightTime(a)) {
+//            userLog("ERROR IN TIME ZONE CONTROL!");
+//        }
+//        cal = getTestCalendar(parsedTimeZone, dstEnd);
+//        cal.add(GregorianCalendar.HOUR, -2);
+//        b = cal.getTime();
+//        cal.add(GregorianCalendar.HOUR, 2);
+//        a = cal.getTime();
+//        if (!parsedTimeZone.inDaylightTime(b)) userLog("ERROR IN TIME ZONE CONTROL");
+//        if (parsedTimeZone.inDaylightTime(a)) userLog("ERROR IN TIME ZONE CONTROL!");
+//    }
+}