am eaf951c5: am 75753107: reconcile main tree with open-source eclair

Merge commit 'eaf951c59631829c84ac71b413cf7b2ff186be17'

* commit 'eaf951c59631829c84ac71b413cf7b2ff186be17':
  android-2.1_r1 snapshot
diff --git a/Android.mk b/Android.mk
index 277d0e5..f1e3f85 100644
--- a/Android.mk
+++ b/Android.mk
@@ -15,10 +15,14 @@
 LOCAL_PATH:= $(call my-dir)
 include $(CLEAR_VARS)
 
+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..8eaa175 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"
@@ -111,6 +115,11 @@
             android:label="@string/account_settings_action"
             >
         </activity>
+        <activity
+            android:name=".activity.setup.AccountSecurity"
+            android.label="@string/account_security_title"
+            >
+        </activity>
 
         <activity
             android:name=".activity.Debug"
@@ -171,6 +180,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 +188,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 +202,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 +243,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 +262,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/drawable-hdpi/divider_vertical_light_opaque.9.png b/res/drawable-hdpi/divider_vertical_light_opaque.9.png
new file mode 100755
index 0000000..8f35315
--- /dev/null
+++ b/res/drawable-hdpi/divider_vertical_light_opaque.9.png
Binary files differ
diff --git a/res/drawable-hdpi/expander_ic_folder_maximized.9.png b/res/drawable-hdpi/expander_ic_folder_maximized.9.png
deleted file mode 100644
index 4d71060..0000000
--- a/res/drawable-hdpi/expander_ic_folder_maximized.9.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-hdpi/expander_ic_folder_minimized.9.png b/res/drawable-hdpi/expander_ic_folder_minimized.9.png
deleted file mode 100644
index 0c04943..0000000
--- a/res/drawable-hdpi/expander_ic_folder_minimized.9.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-mdpi/divider_vertical_light_opaque.9.png b/res/drawable-mdpi/divider_vertical_light_opaque.9.png
new file mode 100755
index 0000000..8f35315
--- /dev/null
+++ b/res/drawable-mdpi/divider_vertical_light_opaque.9.png
Binary files differ
diff --git a/res/drawable-mdpi/expander_ic_folder_maximized.9.png b/res/drawable-mdpi/expander_ic_folder_maximized.9.png
deleted file mode 100644
index c7ff2ac..0000000
--- a/res/drawable-mdpi/expander_ic_folder_maximized.9.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-mdpi/expander_ic_folder_minimized.9.png b/res/drawable-mdpi/expander_ic_folder_minimized.9.png
deleted file mode 100644
index 5b5fdb0..0000000
--- a/res/drawable-mdpi/expander_ic_folder_minimized.9.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-mdpi/icon.png b/res/drawable-mdpi/icon.png
index 590ed70..e874589 100644
--- a/res/drawable-mdpi/icon.png
+++ b/res/drawable-mdpi/icon.png
Binary files differ
diff --git a/res/drawable/expander_ic_folder.xml b/res/drawable/expander_ic_folder.xml
deleted file mode 100644
index d82c00a..0000000
--- a/res/drawable/expander_ic_folder.xml
+++ /dev/null
@@ -1,21 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2008 The Android Open Source Project
-
-     Licensed under the Apache License, Version 2.0 (the "License");
-     you may not use this file except in compliance with the License.
-     You may obtain a copy of the License at
-  
-          http://www.apache.org/licenses/LICENSE-2.0
-  
-     Unless required by applicable law or agreed to in writing, software
-     distributed under the License is distributed on an "AS IS" BASIS,
-     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-     See the License for the specific language governing permissions and
-     limitations under the License.
--->
-
-<selector xmlns:android="http://schemas.android.com/apk/res/android">
-    <item android:state_expanded="true"
-        android:drawable="@drawable/expander_ic_folder_maximized" />
-    <item android:drawable="@drawable/expander_ic_folder_minimized" />
-</selector> 
diff --git a/res/drawable/folder_message_list_child_background.xml b/res/drawable/folder_message_list_child_background.xml
deleted file mode 100644
index a39fb29..0000000
--- a/res/drawable/folder_message_list_child_background.xml
+++ /dev/null
@@ -1,26 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2008 The Android Open Source Project
-
-     Licensed under the Apache License, Version 2.0 (the "License");
-     you may not use this file except in compliance with the License.
-     You may obtain a copy of the License at
-  
-          http://www.apache.org/licenses/LICENSE-2.0
-  
-     Unless required by applicable law or agreed to in writing, software
-     distributed under the License is distributed on an "AS IS" BASIS,
-     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-     See the License for the specific language governing permissions and
-     limitations under the License.
--->
-
-<selector xmlns:android="http://schemas.android.com/apk/res/android">
-    <item android:state_window_focused="false" android:state_selected="true"
-        android:drawable="@color/folder_message_list_child_background" />
-    <item android:state_selected="true"
-        android:drawable="@android:color/transparent" />
-    <item android:state_pressed="true" android:state_selected="false"
-        android:drawable="@android:color/transparent" />
-    <item android:state_selected="false"
-        android:drawable="@color/folder_message_list_child_background" />
-</selector>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index d018b12..807bb6f 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -19,25 +19,7 @@
     <!-- Deprecated strings - Move the identifiers to this section, mark as DO NOT TRANSLATE,
          and remove the actual text.  These will be removed in a bulk operation. -->
     <!-- Do Not Translate.  Unused string. -->
-    <string name="special_mailbox_name_inbox"></string>
-    <!-- Do Not Translate.  Unused string. -->
-    <string name="special_mailbox_display_name_inbox"></string>
-    <!-- Do Not Translate.  Unused string. -->
-    <string name="special_mailbox_display_name_outbox"></string>
-    <!-- Do Not Translate.  Unused string. -->
-    <string name="special_mailbox_display_name_drafts"></string>
-    <!-- Do Not Translate.  Unused string. -->
-    <string name="special_mailbox_display_name_trash"></string>
-    <!-- Do Not Translate.  Unused string. -->
-    <string name="special_mailbox_display_name_sent"></string>
-    <!-- Do Not Translate.  Unused string. -->
-    <string name="special_mailbox_display_name_junk"></string>
-    <!-- Do Not Translate.  Unused string. -->
-    <string name="account_setup_incoming_delete_policy_7days_label"></string>
-    <!-- Do Not Translate.  Unused string. -->
-    <string name="account_setup_incoming_security_ssl_optional_label"></string>
-    <!-- Do Not Translate.  Unused string. -->
-    <string name="account_setup_incoming_security_tls_optional_label"></string>
+    <string name="account_setup_failed_security_policies_required"></string>
 
     <!-- Permissions label for reading attachments -->
     <string name="read_attachment_label">Read Email attachments</string>
@@ -199,14 +181,14 @@
 
     <!-- Version number, shown only on debug screen -->
     <string name="debug_version_fmt">Version: <xliff:g id="version">%s</xliff:g></string>
-    <!-- Checkbox label, shown only on debug screen -->
-    <string name="debug_enable_debug_logging_label">Enable extra debug logging?</string>
-    <!-- Checkbox label, shown only on debug screen -->
-    <string name="debug_enable_sensitive_logging_label">Enable sensitive information debug logging? (May show passwords in logs.)</string>
-    <!-- Checkbox label, shown only on debug screen -->
-    <string name="debug_enable_exchange_logging_label">Enable exchange parser logging? (Extremely verbose)</string>
-    <!-- Checkbox label, shown only on debug screen -->
-    <string name="debug_enable_exchange_file_logging_label">Enable exchange sd card logging?</string>
+    <!-- Do Not Translate.  Checkbox label, shown only on debug screen -->
+    <string name="debug_enable_debug_logging_label" translatable="false">Enable extra debug logging?</string>
+    <!-- Do Not Translate.  Checkbox label, shown only on debug screen -->
+    <string name="debug_enable_sensitive_logging_label" translatable="false">Enable sensitive information debug logging? (May show passwords in logs.)</string>
+    <!-- Do Not Translate.  Checkbox label, shown only on debug screen -->
+    <string name="debug_enable_exchange_logging_label" translatable="false">Enable exchange parser logging? (Extremely verbose)</string>
+    <!-- Do Not Translate.  Checkbox label, shown only on debug screen -->
+    <string name="debug_enable_exchange_file_logging_label" translatable="false">Enable exchange sd card logging?</string>
 
     <!-- The text in the small separator between smart folders and the accounts -->
     <string name="account_folder_list_separator_accounts">Accounts</string>
@@ -295,12 +277,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 +347,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 +442,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 -->
@@ -488,8 +487,33 @@
     <string name="account_setup_failed_security">Unable to open connection to server due to security error.</string>
     <!-- Additional diagnostic text when server connection failed due to io error (connection) -->
     <string name="account_setup_failed_ioerror">Unable to open connection to server.</string>
-    <!-- Additional diagnostic text when validation failed due to required provisioning not being supported -->
-    <string name="account_setup_failed_security_policies_required">This Exchange ActiveSync server requires security features your phone does not support.</string>
+
+    <!-- Dialog title when validation requires security provisioning (e.g. support
+         for device lock PIN, or remote wipe.) and we ask the user permission before continuing -->
+    <string name="account_setup_security_required_title">Remote security administration</string>
+    <!-- Additional diagnostic text when validation requires security provisioning (e.g. support
+         for device lock PIN, or remote wipe.) and we ask the user permission before continuing. -->
+    <string name="account_setup_security_policies_required_fmt">
+         The server <xliff:g id="server">%s</xliff:g> requires that you allow it to remotely control
+         some security features of your phone.  Do you wish to finish setting up this account?
+         </string>
+    <!-- Additional diagnostic text when validation failed due to required provisioning not
+         being supported -->
+    <string name="account_setup_failed_security_policies_unsupported">
+         This server requires security features your phone does not support.</string>
+
+    <!-- Notification ticker  when device security required -->
+    <string name="security_notification_ticker_fmt">
+            Account \"<xliff:g id="account">%s</xliff:g>\" requires security settings update.
+    </string>
+    <!-- Notification content title when device security required -->
+    <string name="security_notification_content_title">Update Security Settings</string>
+    <!-- Title of the activity that dispatches changes to device security.  Not normally seen. -->
+    <string name="account_security_title">Device Security</string>
+    <!-- Additional diagnostic text when the email app asserts control of the phone. -->
+    <string name="account_security_policy_explanation_fmt">
+         The server <xliff:g id="server">%s</xliff:g> requires that you allow it to remotely control
+         some security features of your phone.</string>
 
     <!-- "Setup could not finish" dialog action button -->
     <string name="account_setup_failed_dlg_edit_details_action">Edit details</string>
@@ -518,13 +542,23 @@
     <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>
 
     <!-- On settings screen, sync contacts check box label -->
     <string name="account_settings_sync_contacts_enable">Sync contacts</string>
     <!-- On settings screen, sync contacts summary text -->
-    <string name="account_settings_sync_contacts_summary">Also sync contacts from this account</string>
+    <string name="account_settings_sync_contacts_summary">Also sync contacts from this account
+        </string>
+    <!-- On settings screen, sync calendar check box label -->
+        <string name="account_settings_sync_calendar_enable">Sync calendar</string>
+    <!-- On settings screen, sync calendar summary text -->
+    <string name="account_settings_sync_calendar_summary">Also sync calendar from this account
+        </string>
 
     <!-- On Settings screen, setting check box label -->
     <string name="account_settings_vibrate_enable">Vibrate</string>
@@ -557,7 +591,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 & in user confirmation screen) -->
+    <string name="device_admin_label">Email</string>
+    <!-- Long-form description of the DeviceAdmin (2nd line in settings & in user conf. screen) -->
+    <string name="device_admin_description">Enables server-specified security policies</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 bba2ddf..5aa9fd9 100644
--- a/src/com/android/exchange/EasSyncService.java
+++ b/src/com/android/exchange/EasSyncService.java
@@ -17,6 +17,8 @@
 
 package com.android.exchange;
 
+import com.android.email.SecurityPolicy;
+import com.android.email.SecurityPolicy.PolicySet;
 import com.android.email.codec.binary.Base64;
 import com.android.email.mail.AuthenticationFailedException;
 import com.android.email.mail.MessagingException;
@@ -28,12 +30,17 @@
 import com.android.email.provider.EmailContent.Mailbox;
 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.ProvisionParser;
 import com.android.exchange.adapter.Serializer;
 import com.android.exchange.adapter.Tags;
 import com.android.exchange.adapter.Parser.EasParserException;
@@ -42,25 +49,35 @@
 import org.apache.http.HttpEntity;
 import org.apache.http.HttpResponse;
 import org.apache.http.HttpStatus;
+import org.apache.http.client.ClientProtocolException;
 import org.apache.http.client.HttpClient;
 import org.apache.http.client.methods.HttpOptions;
 import org.apache.http.client.methods.HttpPost;
 import org.apache.http.client.methods.HttpRequestBase;
 import org.apache.http.conn.ClientConnectionManager;
 import org.apache.http.entity.ByteArrayEntity;
+import org.apache.http.entity.StringEntity;
 import org.apache.http.impl.client.DefaultHttpClient;
 import org.apache.http.params.BasicHttpParams;
 import org.apache.http.params.HttpConnectionParams;
 import org.apache.http.params.HttpParams;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlPullParserFactory;
+import org.xmlpull.v1.XmlSerializer;
 
 import android.content.ContentResolver;
 import android.content.ContentUris;
 import android.content.ContentValues;
 import android.content.Context;
 import android.database.Cursor;
+import android.os.Bundle;
 import android.os.RemoteException;
 import android.os.SystemClock;
+import android.util.Log;
+import android.util.Xml;
 
+import java.io.ByteArrayOutputStream;
 import java.io.File;
 import java.io.FileOutputStream;
 import java.io.IOException;
@@ -94,6 +111,11 @@
     // Define our default protocol version as 2.5 (Exchange 2003)
     static private final String DEFAULT_PROTOCOL_VERSION = "2.5";
 
+    static private final String AUTO_DISCOVER_SCHEMA_PREFIX =
+        "http://schemas.microsoft.com/exchange/autodiscover/mobilesync/";
+    static private final String AUTO_DISCOVER_PAGE = "/autodiscover/autodiscover.xml";
+    static private final int AUTO_DISCOVER_REDIRECT_CODE = 451;
+
     /**
      * We start with an 8 minute timeout, and increase/decrease by 3 minutes at a time.  There's
      * no point having a timeout shorter than 5 minutes, I think; at that point, we can just let
@@ -121,6 +143,9 @@
     static private final int PING_FALLBACK_INBOX = 5;
     static private final int PING_FALLBACK_PIM = 25;
 
+    // MSFT's custom HTTP result code indicating the need to provision
+    static private final int HTTP_NEED_PROVISIONING = 449;
+
     // Reasonable default
     public String mProtocolVersion = DEFAULT_PROTOCOL_VERSION;
     public Double mProtocolVersionDouble;
@@ -144,12 +169,22 @@
     // Whether we've ever lowered the heartbeat
     private boolean mPingHeartbeatDropped = false;
     // Whether a POST was aborted due to watchdog timeout
-    private boolean mAborted = false;
+    private boolean mPostAborted = false;
+    // Whether or not the sync service is valid (usable)
+    public boolean mIsValid = true;
 
     public EasSyncService(Context _context, Mailbox _mailbox) {
         super(_context, _mailbox);
         mContentResolver = _context.getContentResolver();
+        if (mAccount == null) {
+            mIsValid = false;
+            return;
+        }
         HostAuth ha = HostAuth.restoreHostAuthWithId(_context, mAccount.mHostAuthKeyRecv);
+        if (ha == null) {
+            mIsValid = false;
+            return;
+        }
         mSsl = (ha.mFlags & HostAuth.FLAG_SSL) != 0;
         mTrustSsl = (ha.mFlags & HostAuth.FLAG_TRUST_ALL_CERTIFICATES) != 0;
     }
@@ -168,7 +203,7 @@
         synchronized(getSynchronizer()) {
             if (mPendingPost != null) {
                 userLog("Aborting pending POST!");
-                mAborted = true;
+                mPostAborted = true;
                 mPendingPost.abort();
             }
         }
@@ -190,7 +225,20 @@
      * @return whether or not the code represents an authentication error
      */
     protected boolean isAuthError(int code) {
-        return ((code == HttpStatus.SC_UNAUTHORIZED) || (code == HttpStatus.SC_FORBIDDEN));
+        return (code == HttpStatus.SC_UNAUTHORIZED) || (code == HttpStatus.SC_FORBIDDEN);
+    }
+
+    private void setupProtocolVersion(EasSyncService service, Header versionHeader) {
+        String versions = versionHeader.getValue();
+        if (versions != null) {
+            if (versions.contains("12.0")) {
+                service.mProtocolVersion = "12.0";
+            }
+            service.mProtocolVersionDouble = Double.parseDouble(service.mProtocolVersion);
+            if (service.mAccount != null) {
+                service.mAccount.mProtocolVersion = service.mProtocolVersion;
+            }
+        }
     }
 
     @Override
@@ -205,7 +253,9 @@
             svc.mPassword = password;
             svc.mSsl = ssl;
             svc.mTrustSsl = trustCertificates;
-            svc.mDeviceId = SyncManager.getDeviceId();
+            // We mustn't use the "real" device id or we'll screw up current accounts
+            // Any string will do, but we'll go for "validate"
+            svc.mDeviceId = "validate";
             HttpResponse resp = svc.sendHttpClientOptions();
             int code = resp.getStatusLine().getStatusCode();
             userLog("Validation (OPTIONS) response: " + code);
@@ -218,6 +268,9 @@
                     throw new MessagingException(MessagingException.IOERROR);
                 }
 
+                // Make sure we've got the right protocol version set up
+                setupProtocolVersion(svc, versions);
+
                 // Run second test here for provisioning failures...
                 Serializer s = new Serializer();
                 userLog("Try folder sync");
@@ -225,8 +278,16 @@
                     .end().end().done();
                 resp = svc.sendHttpClientPost("FolderSync", s.toByteArray());
                 code = resp.getStatusLine().getStatusCode();
-                if (code == HttpStatus.SC_FORBIDDEN) {
-                    throw new MessagingException(MessagingException.SECURITY_POLICIES_REQUIRED);
+                // We'll get one of the following responses if policies are required by the server
+                if (code == HttpStatus.SC_FORBIDDEN || code == HTTP_NEED_PROVISIONING) {
+                    // Get the policies and see if we are able to support them
+                    if (svc.canProvision()) {
+                        // If so, send the advisory Exception (the account may be created later)
+                        throw new MessagingException(MessagingException.SECURITY_POLICIES_REQUIRED);
+                    } else
+                        // If not, send the unsupported Exception (the account won't be created)
+                        throw new MessagingException(
+                                MessagingException.SECURITY_POLICIES_UNSUPPORTED);
                 }
                 userLog("Validation successful");
                 return;
@@ -251,6 +312,302 @@
 
     }
 
+    /**
+     * Gets the redirect location from the HTTP headers and uses that to modify the HttpPost so that
+     * it can be reused
+     *
+     * @param resp the HttpResponse that indicates a redirect (451)
+     * @param post the HttpPost that was originally sent to the server
+     * @return the HttpPost, updated with the redirect location
+     */
+    private HttpPost getRedirect(HttpResponse resp, HttpPost post) {
+        Header locHeader = resp.getFirstHeader("X-MS-Location");
+        if (locHeader != null) {
+            String loc = locHeader.getValue();
+            // If we've gotten one and it shows signs of looking like an address, we try
+            // sending our request there
+            if (loc != null && loc.startsWith("http")) {
+                post.setURI(URI.create(loc));
+                return post;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Send the POST command to the autodiscover server, handling a redirect, if necessary, and
+     * return the HttpResponse
+     *
+     * @param client the HttpClient to be used for the request
+     * @param post the HttpPost we're going to send
+     * @return an HttpResponse from the original or redirect server
+     * @throws IOException on any IOException within the HttpClient code
+     * @throws MessagingException
+     */
+    private HttpResponse postAutodiscover(HttpClient client, HttpPost post)
+            throws IOException, MessagingException {
+        userLog("Posting autodiscover to: " + post.getURI());
+        HttpResponse resp = client.execute(post);
+        int code = resp.getStatusLine().getStatusCode();
+        // On a redirect, try the new location
+        if (code == AUTO_DISCOVER_REDIRECT_CODE) {
+            post = getRedirect(resp, post);
+            if (post != null) {
+                userLog("Posting autodiscover to redirect: " + post.getURI());
+                return client.execute(post);
+            }
+        } else if (code == HttpStatus.SC_UNAUTHORIZED) {
+            // 401 (Unauthorized) is for true auth errors when used in Autodiscover
+            // 403 (and others) we'll just punt on
+            throw new MessagingException(MessagingException.AUTHENTICATION_FAILED);
+        } else if (code != HttpStatus.SC_OK) {
+            // We'll try the next address if this doesn't work
+            userLog("Code: " + code + ", throwing IOException");
+            throw new IOException();
+        }
+        return resp;
+    }
+
+    /**
+     * Use the Exchange 2007 AutoDiscover feature to try to retrieve server information using
+     * only an email address and the password
+     *
+     * @param userName the user's email address
+     * @param password the user's password
+     * @return a HostAuth ready to be saved in an Account or null (failure)
+     */
+    public Bundle tryAutodiscover(String userName, String password) throws RemoteException {
+        XmlSerializer s = Xml.newSerializer();
+        ByteArrayOutputStream os = new ByteArrayOutputStream(1024);
+        HostAuth hostAuth = new HostAuth();
+        Bundle bundle = new Bundle();
+        bundle.putInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE,
+                MessagingException.NO_ERROR);
+        try {
+            // Build the XML document that's sent to the autodiscover server(s)
+            s.setOutput(os, "UTF-8");
+            s.startDocument("UTF-8", false);
+            s.startTag(null, "Autodiscover");
+            s.attribute(null, "xmlns", AUTO_DISCOVER_SCHEMA_PREFIX + "requestschema/2006");
+            s.startTag(null, "Request");
+            s.startTag(null, "EMailAddress").text(userName).endTag(null, "EMailAddress");
+            s.startTag(null, "AcceptableResponseSchema");
+            s.text(AUTO_DISCOVER_SCHEMA_PREFIX + "responseschema/2006");
+            s.endTag(null, "AcceptableResponseSchema");
+            s.endTag(null, "Request");
+            s.endTag(null, "Autodiscover");
+            s.endDocument();
+            String req = os.toString();
+
+            // Initialize the user name and password
+            mUserName = userName;
+            mPassword = password;
+            // Make sure the authentication string is created (mAuthString)
+            makeUriString("foo", null);
+
+            // Split out the domain name
+            int amp = userName.indexOf('@');
+            // The UI ensures that userName is a valid email address
+            if (amp < 0) {
+                throw new RemoteException();
+            }
+            String domain = userName.substring(amp + 1);
+
+            // There are up to four attempts here; the two URLs that we're supposed to try per the
+            // specification, and up to one redirect for each (handled in postAutodiscover)
+
+            // Try the domain first and see if we can get a response
+            HttpPost post = new HttpPost("https://" + domain + AUTO_DISCOVER_PAGE);
+            setHeaders(post);
+            post.setHeader("Content-Type", "text/xml");
+            post.setEntity(new StringEntity(req));
+            HttpClient client = getHttpClient(COMMAND_TIMEOUT);
+            HttpResponse resp;
+            try {
+                resp = postAutodiscover(client, post);
+            } catch (ClientProtocolException e1) {
+                return null;
+            } catch (IOException e1) {
+                // We catch the IOException here because we have an alternate address to try
+                post.setURI(URI.create("https://autodiscover." + domain + AUTO_DISCOVER_PAGE));
+                // If we fail here, we're out of options, so we let the outer try catch the
+                // IOException and return null
+                resp = postAutodiscover(client, post);
+            }
+
+            // Get the "final" code; if it's not 200, just return null
+            int code = resp.getStatusLine().getStatusCode();
+            userLog("Code: " + code);
+            if (code != HttpStatus.SC_OK) return null;
+
+            // At this point, we have a 200 response (SC_OK)
+            HttpEntity e = resp.getEntity();
+            InputStream is = e.getContent();
+            try {
+                // The response to Autodiscover is regular XML (not WBXML)
+                // If we ever get an error in this process, we'll just punt and return null
+                XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
+                XmlPullParser parser = factory.newPullParser();
+                parser.setInput(is, "UTF-8");
+                int type = parser.getEventType();
+                if (type == XmlPullParser.START_DOCUMENT) {
+                    type = parser.next();
+                    if (type == XmlPullParser.START_TAG) {
+                        String name = parser.getName();
+                        if (name.equals("Autodiscover")) {
+                            hostAuth = new HostAuth();
+                            parseAutodiscover(parser, hostAuth);
+                            // On success, we'll have a server address and login
+                            if (hostAuth.mAddress != null && hostAuth.mLogin != null) {
+                                // Fill in the rest of the HostAuth
+                                hostAuth.mPassword = password;
+                                hostAuth.mPort = 443;
+                                hostAuth.mProtocol = "eas";
+                                hostAuth.mFlags =
+                                    HostAuth.FLAG_SSL | HostAuth.FLAG_AUTHENTICATE;
+                                bundle.putParcelable(
+                                        EmailServiceProxy.AUTO_DISCOVER_BUNDLE_HOST_AUTH, hostAuth);
+                            } else {
+                                bundle.putInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE,
+                                        MessagingException.UNSPECIFIED_EXCEPTION);
+                            }
+                        }
+                    }
+                }
+            } catch (XmlPullParserException e1) {
+                // This would indicate an I/O error of some sort
+                // We will simply return null and user can configure manually
+            }
+        // There's no reason at all for exceptions to be thrown, and it's ok if so.
+        // We just won't do auto-discover; user can configure manually
+       } catch (IllegalArgumentException e) {
+             bundle.putInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE,
+                     MessagingException.UNSPECIFIED_EXCEPTION);
+       } catch (IllegalStateException e) {
+            bundle.putInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE,
+                    MessagingException.UNSPECIFIED_EXCEPTION);
+       } catch (IOException e) {
+            userLog("IOException in Autodiscover", e);
+            bundle.putInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE,
+                    MessagingException.IOERROR);
+        } catch (MessagingException e) {
+            bundle.putInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE,
+                    MessagingException.AUTHENTICATION_FAILED);
+        }
+        return bundle;
+    }
+
+    void parseServer(XmlPullParser parser, HostAuth hostAuth)
+            throws XmlPullParserException, IOException {
+        boolean mobileSync = false;
+        while (true) {
+            int type = parser.next();
+            if (type == XmlPullParser.END_TAG && parser.getName().equals("Server")) {
+                break;
+            } else if (type == XmlPullParser.START_TAG) {
+                String name = parser.getName();
+                if (name.equals("Type")) {
+                    if (parser.nextText().equals("MobileSync")) {
+                        mobileSync = true;
+                    }
+                } else if (mobileSync && name.equals("Url")) {
+                    String url = parser.nextText().toLowerCase();
+                    // This will look like https://<server address>/Microsoft-Server-ActiveSync
+                    // We need to extract the <server address>
+                    if (url.startsWith("https://") &&
+                            url.endsWith("/microsoft-server-activesync")) {
+                        int lastSlash = url.lastIndexOf('/');
+                        hostAuth.mAddress = url.substring(8, lastSlash);
+                        userLog("Autodiscover, server: " + hostAuth.mAddress);
+                    }
+                }
+            }
+        }
+    }
+
+    void parseSettings(XmlPullParser parser, HostAuth hostAuth)
+            throws XmlPullParserException, IOException {
+        while (true) {
+            int type = parser.next();
+            if (type == XmlPullParser.END_TAG && parser.getName().equals("Settings")) {
+                break;
+            } else if (type == XmlPullParser.START_TAG) {
+                String name = parser.getName();
+                if (name.equals("Server")) {
+                    parseServer(parser, hostAuth);
+                }
+            }
+        }
+    }
+
+    void parseAction(XmlPullParser parser, HostAuth hostAuth)
+            throws XmlPullParserException, IOException {
+        while (true) {
+            int type = parser.next();
+            if (type == XmlPullParser.END_TAG && parser.getName().equals("Action")) {
+                break;
+            } else if (type == XmlPullParser.START_TAG) {
+                String name = parser.getName();
+                if (name.equals("Error")) {
+                    // Should parse the error
+                } else if (name.equals("Redirect")) {
+                    Log.d(TAG, "Redirect: " + parser.nextText());
+                } else if (name.equals("Settings")) {
+                    parseSettings(parser, hostAuth);
+                }
+            }
+        }
+    }
+
+    void parseUser(XmlPullParser parser, HostAuth hostAuth)
+            throws XmlPullParserException, IOException {
+        while (true) {
+            int type = parser.next();
+            if (type == XmlPullParser.END_TAG && parser.getName().equals("User")) {
+                break;
+            } else if (type == XmlPullParser.START_TAG) {
+                String name = parser.getName();
+                if (name.equals("EMailAddress")) {
+                    String addr = parser.nextText();
+                    hostAuth.mLogin = addr;
+                    userLog("Autodiscover, login: " + addr);
+                } else if (name.equals("DisplayName")) {
+                    String dn = parser.nextText();
+                    userLog("Autodiscover, user: " + dn);
+                }
+            }
+        }
+    }
+
+    void parseResponse(XmlPullParser parser, HostAuth hostAuth)
+            throws XmlPullParserException, IOException {
+        while (true) {
+            int type = parser.next();
+            if (type == XmlPullParser.END_TAG && parser.getName().equals("Response")) {
+                break;
+            } else if (type == XmlPullParser.START_TAG) {
+                String name = parser.getName();
+                if (name.equals("User")) {
+                    parseUser(parser, hostAuth);
+                } else if (name.equals("Action")) {
+                    parseAction(parser, hostAuth);
+                }
+            }
+        }
+    }
+
+    void parseAutodiscover(XmlPullParser parser, HostAuth hostAuth)
+            throws XmlPullParserException, IOException {
+        while (true) {
+            int type = parser.nextTag();
+            if (type == XmlPullParser.END_TAG && parser.getName().equals("Autodiscover")) {
+                break;
+            } else if (type == XmlPullParser.START_TAG && parser.getName().equals("Response")) {
+                parseResponse(parser, hostAuth);
+            }
+        }
+    }
+
     private void doStatusCallback(long messageId, long attachmentId, int status) {
         try {
             SyncManager.callback().loadAttachmentStatus(messageId, attachmentId, status, 0);
@@ -306,7 +663,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);
 
@@ -318,9 +675,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();
@@ -332,7 +689,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
@@ -362,12 +719,12 @@
                                     errorLog("totalRead is greater than attachment length?");
                                     break;
                                 }
-                                int pct = (totalRead * 100 / length);
+                                int pct = (totalRead * 100) / length;
                                 doProgressCallback(msg.mId, att.mId, pct);
                             }
                        }
                     } finally {
-                        mPendingPartRequest = null;
+                        mPendingRequest = null;
                     }
                 }
                 os.flush();
@@ -375,8 +732,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);
@@ -389,6 +746,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
@@ -511,6 +899,29 @@
         }
     }
 
+    // TODO This is Exchange 2007 only at this point
+    private boolean canProvision() throws IOException {
+        Serializer s = new Serializer();
+        s.start(Tags.PROVISION_PROVISION).start(Tags.PROVISION_POLICIES);
+        s.start(Tags.PROVISION_POLICY).data(Tags.PROVISION_POLICY_TYPE, "MS-EAS-Provisioning-WBXML")
+            .end().end().end().done();
+        HttpResponse resp = sendHttpClientPost("Provision", s.toByteArray());
+        int code = resp.getStatusLine().getStatusCode();
+        if (code == HttpStatus.SC_OK) {
+            InputStream is = resp.getEntity().getContent();
+            ProvisionParser pp = new ProvisionParser(is, this);
+            if (pp.parse()) {
+                // If true, we received policies from the server
+                // Retrieve them and write them to the framework
+                PolicySet ps = pp.getPolicySet();
+                if (SecurityPolicy.getInstance(mContext).isSupported(ps)) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
     /**
      * Performs FolderSync
      *
@@ -518,8 +929,6 @@
      * @throws EasParserException
      */
     public void runAccountMailbox() throws IOException, EasParserException {
-        // We'll reuse this ContentValues object
-        ContentValues cv = new ContentValues();
         // Initialize exit status to success
         mExitStatus = EmailServiceStatus.SUCCESS;
         try {
@@ -533,7 +942,7 @@
             if (mAccount.mSyncKey == null) {
                 mAccount.mSyncKey = "0";
                 userLog("Account syncKey INIT to 0");
-                cv.clear();
+                ContentValues cv = new ContentValues();
                 cv.put(AccountColumns.SYNC_KEY, mAccount.mSyncKey);
                 mAccount.update(mContext, cv);
             }
@@ -544,7 +953,7 @@
             }
 
             // When we first start up, change all mailboxes to push.
-            cv.clear();
+            ContentValues cv = new ContentValues();
             cv.put(Mailbox.SYNC_INTERVAL, Mailbox.CHECK_INTERVAL_PUSH);
             if (mContentResolver.update(Mailbox.CONTENT_URI, cv,
                     WHERE_ACCOUNT_AND_SYNC_INTERVAL_PING,
@@ -597,25 +1006,40 @@
             }
 
             while (!mStop) {
-                 userLog("Sending Account syncKey: ", mAccount.mSyncKey);
-                 Serializer s = new Serializer();
-                 s.start(Tags.FOLDER_FOLDER_SYNC).start(Tags.FOLDER_SYNC_KEY)
-                     .text(mAccount.mSyncKey).end().end().done();
-                 HttpResponse resp = sendHttpClientPost("FolderSync", s.toByteArray());
-                 if (mStop) break;
-                 int code = resp.getStatusLine().getStatusCode();
-                 if (code == HttpStatus.SC_OK) {
-                     HttpEntity entity = resp.getEntity();
-                     int len = (int)entity.getContentLength();
-                     if (len != 0) {
-                         InputStream is = entity.getContent();
-                         // Returns true if we need to sync again
-                         if (new FolderSyncParser(is, new AccountSyncAdapter(mMailbox, this))
-                                 .parse()) {
-                             continue;
-                         }
-                     }
-                 } else if (isAuthError(code)) {
+                userLog("Sending Account syncKey: ", mAccount.mSyncKey);
+                Serializer s = new Serializer();
+                s.start(Tags.FOLDER_FOLDER_SYNC).start(Tags.FOLDER_SYNC_KEY)
+                .text(mAccount.mSyncKey).end().end().done();
+                HttpResponse resp = sendHttpClientPost("FolderSync", s.toByteArray());
+                if (mStop) break;
+                int code = resp.getStatusLine().getStatusCode();
+                if (code == HttpStatus.SC_OK) {
+                    HttpEntity entity = resp.getEntity();
+                    int len = (int)entity.getContentLength();
+                    if (len != 0) {
+                        InputStream is = entity.getContent();
+                        // Returns true if we need to sync again
+                        if (new FolderSyncParser(is, new AccountSyncAdapter(mMailbox, this))
+                        .parse()) {
+                            continue;
+                        }
+                    }
+                    // // EVERYTHING IN THE code==403 BLOCK IS PLACEHOLDER/SAMPLE CODE
+                } else if (code == 403) { // security error
+                    // Reality: Find out from server what policies are required
+                    // Fake: Hardcode the policies
+                    SecurityPolicy sp = SecurityPolicy.getInstance(mContext);
+                    PolicySet ps = new PolicySet(4, PolicySet.PASSWORD_MODE_SIMPLE, 0, 0, true);
+                    // Update the account
+                    if (ps.writeAccount(mAccount, "securitySyncKey", true, mContext)) {
+                        sp.updatePolicies(mAccount.mId);
+                    }
+                    // Notify that we are blocked because of policies
+                    sp.policiesRequired(mAccount.mId);
+                    // and exit (don't sync in this condition)
+                    mExitStatus = EXIT_LOGIN_FAILURE;
+                    // END PLACEHOLDER CODE
+                } else if (isAuthError(code)) {
                     mExitStatus = EXIT_LOGIN_FAILURE;
                 } else {
                     userLog("FolderSync response error: ", code);
@@ -713,7 +1137,6 @@
                     int pingStatus = SyncManager.pingStatus(mailboxId);
                     String mailboxName = c.getString(Mailbox.CONTENT_DISPLAY_NAME_COLUMN);
                     if (pingStatus == SyncManager.PING_STATUS_OK) {
-
                         String syncKey = c.getString(Mailbox.CONTENT_SYNC_KEY_COLUMN);
                         if ((syncKey == null) || syncKey.equals("0")) {
                             // We can't push until the initial sync is done
@@ -825,7 +1248,7 @@
                     // haven't yet "fixed" the timeout, back off by two minutes and "fix" it
                     boolean hasMessage = message != null;
                     userLog("IOException runPingLoop: " + (hasMessage ? message : "[no message]"));
-                    if (mAborted || (hasMessage && message.contains("reset by peer"))) {
+                    if (mPostAborted || (hasMessage && message.contains("reset by peer"))) {
                         long pingLength = SystemClock.elapsedRealtime() - pingTime;
                         if ((pingHeartbeat > PING_MIN_HEARTBEAT) &&
                                 (pingHeartbeat > mPingHighWaterMark)) {
@@ -835,7 +1258,7 @@
                                 pingHeartbeat = PING_MIN_HEARTBEAT;
                             }
                             userLog("Decreased ping heartbeat to ", pingHeartbeat, "s");
-                        } else if (mAborted || (pingLength < 2000)) {
+                        } else if (mPostAborted || (pingLength < 2000)) {
                             userLog("Abort or NAT type return < 2 seconds; throwing IOException");
                             throw e;
                         } else {
@@ -955,7 +1378,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: {
@@ -1004,18 +1427,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);
                 }
             }
 
@@ -1041,8 +1475,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) {
@@ -1095,6 +1532,7 @@
         TAG = mThread.getName();
 
         HostAuth ha = HostAuth.restoreHostAuthWithId(mContext, mAccount.mHostAuthKeyRecv);
+        if (ha == null) return false;
         mHostAddress = ha.mAddress;
         mUserName = ha.mLogin;
         mPassword = ha.mPassword;
@@ -1132,6 +1570,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);
                 }
@@ -1145,26 +1585,33 @@
                     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));
+            userLog("Caught IOException: ", (message == null) ? "No message" : message);
             mExitStatus = EXIT_IO_ERROR;
         } catch (Exception e) {
             userLog("Uncaught exception in EasSyncService", e);
         } finally {
+            int status;
+
             if (!mStop) {
                 userLog("Sync finished");
                 SyncManager.done(this);
-                // If this is the account mailbox, wake up SyncManager
-                // Because this box has a "push" interval, it will be restarted immediately
-                // which will cause the folder list to be reloaded...
-                int status;
                 switch (mExitStatus) {
                     case EXIT_IO_ERROR:
                         status = EmailServiceStatus.CONNECTION_ERROR;
                         break;
                     case EXIT_DONE:
                         status = EmailServiceStatus.SUCCESS;
+                        ContentValues cv = new ContentValues();
+                        cv.put(Mailbox.SYNC_TIME, System.currentTimeMillis());
+                        String s = "S" + mSyncReason + ':' + status + ':' + mChangeCount;
+                        cv.put(Mailbox.SYNC_STATUS, s);
+                        mContentResolver.update(ContentUris.withAppendedId(Mailbox.CONTENT_URI,
+                                mMailboxId), cv, null, null);
                         break;
                     case EXIT_LOGIN_FAILURE:
                         status = EmailServiceStatus.LOGIN_FAILED;
@@ -1174,24 +1621,15 @@
                         errorLog("Sync ended due to an exception.");
                         break;
                 }
-
-                try {
-                    SyncManager.callback().syncMailboxStatus(mMailboxId, status, 0);
-                } catch (RemoteException e1) {
-                    // Don't care if this fails
-                }
-
-                if (mExitStatus == EXIT_DONE) {
-                    // Save the sync time and status
-                    ContentValues cv = new ContentValues();
-                    cv.put(Mailbox.SYNC_TIME, System.currentTimeMillis());
-                    String s = "S" + mSyncReason + ':' + status + ':' + mChangeCount;
-                    cv.put(Mailbox.SYNC_STATUS, s);
-                    mContentResolver.update(ContentUris.withAppendedId(Mailbox.CONTENT_URI,
-                            mMailboxId), cv, null, null);
-                }
             } else {
                 userLog("Stopped sync finished.");
+                status = EmailServiceStatus.SUCCESS;
+            }
+
+            try {
+                SyncManager.callback().syncMailboxStatus(mMailboxId, status, 0);
+            } catch (RemoteException e1) {
+                // Don't care if this fails
             }
 
             // Make sure SyncManager knows about this
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/EmailSyncAlarmReceiver.java b/src/com/android/exchange/EmailSyncAlarmReceiver.java
index 4a44860..ea36781 100644
--- a/src/com/android/exchange/EmailSyncAlarmReceiver.java
+++ b/src/com/android/exchange/EmailSyncAlarmReceiver.java
@@ -50,8 +50,16 @@
     private static String TAG = "EmailSyncAlarm";
 
     @Override
-    public void onReceive(Context context, Intent intent) {
+    public void onReceive(final Context context, Intent intent) {
         Log.v(TAG, "onReceive");
+        new Thread(new Runnable() {
+            public void run() {
+                handleReceive(context);
+            }
+        }).start();
+    }
+
+    private void handleReceive(Context context) {
         ArrayList<Long> mailboxesToNotify = new ArrayList<Long>();
         ContentResolver cr = context.getContentResolver();
         int messageCount = 0;
diff --git a/src/com/android/exchange/IEmailService.aidl b/src/com/android/exchange/IEmailService.aidl
deleted file mode 100644
index ec0fd5e..0000000
--- a/src/com/android/exchange/IEmailService.aidl
+++ /dev/null
@@ -1,43 +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.exchange.EmailContent;
-
-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);
-}
\ 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/EmailContent.aidl b/src/com/android/exchange/MeetingResponseRequest.java
similarity index 62%
copy from src/com/android/exchange/EmailContent.aidl
copy to src/com/android/exchange/MeetingResponseRequest.java
index c6b4a7d..1d69a6c 100644
--- a/src/com/android/exchange/EmailContent.aidl
+++ b/src/com/android/exchange/MeetingResponseRequest.java
@@ -1,6 +1,5 @@
 /*
- * Copyright (C) 2008-2009 Marc Blank
- * Licensed to 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.
@@ -17,5 +16,14 @@
 
 package com.android.exchange;
 
-parcelable EmailContent.Attachment;
+/**
+ * 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/EmailContent.aidl b/src/com/android/exchange/Request.java
similarity index 60%
copy from src/com/android/exchange/EmailContent.aidl
copy to src/com/android/exchange/Request.java
index c6b4a7d..185dd7f 100644
--- a/src/com/android/exchange/EmailContent.aidl
+++ b/src/com/android/exchange/Request.java
@@ -1,6 +1,5 @@
 /*
- * Copyright (C) 2008-2009 Marc Blank
- * Licensed to 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.
@@ -17,5 +16,12 @@
 
 package com.android.exchange;
 
-parcelable EmailContent.Attachment;
-
+/**
+ * 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/SSLSocketFactory.java b/src/com/android/exchange/SSLSocketFactory.java
new file mode 100644
index 0000000..56af829
--- /dev/null
+++ b/src/com/android/exchange/SSLSocketFactory.java
@@ -0,0 +1,401 @@
+/*
+ * $HeadURL: http://svn.apache.org/repos/asf/httpcomponents/httpclient/trunk/module-client/src/main/java/org/apache/http/conn/ssl/SSLSocketFactory.java $
+ * $Revision: 659194 $
+ * $Date: 2008-05-22 11:33:47 -0700 (Thu, 22 May 2008) $
+ *
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation.  For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ * 
+ * This class was copied from org.apache.http.conn.ssl, because it didn't have a suitable
+ * constructor.
+ */
+
+package com.android.exchange;
+
+import org.apache.http.conn.scheme.HostNameResolver;
+import org.apache.http.conn.scheme.LayeredSocketFactory;
+import org.apache.http.conn.ssl.AllowAllHostnameVerifier;
+import org.apache.http.conn.ssl.BrowserCompatHostnameVerifier;
+import org.apache.http.conn.ssl.StrictHostnameVerifier;
+import org.apache.http.conn.ssl.X509HostnameVerifier;
+import org.apache.http.params.HttpConnectionParams;
+import org.apache.http.params.HttpParams;
+
+import javax.net.ssl.HttpsURLConnection;
+import javax.net.ssl.KeyManager;
+import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSocket;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.TrustManagerFactory;
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.net.UnknownHostException;
+import java.security.KeyManagementException;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.security.UnrecoverableKeyException;
+
+/**
+ * Layered socket factory for TLS/SSL connections, based on JSSE.
+ *.
+ * <p>
+ * SSLSocketFactory can be used to validate the identity of the HTTPS 
+ * server against a list of trusted certificates and to authenticate to
+ * the HTTPS server using a private key. 
+ * </p>
+ * 
+ * <p>
+ * SSLSocketFactory will enable server authentication when supplied with
+ * a {@link KeyStore truststore} file containg one or several trusted
+ * certificates. The client secure socket will reject the connection during
+ * the SSL session handshake if the target HTTPS server attempts to
+ * authenticate itself with a non-trusted certificate.
+ * </p>
+ * 
+ * <p>
+ * Use JDK keytool utility to import a trusted certificate and generate a truststore file:    
+ *    <pre>
+ *     keytool -import -alias "my server cert" -file server.crt -keystore my.truststore
+ *    </pre>
+ * </p>
+ * 
+ * <p>
+ * SSLSocketFactory will enable client authentication when supplied with
+ * a {@link KeyStore keystore} file containg a private key/public certificate
+ * pair. The client secure socket will use the private key to authenticate
+ * itself to the target HTTPS server during the SSL session handshake if
+ * requested to do so by the server.
+ * The target HTTPS server will in its turn verify the certificate presented
+ * by the client in order to establish client's authenticity
+ * </p>
+ * 
+ * <p>
+ * Use the following sequence of actions to generate a keystore file
+ * </p>
+ *   <ul>
+ *     <li>
+ *      <p>
+ *      Use JDK keytool utility to generate a new key
+ *      <pre>keytool -genkey -v -alias "my client key" -validity 365 -keystore my.keystore</pre>
+ *      For simplicity use the same password for the key as that of the keystore
+ *      </p>
+ *     </li>
+ *     <li>
+ *      <p>
+ *      Issue a certificate signing request (CSR)
+ *      <pre>keytool -certreq -alias "my client key" -file mycertreq.csr -keystore my.keystore</pre>
+ *     </p>
+ *     </li>
+ *     <li>
+ *      <p>
+ *      Send the certificate request to the trusted Certificate Authority for signature. 
+ *      One may choose to act as her own CA and sign the certificate request using a PKI 
+ *      tool, such as OpenSSL.
+ *      </p>
+ *     </li>
+ *     <li>
+ *      <p>
+ *       Import the trusted CA root certificate
+ *       <pre>keytool -import -alias "my trusted ca" -file caroot.crt -keystore my.keystore</pre> 
+ *      </p>
+ *     </li>
+ *     <li>
+ *      <p>
+ *       Import the PKCS#7 file containg the complete certificate chain
+ *       <pre>keytool -import -alias "my client key" -file mycert.p7 -keystore my.keystore</pre> 
+ *      </p>
+ *     </li>
+ *     <li>
+ *      <p>
+ *       Verify the content the resultant keystore file
+ *       <pre>keytool -list -v -keystore my.keystore</pre> 
+ *      </p>
+ *     </li>
+ *   </ul>
+ * @author <a href="mailto:oleg at ural.ru">Oleg Kalnichevski</a>
+ * @author Julius Davies
+ */
+
+public class SSLSocketFactory implements LayeredSocketFactory {
+
+    public static final String TLS   = "TLS";
+    public static final String SSL   = "SSL";
+    public static final String SSLV2 = "SSLv2";
+    
+    public static final X509HostnameVerifier ALLOW_ALL_HOSTNAME_VERIFIER 
+        = new AllowAllHostnameVerifier();
+    
+    public static final X509HostnameVerifier BROWSER_COMPATIBLE_HOSTNAME_VERIFIER 
+        = new BrowserCompatHostnameVerifier();
+    
+    public static final X509HostnameVerifier STRICT_HOSTNAME_VERIFIER 
+        = new StrictHostnameVerifier();
+    /**
+     * The factory using the default JVM settings for secure connections.
+     */
+    private static final SSLSocketFactory DEFAULT_FACTORY = new SSLSocketFactory();
+    
+    /**
+     * Gets an singleton instance of the SSLProtocolSocketFactory.
+     * @return a SSLProtocolSocketFactory
+     */
+    public static SSLSocketFactory getSocketFactory() {
+        return DEFAULT_FACTORY;
+    }
+    
+    private final SSLContext sslcontext;
+    private final javax.net.ssl.SSLSocketFactory socketfactory;
+    private final HostNameResolver nameResolver;
+    private X509HostnameVerifier hostnameVerifier = BROWSER_COMPATIBLE_HOSTNAME_VERIFIER;
+
+    public SSLSocketFactory(
+        String algorithm, 
+        final KeyStore keystore, 
+        final String keystorePassword, 
+        final KeyStore truststore,
+        final SecureRandom random,
+        final HostNameResolver nameResolver) 
+        throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException
+    {
+        super();
+        if (algorithm == null) {
+            algorithm = TLS;
+        }
+        KeyManager[] keymanagers = null;
+        if (keystore != null) {
+            keymanagers = createKeyManagers(keystore, keystorePassword);
+        }
+        TrustManager[] trustmanagers = null;
+        if (truststore != null) {
+            trustmanagers = createTrustManagers(truststore);
+        }
+        this.sslcontext = SSLContext.getInstance(algorithm);
+        this.sslcontext.init(keymanagers, trustmanagers, random);
+        this.socketfactory = this.sslcontext.getSocketFactory();
+        this.nameResolver = nameResolver;
+    }
+
+    public SSLSocketFactory(
+            final KeyStore keystore, 
+            final String keystorePassword, 
+            final KeyStore truststore) 
+            throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException
+    {
+        this(TLS, keystore, keystorePassword, truststore, null, null);
+    }
+
+    public SSLSocketFactory(final KeyStore keystore, final String keystorePassword) 
+            throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException
+    {
+        this(TLS, keystore, keystorePassword, null, null, null);
+    }
+
+    public SSLSocketFactory(final KeyStore truststore) 
+            throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException
+    {
+        this(TLS, null, null, truststore, null, null);
+    }
+
+    /**
+     * Constructs an HttpClient SSLSocketFactory backed by the given JSSE
+     * SSLSocketFactory.
+     */
+    public SSLSocketFactory(javax.net.ssl.SSLSocketFactory socketfactory) {
+        super();
+        this.sslcontext = null;
+        this.socketfactory = socketfactory;
+        this.nameResolver = null;
+    }
+
+    /**
+     * Creates the default SSL socket factory.
+     * This constructor is used exclusively to instantiate the factory for
+     * {@link #getSocketFactory getSocketFactory}.
+     */
+    private SSLSocketFactory() {
+        super();
+        this.sslcontext = null;
+        this.socketfactory = HttpsURLConnection.getDefaultSSLSocketFactory();
+        this.nameResolver = null;
+    }
+
+    private static KeyManager[] createKeyManagers(final KeyStore keystore, final String password)
+        throws KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException {
+        if (keystore == null) {
+            throw new IllegalArgumentException("Keystore may not be null");
+        }
+        KeyManagerFactory kmfactory = KeyManagerFactory.getInstance(
+            KeyManagerFactory.getDefaultAlgorithm());
+        kmfactory.init(keystore, password != null ? password.toCharArray(): null);
+        return kmfactory.getKeyManagers(); 
+    }
+
+    private static TrustManager[] createTrustManagers(final KeyStore keystore)
+        throws KeyStoreException, NoSuchAlgorithmException { 
+        if (keystore == null) {
+            throw new IllegalArgumentException("Keystore may not be null");
+        }
+        TrustManagerFactory tmfactory = TrustManagerFactory.getInstance(
+            TrustManagerFactory.getDefaultAlgorithm());
+        tmfactory.init(keystore);
+        return tmfactory.getTrustManagers();
+    }
+
+
+    // non-javadoc, see interface org.apache.http.conn.SocketFactory
+    public Socket createSocket()
+        throws IOException {
+
+        // the cast makes sure that the factory is working as expected
+        return (SSLSocket) this.socketfactory.createSocket();
+    }
+
+
+    // non-javadoc, see interface org.apache.http.conn.SocketFactory
+    public Socket connectSocket(
+        final Socket sock,
+        final String host,
+        final int port,
+        final InetAddress localAddress,
+        int localPort,
+        final HttpParams params
+    ) throws IOException {
+
+        if (host == null) {
+            throw new IllegalArgumentException("Target host may not be null.");
+        }
+        if (params == null) {
+            throw new IllegalArgumentException("Parameters may not be null.");
+        }
+
+        SSLSocket sslsock = (SSLSocket)
+            ((sock != null) ? sock : createSocket());
+
+        if ((localAddress != null) || (localPort > 0)) {
+
+            // we need to bind explicitly
+            if (localPort < 0)
+                localPort = 0; // indicates "any"
+
+            InetSocketAddress isa =
+                new InetSocketAddress(localAddress, localPort);
+            sslsock.bind(isa);
+        }
+
+        int connTimeout = HttpConnectionParams.getConnectionTimeout(params);
+        int soTimeout = HttpConnectionParams.getSoTimeout(params);
+
+        InetSocketAddress remoteAddress;
+        if (this.nameResolver != null) {
+            remoteAddress = new InetSocketAddress(this.nameResolver.resolve(host), port); 
+        } else {
+            remoteAddress = new InetSocketAddress(host, port);            
+        }
+        
+        sslsock.connect(remoteAddress, connTimeout);
+
+        sslsock.setSoTimeout(soTimeout);
+        try {
+            hostnameVerifier.verify(host, sslsock);
+            // verifyHostName() didn't blowup - good!
+        } catch (IOException iox) {
+            // close the socket before re-throwing the exception
+            try { sslsock.close(); } catch (Exception x) { /*ignore*/ }
+            throw iox;
+        }
+
+        return sslsock;
+    }
+
+
+    /**
+     * Checks whether a socket connection is secure.
+     * This factory creates TLS/SSL socket connections
+     * which, by default, are considered secure.
+     * <br/>
+     * Derived classes may override this method to perform
+     * runtime checks, for example based on the cypher suite.
+     *
+     * @param sock      the connected socket
+     *
+     * @return  <code>true</code>
+     *
+     * @throws IllegalArgumentException if the argument is invalid
+     */
+    public boolean isSecure(Socket sock)
+        throws IllegalArgumentException {
+
+        if (sock == null) {
+            throw new IllegalArgumentException("Socket may not be null.");
+        }
+        // This instanceof check is in line with createSocket() above.
+        if (!(sock instanceof SSLSocket)) {
+            throw new IllegalArgumentException
+                ("Socket not created by this factory.");
+        }
+        // This check is performed last since it calls the argument object.
+        if (sock.isClosed()) {
+            throw new IllegalArgumentException("Socket is closed.");
+        }
+
+        return true;
+
+    } // isSecure
+
+
+    // non-javadoc, see interface LayeredSocketFactory
+    public Socket createSocket(
+        final Socket socket,
+        final String host,
+        final int port,
+        final boolean autoClose
+    ) throws IOException, UnknownHostException {
+        SSLSocket sslSocket = (SSLSocket) this.socketfactory.createSocket(
+              socket,
+              host,
+              port,
+              autoClose
+        );
+        hostnameVerifier.verify(host, sslSocket);
+        // verifyHostName() didn't blowup - good!
+        return sslSocket;
+    }
+
+    public void setHostnameVerifier(X509HostnameVerifier hostnameVerifier) {
+        if ( hostnameVerifier == null ) {
+            throw new IllegalArgumentException("Hostname verifier may not be null");
+        }
+        this.hostnameVerifier = hostnameVerifier;
+    }
+
+    public X509HostnameVerifier getHostnameVerifier() {
+        return hostnameVerifier;
+    }
+
+}
diff --git a/src/com/android/exchange/SyncManager.java b/src/com/android/exchange/SyncManager.java
index 0133aaf..2a1e10b 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,9 +30,10 @@
 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.harmony.xnet.provider.jsse.SSLContextImpl;
 import org.apache.http.conn.ClientConnectionManager;
 import org.apache.http.conn.params.ConnManagerPNames;
 import org.apache.http.conn.params.ConnPerRoute;
@@ -39,7 +41,6 @@
 import org.apache.http.conn.scheme.PlainSocketFactory;
 import org.apache.http.conn.scheme.Scheme;
 import org.apache.http.conn.scheme.SchemeRegistry;
-import org.apache.http.conn.ssl.SSLSocketFactory;
 import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
 import org.apache.http.params.BasicHttpParams;
 import org.apache.http.params.HttpParams;
@@ -195,7 +196,7 @@
     private EasSyncStatusObserver mSyncStatusObserver;
     private EasAccountsUpdatedListener mAccountsUpdatedListener;
 
-    private ContentResolver mResolver;
+    /*package*/ ContentResolver mResolver;
 
     // The singleton SyncManager object, with its thread and stop flag
     protected static SyncManager INSTANCE;
@@ -280,6 +281,10 @@
             }
         }
 
+        public Bundle autoDiscover(String userName, String password) throws RemoteException {
+            return new EasSyncService().tryAutodiscover(userName, password);
+        }
+
         public void startSync(long mailboxId) throws RemoteException {
             checkSyncManagerServiceRunning();
             Mailbox m = Mailbox.restoreMailboxWithId(INSTANCE, mailboxId);
@@ -296,8 +301,8 @@
                 kick("start outbox");
                 // Outbox can't be synced in EAS
                 return;
-            } else if (m.mType == Mailbox.TYPE_DRAFTS) {
-                // Drafts can't be synced in EAS
+            } else if (m.mType == Mailbox.TYPE_DRAFTS || m.mType == Mailbox.TYPE_TRASH) {
+                // Drafts & Trash can't be synced in EAS
                 return;
             }
             startManualSync(mailboxId, SyncManager.SYNC_SERVICE_START_SYNC, null);
@@ -310,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 {
@@ -326,7 +331,10 @@
                         // If it's a login failure, look a little harder
                         Mailbox m = Mailbox.restoreMailboxWithId(INSTANCE, mailboxId);
                         // If it's for the account whose host has changed, clear the error
-                        if (m.mAccountKey == accountId) {
+                        // If the mailbox is no longer around, remove the entry in the map
+                        if (m == null) {
+                            INSTANCE.mSyncErrorMap.remove(mailboxId);
+                        } else if (m.mAccountKey == accountId) {
                             error.fatal = false;
                             error.holdEndTime = 0;
                         }
@@ -343,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
@@ -492,13 +503,17 @@
             try {
                 collectEasAccounts(c, currentAccounts);
                 for (Account account : mAccounts) {
-                    if (!currentAccounts.contains(account.mId)) {
+                    // Ignore accounts not fully created
+                    if ((account.mFlags & Account.FLAGS_INCOMPLETE) != 0) {
+                        log("Account observer noticed incomplete account; ignoring");
+                        continue;
+                    } else if (!currentAccounts.contains(account.mId)) {
                         // This is a deletion; shut down any account-related syncs
                         stopAccountSyncs(account.mId, true);
                         // 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;
@@ -514,8 +529,7 @@
                                     WHERE_IN_ACCOUNT_AND_PUSHABLE,
                                     new String[] {Long.toString(account.mId)});
                             // Stop all current syncs; the appropriate ones will restart
-                            INSTANCE.log("Account " + account.mDisplayName +
-                                " changed; stop running syncs...");
+                            log("Account " + account.mDisplayName + " changed; stop syncs");
                             stopAccountSyncs(account.mId, true);
                         }
                     }
@@ -525,6 +539,7 @@
                 for (Account account: currentAccounts) {
                     if (!mAccounts.contains(account.mId)) {
                         // This is an addition; create our magic hidden mailbox...
+                        log("Account observer found new account: " + account.mDisplayName);
                         addAccountMailbox(account.mId);
                         // Don't forget to cache the HostAuth
                         HostAuth ha =
@@ -692,7 +707,9 @@
 
     public class EasAccountsUpdatedListener implements OnAccountsUpdateListener {
        public void onAccountsUpdated(android.accounts.Account[] accounts) {
-           checkWithAccountManager();
+           reconcileAccountsWithAccountManager(INSTANCE, getAccountList(),
+                   AccountManager.get(INSTANCE).getAccountsByType(
+                           Email.EXCHANGE_ACCOUNT_MANAGER_TYPE));
        }
     }
 
@@ -729,7 +746,7 @@
     static public String getDeviceId() throws IOException {
         return getDeviceId(null);
     }
-    
+
     static public synchronized String getDeviceId(Context context) throws IOException {
         if (sDeviceId != null) {
             return sDeviceId;
@@ -751,7 +768,7 @@
                 return id;
             } else if (f.createNewFile()) {
                 BufferedWriter w = new BufferedWriter(new FileWriter(f), 128);
-                id = "droid" + System.currentTimeMillis();
+                id = "android" + System.currentTimeMillis();
                 w.write(id);
                 w.close();
                 sDeviceId = id;
@@ -768,10 +785,10 @@
     }
 
     /**
-     * Note that there are two ways the EAS SyncManager service can be created: 
+     * Note that there are two ways the EAS SyncManager service can be created:
      *
-     * 1) as a background service instantiated via startService (which happens on boot, when the 
-     * first EAS account is created, etc), in which case the service thread is spun up, mailboxes 
+     * 1) as a background service instantiated via startService (which happens on boot, when the
+     * first EAS account is created, etc), in which case the service thread is spun up, mailboxes
      * sync, etc. and
      * 2) to execute an RPC call from the UI, in which case the background service will already be
      * running most of the time (unless we're creating a first EAS account)
@@ -841,6 +858,7 @@
             mSyncedMessageObserver = null;
             mMessageObserver = null;
             mSyncStatusObserver = null;
+            mAccountsUpdatedListener = null;
         }
     }
 
@@ -867,7 +885,7 @@
             INSTANCE.startService(new Intent(INSTANCE, SyncManager.class));
         }
     }
-    
+
     static public ConnPerRoute sConnPerRoute = new ConnPerRoute() {
         public int getMaxForRoute(HttpRoute route) {
             return 8;
@@ -889,15 +907,13 @@
             SSLContext sslcontext;
             try {
                 sslcontext = SSLContext.getInstance("TLS");
-                sslcontext.init(null, trustManagers, null);
-                SSLContextImpl sslContext = new SSLContextImpl();
                 try {
-                    sslContext.engineInit(null, trustManagers, null, null, null);
+                    sslcontext.init(null, trustManagers, null);
                 } catch (KeyManagementException e) {
                     throw new AssertionError(e);
                 }
                 // Ok, now make our factory
-                SSLSocketFactory sf = new SSLSocketFactory(sslContext.engineGetSocketFactory());
+                SSLSocketFactory sf = new SSLSocketFactory(sslcontext.getSocketFactory());
                 sf.setHostnameVerifier(SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
                 // Register the httpts scheme with our factory
                 registry.register(new Scheme("httpts", sf, 443));
@@ -907,7 +923,6 @@
                 params.setParameter(ConnManagerPNames.MAX_CONNECTIONS_PER_ROUTE, sConnPerRoute);
                 sClientConnectionManager = new ThreadSafeClientConnManager(params, registry);
             } catch (NoSuchAlgorithmException e2) {
-            } catch (KeyManagementException e1) {
             }
         }
         // Null is a valid return result if we get an exception
@@ -1153,6 +1168,10 @@
                     // We ignore drafts completely (doesn't sync).  Changes in Outbox are handled
                     // in the checkMailboxes loop, so we can ignore these pings.
                     if (m.mType == Mailbox.TYPE_DRAFTS || m.mType == Mailbox.TYPE_OUTBOX) {
+                        String[] args = new String[] {Long.toString(m.mId)};
+                        ContentResolver resolver = INSTANCE.mResolver;
+                        resolver.delete(Message.DELETED_CONTENT_URI, WHERE_MAILBOX_KEY, args);
+                        resolver.delete(Message.UPDATED_CONTENT_URI, WHERE_MAILBOX_KEY, args);
                         return;
                     }
                     service.mAccount = Account.restoreAccountWithId(INSTANCE, m.mAccountKey);
@@ -1183,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;
@@ -1216,25 +1235,56 @@
         }
     }
 
-    private void checkWithAccountManager() {
-        android.accounts.Account[] accts =
-            AccountManager.get(this).getAccountsByType(Eas.ACCOUNT_MANAGER_TYPE);
-        List<Account> easAccounts = getAccountList();
-        for (Account easAccount: easAccounts) {
-            String accountName = easAccount.mEmailAddress;
+    /**
+     * Compare our account list (obtained from EmailProvider) with the account list owned by
+     * AccountManager.  If there are any orphans (an account in one list without a corresponding
+     * account in the other list), delete the orphan, as these must remain in sync.
+     *
+     * Note that the duplication of account information is caused by the Email application's
+     * incomplete integration with AccountManager.
+     */
+    /*package*/ void reconcileAccountsWithAccountManager(Context context,
+            List<Account> cachedEasAccounts, android.accounts.Account[] accountManagerAccounts) {
+        // First, look through our cached EAS Accounts (from EmailProvider) to make sure there's a
+        // corresponding AccountManager account
+        for (Account providerAccount: cachedEasAccounts) {
+            String providerAccountName = providerAccount.mEmailAddress;
             boolean found = false;
-            for (android.accounts.Account acct: accts) {
-                if (acct.name.equalsIgnoreCase(accountName)) {
+            for (android.accounts.Account accountManagerAccount: accountManagerAccounts) {
+                if (accountManagerAccount.name.equalsIgnoreCase(providerAccountName)) {
                     found = true;
                     break;
                 }
             }
             if (!found) {
+                if ((providerAccount.mFlags & Account.FLAGS_INCOMPLETE) != 0) {
+                    log("Account reconciler noticed incomplete account; ignoring");
+                    continue;
+                }
                 // This account has been deleted in the AccountManager!
-                log("Account deleted in AccountManager; deleting from provider: " + accountName);
+                log("Account deleted in AccountManager; deleting from provider: " +
+                        providerAccountName);
                 // TODO This will orphan downloaded attachments; need to handle this
-                mResolver.delete(ContentUris.withAppendedId(Account.CONTENT_URI, easAccount.mId),
-                        null, null);
+                mResolver.delete(ContentUris.withAppendedId(Account.CONTENT_URI,
+                        providerAccount.mId), null, null);
+            }
+        }
+        // Now, look through AccountManager accounts to make sure we have a corresponding cached EAS
+        // account from EmailProvider
+        for (android.accounts.Account accountManagerAccount: accountManagerAccounts) {
+            String accountManagerAccountName = accountManagerAccount.name;
+            boolean found = false;
+            for (Account cachedEasAccount: cachedEasAccounts) {
+                if (cachedEasAccount.mEmailAddress.equalsIgnoreCase(accountManagerAccountName)) {
+                    found = true;
+                }
+            }
+            if (!found) {
+                // This account has been deleted from the EmailProvider database
+                log("Account deleted from provider; deleting from AccountManager: " +
+                        accountManagerAccountName);
+                // Delete the account
+                AccountManager.get(context).removeAccount(accountManagerAccount, null, null);
             }
         }
     }
@@ -1295,19 +1345,23 @@
         }
     }
 
-    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) {
             Account acct = Account.restoreAccountWithId(this, m.mAccountKey);
             if (acct != null) {
-                AbstractSyncService service;
-                service = new EasSyncService(this, m);
-                service.mSyncReason = reason;
-                if (req != null) {
-                    service.addPartRequest(req);
+                // Always make sure there's not a running instance of this service
+                AbstractSyncService service = mServiceMap.get(m.mId);
+                if (service == null) {
+                    service = new EasSyncService(this, m);
+                    if (!((EasSyncService)service).mIsValid) return;
+                    service.mSyncReason = reason;
+                    if (req != null) {
+                        service.addRequest(req);
+                    }
+                    startService(service, m);
                 }
-                startService(service, m);
             }
         }
     }
@@ -1392,6 +1446,7 @@
         mResolver.registerContentObserver(Mailbox.CONTENT_URI, false, mMailboxObserver);
         mResolver.registerContentObserver(Message.SYNCED_CONTENT_URI, true, mSyncedMessageObserver);
         mResolver.registerContentObserver(Message.CONTENT_URI, true, mMessageObserver);
+        // TODO SYNC_OBSERVER_TYPE_SETTINGS is hidden.  Waiting for b/2337197
         ContentResolver.addStatusChangeListener(ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS,
                 mSyncStatusObserver);
         mAccountsUpdatedListener = new EasAccountsUpdatedListener();
@@ -1447,7 +1502,7 @@
             if (mConnectivityReceiver != null) {
                 unregisterReceiver(mConnectivityReceiver);
             }
-            
+
             if (INSTANCE != null) {
                 ContentResolver resolver = getContentResolver();
                 resolver.unregisterContentObserver(mAccountObserver);
@@ -1496,18 +1551,21 @@
                     deletedMailboxes.add(mailboxId);
                 }
             }
-        }
-        // If so, stop them or remove them from the map
-        for (Long mailboxId: deletedMailboxes) {
-            AbstractSyncService svc = mServiceMap.get(mailboxId);
-            if (svc != null) {
-                boolean alive = svc.mThread.isAlive();
-                log("Deleted mailbox: " + svc.mMailboxName);
-                if (alive) {
-                    stopManualSync(mailboxId);
-                } else {
-                    log("Removing from serviceMap");
+            // If so, stop them or remove them from the map
+            for (Long mailboxId: deletedMailboxes) {
+                AbstractSyncService svc = mServiceMap.get(mailboxId);
+                if (svc == null || svc.mThread == null) {
                     releaseMailbox(mailboxId);
+                    continue;
+                } else {
+                    boolean alive = svc.mThread.isAlive();
+                    log("Deleted mailbox: " + svc.mMailboxName);
+                    if (alive) {
+                        stopManualSync(mailboxId);
+                    } else {
+                        log("Removing from serviceMap");
+                        releaseMailbox(mailboxId);
+                    }
                 }
             }
         }
@@ -1523,7 +1581,10 @@
         try {
             while (c.moveToNext()) {
                 long mid = c.getLong(Mailbox.CONTENT_ID_COLUMN);
-                AbstractSyncService service = mServiceMap.get(mid);
+                AbstractSyncService service = null;
+                synchronized (sSyncToken) {
+                    service = mServiceMap.get(mid);
+                }
                 if (service == null) {
                     // Check whether we're in a hold (temporary or permanent)
                     SyncError syncError = mSyncErrorMap.get(mid);
@@ -1553,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;
@@ -1672,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;
         }
@@ -1685,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);
         }
     }
 
@@ -1739,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) {
@@ -1812,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..41b6a66 100644
--- a/src/com/android/exchange/adapter/CalendarSyncAdapter.java
+++ b/src/com/android/exchange/adapter/CalendarSyncAdapter.java
@@ -17,11 +17,43 @@
 
 package com.android.exchange.adapter;
 
+import com.android.email.Email;
 import com.android.email.provider.EmailContent.Mailbox;
 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 +61,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 +119,1116 @@
     }
 
     @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));
+
+            String exceptionStartTime = "_noStartTime";
+            while (nextTag(Tags.SYNC_APPLICATION_DATA) != END) {
+                switch (tag) {
+                    case Tags.CALENDAR_EXCEPTION_START_TIME:
+                        exceptionStartTime = getValue();
+                        cv.put(Events.ORIGINAL_INSTANCE_TIME,
+                                CalendarUtilities.parseDateTimeToMillis(exceptionStartTime));
+                        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();
+                }
+            }
+
+            // We need a _sync_id, but it can't be the parent's id, so we generate one
+            cv.put(Events._SYNC_ID, parentCv.getAsString(Events._SYNC_ID) + '_' +
+                    exceptionStartTime);
+
+            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()));
+
+        s.writeStringValue(entityValues, Events.EVENT_LOCATION, Tags.CALENDAR_LOCATION);
+        s.writeStringValue(entityValues, Events.TITLE, Tags.CALENDAR_SUBJECT);
+
+        Integer visibility = entityValues.getAsInteger(Events.VISIBILITY);
+        if (visibility != null) {
+            s.data(Tags.CALENDAR_SENSITIVITY, decodeVisibility(visibility));
+        } else {
+            // Default to 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 = entityValues.getAsString(Events.EVENT_TIMEZONE);
+            if (timeZoneName == null) {
+                timeZoneName = TimeZone.getDefault().getID();
+            }
+            String timeZone = CalendarUtilities.timeZoneToTziString(
+                    TimeZone.getTimeZone(timeZoneName));
+            s.data(Tags.CALENDAR_TIME_ZONE, timeZone);
+
+            String desc = entityValues.getAsString(Events.DESCRIPTION);
+            if (desc != null) {
+                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);
+                }
+            }
+
+            s.writeStringValue(entityValues, Events.ORGANIZER, Tags.CALENDAR_ORGANIZER_EMAIL);
+
+            String rrule = entityValues.getAsString(Events.RRULE);
+            if (rrule != null) {
+                CalendarUtilities.recurrenceFromRrule(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)) {
+                    String uid = ncvValues.getAsString("uid");
+                    if (uid != null) {
+                        clientId = uid;
+                        s.data(Tags.CALENDAR_UID, clientId);
+                    }
+                    s.writeStringValue(ncvValues, "dtstamp", Tags.CALENDAR_DTSTAMP);
+                    String categories = ncvValues.getAsString("categories");
+                    if (categories != null) {
+                        // Send all the categories back to the server
+                        // We've saved them as a String of delimited tokens
+                        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)) {
+                    s.writeStringValue(ncvValues, Reminders.MINUTES,
+                            Tags.CALENDAR_REMINDER_MINS_BEFORE);
+                }
+            }
+
+            // 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)) {
+                    Integer relationship = ncvValues.getAsInteger(Attendees.ATTENDEE_RELATIONSHIP);
+                    if (relationship != null) {
+                        // Organizer isn't among attendees in EAS
+                        if (relationship == Attendees.RELATIONSHIP_ORGANIZER) {
+                            organizerName = ncvValues.getAsString(Attendees.ATTENDEE_NAME);
+                            continue;
+                        }
+                        if (!hasAttendees) {
+                            s.start(Tags.CALENDAR_ATTENDEES);
+                            hasAttendees = true;
+                        }
+                        s.start(Tags.CALENDAR_ATTENDEE);
+                        s.writeStringValue(ncvValues, Attendees.ATTENDEE_NAME,
+                                Tags.CALENDAR_ATTENDEE_NAME);
+                        s.writeStringValue(ncvValues, Attendees.ATTENDEE_EMAIL,
+                                Tags.CALENDAR_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!)
+            Long originalTime = entityValues.getAsLong(Events.ORIGINAL_INSTANCE_TIME);
+            if (originalTime != null) {
+                s.data(Tags.CALENDAR_EXCEPTION_START_TIME,
+                        CalendarUtilities.millisToEasDateTime(originalTime));
+            } 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 fff6400..b95e475 100644
--- a/src/com/android/exchange/adapter/ContactsSyncAdapter.java
+++ b/src/com/android/exchange/adapter/ContactsSyncAdapter.java
@@ -41,6 +41,7 @@
 import android.provider.ContactsContract.Data;
 import android.provider.ContactsContract.Groups;
 import android.provider.ContactsContract.RawContacts;
+import android.provider.ContactsContract.RawContactsEntity;
 import android.provider.ContactsContract.Settings;
 import android.provider.ContactsContract.SyncState;
 import android.provider.ContactsContract.CommonDataKinds.Email;
@@ -162,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";
@@ -197,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;
     }
@@ -741,16 +742,19 @@
                         try {
                             if (c.moveToFirst()) {
                                 // TODO Handle deleted individual rows...
+                                Uri uri = ContentUris.withAppendedId(
+                                        RawContacts.CONTENT_URI, c.getLong(0));
+                                uri = Uri.withAppendedPath(
+                                        uri, RawContacts.Entity.CONTENT_DIRECTORY);
+                                EntityIterator entityIterator = RawContacts.newEntityIterator(
+                                    mContentResolver.query(uri, null, null, null, null));
                                 try {
-                                    EntityIterator entityIterator =
-                                        mContentResolver.queryEntities(ContentUris
-                                            .withAppendedId(RawContacts.CONTENT_URI, c.getLong(0)),
-                                            null, null, null);
                                     if (entityIterator.hasNext()) {
                                         entity = entityIterator.next();
                                     }
                                     userLog("Changing contact ", serverId);
                                 } catch (RemoteException e) {
+                                    // TODO: log the fact that we failed to read the entity
                                 }
                             }
                         } finally {
@@ -889,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();
     }
@@ -1628,6 +1633,15 @@
         }
     }
 
+    private void sendPhoto(Serializer s, ContentValues cv) throws IOException {
+        if (cv.containsKey(Photo.PHOTO)) {
+            byte[] bytes = cv.getAsByteArray(Photo.PHOTO);
+            byte[] encodedBytes = Base64.encodeBase64(bytes);
+            String pic = new String(encodedBytes);
+            s.data(Tags.CONTACTS_PICTURE, pic);
+        }
+    }
+
     private void sendOrganization(Serializer s, ContentValues cv) throws IOException {
         if (cv.containsKey(Organization.TITLE)) {
             s.data(Tags.CONTACTS_JOB_TITLE, cv.getAsString(Organization.TITLE));
@@ -1756,9 +1770,10 @@
     public boolean sendLocalChanges(Serializer s) throws IOException {
         // First, let's find Contacts that have changed.
         ContentResolver cr = mService.mContentResolver;
-        Uri uri = RawContacts.CONTENT_URI.buildUpon()
+        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();
 
@@ -1768,7 +1783,8 @@
 
         try {
             // Get them all atomically
-            EntityIterator ei = cr.queryEntities(uri, RawContacts.DIRTY + "=1", null, null);
+            EntityIterator ei = RawContacts.newEntityIterator(
+                    cr.query(uri, null, RawContacts.DIRTY + "=1", null, null));
             ContentValues cidValues = new ContentValues();
             try {
                 boolean first = true;
@@ -1854,8 +1870,7 @@
                         } else if (mimeType.equals(Note.CONTENT_ITEM_TYPE)) {
                             sendNote(s, cv);
                         } else if (mimeType.equals(Photo.CONTENT_ITEM_TYPE)) {
-                            // For now, the user can change the photo, but the change won't be
-                            // uploaded.
+                            sendPhoto(s, cv);
                         } else {
                             userLog("Contacts upsync, unknown data: ", mimeType);
                         }
@@ -1866,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 946ae4e..c835f85 100644
--- a/src/com/android/exchange/adapter/EmailSyncAdapter.java
+++ b/src/com/android/exchange/adapter/EmailSyncAdapter.java
@@ -63,6 +63,9 @@
     private static final String[] UPDATES_PROJECTION =
         {MessageColumns.FLAG_READ, MessageColumns.MAILBOX_KEY, SyncColumns.SERVER_ID,
             MessageColumns.FLAG_FAVORITE};
+
+    private static final int MESSAGE_ID_SUBJECT_ID_COLUMN = 0;
+    private static final int MESSAGE_ID_SUBJECT_SUBJECT_COLUMN = 1;
     private static final String[] MESSAGE_ID_SUBJECT_PROJECTION =
         new String[] { Message.RECORD_ID, MessageColumns.SUBJECT };
 
@@ -163,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();
                 }
@@ -317,7 +328,7 @@
                     WHERE_SERVER_ID_AND_MAILBOX_KEY, mBindArguments, null);
         }
 
-        private void deleteParser(ArrayList<Long> deletes, int entryTag) throws IOException {
+        /*package*/ void deleteParser(ArrayList<Long> deletes, int entryTag) throws IOException {
             while (nextTag(entryTag) != END) {
                 switch (tag) {
                     case Tags.SYNC_SERVER_ID:
@@ -326,9 +337,10 @@
                         Cursor c = getServerIdCursor(serverId, MESSAGE_ID_SUBJECT_PROJECTION);
                         try {
                             if (c.moveToFirst()) {
-                                deletes.add(c.getLong(0));
+                                deletes.add(c.getLong(MESSAGE_ID_SUBJECT_ID_COLUMN));
                                 if (Eas.USER_LOG) {
-                                    userLog("Deleting ", serverId + ", " + c.getString(1));
+                                    userLog("Deleting ", serverId + ", "
+                                            + c.getString(MESSAGE_ID_SUBJECT_SUBJECT_COLUMN));
                                 }
                             }
                         } finally {
@@ -353,7 +365,7 @@
             }
         }
 
-        private void changeParser(ArrayList<ServerChange> changes) throws IOException {
+        /*package*/ void changeParser(ArrayList<ServerChange> changes) throws IOException {
             String serverId = null;
             Boolean oldRead = false;
             Boolean oldFlag = false;
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 ca99a77..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
@@ -304,14 +326,18 @@
         tagTable = tagTables[0];
     }
 
+    /*package*/ void resetInput(InputStream in) {
+        this.in = in;
+    }
+    
     void log(String str) {
         int cr = str.indexOf('\n');
         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);
         }
     }
 
@@ -381,7 +407,7 @@
                     text = readInlineString();
                 }
                 if (logging) {
-                    name = tagTable[startTag - 5];
+                    name = tagTable[startTag - TAG_BASE];
                     log(name + ": " + (asInt ? Integer.toString(num) : text));
                 }
                 break;
@@ -395,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/ProvisionParser.java b/src/com/android/exchange/adapter/ProvisionParser.java
new file mode 100644
index 0000000..ffa1e45
--- /dev/null
+++ b/src/com/android/exchange/adapter/ProvisionParser.java
@@ -0,0 +1,178 @@
+/* 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.SecurityPolicy;
+import com.android.email.SecurityPolicy.PolicySet;
+import com.android.exchange.EasSyncService;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Parse the result of the Provision command
+ *
+ * Assuming a successful parse, we store the PolicySet and the policy key
+ */
+public class ProvisionParser extends Parser {
+    private EasSyncService mService;
+    PolicySet mPolicySet = null;
+    String mPolicyKey = null;
+
+    public ProvisionParser(InputStream in, EasSyncService service) throws IOException {
+        super(in);
+        mService = service;
+        setDebug(true);
+    }
+
+    public PolicySet getPolicySet() {
+        return mPolicySet;
+    }
+
+    public String getPolicyKey() {
+        return mPolicyKey;
+    }
+
+    public void parseProvisionDoc() throws IOException {
+        int minPasswordLength = 0;
+        int passwordMode = PolicySet.PASSWORD_MODE_NONE;
+        int maxPasswordFails = 0;
+        int maxScreenLockTime = 0;
+        boolean canSupport = false;
+
+        while (nextTag(Tags.PROVISION_EAS_PROVISION_DOC) != END) {
+            switch (tag) {
+                case Tags.PROVISION_DEVICE_PASSWORD_ENABLED:
+                    if (getValueInt() == 1) {
+                        if (passwordMode == PolicySet.PASSWORD_MODE_NONE) {
+                            passwordMode = PolicySet.PASSWORD_MODE_SIMPLE;
+                        }
+                    }
+                    break;
+                case Tags.PROVISION_MIN_DEVICE_PASSWORD_LENGTH:
+                    minPasswordLength = getValueInt();
+                    break;
+                case Tags.PROVISION_ALPHA_DEVICE_PASSWORD_ENABLED:
+                    if (getValueInt() == 1) {
+                        passwordMode = PolicySet.PASSWORD_MODE_STRONG;
+                    }
+                    break;
+                case Tags.PROVISION_MAX_INACTIVITY_TIME_DEVICE_LOCK:
+                    // EAS gives us seconds, which is, happily, what the PolicySet requires
+                    maxScreenLockTime = getValueInt();
+                    break;
+                case Tags.PROVISION_MAX_DEVICE_PASSWORD_FAILED_ATTEMPTS:
+                    maxPasswordFails = getValueInt();
+                    break;
+                case Tags.PROVISION_ALLOW_SIMPLE_DEVICE_PASSWORD:
+                    // Ignore this unless there's any MSFT documentation for what this means
+                    // Hint: I haven't seen any that's more specific than "simple"
+                    getValue();
+                    break;
+                // The following policy, if false, can't be supported at the moment
+                case Tags.PROVISION_ATTACHMENTS_ENABLED:
+                    if (getValueInt() == 0) {
+                        canSupport = false;
+                    }
+                    break;
+                // The following policies, if true, can't be supported at the moment
+                case Tags.PROVISION_DEVICE_ENCRYPTION_ENABLED:
+                case Tags.PROVISION_PASSWORD_RECOVERY_ENABLED:
+                case Tags.PROVISION_DEVICE_PASSWORD_EXPIRATION:
+                case Tags.PROVISION_DEVICE_PASSWORD_HISTORY:
+                case Tags.PROVISION_MAX_ATTACHMENT_SIZE:
+                    if (getValueInt() == 1) {
+                        canSupport = false;
+                    }
+                    break;
+                default:
+                    skipTag();
+            }
+        }
+
+        if (canSupport) {
+            mPolicySet = new SecurityPolicy.PolicySet(minPasswordLength, passwordMode,
+                    maxPasswordFails, maxScreenLockTime, true);
+        }
+    }
+
+    public void parseProvisionData() throws IOException {
+        while (nextTag(Tags.PROVISION_DATA) != END) {
+            if (tag == Tags.PROVISION_EAS_PROVISION_DOC) {
+                parseProvisionDoc();
+            } else {
+                skipTag();
+            }
+        }
+    }
+
+    public void parsePolicy() throws IOException {
+        while (nextTag(Tags.PROVISION_POLICY) != END) {
+            switch (tag) {
+                case Tags.PROVISION_POLICIES:
+                    parsePolicies();
+                    break;
+                case Tags.PROVISION_POLICY_TYPE:
+                    mService.userLog("Policy type: ", getValue());
+                    break;
+                case Tags.PROVISION_POLICY_KEY:
+                    mPolicyKey = getValue();
+                    break;
+                case Tags.PROVISION_STATUS:
+                    mService.userLog("Policy status: ", getValue());
+                    break;
+                case Tags.PROVISION_DATA:
+                    parseProvisionData();
+                    break;
+                default:
+                    skipTag();
+            }
+        }
+    }
+
+    public void parsePolicies() throws IOException {
+        while (nextTag(Tags.PROVISION_POLICIES) != END) {
+            if (tag == Tags.PROVISION_POLICY) {
+                parsePolicy();
+            } else {
+                skipTag();
+            }
+        }
+    }
+
+    @Override
+    public boolean parse() throws IOException {
+        boolean res = false;
+        if (nextTag(START_DOCUMENT) != Tags.PROVISION_PROVISION) {
+            throw new IOException();
+        }
+        while (nextTag(START_DOCUMENT) != END_DOCUMENT) {
+            switch (tag) {
+                case Tags.PROVISION_STATUS:
+                    int status = getValueInt();
+                    mService.userLog("Provision status: ", status);
+                    break;
+                case Tags.PROVISION_POLICIES:
+                    parsePolicies();
+                    return (mPolicySet != null);
+                default:
+                    skipTag();
+            }
+        }
+        return res;
+    }
+}
+
diff --git a/src/com/android/exchange/adapter/Serializer.java b/src/com/android/exchange/adapter/Serializer.java
index f314772..8cdb99b 100644
--- a/src/com/android/exchange/adapter/Serializer.java
+++ b/src/com/android/exchange/adapter/Serializer.java
@@ -26,6 +26,7 @@
 import com.android.exchange.Eas;
 import com.android.exchange.utility.FileLogger;
 
+import android.content.ContentValues;
 import android.util.Log;
 
 import java.io.ByteArrayOutputStream;
@@ -54,12 +55,20 @@
     private int tagPage;
 
     public Serializer() {
+        this(true);
+    }
+
+    public Serializer(boolean startDocument) {
         super();
-        try {
-            startDocument();
-            //logging = Eas.PARSER_LOG;
-        } catch (IOException e) {
-            // Nothing to be done
+        if (startDocument) {
+            try {
+                startDocument();
+                //logging = Eas.PARSER_LOG;
+            } catch (IOException e) {
+                // Nothing to be done
+            }
+        } else {
+            out.write(0);
         }
     }
 
@@ -185,4 +194,11 @@
         out.write(data);
         out.write(0);
     }
+
+    void writeStringValue (ContentValues cv, String key, int tag) throws IOException {
+        String value = cv.getAsString(key);
+        if (value != null) {
+            data(tag, value);
+        }
+    }
 }
diff --git a/src/com/android/exchange/adapter/Tags.java b/src/com/android/exchange/adapter/Tags.java
index d822181..c2385d5 100644
--- a/src/com/android/exchange/adapter/Tags.java
+++ b/src/com/android/exchange/adapter/Tags.java
@@ -39,9 +39,11 @@
     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;
+    public static final int PROVISION = 0x0E;
     public static final int GAL = 0x10;
     public static final int BASE = 0x11;
 
@@ -218,6 +220,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;
@@ -333,7 +346,6 @@
     public static final int CONTACTS2_NICKNAME = CONTACTS2_PAGE + 0xD;
     public static final int CONTACTS2_MMS = CONTACTS2_PAGE + 0xE;
 
-    // The Ping constants are used by EasSyncService, and need to be public
     public static final int PING_PAGE = PING << PAGE_SHIFT;
     public static final int PING_PING = PING_PAGE + 5;
     public static final int PING_AUTD_STATE = PING_PAGE + 6;
@@ -345,6 +357,66 @@
     public static final int PING_CLASS = PING_PAGE + 0xC;
     public static final int PING_MAX_FOLDERS = PING_PAGE + 0xD;
 
+    public static final int PROVISION_PAGE = PROVISION << PAGE_SHIFT;
+    // EAS 2.5
+    public static final int PROVISION_PROVISION = PROVISION_PAGE + 5;
+    public static final int PROVISION_POLICIES = PROVISION_PAGE + 6;
+    public static final int PROVISION_POLICY = PROVISION_PAGE + 7;
+    public static final int PROVISION_POLICY_TYPE = PROVISION_PAGE + 8;
+    public static final int PROVISION_POLICY_KEY = PROVISION_PAGE + 9;
+    public static final int PROVISION_DATA = PROVISION_PAGE + 0xA;
+    public static final int PROVISION_STATUS = PROVISION_PAGE + 0xB;
+    public static final int PROVISION_REMOTE_WIPE = PROVISION_PAGE + 0xC;
+    // EAS 12.0
+    public static final int PROVISION_EAS_PROVISION_DOC = PROVISION_PAGE + 0xD;
+    public static final int PROVISION_DEVICE_PASSWORD_ENABLED = PROVISION_PAGE + 0xE;
+    public static final int PROVISION_ALPHA_DEVICE_PASSWORD_ENABLED = PROVISION_PAGE + 0xF;
+    public static final int PROVISION_DEVICE_ENCRYPTION_ENABLED = PROVISION_PAGE + 0x10;
+    public static final int PROVISION_PASSWORD_RECOVERY_ENABLED = PROVISION_PAGE + 0x11;
+    public static final int PROVISION_ATTACHMENTS_ENABLED = PROVISION_PAGE + 0x13;
+    public static final int PROVISION_MIN_DEVICE_PASSWORD_LENGTH = PROVISION_PAGE + 0x14;
+    public static final int PROVISION_MAX_INACTIVITY_TIME_DEVICE_LOCK = PROVISION_PAGE + 0x15;
+    public static final int PROVISION_MAX_DEVICE_PASSWORD_FAILED_ATTEMPTS = PROVISION_PAGE + 0x16;
+    public static final int PROVISION_MAX_ATTACHMENT_SIZE = PROVISION_PAGE + 0x17;
+    public static final int PROVISION_ALLOW_SIMPLE_DEVICE_PASSWORD = PROVISION_PAGE + 0x18;
+    public static final int PROVISION_DEVICE_PASSWORD_EXPIRATION = PROVISION_PAGE + 0x19;
+    public static final int PROVISION_DEVICE_PASSWORD_HISTORY = PROVISION_PAGE + 0x1A;
+    public static final int PROVISION_MAX_SUPPORTED_TAG = PROVISION_DEVICE_PASSWORD_HISTORY;
+
+    // EAS 12.1
+    public static final int PROVISION_ALLOW_STORAGE_CARD = PROVISION_PAGE + 0x1B;
+    public static final int PROVISION_ALLOW_CAMERA = PROVISION_PAGE + 0x1C;
+    public static final int PROVISION_REQUIRE_DEVICE_ENCRYPTION = PROVISION_PAGE + 0x1D;
+    public static final int PROVISION_ALLOW_UNSIGNED_APPLICATIONS = PROVISION_PAGE + 0x1E;
+    public static final int PROVISION_ALLOW_UNSIGNED_INSTALLATION_PACKAGES = PROVISION_PAGE + 0x1F;
+    public static final int PROVISION_MIN_DEVICE_PASSWORD_COMPLEX_CHARS = PROVISION_PAGE + 0x20;
+    public static final int PROVISION_ALLOW_WIFI = PROVISION_PAGE + 0x21;
+    public static final int PROVISION_ALLOW_TEXT_MESSAGING = PROVISION_PAGE + 0x22;
+    public static final int PROVISION_ALLOW_POP_IMAP_EMAIL = PROVISION_PAGE + 0x23;
+    public static final int PROVISION_ALLOW_BLUETOOTH = PROVISION_PAGE + 0x24;
+    public static final int PROVISION_ALLOW_IRDA = PROVISION_PAGE + 0x25;
+    public static final int PROVISION_REQUIRE_MANUAL_SYNC_WHEN_ROAMING = PROVISION_PAGE + 0x26;
+    public static final int PROVISION_ALLOW_DESKTOP_SYNC = PROVISION_PAGE + 0x27;
+    public static final int PROVISION_MAX_CALENDAR_AGE_FILTER = PROVISION_PAGE + 0x28;
+    public static final int PROVISION_ALLOW_HTML_EMAIL = PROVISION_PAGE + 0x29;
+    public static final int PROVISION_MAX_EMAIL_AGE_FILTER = PROVISION_PAGE + 0x2A;
+    public static final int PROVISION_MAX_EMAIL_BODY_TRUNCATION_SIZE = PROVISION_PAGE + 0x2B;
+    public static final int PROVISION_MAX_EMAIL_HTML_BODY_TRUNCATION_SIZE = PROVISION_PAGE + 0x2C;
+    public static final int PROVISION_REQUIRE_SIGNED_SMIME_MESSAGES = PROVISION_PAGE + 0x2D;
+    public static final int PROVISION_REQUIRE_ENCRYPTED_SMIME_MESSAGES = PROVISION_PAGE + 0x2E;
+    public static final int PROVISION_REQUIRE_SIGNED_SMIME_ALGORITHM = PROVISION_PAGE + 0x2F;
+    public static final int PROVISION_REQUIRE_ENCRYPTION_SMIME_ALGORITHM = PROVISION_PAGE + 0x30;
+    public static final int PROVISION_ALLOW_SMIME_ENCRYPTION_NEGOTIATION = PROVISION_PAGE + 0x31;
+    public static final int PROVISION_ALLOW_SMIME_SOFT_CERTS = PROVISION_PAGE + 0x32;
+    public static final int PROVISION_ALLOW_BROWSER = PROVISION_PAGE + 0x33;
+    public static final int PROVISION_ALLOW_CONSUMER_EMAIL = PROVISION_PAGE + 0x34;
+    public static final int PROVISION_ALLOW_REMOTE_DESKTOP = PROVISION_PAGE + 0x35;
+    public static final int PROVISION_ALLOW_INTERNET_SHARING = PROVISION_PAGE + 0x36;
+    public static final int PROVISION_UNAPPROVED_IN_ROM_APPLICATION_LIST = PROVISION_PAGE + 0x37;
+    public static final int PROVISION_APPLICATION_NAME = PROVISION_PAGE + 0x38;
+    public static final int PROVISION_APPROVED_APPLICATION_LIST = PROVISION_PAGE + 0x39;
+    public static final int PROVISION_HASH = PROVISION_PAGE + 0x3A;
+
     public static final int BASE_PAGE = BASE << PAGE_SHIFT;
     public static final int BASE_BODY_PREFERENCE = BASE_PAGE + 5;
     public static final int BASE_TYPE = BASE_PAGE + 6;
@@ -441,6 +513,8 @@
         },
         {
             // 0x08 MeetingResponse
+            "CalId", "CollectionId", "MeetingResponse", "ReqId", "Request",
+            "Result", "Status", "UserResponse", "Version"
         },
         {
             // 0x09 Tasks
@@ -473,7 +547,8 @@
             "Provision", "Policies", "Policy", "PolicyType", "PolicyKey", "Data", "ProvisionStatus",
             "RemoteWipe", "EASProvidionDoc", "DevicePasswordEnabled",
             "AlphanumericDevicePasswordRequired",
-            "DeviceEncryptionEnabled", "-unused-", "AttachmentsEnabled", "MinDevicePasswordLength",
+            "DeviceEncryptionEnabled", "PasswordRecoveryEnabled", "-unused-", "AttachmentsEnabled",
+            "MinDevicePasswordLength",
             "MaxInactivityTimeDeviceLock", "MaxDevicePasswordFailedAttempts", "MaxAttachmentSize",
             "AllowSimpleDevicePassword", "DevicePasswordExpiration", "DevicePasswordHistory",
             "AllowStorageCard", "AllowCamera", "RequireDeviceEncryption",
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
new file mode 100644
index 0000000..66c20e9
--- /dev/null
+++ b/tests/src/com/android/exchange/SyncManagerAccountTests.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright (C) 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.email.Email;
+import com.android.email.provider.EmailContent;
+import com.android.email.provider.EmailProvider;
+import com.android.email.provider.ProviderTestUtils;
+import com.android.email.provider.EmailContent.Account;
+import com.android.exchange.SyncManager.AccountList;
+
+import android.accounts.AccountManager;
+import android.accounts.AccountManagerFuture;
+import android.accounts.AuthenticatorException;
+import android.accounts.OperationCanceledException;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.Context;
+import android.database.Cursor;
+import android.test.ProviderTestCase2;
+
+import java.io.IOException;
+
+public class SyncManagerAccountTests extends ProviderTestCase2<EmailProvider> {
+
+    private static final String TEST_ACCOUNT_PREFIX = "__test";
+    private static final String TEST_ACCOUNT_SUFFIX = "@android.com";
+
+    EmailProvider mProvider;
+    Context mMockContext;
+
+    public SyncManagerAccountTests() {
+        super(EmailProvider.class, EmailProvider.EMAIL_AUTHORITY);
+    }
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        mMockContext = getMockContext();
+        // Delete any test accounts we might have created earlier
+        deleteTemporaryAccountManagerAccounts(getContext());
+    }
+
+    @Override
+    public void tearDown() throws Exception {
+        super.tearDown();
+        // Delete any test accounts we might have created earlier
+        deleteTemporaryAccountManagerAccounts(getContext());
+    }
+
+    private android.accounts.Account makeAccountManagerAccount(String username) {
+        return new android.accounts.Account(username, Email.EXCHANGE_ACCOUNT_MANAGER_TYPE);
+    }
+
+    private void createAccountManagerAccount(String username) {
+        final android.accounts.Account account = makeAccountManagerAccount(username);
+        AccountManager.get(getContext()).addAccountExplicitly(account, "password", null);
+    }
+
+    private Account setupProviderAndAccountManagerAccount(String username) {
+        // Note that setupAccount creates the email address username@android.com, so that's what
+        // we need to use for the account manager
+        createAccountManagerAccount(username + "@android.com");
+        return ProviderTestUtils.setupAccount(username, true, mMockContext);
+    }
+
+    private AccountList makeSyncManagerAccountList() {
+        AccountList accountList = new AccountList();
+        Cursor c = mMockContext.getContentResolver().query(Account.CONTENT_URI,
+                Account.CONTENT_PROJECTION, null, null, null);
+        try {
+            while (c.moveToNext()) {
+                accountList.add(new Account().restore(c));
+            }
+        } finally {
+            c.close();
+        }
+        return accountList;
+    }
+
+    private void deleteAccountManagerAccount(Context context, android.accounts.Account account) {
+        AccountManagerFuture<Boolean> future =
+            AccountManager.get(context).removeAccount(account, null, null);
+        try {
+            future.getResult();
+        } catch (OperationCanceledException e) {
+        } catch (AuthenticatorException e) {
+        } catch (IOException e) {
+        }
+    }
+
+    private void deleteTemporaryAccountManagerAccounts(Context context) {
+        android.accounts.Account[] accountManagerAccounts =
+                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)) {
+                deleteAccountManagerAccount(context, accountManagerAccount);
+            }
+        }
+    }
+
+    private String getTestAccountName(String name) {
+        return TEST_ACCOUNT_PREFIX + name;
+    }
+
+    private String getTestAccountEmailAddress(String name) {
+        return TEST_ACCOUNT_PREFIX + name + TEST_ACCOUNT_SUFFIX;
+    }
+
+    public void testReconcileAccounts() {
+        // Note that we can't use mMockContext for AccountManager interactions, as it isn't a fully
+        // functional Context.
+        Context context = getContext();
+
+        // Set up three accounts, both in AccountManager and in EmailProvider
+        Account firstAccount = setupProviderAndAccountManagerAccount(getTestAccountName("1"));
+        setupProviderAndAccountManagerAccount(getTestAccountName("2"));
+        setupProviderAndAccountManagerAccount(getTestAccountName("3"));
+
+        // 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(Email.EXCHANGE_ACCOUNT_MANAGER_TYPE);
+        assertEquals(3, accountManagerAccounts.length);
+
+        // Delete account "2" from AccountManager
+        android.accounts.Account removedAccount =
+            makeAccountManagerAccount(getTestAccountEmailAddress("2"));
+        deleteAccountManagerAccount(context, removedAccount);
+
+        // Confirm it's deleted
+        accountManagerAccounts =
+                AccountManager.get(context).getAccountsByType(Email.EXCHANGE_ACCOUNT_MANAGER_TYPE);
+        assertEquals(2, accountManagerAccounts.length);
+
+        // Run the reconciler
+        SyncManager syncManager = new SyncManager();
+        ContentResolver resolver = mMockContext.getContentResolver();
+        syncManager.mResolver = resolver;
+        syncManager.reconcileAccountsWithAccountManager(context,
+                makeSyncManagerAccountList(), accountManagerAccounts);
+
+        // There should now be only two EmailProvider accounts
+        assertEquals(2, EmailContent.count(mMockContext, Account.CONTENT_URI, null, null));
+
+        // Ok, now we've got two of each; let's delete a provider account
+        resolver.delete(ContentUris.withAppendedId(Account.CONTENT_URI, firstAccount.mId),
+                null, null);
+        // ...and then there was one
+        assertEquals(1, EmailContent.count(mMockContext, Account.CONTENT_URI, null, null));
+
+        // Run the reconciler
+        syncManager.reconcileAccountsWithAccountManager(context,
+                makeSyncManagerAccountList(), accountManagerAccounts);
+
+        // There should now be only one AccountManager account
+        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/src/com/android/exchange/EmailContent.aidl b/tests/src/com/android/exchange/adapter/CalendarSyncAdapterTests.java
similarity index 70%
rename from src/com/android/exchange/EmailContent.aidl
rename to tests/src/com/android/exchange/adapter/CalendarSyncAdapterTests.java
index c6b4a7d..7f380d5 100644
--- a/src/com/android/exchange/EmailContent.aidl
+++ b/tests/src/com/android/exchange/adapter/CalendarSyncAdapterTests.java
@@ -1,6 +1,5 @@
 /*
- * Copyright (C) 2008-2009 Marc Blank
- * Licensed to 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.
@@ -15,7 +14,11 @@
  * limitations under the License.
  */
 
-package com.android.exchange;
+package com.android.exchange.adapter;
 
-parcelable EmailContent.Attachment;
+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 a702428..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.
@@ -23,67 +23,34 @@
 import com.android.email.provider.EmailContent.Body;
 import com.android.email.provider.EmailContent.Mailbox;
 import com.android.email.provider.EmailContent.Message;
+import com.android.email.provider.EmailContent.SyncColumns;
 import com.android.exchange.EasSyncService;
 import com.android.exchange.adapter.EmailSyncAdapter.EasEmailSyncParser;
+import com.android.exchange.adapter.EmailSyncAdapter.EasEmailSyncParser.ServerChange;
 
 import android.content.ContentResolver;
 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;
+    ContentResolver mMockResolver;
+    Mailbox mMailbox;
+    Account mAccount;
+    EmailSyncAdapter mSyncAdapter;
+    EasEmailSyncParser mSyncParser;
 
     public EmailSyncAdapterTests() {
-        super(EmailProvider.class, EmailProvider.EMAIL_AUTHORITY);
-    }
-
-    @Override
-    public void setUp() throws Exception {
-        super.setUp();
-        mMockContext = getMockContext();
-    }
-
-    @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 = getContext();
-        service.mMailbox = mailbox;
-        service.mAccount = account;
-        return service;
-    }
-
-    EmailSyncAdapter getTestSyncAdapter() {
-        EasSyncService service = getTestService();
-        EmailSyncAdapter adapter = new EmailSyncAdapter(service.mMailbox, service);
-        return adapter;
+        super();
     }
 
     /**
@@ -136,36 +103,35 @@
         ArrayList<Long> ids = new ArrayList<Long>();
         ArrayList<Long> deletedIds = new ArrayList<Long>();
 
-        Context context = mMockContext;
-        adapter.mContext = context;
-        final ContentResolver resolver = context.getContentResolver();
+        adapter.mContext = mMockContext;
 
         // Create account and two mailboxes
-        Account acct = ProviderTestUtils.setupAccount("account", true, context);
+        Account acct = ProviderTestUtils.setupAccount("account", true, mMockContext);
         adapter.mAccount = acct;
-        Mailbox box1 = ProviderTestUtils.setupMailbox("box1", acct.mId, true, context);
+        Mailbox box1 = ProviderTestUtils.setupMailbox("box1", acct.mId, true, mMockContext);
         adapter.mMailbox = box1;
 
         // Create 3 messages
-        Message msg1 =
-            ProviderTestUtils.setupMessage("message1", acct.mId, box1.mId, true, true, context);
+        Message msg1 = ProviderTestUtils.setupMessage("message1", acct.mId, box1.mId,
+                true, true, mMockContext);
         ids.add(msg1.mId);
-        Message msg2 =
-            ProviderTestUtils.setupMessage("message2", acct.mId, box1.mId, true, true, context);
+        Message msg2 = ProviderTestUtils.setupMessage("message2", acct.mId, box1.mId,
+                true, true, mMockContext);
         ids.add(msg2.mId);
-        Message msg3 =
-            ProviderTestUtils.setupMessage("message3", acct.mId, box1.mId, true, true, context);
+        Message msg3 = ProviderTestUtils.setupMessage("message3", acct.mId, box1.mId,
+                true, true, mMockContext);
         ids.add(msg3.mId);
-        assertEquals(3, EmailContent.count(context, Message.CONTENT_URI, null, null));
+        assertEquals(3, EmailContent.count(mMockContext, Message.CONTENT_URI, null, null));
 
         // Delete them
         for (long id: ids) {
-            resolver.delete(ContentUris.withAppendedId(Message.SYNCED_CONTENT_URI, id), null, null);
+            mMockResolver.delete(ContentUris.withAppendedId(Message.SYNCED_CONTENT_URI, id),
+                    null, null);
         }
 
         // Confirm that the messages are in the proper table
-        assertEquals(0, EmailContent.count(context, Message.CONTENT_URI, null, null));
-        assertEquals(3, EmailContent.count(context, Message.DELETED_CONTENT_URI, null, null));
+        assertEquals(0, EmailContent.count(mMockContext, Message.CONTENT_URI, null, null));
+        assertEquals(3, EmailContent.count(mMockContext, Message.DELETED_CONTENT_URI, null, null));
 
         // Call code to send deletions; the id's of the ones actually deleted will be in the
         // deletedIds list
@@ -176,15 +142,15 @@
         deletedIds.clear();
 
         // Create a new message
-        Message msg4 =
-            ProviderTestUtils.setupMessage("message3", acct.mId, box1.mId, true, true, context);
-        assertEquals(1, EmailContent.count(context, Message.CONTENT_URI, null, null));
+        Message msg4 = ProviderTestUtils.setupMessage("message4", acct.mId, box1.mId,
+                true, true, mMockContext);
+        assertEquals(1, EmailContent.count(mMockContext, Message.CONTENT_URI, null, null));
         // Find the body for this message
-        Body body = Body.restoreBodyWithMessageId(context, msg4.mId);
+        Body body = Body.restoreBodyWithMessageId(mMockContext, msg4.mId);
         // Set its source message to msg2's id
         ContentValues values = new ContentValues();
         values.put(Body.SOURCE_MESSAGE_KEY, msg2.mId);
-        body.update(context, values);
+        body.update(mMockContext, values);
 
         // Now send deletions again; this time only two should get deleted; msg2 should NOT be
         // deleted as it's referenced by msg4
@@ -192,4 +158,128 @@
         assertEquals(2, deletedIds.size());
         assertFalse(deletedIds.contains(msg2.mId));
     }
+
+    void setupSyncParserAndAdapter(Account account, Mailbox mailbox) throws IOException {
+        EasSyncService service = getTestService(account, mailbox);
+        mSyncAdapter = new EmailSyncAdapter(mailbox, service);
+        mSyncParser = mSyncAdapter.new EasEmailSyncParser(getTestInputStream(), mSyncAdapter);
+    }
+
+    ArrayList<Long> setupAccountMailboxAndMessages(int numMessages) {
+        ArrayList<Long> ids = new ArrayList<Long>();
+
+        // Create account and two mailboxes
+        mAccount = ProviderTestUtils.setupAccount("account", true, mMockContext);
+        mMailbox = ProviderTestUtils.setupMailbox("box1", mAccount.mId, true, mMockContext);
+
+        for (int i = 0; i < numMessages; i++) {
+            Message msg = ProviderTestUtils.setupMessage("message" + i, mAccount.mId, mMailbox.mId,
+                    true, true, mMockContext);
+            ids.add(msg.mId);
+        }
+
+        assertEquals(numMessages, EmailContent.count(mMockContext, Message.CONTENT_URI,
+                null, null));
+        return ids;
+    }
+
+    public void testDeleteParser() throws IOException {
+        // Setup some messages
+        ArrayList<Long> messageIds = setupAccountMailboxAndMessages(3);
+        ContentValues cv = new ContentValues();
+        cv.put(SyncColumns.SERVER_ID, "1:22");
+        long deleteMessageId = messageIds.get(1);
+        mMockResolver.update(ContentUris.withAppendedId(Message.CONTENT_URI, deleteMessageId), cv,
+                null, null);
+
+        // Setup our adapter and parser
+        setupSyncParserAndAdapter(mAccount, mMailbox);
+
+        // Set up an input stream with a delete command
+        Serializer s = new Serializer(false);
+        s.start(Tags.SYNC_DELETE).data(Tags.SYNC_SERVER_ID, "1:22").end().done();
+        byte[] bytes = s.toByteArray();
+        mSyncParser.resetInput(new ByteArrayInputStream(bytes));
+        mSyncParser.nextTag(0);
+
+        // Run the delete parser
+        ArrayList<Long> deleteList = new ArrayList<Long>();
+        mSyncParser.deleteParser(deleteList, Tags.SYNC_DELETE);
+        // It should have found the message
+        assertEquals(1, deleteList.size());
+        long id = deleteList.get(0);
+        // And the id's should match
+        assertEquals(deleteMessageId, id);
+    }
+
+    public void testChangeParser() throws IOException {
+        // Setup some messages
+        ArrayList<Long> messageIds = setupAccountMailboxAndMessages(3);
+        ContentValues cv = new ContentValues();
+        cv.put(SyncColumns.SERVER_ID, "1:22");
+        long changeMessageId = messageIds.get(1);
+        mMockResolver.update(ContentUris.withAppendedId(Message.CONTENT_URI, changeMessageId), cv,
+                null, null);
+
+        // Setup our adapter and parser
+        setupSyncParserAndAdapter(mAccount, mMailbox);
+
+        // Set up an input stream with a change command (marking 1:22 unread)
+        // Note that the test message creation code sets read to "true"
+        Serializer s = new Serializer(false);
+        s.start(Tags.SYNC_CHANGE).data(Tags.SYNC_SERVER_ID, "1:22");
+        s.start(Tags.SYNC_APPLICATION_DATA).data(Tags.EMAIL_READ, "0").end();
+        s.end().done();
+        byte[] bytes = s.toByteArray();
+        mSyncParser.resetInput(new ByteArrayInputStream(bytes));
+        mSyncParser.nextTag(0);
+
+        // Run the delete parser
+        ArrayList<ServerChange> changeList = new ArrayList<ServerChange>();
+        mSyncParser.changeParser(changeList);
+        // It should have found the message
+        assertEquals(1, changeList.size());
+        // And the id's should match
+        ServerChange change = changeList.get(0);
+        assertEquals(changeMessageId, change.id);
+        assertNotNull(change.read);
+        assertFalse(change.read);
+    }
+
+    public void testCleanup() throws IOException {
+        // Setup some messages
+        ArrayList<Long> messageIds = setupAccountMailboxAndMessages(3);
+        // Setup our adapter and parser
+        setupSyncParserAndAdapter(mAccount, mMailbox);
+
+        // Delete two of the messages, change one
+        long id = messageIds.get(0);
+        mMockResolver.delete(ContentUris.withAppendedId(Message.SYNCED_CONTENT_URI, id),
+                null, null);
+        mSyncAdapter.mDeletedIdList.add(id);
+        id = messageIds.get(1);
+        mMockResolver.delete(ContentUris.withAppendedId(Message.SYNCED_CONTENT_URI,
+                id), null, null);
+        mSyncAdapter.mDeletedIdList.add(id);
+        id = messageIds.get(2);
+        ContentValues cv = new ContentValues();
+        cv.put(Message.FLAG_READ, 0);
+        mMockResolver.update(ContentUris.withAppendedId(Message.SYNCED_CONTENT_URI,
+                id), cv, null, null);
+        mSyncAdapter.mUpdatedIdList.add(id);
+
+        // The changed message should still exist
+        assertEquals(1, EmailContent.count(mMockContext, Message.CONTENT_URI, null, null));
+
+        // As well, the two deletions and one update
+        assertEquals(2, EmailContent.count(mMockContext, Message.DELETED_CONTENT_URI, null, null));
+        assertEquals(1, EmailContent.count(mMockContext, Message.UPDATED_CONTENT_URI, null, null));
+
+        // Cleanup (i.e. after sync); should remove items from delete/update tables
+        mSyncAdapter.cleanup();
+
+        // The three should be gone
+        assertEquals(0, EmailContent.count(mMockContext, Message.DELETED_CONTENT_URI, null, null));
+        assertEquals(0, EmailContent.count(mMockContext, Message.UPDATED_CONTENT_URI, null, null));
+    }
 }
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!");
+//    }
+}