eclair snapshot
diff --git a/Android.mk b/Android.mk
index 6ca4633..277d0e5 100644
--- a/Android.mk
+++ b/Android.mk
@@ -16,6 +16,10 @@
 include $(CLEAR_VARS)
 
 LOCAL_SRC_FILES := $(call all-java-files-under, src)
+LOCAL_SRC_FILES += \
+    src/com/android/exchange/IEmailService.aidl \
+    src/com/android/exchange/IEmailServiceCallback.aidl
+
 
 LOCAL_PACKAGE_NAME := Email
 
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 6f2e087..9b376af 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -23,17 +23,34 @@
     <uses-permission android:name="android.permission.INTERNET"/>
     <uses-permission android:name="android.permission.VIBRATE"/>
     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
- 
+    <uses-permission android:name="android.permission.GET_ACCOUNTS" />
+    <uses-permission android:name="android.permission.MANAGE_ACCOUNTS" />
+    <uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS" />
+    <uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
+    <uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" />
+
+    <!--  For EAS purposes; could be removed when EAS has a permanent home -->
+    <uses-permission android:name="android.permission.WRITE_CONTACTS"/>
+
     <!-- Only required if a store implements push mail and needs to keep network open -->
     <uses-permission android:name="android.permission.WAKE_LOCK"/>
     <uses-permission android:name="android.permission.READ_PHONE_STATE"/>
-    
+
+    <!-- Grant permission to other apps to view attachments -->
     <permission android:name="com.android.email.permission.READ_ATTACHMENT"
                 android:permissionGroup="android.permission-group.MESSAGES"
                 android:protectionLevel="dangerous"
                 android:label="@string/read_attachment_label"
                 android:description="@string/read_attachment_desc"/>
     <uses-permission android:name="com.android.email.permission.READ_ATTACHMENT"/>
+
+    <!-- Grant permission to system apps to access provider (see provider below) -->
+    <permission android:name="com.android.email.permission.ACCESS_PROVIDER"
+                android:protectionLevel="signatureOrSystem"
+                android:label="@string/permission_access_provider_label"
+                android:description="@string/permission_access_provider_desc"/>
+    <uses-permission android:name="com.android.email.permission.ACCESS_PROVIDER"/>
+
     <application android:icon="@drawable/icon" android:label="@string/app_name"
         android:name="Email">
         <activity android:name=".activity.Welcome">
@@ -44,9 +61,11 @@
             </intent-filter>
         </activity>
 
+        <!-- Must be exported in order for the AccountManager to launch it -->
         <activity
             android:name=".activity.setup.AccountSetupBasics"
             android:label="@string/account_setup_basics_title"
+            android:exported="true"
             >
         </activity>
         <activity
@@ -98,8 +117,7 @@
             android:label="@string/debug_title">
         </activity>
         <activity
-            android:name=".activity.Accounts"
-            android:label="@string/accounts_title"
+            android:name=".activity.AccountFolderList"
             android:launchMode="singleTop" >
         </activity>
         
@@ -115,12 +133,17 @@
         </activity>
         
         <activity
-            android:name=".activity.FolderMessageList">
+            android:name=".activity.MailboxList">
+        </activity>
+        
+        <activity
+            android:name=".activity.MessageList">
             <intent-filter>
                 <!-- This action is only to allow an entry point for launcher shortcuts -->
                 <action android:name="android.intent.action.MAIN" />
             </intent-filter>
         </activity>
+                
         <activity
             android:name=".activity.MessageView"
             android:theme="@android:style/Theme.NoTitleBar" >
@@ -139,15 +162,24 @@
             </intent-filter>
             <intent-filter android:label="@string/app_name">
                 <action android:name="android.intent.action.SEND" />
-                <data android:mimeType="text/plain" />
-                <data android:mimeType="image/*" />
-                <data android:mimeType="video/*" />
+                <data android:mimeType="*/*" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+            <intent-filter android:label="@string/app_name">
+                <action android:name="android.intent.action.SEND_MULTIPLE" />
+                <data android:mimeType="*/*" />
                 <category android:name="android.intent.category.DEFAULT" />
             </intent-filter>
         </activity>
-        <receiver android:name=".service.BootReceiver"
-              android:enabled="false"
-              >
+       <receiver android:name="com.android.exchange.EmailSyncAlarmReceiver"/>
+       <receiver android:name="com.android.exchange.MailboxAlarmReceiver"/>
+       <receiver android:name="com.android.exchange.BootReceiver" android:enabled="true">
+             <intent-filter>
+                <action android:name="android.intent.action.BOOT_COMPLETED" />
+            </intent-filter>
+       </receiver>
+
+       <receiver android:name=".service.BootReceiver" android:enabled="true">
             <intent-filter>
                 <action android:name="android.intent.action.BOOT_COMPLETED" />
             </intent-filter>
@@ -158,11 +190,40 @@
                 <action android:name="android.intent.action.DEVICE_STORAGE_OK" />
             </intent-filter>
         </receiver>
+
         <service
             android:name=".service.MailService"
             android:enabled="false"
             >
         </service>
+        
+        <!--Required stanza to register the ContactsSyncAdapterService with SyncManager -->
+        <service 
+            android:name="com.android.exchange.ContactsSyncAdapterService" 
+        	android:exported="true">
+            <intent-filter>
+                <action android:name="android.content.SyncAdapter" />
+            </intent-filter>
+            <meta-data android:name="android.content.SyncAdapter"
+                       android:resource="@xml/syncadapter_contacts" />
+        </service>
+
+        <!-- Add android:process=":remote" below to enable SyncManager as a separate process -->
+        <service
+            android:name="com.android.exchange.SyncManager"
+            android:enabled="true"
+            >
+        </service>
+
+        <!--Required stanza to register the EasAuthenticatorService with AccountManager -->
+        <service android:name=".service.EasAuthenticatorService" android:exported="true">
+            <intent-filter>
+                <action android:name="android.accounts.AccountAuthenticator" />
+            </intent-filter>
+            <meta-data android:name="android.accounts.AccountAuthenticator"
+                       android:resource="@xml/authenticator" />
+        </service>
+
         <provider
             android:name=".provider.AttachmentProvider"
             android:authorities="com.android.email.attachmentprovider"
@@ -170,5 +231,14 @@
             android:grantUriPermissions="true"
             android:readPermission="com.android.email.permission.READ_ATTACHMENT"
             />
+
+        <!-- This provider MUST be protected by strict permissions, as granting access to
+             it exposes user passwords and other confidential information. -->
+        <provider
+            android:name=".provider.EmailProvider"
+            android:authorities="com.android.email.provider"
+            android:multiprocess="true"
+            android:permission="com.android.email.permission.ACCESS_PROVIDER"
+            />
     </application>
 </manifest>
diff --git a/res/drawable-hdpi/appointment_indicator_leftside_10.9.png b/res/drawable-hdpi/appointment_indicator_leftside_10.9.png
new file mode 100644
index 0000000..ff09049
--- /dev/null
+++ b/res/drawable-hdpi/appointment_indicator_leftside_10.9.png
Binary files differ
diff --git a/res/drawable-hdpi/appointment_indicator_leftside_11.9.png b/res/drawable-hdpi/appointment_indicator_leftside_11.9.png
new file mode 100644
index 0000000..6a2e4f2
--- /dev/null
+++ b/res/drawable-hdpi/appointment_indicator_leftside_11.9.png
Binary files differ
diff --git a/res/drawable-hdpi/appointment_indicator_leftside_12.9.png b/res/drawable-hdpi/appointment_indicator_leftside_12.9.png
new file mode 100644
index 0000000..0f19c83
--- /dev/null
+++ b/res/drawable-hdpi/appointment_indicator_leftside_12.9.png
Binary files differ
diff --git a/res/drawable-hdpi/appointment_indicator_leftside_13.9.png b/res/drawable-hdpi/appointment_indicator_leftside_13.9.png
new file mode 100644
index 0000000..7501e35
--- /dev/null
+++ b/res/drawable-hdpi/appointment_indicator_leftside_13.9.png
Binary files differ
diff --git a/res/drawable-hdpi/appointment_indicator_leftside_14.9.png b/res/drawable-hdpi/appointment_indicator_leftside_14.9.png
new file mode 100644
index 0000000..53f97a6
--- /dev/null
+++ b/res/drawable-hdpi/appointment_indicator_leftside_14.9.png
Binary files differ
diff --git a/res/drawable-hdpi/appointment_indicator_leftside_15.9.png b/res/drawable-hdpi/appointment_indicator_leftside_15.9.png
new file mode 100644
index 0000000..846f6f8
--- /dev/null
+++ b/res/drawable-hdpi/appointment_indicator_leftside_15.9.png
Binary files differ
diff --git a/res/drawable-hdpi/appointment_indicator_leftside_16.9.png b/res/drawable-hdpi/appointment_indicator_leftside_16.9.png
new file mode 100644
index 0000000..1707540
--- /dev/null
+++ b/res/drawable-hdpi/appointment_indicator_leftside_16.9.png
Binary files differ
diff --git a/res/drawable-hdpi/appointment_indicator_leftside_17.9.png b/res/drawable-hdpi/appointment_indicator_leftside_17.9.png
new file mode 100644
index 0000000..7fd945d
--- /dev/null
+++ b/res/drawable-hdpi/appointment_indicator_leftside_17.9.png
Binary files differ
diff --git a/res/drawable-hdpi/appointment_indicator_leftside_18.9.png b/res/drawable-hdpi/appointment_indicator_leftside_18.9.png
new file mode 100644
index 0000000..8cf47ae
--- /dev/null
+++ b/res/drawable-hdpi/appointment_indicator_leftside_18.9.png
Binary files differ
diff --git a/res/drawable-hdpi/appointment_indicator_leftside_19.9.png b/res/drawable-hdpi/appointment_indicator_leftside_19.9.png
new file mode 100644
index 0000000..6831c01
--- /dev/null
+++ b/res/drawable-hdpi/appointment_indicator_leftside_19.9.png
Binary files differ
diff --git a/res/drawable-hdpi/appointment_indicator_leftside_2.9.png b/res/drawable-hdpi/appointment_indicator_leftside_2.9.png
new file mode 100644
index 0000000..b4cee11
--- /dev/null
+++ b/res/drawable-hdpi/appointment_indicator_leftside_2.9.png
Binary files differ
diff --git a/res/drawable-hdpi/appointment_indicator_leftside_20.9.png b/res/drawable-hdpi/appointment_indicator_leftside_20.9.png
new file mode 100644
index 0000000..d07d826
--- /dev/null
+++ b/res/drawable-hdpi/appointment_indicator_leftside_20.9.png
Binary files differ
diff --git a/res/drawable-hdpi/appointment_indicator_leftside_21.9.png b/res/drawable-hdpi/appointment_indicator_leftside_21.9.png
new file mode 100644
index 0000000..f410269
--- /dev/null
+++ b/res/drawable-hdpi/appointment_indicator_leftside_21.9.png
Binary files differ
diff --git a/res/drawable-hdpi/appointment_indicator_leftside_3.9.png b/res/drawable-hdpi/appointment_indicator_leftside_3.9.png
new file mode 100644
index 0000000..69bd6a9
--- /dev/null
+++ b/res/drawable-hdpi/appointment_indicator_leftside_3.9.png
Binary files differ
diff --git a/res/drawable-hdpi/appointment_indicator_leftside_4.9.png b/res/drawable-hdpi/appointment_indicator_leftside_4.9.png
new file mode 100644
index 0000000..d09ea90
--- /dev/null
+++ b/res/drawable-hdpi/appointment_indicator_leftside_4.9.png
Binary files differ
diff --git a/res/drawable-hdpi/appointment_indicator_leftside_5.9.png b/res/drawable-hdpi/appointment_indicator_leftside_5.9.png
new file mode 100644
index 0000000..d27fc91
--- /dev/null
+++ b/res/drawable-hdpi/appointment_indicator_leftside_5.9.png
Binary files differ
diff --git a/res/drawable-hdpi/appointment_indicator_leftside_6.9.png b/res/drawable-hdpi/appointment_indicator_leftside_6.9.png
new file mode 100644
index 0000000..c014633
--- /dev/null
+++ b/res/drawable-hdpi/appointment_indicator_leftside_6.9.png
Binary files differ
diff --git a/res/drawable-hdpi/appointment_indicator_leftside_7.9.png b/res/drawable-hdpi/appointment_indicator_leftside_7.9.png
new file mode 100644
index 0000000..febb514
--- /dev/null
+++ b/res/drawable-hdpi/appointment_indicator_leftside_7.9.png
Binary files differ
diff --git a/res/drawable-hdpi/appointment_indicator_leftside_8.9.png b/res/drawable-hdpi/appointment_indicator_leftside_8.9.png
new file mode 100644
index 0000000..1415e44
--- /dev/null
+++ b/res/drawable-hdpi/appointment_indicator_leftside_8.9.png
Binary files differ
diff --git a/res/drawable-hdpi/appointment_indicator_leftside_9.9.png b/res/drawable-hdpi/appointment_indicator_leftside_9.9.png
new file mode 100644
index 0000000..d018fcf
--- /dev/null
+++ b/res/drawable-hdpi/appointment_indicator_leftside_9.9.png
Binary files differ
diff --git a/res/drawable-hdpi/button_indicator_next.png b/res/drawable-hdpi/button_indicator_next.png
new file mode 100644
index 0000000..c7c553b
--- /dev/null
+++ b/res/drawable-hdpi/button_indicator_next.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
new file mode 100644
index 0000000..4d71060
--- /dev/null
+++ b/res/drawable-hdpi/expander_ic_folder_maximized.9.png
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
new file mode 100644
index 0000000..0c04943
--- /dev/null
+++ b/res/drawable-hdpi/expander_ic_folder_minimized.9.png
Binary files differ
diff --git a/res/drawable-hdpi/icon.png b/res/drawable-hdpi/icon.png
new file mode 100644
index 0000000..b0b3f71
--- /dev/null
+++ b/res/drawable-hdpi/icon.png
Binary files differ
diff --git a/res/drawable-hdpi/presence_inactive.png b/res/drawable-hdpi/presence_inactive.png
new file mode 100644
index 0000000..d96d351
--- /dev/null
+++ b/res/drawable-hdpi/presence_inactive.png
Binary files differ
diff --git a/res/drawable-hdpi/section_dark.9.png b/res/drawable-hdpi/section_dark.9.png
new file mode 100644
index 0000000..3e63fa6
--- /dev/null
+++ b/res/drawable-hdpi/section_dark.9.png
Binary files differ
diff --git a/res/drawable-hdpi/section_light.9.png b/res/drawable-hdpi/section_light.9.png
new file mode 100644
index 0000000..6fc53ca
--- /dev/null
+++ b/res/drawable-hdpi/section_light.9.png
Binary files differ
diff --git a/res/drawable-hdpi/text_box_light.9.png b/res/drawable-hdpi/text_box_light.9.png
new file mode 100644
index 0000000..de9c0ad
--- /dev/null
+++ b/res/drawable-hdpi/text_box_light.9.png
Binary files differ
diff --git a/res/drawable/appointment_indicator_leftside_10.9.png b/res/drawable-mdpi/appointment_indicator_leftside_10.9.png
similarity index 100%
rename from res/drawable/appointment_indicator_leftside_10.9.png
rename to res/drawable-mdpi/appointment_indicator_leftside_10.9.png
Binary files differ
diff --git a/res/drawable/appointment_indicator_leftside_11.9.png b/res/drawable-mdpi/appointment_indicator_leftside_11.9.png
similarity index 100%
rename from res/drawable/appointment_indicator_leftside_11.9.png
rename to res/drawable-mdpi/appointment_indicator_leftside_11.9.png
Binary files differ
diff --git a/res/drawable/appointment_indicator_leftside_12.9.png b/res/drawable-mdpi/appointment_indicator_leftside_12.9.png
similarity index 100%
rename from res/drawable/appointment_indicator_leftside_12.9.png
rename to res/drawable-mdpi/appointment_indicator_leftside_12.9.png
Binary files differ
diff --git a/res/drawable/appointment_indicator_leftside_13.9.png b/res/drawable-mdpi/appointment_indicator_leftside_13.9.png
similarity index 100%
rename from res/drawable/appointment_indicator_leftside_13.9.png
rename to res/drawable-mdpi/appointment_indicator_leftside_13.9.png
Binary files differ
diff --git a/res/drawable/appointment_indicator_leftside_14.9.png b/res/drawable-mdpi/appointment_indicator_leftside_14.9.png
similarity index 100%
rename from res/drawable/appointment_indicator_leftside_14.9.png
rename to res/drawable-mdpi/appointment_indicator_leftside_14.9.png
Binary files differ
diff --git a/res/drawable/appointment_indicator_leftside_15.9.png b/res/drawable-mdpi/appointment_indicator_leftside_15.9.png
similarity index 100%
rename from res/drawable/appointment_indicator_leftside_15.9.png
rename to res/drawable-mdpi/appointment_indicator_leftside_15.9.png
Binary files differ
diff --git a/res/drawable/appointment_indicator_leftside_16.9.png b/res/drawable-mdpi/appointment_indicator_leftside_16.9.png
similarity index 100%
rename from res/drawable/appointment_indicator_leftside_16.9.png
rename to res/drawable-mdpi/appointment_indicator_leftside_16.9.png
Binary files differ
diff --git a/res/drawable/appointment_indicator_leftside_17.9.png b/res/drawable-mdpi/appointment_indicator_leftside_17.9.png
similarity index 100%
rename from res/drawable/appointment_indicator_leftside_17.9.png
rename to res/drawable-mdpi/appointment_indicator_leftside_17.9.png
Binary files differ
diff --git a/res/drawable/appointment_indicator_leftside_18.9.png b/res/drawable-mdpi/appointment_indicator_leftside_18.9.png
similarity index 100%
rename from res/drawable/appointment_indicator_leftside_18.9.png
rename to res/drawable-mdpi/appointment_indicator_leftside_18.9.png
Binary files differ
diff --git a/res/drawable/appointment_indicator_leftside_19.9.png b/res/drawable-mdpi/appointment_indicator_leftside_19.9.png
similarity index 100%
rename from res/drawable/appointment_indicator_leftside_19.9.png
rename to res/drawable-mdpi/appointment_indicator_leftside_19.9.png
Binary files differ
diff --git a/res/drawable/appointment_indicator_leftside_2.9.png b/res/drawable-mdpi/appointment_indicator_leftside_2.9.png
similarity index 100%
rename from res/drawable/appointment_indicator_leftside_2.9.png
rename to res/drawable-mdpi/appointment_indicator_leftside_2.9.png
Binary files differ
diff --git a/res/drawable/appointment_indicator_leftside_20.9.png b/res/drawable-mdpi/appointment_indicator_leftside_20.9.png
similarity index 100%
rename from res/drawable/appointment_indicator_leftside_20.9.png
rename to res/drawable-mdpi/appointment_indicator_leftside_20.9.png
Binary files differ
diff --git a/res/drawable/appointment_indicator_leftside_21.9.png b/res/drawable-mdpi/appointment_indicator_leftside_21.9.png
similarity index 100%
rename from res/drawable/appointment_indicator_leftside_21.9.png
rename to res/drawable-mdpi/appointment_indicator_leftside_21.9.png
Binary files differ
diff --git a/res/drawable/appointment_indicator_leftside_3.9.png b/res/drawable-mdpi/appointment_indicator_leftside_3.9.png
similarity index 100%
rename from res/drawable/appointment_indicator_leftside_3.9.png
rename to res/drawable-mdpi/appointment_indicator_leftside_3.9.png
Binary files differ
diff --git a/res/drawable/appointment_indicator_leftside_4.9.png b/res/drawable-mdpi/appointment_indicator_leftside_4.9.png
similarity index 100%
rename from res/drawable/appointment_indicator_leftside_4.9.png
rename to res/drawable-mdpi/appointment_indicator_leftside_4.9.png
Binary files differ
diff --git a/res/drawable/appointment_indicator_leftside_5.9.png b/res/drawable-mdpi/appointment_indicator_leftside_5.9.png
similarity index 100%
rename from res/drawable/appointment_indicator_leftside_5.9.png
rename to res/drawable-mdpi/appointment_indicator_leftside_5.9.png
Binary files differ
diff --git a/res/drawable/appointment_indicator_leftside_6.9.png b/res/drawable-mdpi/appointment_indicator_leftside_6.9.png
similarity index 100%
rename from res/drawable/appointment_indicator_leftside_6.9.png
rename to res/drawable-mdpi/appointment_indicator_leftside_6.9.png
Binary files differ
diff --git a/res/drawable/appointment_indicator_leftside_7.9.png b/res/drawable-mdpi/appointment_indicator_leftside_7.9.png
similarity index 100%
rename from res/drawable/appointment_indicator_leftside_7.9.png
rename to res/drawable-mdpi/appointment_indicator_leftside_7.9.png
Binary files differ
diff --git a/res/drawable/appointment_indicator_leftside_8.9.png b/res/drawable-mdpi/appointment_indicator_leftside_8.9.png
similarity index 100%
rename from res/drawable/appointment_indicator_leftside_8.9.png
rename to res/drawable-mdpi/appointment_indicator_leftside_8.9.png
Binary files differ
diff --git a/res/drawable/appointment_indicator_leftside_9.9.png b/res/drawable-mdpi/appointment_indicator_leftside_9.9.png
similarity index 100%
rename from res/drawable/appointment_indicator_leftside_9.9.png
rename to res/drawable-mdpi/appointment_indicator_leftside_9.9.png
Binary files differ
diff --git a/res/drawable/button_indicator_next.png b/res/drawable-mdpi/button_indicator_next.png
similarity index 100%
rename from res/drawable/button_indicator_next.png
rename to res/drawable-mdpi/button_indicator_next.png
Binary files differ
diff --git a/res/drawable/expander_ic_folder_maximized.9.png b/res/drawable-mdpi/expander_ic_folder_maximized.9.png
similarity index 100%
rename from res/drawable/expander_ic_folder_maximized.9.png
rename to res/drawable-mdpi/expander_ic_folder_maximized.9.png
Binary files differ
diff --git a/res/drawable/expander_ic_folder_minimized.9.png b/res/drawable-mdpi/expander_ic_folder_minimized.9.png
similarity index 100%
rename from res/drawable/expander_ic_folder_minimized.9.png
rename to res/drawable-mdpi/expander_ic_folder_minimized.9.png
Binary files differ
diff --git a/res/drawable/icon.png b/res/drawable-mdpi/icon.png
similarity index 100%
rename from res/drawable/icon.png
rename to res/drawable-mdpi/icon.png
Binary files differ
diff --git a/res/drawable/presence_inactive.png b/res/drawable-mdpi/presence_inactive.png
similarity index 100%
rename from res/drawable/presence_inactive.png
rename to res/drawable-mdpi/presence_inactive.png
Binary files differ
diff --git a/res/drawable-mdpi/section_dark.9.png b/res/drawable-mdpi/section_dark.9.png
new file mode 100644
index 0000000..7242b61
--- /dev/null
+++ b/res/drawable-mdpi/section_dark.9.png
Binary files differ
diff --git a/res/drawable-mdpi/section_light.9.png b/res/drawable-mdpi/section_light.9.png
new file mode 100644
index 0000000..9852851
--- /dev/null
+++ b/res/drawable-mdpi/section_light.9.png
Binary files differ
diff --git a/res/drawable/text_box_light.9.png b/res/drawable-mdpi/text_box_light.9.png
similarity index 100%
rename from res/drawable/text_box_light.9.png
rename to res/drawable-mdpi/text_box_light.9.png
Binary files differ
diff --git a/res/drawable/appointment_indicator_leftside_1.9.png b/res/drawable/appointment_indicator_leftside_1.9.png
deleted file mode 100644
index 5e40235..0000000
--- a/res/drawable/appointment_indicator_leftside_1.9.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable/attached_image_placeholder.png b/res/drawable/attached_image_placeholder.png
deleted file mode 100644
index 90acbf7..0000000
--- a/res/drawable/attached_image_placeholder.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable/divider_horizontal_email.9.png b/res/drawable/divider_horizontal_email.9.png
deleted file mode 100644
index 1f56364..0000000
--- a/res/drawable/divider_horizontal_email.9.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable/email_quoted_bar.9.png b/res/drawable/email_quoted_bar.9.png
deleted file mode 100644
index 3d197d5..0000000
--- a/res/drawable/email_quoted_bar.9.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable/ic_email_attachment.png b/res/drawable/ic_email_attachment.png
deleted file mode 100644
index 6fe0d3e..0000000
--- a/res/drawable/ic_email_attachment.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable/ic_email_attachment_small.png b/res/drawable/ic_email_attachment_small.png
deleted file mode 100644
index eaed42c..0000000
--- a/res/drawable/ic_email_attachment_small.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable/ic_email_thread_open_top_default.9.png b/res/drawable/ic_email_thread_open_top_default.9.png
deleted file mode 100644
index 9aebde9..0000000
--- a/res/drawable/ic_email_thread_open_top_default.9.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable/ic_left_prev_arrow_default.png b/res/drawable/ic_left_prev_arrow_default.png
deleted file mode 100755
index acc60b7..0000000
--- a/res/drawable/ic_left_prev_arrow_default.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable/ic_menu_forward_mail.png b/res/drawable/ic_menu_forward_mail.png
deleted file mode 100644
index 97b63af..0000000
--- a/res/drawable/ic_menu_forward_mail.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable/ic_menu_reply.png b/res/drawable/ic_menu_reply.png
deleted file mode 100644
index bd18b58..0000000
--- a/res/drawable/ic_menu_reply.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable/ic_menu_reply_all.png b/res/drawable/ic_menu_reply_all.png
deleted file mode 100644
index 731e997..0000000
--- a/res/drawable/ic_menu_reply_all.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable/ic_menu_save_draft.png b/res/drawable/ic_menu_save_draft.png
deleted file mode 100644
index 7231933..0000000
--- a/res/drawable/ic_menu_save_draft.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable/ic_mms_attachment_small.png b/res/drawable/ic_mms_attachment_small.png
deleted file mode 100644
index a661ce4..0000000
--- a/res/drawable/ic_mms_attachment_small.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable/ic_right_next_arrow_default.png b/res/drawable/ic_right_next_arrow_default.png
deleted file mode 100755
index 62d3bbc..0000000
--- a/res/drawable/ic_right_next_arrow_default.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable/stat_notify_email_generic.png b/res/drawable/stat_notify_email_generic.png
deleted file mode 100644
index 686033f..0000000
--- a/res/drawable/stat_notify_email_generic.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable/text_box.9.png b/res/drawable/text_box.9.png
deleted file mode 100644
index e7d4207..0000000
--- a/res/drawable/text_box.9.png
+++ /dev/null
Binary files differ
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 6abaa2e..fc811b9 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -16,14 +16,44 @@
 
 <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
 
-    <!-- Permissions label -->
-    <string name="read_attachment_label">read Email attachments</string>
-    <!-- Permissions description -->
-    <string name="read_attachment_desc">Allows this application to read your Email attachments.</string>
+    <!-- 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>
+
+    <!-- Permissions label for reading attachments -->
+    <string name="read_attachment_label">Read Email attachments</string>
+    <!-- Permissions description for reading attachments -->
+    <string name="read_attachment_desc">Allows this application to read your Email
+        attachments.</string>
+    <!-- Permissions label for accessing the main provider -->
+    <string name="permission_access_provider_label">Access Email provider data</string>
+    <!-- Permissions description for accessing the main provider -->
+    <string name="permission_access_provider_desc">Allows this application to access your Email
+        database, including received messages, sent messages, usernames and passwords.</string>
+
     <!-- Name of application on Home screen -->
     <string name="app_name">Email</string>
     <!-- Title of Accounts screen -->
-    <string name="accounts_title">Your accounts</string>
+    <string name="accounts_title">Email</string>
     <!-- Title of compose screen -->
     <string name="compose_title">Compose</string>
     <!-- Title of debug screen -->
@@ -54,11 +84,25 @@
     <string name="discard_action">Discard</string>
     <!-- Menu item/button name -->
     <string name="save_draft_action">Save as draft</string>
+    <!-- Menu item/button name -->
+    <string name="read_unread_action">Read/Unread</string>
+    <!-- Menu item/button name -->
+    <string name="read_action">Mark read</string>
+    <!-- Menu item/button name -->
+    <string name="unread_action">Mark unread</string>
+    <!-- Menu item/button name -->
+    <string name="favorite_action">Favorite</string>
+    <!-- Menu item/button name -->
+    <string name="set_star_action">Add star</string>
+    <!-- Menu item/button name -->
+    <string name="remove_star_action">Remove star</string>
     <!-- Menu item -->
     <string name="refresh_action">Refresh</string>
     <!-- Menu item -->
     <string name="add_account_action">Add account</string>
     <!-- Menu item -->
+    <string name="deselect_all_action">Deselect all</string>
+    <!-- Menu item -->
     <string name="compose_action">Compose</string>
     <!-- Menu item/button name -->
     <string name="search_action">Search</string>
@@ -69,6 +113,8 @@
     <!-- Menu item -->
     <string name="remove_account_action">Remove account</string>
     <!-- Menu item -->
+    <string name="folders_action">Folders</string>
+    <!-- Menu item -->
     <string name="accounts_action">Accounts</string>
     <!-- Menu item -->
     <string name="mark_as_read_action">Mark as read</string>
@@ -97,6 +143,9 @@
     <string name="status_loading_more_failed">Retry loading more messages</string>
     <!-- Notification title in status bar -->
     <string name="notification_new_title">New email</string>
+    <!-- Dialog text when we are unable to load the message for display -->
+    <string name="error_loading_message_body">Unexpected error while loading message text.  Message
+        may be too large to view.</string>
 
     <!-- Notification message in notifications window when one account has
          one or more new messages; e.g, "279 unread (someone@google.com)". -->
@@ -120,27 +169,33 @@
         <item quantity="other">in <xliff:g id="number_accounts" example="10">%d</xliff:g> accounts</item>
     </plurals>
         
-
-    <!-- In the folder list view, the inbox will be displayed with this name -->
-    <string name="special_mailbox_name_inbox">Inbox</string>
+    <!-- The next set of strings are used server-side and must not be localized. -->
+    <!-- Do Not Translate.  This is the name of the "inbox" folder, on the server. -->
+    <string name="mailbox_name_server_inbox">Inbox</string>
     <!-- Do Not Translate.  This is the name of the "outbox" folder, on the server. -->
-    <string name="special_mailbox_name_outbox">Outbox</string>
+    <string name="mailbox_name_server_outbox">Outbox</string>
     <!-- Do Not Translate.  This is the name of the "drafts" folder, on the server. -->
-    <string name="special_mailbox_name_drafts">Drafts</string>
+    <string name="mailbox_name_server_drafts">Drafts</string>
     <!-- Do Not Translate.  This is the name of the "trash" folder, on the server. -->
-    <string name="special_mailbox_name_trash">Trash</string>
+    <string name="mailbox_name_server_trash">Trash</string>
     <!-- Do Not Translate.  This is the name of the "sent" folder, on the server. -->
-    <string name="special_mailbox_name_sent">Sent</string>
-    <!-- In the folder list view, the outbox will be displayed with this name -->
-    <string name="special_mailbox_display_name_outbox">Outbox</string>
-    <!-- In the folder list view, the drafts will be displayed with this name -->
-    <string name="special_mailbox_display_name_drafts">Drafts</string>
-    <!-- In the folder list view, the trash will be displayed with this name -->
-    <string name="special_mailbox_display_name_trash">Trash</string>
-    <!-- In the folder list view, the sent will be displayed with this name -->
-    <string name="special_mailbox_display_name_sent">Sent</string>
-    <!-- Text on screen when you don't have any external email accounts set up -->
-    <string name="accounts_welcome">Welcome to Email setup!\n\nUse any email account with Email.\n\nMost popular email accounts can be set up in 2 steps!</string>
+    <string name="mailbox_name_server_sent">Sent</string>
+    <!-- Do Not Translate.  This is the name of the "junk" folder, on the server. -->
+    <string name="mailbox_name_server_junk">Junk</string>
+
+    <!-- The next set of strings are used in local display and may be localized. -->
+    <!-- In the UI, the inbox will be displayed with this name -->
+    <string name="mailbox_name_display_inbox">Inbox</string>
+    <!-- In the UI, the outbox will be displayed with this name -->
+    <string name="mailbox_name_display_outbox">Outbox</string>
+    <!-- In the UI, the drafts will be displayed with this name -->
+    <string name="mailbox_name_display_drafts">Drafts</string>
+    <!-- In the UI, the trash will be displayed with this name -->
+    <string name="mailbox_name_display_trash">Trash</string>
+    <!-- In the UI, the sent will be displayed with this name -->
+    <string name="mailbox_name_display_sent">Sent</string>
+    <!-- In the UI, the junk will be displayed with this name -->
+    <string name="mailbox_name_display_junk">Junk</string>
 
     <!-- Version number, shown only on debug screen -->
     <string name="debug_version_fmt">Version: <xliff:g id="version">%s</xliff:g></string>
@@ -148,12 +203,34 @@
     <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>
+
+    <!-- The text in the small separator between smart folders and the accounts -->
+    <string name="account_folder_list_separator_accounts">Accounts</string>
+    <!-- The summary section entry in the AccountFolder list to display all inboxes -->
+    <string name="account_folder_list_summary_inbox">Combined Inbox</string>
+    <!-- The summary section entry in the AccountFolder list to display all unread -->
+    <string name="account_folder_list_summary_unread">Unread</string>
+    <!-- The summary section entry in the AccountFolder list to display all starred -->
+    <string name="account_folder_list_summary_starred">Starred</string>
+    <!-- The summary section entry in the AccountFolder list to display all drafts -->
+    <string name="account_folder_list_summary_drafts">Drafts</string>
+    <!-- The summary section entry in the AccountFolder list to display all outboxes -->
+    <string name="account_folder_list_summary_outbox">Outbox</string>
+
+    <!-- Title of the screen that shows a list of mailboxes for an account -->
+    <string name="mailbox_list_title">Mailbox</string>
 
     <!-- Appears at the bottom of list of messages; user selects to load more messages from that folder. -->
     <string name="message_list_load_more_messages_action">Load more messages</string>
     <!-- Appears at the bottom of list of messages of outbox;
          user selects to send pending messages. -->
     <string name="message_list_send_pending_messages_action">Send outgoing messages</string>
+    <!-- Appears at the title bar of list of messages. -->
+    <string name="message_list_title"><xliff:g id="account">%s</xliff:g>: <xliff:g id="mailbox">%s</xliff:g></string>
     <!-- Hint text in To field -->
     <string name="message_compose_to_hint">To</string>
     <!-- Hint text in Cc field -->
@@ -172,16 +249,19 @@
     <string name="message_compose_quoted_text_label">Quoted text</string>
     <!-- Toast that appears if you try to send with no recipients. -->
     <string name="message_compose_error_no_recipients">You must add at least one recipient.</string>
+    <!-- An address field contains invalid email addresses. -->
+    <string name="message_compose_error_invalid_email">Some email addresses are invalid.</string>
     <!-- Toast that appears in the context of forwarding a message with attachment(s) -->
     <string name="message_compose_attachments_skipped_toast">Some attachments cannot be forwarded because they have not downloaded.</string>
     <!-- Toast that appears when an attachment is too big to send. -->
     <string name="message_compose_attachment_size">File too large to attach.</string>
+    <!-- Display name for composed message, indicating the destination of the message.
+      e.g. "John and 2 others" -->
+    <string name="message_compose_display_name"><xliff:g id="name" example="John">%1$s</xliff:g> and <xliff:g id="number" example="27">%2$d</xliff:g> others</string>
     <!-- Label for To field in read message view -->
     <string name="message_view_to_label">To:</string>
     <!-- Label for CC field in read message view -->
     <string name="message_view_cc_label">Cc:</string>
-    <!-- Label for BCC field in read message view -->
-    <string name="message_view_bcc_label">Bcc:</string>
     <!-- Menu item -->
     <string name="message_view_attachment_view_action">Open</string>
     <!-- Button name -->
@@ -196,8 +276,6 @@
     <string name="message_view_show_pictures_action">Show pictures</string>
     <!-- Toast while fetching attachment -->
     <string name="message_view_fetching_attachment_toast">Fetching attachment.</string>
-    <!-- Appears progress dialog for fetching pictures -->
-    <string name="message_view_fetching_pictures_toast">Fetching images\u2026</string>
     <!-- Appears progress dialog for fetching attachment -->
     <string name="message_view_fetching_attachment_progress">Fetching attachment <xliff:g id="filename">%s</xliff:g></string>
     <!-- Toast shown briefly while deleting a message -->
@@ -217,8 +295,15 @@
 
     <!-- Title of screen when setting up new email account -->
     <string name="account_setup_basics_title">Set up email</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, brief instructions -->
-    <string name="account_setup_basics_instructions">Type your account email address:</string>
+    <!--  DEPRECATED - REMOVE -->
+    <string name="account_setup_basics_instructions"></string>
     <!-- On "Set up email" screen, hint for account email address text field -->
     <string name="account_setup_basics_email_hint">Email address</string>
     <!-- On "Set up email" screen, hint for account email password text field -->
@@ -230,6 +315,13 @@
     <!-- Toast when we can't build a URI from the given email & password -->
     <!-- Note, the error message in the toast is purposefully vague, because I *don't* know exactly what's wrong. -->
     <string name="account_setup_username_password_toast">Please type a valid email address and password.</string>
+    <!-- Title of dialog shown when a duplicate account is created -->
+    <string name="account_duplicate_dlg_title">Duplicate Account</string>
+    <!-- Message of dialog shown when a duplicate account is created.  The display name of
+         the duplicate account is displayed. -->
+    <string name="account_duplicate_dlg_message_fmt">
+        This login is already in use for the account \"<xliff:g id="duplicate">%s</xliff:g>\".
+    </string>
 
     <!-- Do Not Translate.  Activity Title for check-settings screen -->
     <string name="account_setup_check_settings_title"></string>
@@ -259,7 +351,7 @@
     <!-- "Add new email account" screen, button name in response to what type of account this is -->
     <string name="account_setup_account_type_imap_action">IMAP account</string>
     <!-- "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/ActiveSync</string>
+    <string name="account_setup_account_type_exchange_action">Exchange account</string>
     <!-- "Incoming server settings" screen, label for text field -->
     <string name="account_setup_incoming_title">Incoming server settings</string>
     <!-- "Incoming server settings" screen, label for text field -->
@@ -277,21 +369,19 @@
     <!-- "Incoming server settings" screen, options for "Security type" pop-up menu -->
     <string name="account_setup_incoming_security_none_label">None</string>
     <!-- "Incoming server settings" screen, options for "Security type" pop-up menu -->
-    <string name="account_setup_incoming_security_ssl_optional_label">SSL (if available)</string>
+    <string name="account_setup_incoming_security_ssl_trust_certificates_label">SSL (Accept all certificates)</string>
     <!-- "Incoming server settings" screen, options for "Security type" pop-up menu -->
-    <string name="account_setup_incoming_security_ssl_label">SSL (always)</string>
+    <string name="account_setup_incoming_security_ssl_label">SSL</string>
     <!-- "Incoming server settings" screen, options for "Security type" pop-up menu -->
-    <string name="account_setup_incoming_security_tls_optional_label">TLS (if available)</string>
+    <string name="account_setup_incoming_security_tls_trust_certificates_label">TLS (Accept all certificates)</string>
     <!-- "Incoming server settings" screen, options for "Security type" pop-up menu -->
-    <string name="account_setup_incoming_security_tls_label">TLS (always)</string>
+    <string name="account_setup_incoming_security_tls_label">TLS</string>
     <!-- "Incoming server settings" screen, label for pop-up menu -->
     <string name="account_setup_incoming_delete_policy_label">Delete email from server</string>
     <!-- "Incoming server settings" screen, options in pop-up menu for Delete email from server: -->
     <!-- "Incoming server settings" screen, options in pop-up menu for Delete email from server: -->
     <string name="account_setup_incoming_delete_policy_never_label">Never</string>
     <!-- "Incoming server settings" screen, options in pop-up menu for Delete email from server: -->
-    <string name="account_setup_incoming_delete_policy_7days_label">After 7 days</string>
-    <!-- "Incoming server settings" screen, options in pop-up menu for Delete email from server: -->
     <string name="account_setup_incoming_delete_policy_delete_label">When I delete from Inbox</string>
     
     <!-- "Incoming server settings" screen, label for setting IMAP path prefix: -->
@@ -317,12 +407,16 @@
     <string name="account_setup_exchange_title">Exchange server settings</string>
     <!-- On "Exchange" setup screen, the name of the server -->
     <string name="account_setup_exchange_server_label">Exchange Server</string>
-    <!-- On "Exchange" setup screen, the domain name label -->
+    <!-- On "Exchange" setup screen, the domain\\username -->
+    <string name="account_setup_exchange_username_label">Domain\\Username</string>
+    <!-- Deprecated: On "Exchange" setup screen, the domain name label -->
     <string name="account_setup_exchange_domain_label">Domain</string>
-    <!-- On "Exchange" setup screen, the domain name hint -->
+    <!-- Deprecated: On "Exchange" setup screen, the domain name hint -->
     <string name="account_setup_exchange_domain_hint">Enter domain here</string>
     <!-- On "Exchange" setup screen, the use-SSL checkbox label -->
     <string name="account_setup_exchange_ssl_label">Use secure connection (SSL)</string>
+    <!-- On "Exchange" setup screen, the trust ssl certificates checkbox label -->
+    <string name="account_setup_exchange_trust_certificates_label">Accept all SSL certificates</string>
 
     <!-- In Account setup options screen, Activity title -->
     <string name="account_setup_options_title">Account options</string>
@@ -347,6 +441,8 @@
     <string name="account_setup_options_default_label">Send email from this account by default.</string>
     <!-- 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>
     <!-- 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 -->
@@ -399,6 +495,8 @@
     <string name="account_settings_default_summary">Send email from this account by default</string>
     <!-- On Settings screen, setting option name -->
     <string name="account_settings_notify_label">Email notifications</string>
+    <!-- On Settings screen, summary line when called via AccountManager for Exchange accounts -->
+    <string name="account_settings_exchange_summary">Sync frequency, notifications, etc.</string>
     <!-- On Settings screen, setting summary text -->
     <string name="account_settings_notify_summary">Notify in status bar when email arrives</string>
     <!-- On Settings screen, setting option name and title of dialog box that opens -->
@@ -415,6 +513,12 @@
     <string name="account_settings_name_label">Your name</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>
+
     <!-- On Settings screen, setting check box label -->
     <string name="account_settings_vibrate_enable">Vibrate</string>
     <!-- On Settings screen, setting summary text -->
@@ -443,4 +547,10 @@
         these mail accounts.</string>
     <!-- Message that appears when adding a T-Online account -->
     <string name="provider_note_t_online">Before setting up this email account, please visit the T-Online Web site and create a password for POP3 email access.</string>
+
+    <!-- Name of Microsoft Exchange account type; used by AccountManager -->
+    <string name="exchange_name">Corporate</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>
 </resources>
diff --git a/res/xml/authenticator.xml b/res/xml/authenticator.xml
new file mode 100644
index 0000000..ae82c17
--- /dev/null
+++ b/res/xml/authenticator.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/**
+ * Copyright (c) 2009, 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 Account Manager. -->
+
+<account-authenticator xmlns:android="http://schemas.android.com/apk/res/android"
+    android:accountType="com.android.exchange"
+    android:icon="@drawable/ic_exchange_selected"
+    android:smallIcon="@drawable/ic_exchange_minitab_selected"
+    android:label="@string/exchange_name"
+    android:accountPreferences="@xml/account_preferences"
+/>
diff --git a/res/xml/syncadapter_contacts.xml b/res/xml/syncadapter_contacts.xml
new file mode 100644
index 0000000..4691aee
--- /dev/null
+++ b/res/xml/syncadapter_contacts.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/**
+ * Copyright (c) 2009, 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.contacts"
+    android:accountType="com.android.exchange"
+/>
diff --git a/src/com/android/exchange/AbstractSyncService.java b/src/com/android/exchange/AbstractSyncService.java
new file mode 100644
index 0000000..425424d
--- /dev/null
+++ b/src/com/android/exchange/AbstractSyncService.java
@@ -0,0 +1,365 @@
+/*
+ * 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.email.mail.MessagingException;
+import com.android.email.provider.EmailContent.Account;
+import com.android.email.provider.EmailContent.Mailbox;
+import com.android.exchange.utility.FileLogger;
+
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.net.Uri;
+import android.net.NetworkInfo.DetailedState;
+import android.util.Log;
+
+import java.util.ArrayList;
+
+/**
+ * Base class for all protocol services SyncManager (extends Service, implements
+ * Runnable) instantiates subclasses to run a sync (either timed, or push, or
+ * mail placed in outbox, etc.) EasSyncService is currently implemented; my goal
+ * would be to move IMAP to this structure when it comes time to introduce push
+ * functionality.
+ */
+public abstract class AbstractSyncService implements Runnable {
+
+    public String TAG = "AbstractSyncService";
+
+    public static final String SUMMARY_PROTOCOL = "_SUMMARY_";
+    public static final String SYNCED_PROTOCOL = "_SYNCING_";
+    public static final String MOVE_FAVORITES_PROTOCOL = "_MOVE_FAVORITES_";
+    public static final int SECONDS = 1000;
+    public static final int MINUTES = 60*SECONDS;
+    public static final int HOURS = 60*MINUTES;
+    public static final int DAYS = 24*HOURS;
+
+    public static final int CONNECT_TIMEOUT = 30*SECONDS;
+    public static final int NETWORK_WAIT = 15*SECONDS;
+
+    public static final String IMAP_PROTOCOL = "imap";
+    public static final String EAS_PROTOCOL = "eas";
+    public static final int EXIT_DONE = 0;
+    public static final int EXIT_IO_ERROR = 1;
+    public static final int EXIT_LOGIN_FAILURE = 2;
+    public static final int EXIT_EXCEPTION = 3;
+
+    public Mailbox mMailbox;
+    protected long mMailboxId;
+    protected Thread mThread;
+    protected int mExitStatus = EXIT_EXCEPTION;
+    protected String mMailboxName;
+    public Account mAccount;
+    public Context mContext;
+    public int mChangeCount = 0;
+    public int mSyncReason = 0;
+    protected volatile boolean mStop = false;
+    protected Object mSynchronizer = new Object();
+
+    protected volatile long mRequestTime = 0;
+    protected ArrayList<PartRequest> mPartRequests = new ArrayList<PartRequest>();
+    protected PartRequest mPendingPartRequest = null;
+
+    /**
+     * Sent by SyncManager to request that the service stop itself cleanly
+     */
+    public abstract void stop();
+
+    /**
+     * Sent by SyncManager to indicate a user request requiring service has been
+     * added to the service's pending request queue
+     */
+    public abstract void ping();
+
+    /**
+     * Called to validate an account; abstract to allow each protocol to do what
+     * is necessary. For consistency with the Email app's original
+     * functionality, success is indicated by a failure to throw an Exception
+     * (ugh). Parameters are self-explanatory
+     *
+     * @param host
+     * @param userName
+     * @param password
+     * @param port
+     * @param ssl
+     * @param context
+     * @throws MessagingException
+     */
+    public abstract void validateAccount(String host, String userName, String password, int port,
+            boolean ssl, boolean trustCertificates, Context context) throws MessagingException;
+
+    public AbstractSyncService(Context _context, Mailbox _mailbox) {
+        mContext = _context;
+        mMailbox = _mailbox;
+        mMailboxId = _mailbox.mId;
+        mMailboxName = _mailbox.mServerId;
+        mAccount = Account.restoreAccountWithId(_context, _mailbox.mAccountKey);
+    }
+
+    // Will be required when subclasses are instantiated by name
+    public AbstractSyncService(String prefix) {
+    }
+
+    /**
+     * The UI can call this static method to perform account validation.  This method wraps each
+     * protocol's validateAccount method.   Arguments are self-explanatory, except where noted.
+     *
+     * @param klass the protocol class (EasSyncService.class for example)
+     * @param host
+     * @param userName
+     * @param password
+     * @param port
+     * @param ssl
+     * @param context
+     * @throws MessagingException
+     */
+    static public void validate(Class<? extends AbstractSyncService> klass, String host,
+            String userName, String password, int port, boolean ssl, boolean trustCertificates,
+            Context context)
+            throws MessagingException {
+        AbstractSyncService svc;
+        try {
+            svc = klass.newInstance();
+            svc.validateAccount(host, userName, password, port, ssl, trustCertificates, context);
+        } catch (IllegalAccessException e) {
+            throw new MessagingException("internal error", e);
+        } catch (InstantiationException e) {
+            throw new MessagingException("internal error", e);
+        }
+    }
+
+    public static class ValidationResult {
+        static final int NO_FAILURE = 0;
+        static final int CONNECTION_FAILURE = 1;
+        static final int VALIDATION_FAILURE = 2;
+        static final int EXCEPTION = 3;
+
+        static final ValidationResult succeeded = new ValidationResult(true, NO_FAILURE, null);
+        boolean success;
+        int failure = NO_FAILURE;
+        String reason = null;
+        Exception exception = null;
+
+        ValidationResult(boolean _success, int _failure, String _reason) {
+            success = _success;
+            failure = _failure;
+            reason = _reason;
+        }
+
+        ValidationResult(boolean _success) {
+            success = _success;
+        }
+
+        ValidationResult(Exception e) {
+            success = false;
+            failure = EXCEPTION;
+            exception = e;
+        }
+
+        public boolean isSuccess() {
+            return success;
+        }
+
+        public String getReason() {
+            return reason;
+        }
+    }
+
+    public boolean isStopped() {
+        return mStop;
+    }
+
+    public Object getSynchronizer() {
+        return mSynchronizer;
+    }
+
+    /**
+     * Convenience methods to do user logging (i.e. connection activity).  Saves a bunch of
+     * repetitive code.
+     */
+    public void userLog(String string, int code, String string2) {
+        if (Eas.USER_LOG) {
+            userLog(string + code + string2);
+        }
+    }
+
+    public void userLog(String string, int code) {
+        if (Eas.USER_LOG) {
+            userLog(string + code);
+        }
+    }
+
+    public void userLog(String str, Exception e) {
+        if (Eas.USER_LOG) {
+            Log.e(TAG, str, e);
+        } else {
+            Log.e(TAG, str + e);
+        }
+        if (Eas.FILE_LOG) {
+            FileLogger.log(e);
+        }
+    }
+
+    /**
+     * Standard logging for EAS.
+     * If user logging is active, we concatenate any arguments and log them using Log.d
+     * We also check for file logging, and log appropriately
+     * @param strings strings to concatenate and log
+     */
+    public void userLog(String ...strings) {
+        if (Eas.USER_LOG) {
+            String logText;
+            if (strings.length == 1) {
+                logText = strings[0];
+            } else {
+                StringBuilder sb = new StringBuilder(64);
+                for (String string: strings) {
+                    sb.append(string);
+                }
+                logText = sb.toString();
+            }
+            Log.d(TAG, logText);
+            if (Eas.FILE_LOG) {
+                FileLogger.log(TAG, logText);
+            }
+        }
+    }
+
+    /**
+     * Error log is used for serious issues that should always be logged
+     * @param str the string to log
+     */
+    public void errorLog(String str) {
+        Log.e(TAG, str);
+        if (Eas.FILE_LOG) {
+            FileLogger.log(TAG, str);
+        }
+    }
+
+    /**
+     * Waits for up to 10 seconds for network connectivity; returns whether or not there is
+     * network connectivity.
+     *
+     * @return whether there is network connectivity
+     */
+    public boolean hasConnectivity() {
+        ConnectivityManager cm =
+            (ConnectivityManager)mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
+        int tries = 0;
+        while (tries++ < 1) {
+            NetworkInfo info = cm.getActiveNetworkInfo();
+            if (info != null && info.isConnected()) {
+                DetailedState state = info.getDetailedState();
+                if (state == DetailedState.CONNECTED) {
+                    return true;
+                }
+            }
+            try {
+                Thread.sleep(10*SECONDS);
+            } catch (InterruptedException e) {
+            }
+        }
+        return false;
+    }
+
+    /**
+     * PartRequest handling (common functionality)
+     * Can be overridden if desired, but IMAP/EAS both use the next three methods as-is
+     */
+
+    public void addPartRequest(PartRequest req) {
+        synchronized (mPartRequests) {
+            mPartRequests.add(req);
+            mRequestTime = System.currentTimeMillis();
+        }
+    }
+
+    public void removePartRequest(PartRequest req) {
+        synchronized (mPartRequests) {
+            mPartRequests.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
+     * an array of Strings corresponding to the columns in the projection.
+     */
+    protected String[] getRowColumns(Uri contentUri, String[] projection, String selection,
+            String[] selectionArgs) {
+        String[] values = new String[projection.length];
+        ContentResolver cr = mContext.getContentResolver();
+        Cursor c = cr.query(contentUri, projection, selection, selectionArgs, null);
+        try {
+            if (c.moveToFirst()) {
+                for (int i = 0; i < projection.length; i++) {
+                    values[i] = c.getString(i);
+                }
+            } else {
+                return null;
+            }
+        } finally {
+            c.close();
+        }
+        return values;
+    }
+
+    /**
+     * Convenience method for retrieving columns from a particular row in EmailProvider.
+     * Passed in here are a base uri (e.g. Message.CONTENT_URI), the unique id of a row, and
+     * a projection.  This method calls the previous one with the appropriate URI.
+     */
+    protected String[] getRowColumns(Uri baseUri, long id, String ... projection) {
+        return getRowColumns(ContentUris.withAppendedId(baseUri, id), projection, null, null);
+    }
+}
diff --git a/src/com/android/exchange/BootReceiver.java b/src/com/android/exchange/BootReceiver.java
new file mode 100644
index 0000000..5312bfb
--- /dev/null
+++ b/src/com/android/exchange/BootReceiver.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2009 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 android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.util.Log;
+
+public class BootReceiver extends BroadcastReceiver {
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        Log.d("Exchange", "BootReceiver onReceive");
+        context.startService(new Intent(context, SyncManager.class));
+    }
+}
diff --git a/src/com/android/exchange/ContactsSyncAdapterService.java b/src/com/android/exchange/ContactsSyncAdapterService.java
new file mode 100644
index 0000000..69fe792
--- /dev/null
+++ b/src/com/android/exchange/ContactsSyncAdapterService.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2009 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.net.Uri;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.provider.ContactsContract.RawContacts;
+import android.util.Log;
+
+public class ContactsSyncAdapterService extends Service {
+    private static final String TAG = "EAS ContactsSyncAdapterService";
+    private static SyncAdapterImpl sSyncAdapter = null;
+    private static final Object sSyncAdapterLock = new Object();
+
+    private static final String[] ID_PROJECTION = new String[] {EmailContent.RECORD_ID};
+    private static final String ACCOUNT_AND_TYPE_CONTACTS =
+        MailboxColumns.ACCOUNT_KEY + "=? AND " + MailboxColumns.TYPE + '=' + Mailbox.TYPE_CONTACTS;
+
+    public ContactsSyncAdapterService() {
+        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 {
+                ContactsSyncAdapterService.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 contacts
+     * 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();
+        Log.i(TAG, "performSync");
+        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)
+                .build();
+            Cursor c = cr.query(uri,
+                    new String[] {RawContacts._ID}, RawContacts.DIRTY + "=1", null, null);
+            try {
+                if (!c.moveToFirst()) {
+                    Log.i(TAG, "Upload sync; no changes");
+                    return;
+                }
+            } finally {
+                c.close();
+            }
+        }
+
+        // Find the (EmailProvider) account associated with this email address
+        Cursor accountCursor =
+            cr.query(com.android.email.provider.EmailContent.Account.CONTENT_URI, ID_PROJECTION,
+                AccountColumns.EMAIL_ADDRESS + "=?", new String[] {account.name}, null);
+        try {
+            if (accountCursor.moveToFirst()) {
+                long accountId = accountCursor.getLong(0);
+                // Now, find the contacts mailbox associated with the account
+                Cursor mailboxCursor = cr.query(Mailbox.CONTENT_URI, ID_PROJECTION,
+                        ACCOUNT_AND_TYPE_CONTACTS, new String[] {Long.toString(accountId)}, null);
+                try {
+                     if (mailboxCursor.moveToFirst()) {
+                        Log.i(TAG, "Contact 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/Eas.java b/src/com/android/exchange/Eas.java
new file mode 100644
index 0000000..c005544
--- /dev/null
+++ b/src/com/android/exchange/Eas.java
@@ -0,0 +1,89 @@
+/*
+ * 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 android.util.Log;
+
+/**
+ * Constants used throughout the EAS implementation are stored here.
+ *
+ */
+public class Eas {
+    // For debugging
+    public static boolean WAIT_DEBUG = false;   // DO NOT CHECK IN WITH THIS SET TO TRUE
+    public static boolean DEBUG = false;         // DO NOT CHECK IN WITH THIS SET TO TRUE
+
+    // The following two are for user logging (the second providing more detail)
+    public static boolean USER_LOG = false;     // DO NOT CHECK IN WITH THIS SET TO TRUE
+    public static boolean PARSER_LOG = false;   // DO NOT CHECK IN WITH THIS SET TO TRUE
+    public static boolean FILE_LOG = false;     // DO NOT CHECK IN WITH THIS SET TO TRUE
+
+    public static final int DEBUG_BIT = 1;
+    public static final int DEBUG_EXCHANGE_BIT = 2;
+    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
+    //                Mail Cal
+    // 0 No filter    Yes  Yes
+    // 1 1 day ago    Yes  No
+    // 2 3 days ago   Yes  No
+    // 3 1 week ago   Yes  No
+    // 4 2 weeks ago  Yes  Yes
+    // 5 1 month ago  Yes  Yes
+    // 6 3 months ago No   Yes
+    // 7 6 months ago No   Yes
+
+    public static final String FILTER_ALL = "0";
+    public static final String FILTER_1_DAY = "1";
+    public static final String FILTER_3_DAYS = "2";
+    public static final String FILTER_1_WEEK = "3";
+    public static final String FILTER_2_WEEKS = "4";
+    public static final String FILTER_1_MONTH = "5";
+    public static final String FILTER_3_MONTHS = "6";
+    public static final String FILTER_6_MONTHS = "7";
+    public static final String BODY_PREFERENCE_TEXT = "1";
+    public static final String BODY_PREFERENCE_HTML = "2";
+
+    // For EAS 12, we use HTML, so we want a larger size than in EAS 2.5
+    public static final String EAS12_TRUNCATION_SIZE = "200000";
+    // For EAS 2.5, truncation is a code; the largest is "7", which is 100k
+    public static final String EAS2_5_TRUNCATION_SIZE = "7";
+
+    public static final int FOLDER_STATUS_OK = 1;
+    public static final int FOLDER_STATUS_INVALID_KEY = 9;
+
+    public static final int EXCHANGE_ERROR_NOTIFICATION = 0x10;
+
+    public static void setUserDebug(int state) {
+        // DEBUG takes precedence and is never true in a user build
+        if (!DEBUG) {
+            USER_LOG = (state & DEBUG_BIT) != 0;
+            PARSER_LOG = (state & DEBUG_EXCHANGE_BIT) != 0;
+            FILE_LOG = (state & DEBUG_FILE_BIT) != 0;
+            if (FILE_LOG || PARSER_LOG) {
+                USER_LOG = true;
+            }
+            Log.d("Eas Debug", "Logging: " + (USER_LOG ? "User " : "") +
+                    (PARSER_LOG ? "Parser " : "") + (FILE_LOG ? "File" : ""));
+        }
+     }
+}
diff --git a/src/com/android/exchange/EasException.java b/src/com/android/exchange/EasException.java
new file mode 100644
index 0000000..e7613d7
--- /dev/null
+++ b/src/com/android/exchange/EasException.java
@@ -0,0 +1,22 @@
+/*
+ *  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;
+
+public class EasException extends Exception {
+    private static final long serialVersionUID = 5894556952470989968L;
+}
diff --git a/src/com/android/exchange/EasOutboxService.java b/src/com/android/exchange/EasOutboxService.java
new file mode 100644
index 0000000..4e1e61c
--- /dev/null
+++ b/src/com/android/exchange/EasOutboxService.java
@@ -0,0 +1,213 @@
+/*
+ * 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.email.mail.MessagingException;
+import com.android.email.mail.transport.Rfc822Output;
+import com.android.email.provider.EmailContent.Body;
+import com.android.email.provider.EmailContent.BodyColumns;
+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.provider.EmailContent.MessageColumns;
+import com.android.email.provider.EmailContent.SyncColumns;
+
+import org.apache.http.HttpResponse;
+import org.apache.http.HttpStatus;
+import org.apache.http.entity.InputStreamEntity;
+
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.os.RemoteException;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+
+public class EasOutboxService extends EasSyncService {
+
+    public static final int SEND_FAILED = 1;
+    public static final String MAILBOX_KEY_AND_NOT_SEND_FAILED =
+        MessageColumns.MAILBOX_KEY + "=? and (" + SyncColumns.SERVER_ID + " is null or " + 
+            SyncColumns.SERVER_ID + "!=" + SEND_FAILED + ')';
+    public static final String[] BODY_SOURCE_PROJECTION =
+        new String[] {BodyColumns.SOURCE_MESSAGE_KEY};
+    public static final String WHERE_MESSAGE_KEY = Body.MESSAGE_KEY + "=?";
+
+    // This needs to be long enough to send the longest reasonable message, without being so long
+    // as to effectively "hang" sending of mail.  The standard 30 second timeout isn't long enough
+    // for pictures and the like.  For now, we'll use 15 minutes, in the knowledge that any socket
+    // failure would probably generate an Exception before timing out anyway
+    public static final int SEND_MAIL_TIMEOUT = 15*MINUTES;
+
+    public EasOutboxService(Context _context, Mailbox _mailbox) {
+        super(_context, _mailbox);
+    }
+
+    private void sendCallback(long msgId, String subject, int status) {
+        try {
+            SyncManager.callback().sendMessageStatus(msgId, subject, status, 0);
+        } catch (RemoteException e) {
+            // It's all good
+        }
+    }
+
+    /**
+     * Send a single message via EAS
+     * Note that we mark messages SEND_FAILED when there is a permanent failure, rather than an
+     * IOException, which is handled by SyncManager with retries, backoffs, etc.
+     *
+     * @param cacheDir the cache directory for this context
+     * @param msgId the _id of the message to send
+     * @throws IOException
+     */
+    int sendMessage(File cacheDir, long msgId) throws IOException, MessagingException {
+        int result;
+        sendCallback(msgId, null, EmailServiceStatus.IN_PROGRESS);
+        File tmpFile = File.createTempFile("eas_", "tmp", cacheDir);
+        // Write the output to a temporary file
+        try {
+            String[] cols = getRowColumns(Message.CONTENT_URI, msgId, MessageColumns.FLAGS,
+                    MessageColumns.SUBJECT);
+            int flags = Integer.parseInt(cols[0]);
+            String subject = cols[1];
+
+            boolean reply = (flags & Message.FLAG_TYPE_REPLY) != 0;
+            boolean forward = (flags & Message.FLAG_TYPE_FORWARD) != 0;
+            // The reference message and mailbox are called item and collection in EAS
+            String itemId = null;
+            String collectionId = null;
+            if (reply || forward) {
+                // First, we need to get the id of the reply/forward message
+                cols = getRowColumns(Body.CONTENT_URI, BODY_SOURCE_PROJECTION,
+                        WHERE_MESSAGE_KEY, new String[] {Long.toString(msgId)});
+                if (cols != null) {
+                    long refId = Long.parseLong(cols[0]);
+                    // Then, we need the serverId and mailboxKey of the message
+                    cols = getRowColumns(Message.CONTENT_URI, refId, SyncColumns.SERVER_ID,
+                            MessageColumns.MAILBOX_KEY);
+                    if (cols != null) {
+                        itemId = cols[0];
+                        long boxId = Long.parseLong(cols[1]);
+                        // Then, we need the serverId of the mailbox
+                        cols = getRowColumns(Mailbox.CONTENT_URI, boxId, MailboxColumns.SERVER_ID);
+                        if (cols != null) {
+                            collectionId = cols[0];
+                        }
+                    }
+                }
+            }
+
+            boolean smartSend = itemId != null && collectionId != null;
+
+            // Write the message in rfc822 format to the temporary file
+            FileOutputStream fileStream = new FileOutputStream(tmpFile);
+            Rfc822Output.writeTo(mContext, msgId, fileStream, !smartSend, true);
+            fileStream.close();
+
+            // Now, get an input stream to our temporary file and create an entity with it
+            FileInputStream inputStream = new FileInputStream(tmpFile);
+            InputStreamEntity inputEntity =
+                new InputStreamEntity(inputStream, tmpFile.length());
+
+            // Create the appropriate command and POST it to the server
+            String cmd = "SendMail&SaveInSent=T";
+            if (smartSend) {
+                cmd = reply ? "SmartReply" : "SmartForward";
+                cmd += "&ItemId=" + itemId + "&CollectionId=" + collectionId + "&SaveInSent=T";
+            }
+            userLog("Send cmd: " + cmd);
+            HttpResponse resp = sendHttpClientPost(cmd, inputEntity, SEND_MAIL_TIMEOUT);
+
+            inputStream.close();
+            int code = resp.getStatusLine().getStatusCode();
+            if (code == HttpStatus.SC_OK) {
+                userLog("Deleting message...");
+                mContentResolver.delete(ContentUris.withAppendedId(Message.CONTENT_URI, msgId),
+                        null, null);
+                result = EmailServiceStatus.SUCCESS;
+                sendCallback(-1, subject, EmailServiceStatus.SUCCESS);
+            } else {
+                userLog("Message sending failed, code: " + code);
+                ContentValues cv = new ContentValues();
+                cv.put(SyncColumns.SERVER_ID, SEND_FAILED);
+                Message.update(mContext, Message.CONTENT_URI, msgId, cv);
+                result = EmailServiceStatus.REMOTE_EXCEPTION;
+                if (isAuthError(code)) {
+                    result = EmailServiceStatus.LOGIN_FAILED;
+                }
+                sendCallback(msgId, null, result);
+
+            }
+        } catch (IOException e) {
+            // We catch this just to send the callback
+            sendCallback(msgId, null, EmailServiceStatus.CONNECTION_ERROR);
+            throw e;
+        } finally {
+            // Clean up the temporary file
+            if (tmpFile.exists()) {
+                tmpFile.delete();
+            }
+        }
+        return result;
+    }
+
+    @Override
+    public void run() {
+        setupService();
+        File cacheDir = mContext.getCacheDir();
+        try {
+            mDeviceId = SyncManager.getDeviceId();
+            Cursor c = mContext.getContentResolver().query(Message.CONTENT_URI,
+                    Message.ID_COLUMN_PROJECTION, MAILBOX_KEY_AND_NOT_SEND_FAILED,
+                    new String[] {Long.toString(mMailbox.mId)}, null);
+             try {
+                while (c.moveToNext()) {
+                    long msgId = c.getLong(0);
+                    if (msgId != 0) {
+                        int result = sendMessage(cacheDir, msgId);
+                        // If there's an error, it should stop the service; we will distinguish
+                        // at least between login failures and everything else
+                        if (result == EmailServiceStatus.LOGIN_FAILED) {
+                            mExitStatus = EXIT_LOGIN_FAILURE;
+                            return;
+                        } else if (result == EmailServiceStatus.REMOTE_EXCEPTION) {
+                            mExitStatus = EXIT_EXCEPTION;
+                            return;
+                        }
+                    }
+                }
+            } finally {
+                 c.close();
+            }
+            mExitStatus = EXIT_DONE;
+        } catch (IOException e) {
+            mExitStatus = EXIT_IO_ERROR;
+        } catch (Exception e) {
+            userLog("Exception caught in EasOutboxService", e);
+            mExitStatus = EXIT_EXCEPTION;
+        } finally {
+            userLog(mMailbox.mDisplayName, ": sync finished");
+            userLog("Outbox exited with status ", mExitStatus);
+            SyncManager.done(this);
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/exchange/EasSyncService.java b/src/com/android/exchange/EasSyncService.java
new file mode 100644
index 0000000..3906024
--- /dev/null
+++ b/src/com/android/exchange/EasSyncService.java
@@ -0,0 +1,1173 @@
+/*
+ * 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.email.codec.binary.Base64;
+import com.android.email.mail.AuthenticationFailedException;
+import com.android.email.mail.MessagingException;
+import com.android.email.provider.EmailContent.Account;
+import com.android.email.provider.EmailContent.AccountColumns;
+import com.android.email.provider.EmailContent.Attachment;
+import com.android.email.provider.EmailContent.AttachmentColumns;
+import com.android.email.provider.EmailContent.HostAuth;
+import com.android.email.provider.EmailContent.Mailbox;
+import com.android.email.provider.EmailContent.MailboxColumns;
+import com.android.email.provider.EmailContent.Message;
+import com.android.exchange.adapter.AbstractSyncAdapter;
+import com.android.exchange.adapter.AccountSyncAdapter;
+import com.android.exchange.adapter.ContactsSyncAdapter;
+import com.android.exchange.adapter.EmailSyncAdapter;
+import com.android.exchange.adapter.FolderSyncParser;
+import com.android.exchange.adapter.PingParser;
+import com.android.exchange.adapter.Serializer;
+import com.android.exchange.adapter.Tags;
+import com.android.exchange.adapter.Parser.EasParserException;
+
+import org.apache.http.Header;
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpResponse;
+import org.apache.http.HttpStatus;
+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.impl.client.DefaultHttpClient;
+import org.apache.http.params.BasicHttpParams;
+import org.apache.http.params.HttpConnectionParams;
+import org.apache.http.params.HttpParams;
+
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.os.RemoteException;
+import android.os.SystemClock;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.net.URLEncoder;
+import java.security.cert.CertificateException;
+import java.util.ArrayList;
+import java.util.HashMap;
+
+public class EasSyncService extends AbstractSyncService {
+    private static final String EMAIL_WINDOW_SIZE = "5";
+    public static final String PIM_WINDOW_SIZE = "5";
+    private static final String WHERE_ACCOUNT_KEY_AND_SERVER_ID =
+        MailboxColumns.ACCOUNT_KEY + "=? and " + MailboxColumns.SERVER_ID + "=?";
+    private static final String WHERE_ACCOUNT_AND_SYNC_INTERVAL_PING =
+        MailboxColumns.ACCOUNT_KEY + "=? and " + MailboxColumns.SYNC_INTERVAL +
+        '=' + Mailbox.CHECK_INTERVAL_PING;
+    private static final String AND_FREQUENCY_PING_PUSH_AND_NOT_ACCOUNT_MAILBOX = " AND " +
+        MailboxColumns.SYNC_INTERVAL + " IN (" + Mailbox.CHECK_INTERVAL_PING +
+        ',' + Mailbox.CHECK_INTERVAL_PUSH + ") AND " + MailboxColumns.TYPE + "!=\"" +
+        Mailbox.TYPE_EAS_ACCOUNT_MAILBOX + '\"';
+    private static final String WHERE_PUSH_HOLD_NOT_ACCOUNT_MAILBOX =
+        MailboxColumns.ACCOUNT_KEY + "=? and " + MailboxColumns.SYNC_INTERVAL +
+        '=' + Mailbox.CHECK_INTERVAL_PUSH_HOLD;
+    static private final int CHUNK_SIZE = 16*1024;
+
+    static private final String PING_COMMAND = "Ping";
+    static private final int COMMAND_TIMEOUT = 20*SECONDS;
+
+    /**
+     * 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
+     * the ping exception out.  The maximum I use is 17 minutes, which is really an empirical
+     * choice; too long and we risk silent connection loss and loss of push for that period.  Too
+     * short and we lose efficiency/battery life.
+     *
+     * If we ever have to drop the ping timeout, we'll never increase it again.  There's no point
+     * going into hysteresis; the NAT timeout isn't going to change without a change in connection,
+     * which will cause the sync service to be restarted at the starting heartbeat and going through
+     * the process again.
+     */
+    static private final int PING_MINUTES = 60; // in seconds
+    static private final int PING_FUDGE_LOW = 10;
+    static private final int PING_STARTING_HEARTBEAT = (8*PING_MINUTES)-PING_FUDGE_LOW;
+    static private final int PING_MIN_HEARTBEAT = (5*PING_MINUTES)-PING_FUDGE_LOW;
+    static private final int PING_MAX_HEARTBEAT = (17*PING_MINUTES)-PING_FUDGE_LOW;
+    static private final int PING_HEARTBEAT_INCREMENT = 3*PING_MINUTES;
+    static private final int PING_FORCE_HEARTBEAT = 2*PING_MINUTES;
+
+    static private final int PROTOCOL_PING_STATUS_COMPLETED = 1;
+
+    // Fallbacks (in minutes) for ping loop failures
+    static private final int MAX_PING_FAILURES = 1;
+    static private final int PING_FALLBACK_INBOX = 5;
+    static private final int PING_FALLBACK_PIM = 25;
+
+    // Reasonable default
+    String mProtocolVersion = "2.5";
+    public Double mProtocolVersionDouble;
+    protected String mDeviceId = null;
+    private String mDeviceType = "Android";
+    private String mAuthString = null;
+    private String mCmdString = null;
+    public String mHostAddress;
+    public String mUserName;
+    public String mPassword;
+    private boolean mSsl = true;
+    private boolean mTrustSsl = false;
+    public ContentResolver mContentResolver;
+    private String[] mBindArguments = new String[2];
+    private ArrayList<String> mPingChangeList;
+    private HttpPost mPendingPost = null;
+    // The ping time (in seconds)
+    private int mPingHeartbeat = PING_STARTING_HEARTBEAT;
+    // The longest successful ping heartbeat
+    private int mPingHighWaterMark = 0;
+    // Whether we've ever lowered the heartbeat
+    private boolean mPingHeartbeatDropped = false;
+    // Whether a POST was aborted due to watchdog timeout
+    private boolean mAborted = false;
+
+    public EasSyncService(Context _context, Mailbox _mailbox) {
+        super(_context, _mailbox);
+        mContentResolver = _context.getContentResolver();
+        HostAuth ha = HostAuth.restoreHostAuthWithId(_context, mAccount.mHostAuthKeyRecv);
+        mSsl = (ha.mFlags & HostAuth.FLAG_SSL) != 0;
+        mTrustSsl = (ha.mFlags & HostAuth.FLAG_TRUST_ALL_CERTIFICATES) != 0;
+    }
+
+    private EasSyncService(String prefix) {
+        super(prefix);
+    }
+
+    public EasSyncService() {
+        this("EAS Validation");
+    }
+
+    @Override
+    public void ping() {
+        userLog("Alarm ping received!");
+        synchronized(getSynchronizer()) {
+            if (mPendingPost != null) {
+                userLog("Aborting pending POST!");
+                mAborted = true;
+                mPendingPost.abort();
+            }
+        }
+    }
+
+    @Override
+    public void stop() {
+        mStop = true;
+        synchronized(getSynchronizer()) {
+            if (mPendingPost != null) {
+                mPendingPost.abort();
+            }
+        }
+    }
+
+    /**
+     * Determine whether an HTTP code represents an authentication error
+     * @param code the HTTP code returned by the server
+     * @return whether or not the code represents an authentication error
+     */
+    protected boolean isAuthError(int code) {
+        return ((code == HttpStatus.SC_UNAUTHORIZED) || (code == HttpStatus.SC_FORBIDDEN));
+    }
+
+    @Override
+    public void validateAccount(String hostAddress, String userName, String password, int port,
+            boolean ssl, boolean trustCertificates, Context context) throws MessagingException {
+        try {
+            userLog("Testing EAS: ", hostAddress, ", ", userName, ", ssl = ", ssl ? "1" : "0");
+            EasSyncService svc = new EasSyncService("%TestAccount%");
+            svc.mContext = context;
+            svc.mHostAddress = hostAddress;
+            svc.mUserName = userName;
+            svc.mPassword = password;
+            svc.mSsl = ssl;
+            svc.mTrustSsl = trustCertificates;
+            svc.mDeviceId = SyncManager.getDeviceId();
+            HttpResponse resp = svc.sendHttpClientOptions();
+            int code = resp.getStatusLine().getStatusCode();
+            userLog("Validation (OPTIONS) response: " + code);
+            if (code == HttpStatus.SC_OK) {
+                // No exception means successful validation
+                Header commands = resp.getFirstHeader("MS-ASProtocolCommands");
+                Header versions = resp.getFirstHeader("ms-asprotocolversions");
+                if (commands == null || versions == null) {
+                    userLog("OPTIONS response without commands or versions; reporting I/O error");
+                    throw new MessagingException(MessagingException.IOERROR);
+                }
+                userLog("Validation successful");
+                return;
+            }
+            if (isAuthError(code)) {
+                userLog("Authentication failed");
+                throw new AuthenticationFailedException("Validation failed");
+            } else {
+                // TODO Need to catch other kinds of errors (e.g. policy) For now, report the code.
+                userLog("Validation failed, reporting I/O error: ", code);
+                throw new MessagingException(MessagingException.IOERROR);
+            }
+        } catch (IOException e) {
+            Throwable cause = e.getCause();
+            if (cause != null && cause instanceof CertificateException) {
+                userLog("CertificateException caught: ", e.getMessage());
+                throw new MessagingException(MessagingException.GENERAL_SECURITY);
+            }
+            userLog("IOException caught: ", e.getMessage());
+            throw new MessagingException(MessagingException.IOERROR);
+        }
+
+    }
+
+    private void doStatusCallback(long messageId, long attachmentId, int status) {
+        try {
+            SyncManager.callback().loadAttachmentStatus(messageId, attachmentId, status, 0);
+        } catch (RemoteException e) {
+            // No danger if the client is no longer around
+        }
+    }
+
+    private void doProgressCallback(long messageId, long attachmentId, int progress) {
+        try {
+            SyncManager.callback().loadAttachmentStatus(messageId, attachmentId,
+                    EmailServiceStatus.IN_PROGRESS, progress);
+        } catch (RemoteException e) {
+            // No danger if the client is no longer around
+        }
+    }
+
+    public File createUniqueFileInternal(String dir, String filename) {
+        File directory;
+        if (dir == null) {
+            directory = mContext.getFilesDir();
+        } else {
+            directory = new File(dir);
+        }
+        if (!directory.exists()) {
+            directory.mkdirs();
+        }
+        File file = new File(directory, filename);
+        if (!file.exists()) {
+            return file;
+        }
+        // Get the extension of the file, if any.
+        int index = filename.lastIndexOf('.');
+        String name = filename;
+        String extension = "";
+        if (index != -1) {
+            name = filename.substring(0, index);
+            extension = filename.substring(index);
+        }
+        for (int i = 2; i < Integer.MAX_VALUE; i++) {
+            file = new File(directory, name + '-' + i + extension);
+            if (!file.exists()) {
+                return file;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Loads an attachment, based on the PartRequest passed in.  The PartRequest is basically our
+     * wrapper for Attachment
+     * @param req the part (attachment) to be retrieved
+     * @throws IOException
+     */
+    protected void getAttachment(PartRequest req) throws IOException {
+        Attachment att = req.att;
+        Message msg = Message.restoreMessageWithId(mContext, att.mMessageKey);
+        doProgressCallback(msg.mId, att.mId, 0);
+
+        String cmd = "GetAttachment&AttachmentName=" + att.mLocation;
+        HttpResponse res = sendHttpClientPost(cmd, null, COMMAND_TIMEOUT);
+
+        int status = res.getStatusLine().getStatusCode();
+        if (status == HttpStatus.SC_OK) {
+            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);
+            if (f != null) {
+                // Ensure that the target directory exists
+                File destDir = f.getParentFile();
+                if (!destDir.exists()) {
+                    destDir.mkdirs();
+                }
+                FileOutputStream os = new FileOutputStream(f);
+                // len > 0 means that Content-Length was set in the headers
+                // len < 0 means "chunked" transfer-encoding
+                if (len != 0) {
+                    try {
+                        mPendingPartRequest = req;
+                        byte[] bytes = new byte[CHUNK_SIZE];
+                        int length = len;
+                        // Loop terminates 1) when EOF is reached or 2) if an IOException occurs
+                        // One of these is guaranteed to occur
+                        int totalRead = 0;
+                        userLog("Attachment content-length: ", len);
+                        while (true) {
+                            int read = is.read(bytes, 0, CHUNK_SIZE);
+
+                            // read < 0 means that EOF was reached
+                            if (read < 0) {
+                                userLog("Attachment load reached EOF, totalRead: ", totalRead);
+                                break;
+                            }
+
+                            // Keep track of how much we've read for progress callback
+                            totalRead += read;
+
+                            // Write these bytes out
+                            os.write(bytes, 0, read);
+
+                            // We can't report percentages if this is chunked; by definition, the
+                            // length of incoming data is unknown
+                            if (length > 0) {
+                                // Belt and suspenders check to prevent runaway reading
+                                if (totalRead > length) {
+                                    errorLog("totalRead is greater than attachment length?");
+                                    break;
+                                }
+                                int pct = (totalRead * 100 / length);
+                                doProgressCallback(msg.mId, att.mId, pct);
+                            }
+                       }
+                    } finally {
+                        mPendingPartRequest = null;
+                    }
+                }
+                os.flush();
+                os.close();
+
+                // EmailProvider will throw an exception if we try to update an unsaved attachment
+                if (att.isSaved()) {
+                    String contentUriString = (req.contentUriString != null)
+                            ? req.contentUriString
+                            : "file://" + f.getAbsolutePath();
+                    ContentValues cv = new ContentValues();
+                    cv.put(AttachmentColumns.CONTENT_URI, contentUriString);
+                    att.update(mContext, cv);
+                    doStatusCallback(msg.mId, att.mId, EmailServiceStatus.SUCCESS);
+                }
+            }
+        } else {
+            doStatusCallback(msg.mId, att.mId, EmailServiceStatus.MESSAGE_NOT_FOUND);
+        }
+    }
+
+    @SuppressWarnings("deprecation")
+    private String makeUriString(String cmd, String extra) throws IOException {
+         // Cache the authentication string and the command string
+        String safeUserName = URLEncoder.encode(mUserName);
+        if (mAuthString == null) {
+            String cs = mUserName + ':' + mPassword;
+            mAuthString = "Basic " + new String(Base64.encodeBase64(cs.getBytes()));
+            mCmdString = "&User=" + safeUserName + "&DeviceId=" + mDeviceId + "&DeviceType="
+                    + mDeviceType;
+        }
+        String us = (mSsl ? (mTrustSsl ? "httpts" : "https") : "http") + "://" + mHostAddress +
+            "/Microsoft-Server-ActiveSync";
+        if (cmd != null) {
+            us += "?Cmd=" + cmd + mCmdString;
+        }
+        if (extra != null) {
+            us += extra;
+        }
+        return us;
+    }
+
+    private void setHeaders(HttpRequestBase method) {
+        method.setHeader("Authorization", mAuthString);
+        method.setHeader("MS-ASProtocolVersion", mProtocolVersion);
+        method.setHeader("Connection", "keep-alive");
+        method.setHeader("User-Agent", mDeviceType + '/' + Eas.VERSION);
+    }
+
+    private ClientConnectionManager getClientConnectionManager() {
+        return SyncManager.getClientConnectionManager();
+    }
+
+    private HttpClient getHttpClient(int timeout) {
+        HttpParams params = new BasicHttpParams();
+        HttpConnectionParams.setConnectionTimeout(params, 15*SECONDS);
+        HttpConnectionParams.setSoTimeout(params, timeout);
+        HttpConnectionParams.setSocketBufferSize(params, 8192);
+        HttpClient client = new DefaultHttpClient(getClientConnectionManager(), params);
+        return client;
+    }
+
+    protected HttpResponse sendHttpClientPost(String cmd, byte[] bytes) throws IOException {
+        return sendHttpClientPost(cmd, new ByteArrayEntity(bytes), COMMAND_TIMEOUT);
+    }
+
+    protected HttpResponse sendHttpClientPost(String cmd, HttpEntity entity) throws IOException {
+        return sendHttpClientPost(cmd, entity, COMMAND_TIMEOUT);
+    }
+
+    protected HttpResponse sendPing(byte[] bytes, int heartbeat) throws IOException {
+       Thread.currentThread().setName(mAccount.mDisplayName + ": Ping");
+       if (Eas.USER_LOG) {
+           userLog("Send ping, timeout: " + heartbeat + "s, high: " + mPingHighWaterMark + 's');
+       }
+       return sendHttpClientPost(PING_COMMAND, new ByteArrayEntity(bytes), (heartbeat+5)*SECONDS);
+    }
+
+    protected HttpResponse sendHttpClientPost(String cmd, HttpEntity entity, int timeout)
+            throws IOException {
+        HttpClient client = getHttpClient(timeout);
+        boolean sleepAllowed = cmd.equals(PING_COMMAND);
+
+        // Split the mail sending commands
+        String extra = null;
+        boolean msg = false;
+        if (cmd.startsWith("SmartForward&") || cmd.startsWith("SmartReply&")) {
+            int cmdLength = cmd.indexOf('&');
+            extra = cmd.substring(cmdLength);
+            cmd = cmd.substring(0, cmdLength);
+            msg = true;
+        } else if (cmd.startsWith("SendMail&")) {
+            msg = true;
+        }
+
+        String us = makeUriString(cmd, extra);
+        HttpPost method = new HttpPost(URI.create(us));
+        // Send the proper Content-Type header
+        // If entity is null (e.g. for attachments), don't set this header
+        if (msg) {
+            method.setHeader("Content-Type", "message/rfc822");
+        } else if (entity != null) {
+            method.setHeader("Content-Type", "application/vnd.ms-sync.wbxml");
+        }
+        setHeaders(method);
+        method.setEntity(entity);
+        synchronized(getSynchronizer()) {
+            mPendingPost = method;
+            if (sleepAllowed) {
+                SyncManager.runAsleep(mMailboxId, timeout+(10*SECONDS));
+            }
+        }
+        try {
+            return client.execute(method);
+        } finally {
+            synchronized(getSynchronizer()) {
+                if (sleepAllowed) {
+                    SyncManager.runAwake(mMailboxId);
+                }
+                mPendingPost = null;
+            }
+        }
+    }
+
+    protected HttpResponse sendHttpClientOptions() throws IOException {
+        HttpClient client = getHttpClient(COMMAND_TIMEOUT);
+        String us = makeUriString("OPTIONS", null);
+        HttpOptions method = new HttpOptions(URI.create(us));
+        setHeaders(method);
+        return client.execute(method);
+    }
+
+    String getTargetCollectionClassFromCursor(Cursor c) {
+        int type = c.getInt(Mailbox.CONTENT_TYPE_COLUMN);
+        if (type == Mailbox.TYPE_CONTACTS) {
+            return "Contacts";
+        } else if (type == Mailbox.TYPE_CALENDAR) {
+            return "Calendar";
+        } else {
+            return "Email";
+        }
+    }
+
+    /**
+     * Performs FolderSync
+     *
+     * @throws IOException
+     * @throws EasParserException
+     */
+    public void runAccountMailbox() throws IOException, EasParserException {
+        // Initialize exit status to success
+        mExitStatus = EmailServiceStatus.SUCCESS;
+        try {
+            try {
+                SyncManager.callback()
+                    .syncMailboxListStatus(mAccount.mId, EmailServiceStatus.IN_PROGRESS, 0);
+            } catch (RemoteException e1) {
+                // Don't care if this fails
+            }
+
+            if (mAccount.mSyncKey == null) {
+                mAccount.mSyncKey = "0";
+                userLog("Account syncKey INIT to 0");
+                ContentValues cv = new ContentValues();
+                cv.put(AccountColumns.SYNC_KEY, mAccount.mSyncKey);
+                mAccount.update(mContext, cv);
+            }
+
+            boolean firstSync = mAccount.mSyncKey.equals("0");
+            if (firstSync) {
+                userLog("Initial FolderSync");
+            }
+
+            // When we first start up, change all mailboxes to push.
+            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,
+                    new String[] {Long.toString(mAccount.mId)}) > 0) {
+                SyncManager.kick("change ping boxes to push");
+            }
+
+            // Determine our protocol version, if we haven't already
+            if (mAccount.mProtocolVersion == null) {
+                userLog("Determine EAS protocol version");
+                HttpResponse resp = sendHttpClientOptions();
+                int code = resp.getStatusLine().getStatusCode();
+                userLog("OPTIONS response: ", code);
+                if (code == HttpStatus.SC_OK) {
+                    Header header = resp.getFirstHeader("MS-ASProtocolCommands");
+                    userLog(header.getValue());
+                    header = resp.getFirstHeader("ms-asprotocolversions");
+                    String versions = header.getValue();
+                    if (versions != null) {
+                        if (versions.contains("12.0")) {
+                            mProtocolVersion = "12.0";
+                        }
+                        mProtocolVersionDouble = Double.parseDouble(mProtocolVersion);
+                        mAccount.mProtocolVersion = mProtocolVersion;
+                        userLog(versions);
+                        userLog("Using version ", mProtocolVersion);
+                    } else {
+                        errorLog("No protocol versions in OPTIONS response");
+                        throw new IOException();
+                    }
+                } else {
+                    errorLog("OPTIONS command failed; throwing IOException");
+                    throw new IOException();
+                }
+            }
+
+            // Change all pushable boxes to push when we start the account mailbox
+            if (mAccount.mSyncInterval == Account.CHECK_INTERVAL_PUSH) {
+                cv = new ContentValues();
+                cv.put(Mailbox.SYNC_INTERVAL, Mailbox.CHECK_INTERVAL_PUSH);
+                if (mContentResolver.update(Mailbox.CONTENT_URI, cv,
+                        SyncManager.WHERE_IN_ACCOUNT_AND_PUSHABLE,
+                        new String[] {Long.toString(mAccount.mId)}) > 0) {
+                    userLog("Push account; set pushable boxes to push...");
+                }
+            }
+
+            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)) {
+                    mExitStatus = EXIT_LOGIN_FAILURE;
+                } else {
+                    userLog("FolderSync response error: ", code);
+                }
+
+                // Change all push/hold boxes to push
+                cv = new ContentValues();
+                cv.put(Mailbox.SYNC_INTERVAL, Account.CHECK_INTERVAL_PUSH);
+                if (mContentResolver.update(Mailbox.CONTENT_URI, cv,
+                        WHERE_PUSH_HOLD_NOT_ACCOUNT_MAILBOX,
+                        new String[] {Long.toString(mAccount.mId)}) > 0) {
+                    userLog("Set push/hold boxes to push...");
+                }
+
+                try {
+                    SyncManager.callback()
+                        .syncMailboxListStatus(mAccount.mId, mExitStatus, 0);
+                } catch (RemoteException e1) {
+                    // Don't care if this fails
+                }
+
+                // Wait for push notifications.
+                String threadName = Thread.currentThread().getName();
+                try {
+                    runPingLoop();
+                } catch (StaleFolderListException e) {
+                    // We break out if we get told about a stale folder list
+                    userLog("Ping interrupted; folder list requires sync...");
+                } finally {
+                    Thread.currentThread().setName(threadName);
+                }
+            }
+         } catch (IOException e) {
+            // We catch this here to send the folder sync status callback
+            // A folder sync failed callback will get sent from run()
+            try {
+                if (!mStop) {
+                    SyncManager.callback()
+                        .syncMailboxListStatus(mAccount.mId,
+                                EmailServiceStatus.CONNECTION_ERROR, 0);
+                }
+            } catch (RemoteException e1) {
+                // Don't care if this fails
+            }
+            throw e;
+        }
+    }
+
+    void pushFallback(long mailboxId) {
+        Mailbox mailbox = Mailbox.restoreMailboxWithId(mContext, mailboxId);
+        ContentValues cv = new ContentValues();
+        int mins = PING_FALLBACK_PIM;
+        if (mailbox.mType == Mailbox.TYPE_INBOX) {
+            mins = PING_FALLBACK_INBOX;
+        }
+        cv.put(Mailbox.SYNC_INTERVAL, mins);
+        mContentResolver.update(ContentUris.withAppendedId(Mailbox.CONTENT_URI, mailboxId),
+                cv, null, null);
+        errorLog("*** PING ERROR LOOP: Set " + mailbox.mDisplayName + " to " + mins + " min sync");
+        SyncManager.kick("push fallback");
+    }
+
+    void runPingLoop() throws IOException, StaleFolderListException {
+        int pingHeartbeat = mPingHeartbeat;
+        userLog("runPingLoop");
+        // Do push for all sync services here
+        long endTime = System.currentTimeMillis() + (30*MINUTES);
+        HashMap<String, Integer> pingErrorMap = new HashMap<String, Integer>();
+        ArrayList<String> readyMailboxes = new ArrayList<String>();
+        ArrayList<String> notReadyMailboxes = new ArrayList<String>();
+        int pingWaitCount = 0;
+        
+        while ((System.currentTimeMillis() < endTime) && !mStop) {
+            // Count of pushable mailboxes
+            int pushCount = 0;
+            // Count of mailboxes that can be pushed right now
+            int canPushCount = 0;
+            // Count of uninitialized boxes
+            int uninitCount = 0;
+            
+            Serializer s = new Serializer();
+            Cursor c = mContentResolver.query(Mailbox.CONTENT_URI, Mailbox.CONTENT_PROJECTION,
+                    MailboxColumns.ACCOUNT_KEY + '=' + mAccount.mId +
+                    AND_FREQUENCY_PING_PUSH_AND_NOT_ACCOUNT_MAILBOX, null, null);
+            notReadyMailboxes.clear();
+            readyMailboxes.clear();
+            try {
+                // Loop through our pushed boxes seeing what is available to push
+                while (c.moveToNext()) {
+                    pushCount++;
+                    // Two requirements for push:
+                    // 1) SyncManager tells us the mailbox is syncable (not running, not stopped)
+                    // 2) The syncKey isn't "0" (i.e. it's synced at least once)
+                    long mailboxId = c.getLong(Mailbox.CONTENT_ID_COLUMN);
+                    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
+                            pushCount--;
+                            uninitCount++;
+                            continue;
+                        }
+
+                        if (canPushCount++ == 0) {
+                            // Initialize the Ping command
+                            s.start(Tags.PING_PING)
+                                .data(Tags.PING_HEARTBEAT_INTERVAL,
+                                        Integer.toString(pingHeartbeat))
+                                .start(Tags.PING_FOLDERS);
+                        }
+
+                        String folderClass = getTargetCollectionClassFromCursor(c);
+                        s.start(Tags.PING_FOLDER)
+                            .data(Tags.PING_ID, c.getString(Mailbox.CONTENT_SERVER_ID_COLUMN))
+                            .data(Tags.PING_CLASS, folderClass)
+                            .end();
+                        readyMailboxes.add(mailboxName);
+                    } else if ((pingStatus == SyncManager.PING_STATUS_RUNNING) ||
+                            (pingStatus == SyncManager.PING_STATUS_WAITING)) {
+                        notReadyMailboxes.add(mailboxName);
+                    } else if (pingStatus == SyncManager.PING_STATUS_UNABLE) {
+                        pushCount--;
+                        userLog(mailboxName, " in error state; ignore");
+                        continue;
+                    }
+                }
+            } finally {
+                c.close();
+            }
+
+            if (Eas.USER_LOG) {
+                if (!notReadyMailboxes.isEmpty()) {
+                    userLog("Ping not ready for: " + notReadyMailboxes);
+                }
+                if (!readyMailboxes.isEmpty()) {
+                    userLog("Ping ready for: " + readyMailboxes);
+                }
+            }
+            
+            // If we've waited 10 seconds or more, just ping with whatever boxes are ready
+            // But use a shorter than normal heartbeat
+            boolean forcePing = !notReadyMailboxes.isEmpty() && (pingWaitCount > 5);
+
+            if ((canPushCount > 0) && ((canPushCount == pushCount) || forcePing)) {
+                // If all pingable boxes are ready for push, send Ping to the server
+                s.end().end().done();
+                pingWaitCount = 0;
+
+                // If we've been stopped, this is a good time to return
+                if (mStop) return;
+
+                long pingTime = SystemClock.elapsedRealtime();
+                try {
+                    // Send the ping, wrapped by appropriate timeout/alarm
+                    if (forcePing) {
+                        userLog("Forcing ping after waiting for all boxes to be ready");
+                    }
+                    HttpResponse res =
+                        sendPing(s.toByteArray(), forcePing ? PING_FORCE_HEARTBEAT : pingHeartbeat);
+
+                    int code = res.getStatusLine().getStatusCode();
+                    userLog("Ping response: ", code);
+
+                    // Return immediately if we've been asked to stop during the ping
+                    if (mStop) {
+                        userLog("Stopping pingLoop");
+                        return;
+                    }
+
+                    if (code == HttpStatus.SC_OK) {
+                        HttpEntity e = res.getEntity();
+                        int len = (int)e.getContentLength();
+                        InputStream is = res.getEntity().getContent();
+                        if (len != 0) {
+                            int pingResult = parsePingResult(is, mContentResolver, pingErrorMap);
+                            // If our ping completed (status = 1), and we weren't forced and we're
+                            // not at the maximum, try increasing timeout by two minutes
+                            if (pingResult == PROTOCOL_PING_STATUS_COMPLETED && !forcePing) {
+                                if (pingHeartbeat > mPingHighWaterMark) {
+                                    mPingHighWaterMark = pingHeartbeat;
+                                    userLog("Setting high water mark at: ", mPingHighWaterMark);
+                                }
+                                if ((pingHeartbeat < PING_MAX_HEARTBEAT) &&
+                                        !mPingHeartbeatDropped) {
+                                    pingHeartbeat += PING_HEARTBEAT_INCREMENT;
+                                    if (pingHeartbeat > PING_MAX_HEARTBEAT) {
+                                        pingHeartbeat = PING_MAX_HEARTBEAT;
+                                    }
+                                    userLog("Increasing ping heartbeat to ", pingHeartbeat, "s");
+                                }
+                            }
+                        } else {
+                            userLog("Ping returned empty result; throwing IOException");
+                            throw new IOException();
+                        }
+                    } else if (isAuthError(code)) {
+                        mExitStatus = EXIT_LOGIN_FAILURE;
+                        userLog("Authorization error during Ping: ", code);
+                        throw new IOException();
+                    }
+                } catch (IOException e) {
+                    String message = e.getMessage();
+                    // If we get the exception that is indicative of a NAT timeout and if we
+                    // 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"))) {
+                        long pingLength = SystemClock.elapsedRealtime() - pingTime;
+                        if ((pingHeartbeat > PING_MIN_HEARTBEAT) &&
+                                (pingHeartbeat > mPingHighWaterMark)) {
+                            pingHeartbeat -= PING_HEARTBEAT_INCREMENT;
+                            mPingHeartbeatDropped = true;
+                            if (pingHeartbeat < PING_MIN_HEARTBEAT) {
+                                pingHeartbeat = PING_MIN_HEARTBEAT;
+                            }
+                            userLog("Decreased ping heartbeat to ", pingHeartbeat, "s");
+                        } else if (mAborted || (pingLength < 2000)) {
+                            userLog("Abort or NAT type return < 2 seconds; throwing IOException");
+                            throw e;
+                        } else {
+                            userLog("NAT type IOException > 2 seconds?");
+                        }
+                    } else {
+                        throw e;
+                    }
+                }
+            } else if (forcePing) {
+                // In this case, there aren't any boxes that are pingable, but there are boxes
+                // waiting (for IOExceptions)
+                userLog("pingLoop waiting 60s for any pingable boxes");
+                sleep(60*SECONDS, true);
+            } else if (pushCount > 0) {
+                // If we want to Ping, but can't just yet, wait a little bit
+                // TODO Change sleep to wait and use notify from SyncManager when a sync ends
+                sleep(2*SECONDS, false);
+                pingWaitCount++;
+                //userLog("pingLoop waited 2s for: ", (pushCount - canPushCount), " box(es)");
+            } else if (uninitCount > 0) {
+                // In this case, we're doing an initial sync of at least one mailbox.  Since this
+                // is typically a one-time case, I'm ok with trying again every 10 seconds until
+                // we're in one of the other possible states.
+                userLog("pingLoop waiting for initial sync of ", uninitCount, " box(es)");
+                sleep(10*SECONDS, true);
+            } else {
+                // We've got nothing to do, so we'll check again in 30 minutes at which time
+                // we'll update the folder list.  Let the device sleep in the meantime...
+                userLog("pingLoop sleeping for 30m");
+                sleep(30*MINUTES, true);
+            }
+        }
+    }
+
+    void sleep(long ms, boolean runAsleep) {
+        if (runAsleep) {
+            SyncManager.runAsleep(mMailboxId, ms+(5*SECONDS));
+        }
+        try {
+            Thread.sleep(ms);
+        } catch (InterruptedException e) {
+            // Doesn't matter whether we stop early; it's the thought that counts
+        } finally {
+            if (runAsleep) {
+                SyncManager.runAwake(mMailboxId);
+            }
+        }
+    }
+
+    private int parsePingResult(InputStream is, ContentResolver cr,
+            HashMap<String, Integer> errorMap)
+        throws IOException, StaleFolderListException {
+        PingParser pp = new PingParser(is, this);
+        if (pp.parse()) {
+            // True indicates some mailboxes need syncing...
+            // syncList has the serverId's of the mailboxes...
+            mBindArguments[0] = Long.toString(mAccount.mId);
+            mPingChangeList = pp.getSyncList();
+            for (String serverId: mPingChangeList) {
+                mBindArguments[1] = serverId;
+                Cursor c = cr.query(Mailbox.CONTENT_URI, Mailbox.CONTENT_PROJECTION,
+                        WHERE_ACCOUNT_KEY_AND_SERVER_ID, mBindArguments, null);
+                try {
+                    if (c.moveToFirst()) {
+
+                        /**
+                         * Check the boxes reporting changes to see if there really were any...
+                         * We do this because bugs in various Exchange servers can put us into a
+                         * looping behavior by continually reporting changes in a mailbox, even when
+                         * there aren't any.
+                         *
+                         * This behavior is seemingly random, and therefore we must code defensively
+                         * by backing off of push behavior when it is detected.
+                         *
+                         * One known cause, on certain Exchange 2003 servers, is acknowledged by
+                         * Microsoft, and the server hotfix for this case can be found at
+                         * http://support.microsoft.com/kb/923282
+                         */
+
+                        // Check the status of the last sync
+                        String status = c.getString(Mailbox.CONTENT_SYNC_STATUS_COLUMN);
+                        int type = SyncManager.getStatusType(status);
+                        // This check should always be true...
+                        if (type == SyncManager.SYNC_PING) {
+                            int changeCount = SyncManager.getStatusChangeCount(status);
+                            if (changeCount > 0) {
+                                errorMap.remove(serverId);
+                            } else if (changeCount == 0) {
+                                // This means that a ping reported changes in error; we keep a count
+                                // of consecutive errors of this kind
+                                String name = c.getString(Mailbox.CONTENT_DISPLAY_NAME_COLUMN);
+                                Integer failures = errorMap.get(serverId);
+                                if (failures == null) {
+                                    userLog("Last ping reported changes in error for: ", name);
+                                    errorMap.put(serverId, 1);
+                                } else if (failures > MAX_PING_FAILURES) {
+                                    // We'll back off of push for this box
+                                    pushFallback(c.getLong(Mailbox.CONTENT_ID_COLUMN));
+                                    continue;
+                                } else {
+                                    userLog("Last ping reported changes in error for: ", name);
+                                    errorMap.put(serverId, failures + 1);
+                                }
+                            }
+                        }
+
+                        // If there were no problems with previous sync, we'll start another one
+                        SyncManager.startManualSync(c.getLong(Mailbox.CONTENT_ID_COLUMN),
+                                SyncManager.SYNC_PING, null);
+                    }
+                } finally {
+                    c.close();
+                }
+            }
+        }
+        return pp.getSyncStatus();
+    }
+
+    private String getFilterType() {
+        String filter = Eas.FILTER_1_WEEK;
+        switch (mAccount.mSyncLookback) {
+            case com.android.email.Account.SYNC_WINDOW_1_DAY: {
+                filter = Eas.FILTER_1_DAY;
+                break;
+            }
+            case com.android.email.Account.SYNC_WINDOW_3_DAYS: {
+                filter = Eas.FILTER_3_DAYS;
+                break;
+            }
+            case com.android.email.Account.SYNC_WINDOW_1_WEEK: {
+                filter = Eas.FILTER_1_WEEK;
+                break;
+            }
+            case com.android.email.Account.SYNC_WINDOW_2_WEEKS: {
+                filter = Eas.FILTER_2_WEEKS;
+                break;
+            }
+            case com.android.email.Account.SYNC_WINDOW_1_MONTH: {
+                filter = Eas.FILTER_1_MONTH;
+                break;
+            }
+            case com.android.email.Account.SYNC_WINDOW_ALL: {
+                filter = Eas.FILTER_ALL;
+                break;
+            }
+        }
+        return filter;
+    }
+
+    /**
+     * Common code to sync E+PIM data
+     *
+     * @param target, an EasMailbox, EasContacts, or EasCalendar object
+     */
+    public void sync(AbstractSyncAdapter target) throws IOException {
+        Mailbox mailbox = target.mMailbox;
+
+        boolean moreAvailable = true;
+        while (!mStop && moreAvailable) {
+            // If we have no connectivity, just exit cleanly.  SyncManager will start us up again
+            // when connectivity has returned
+            if (!hasConnectivity()) {
+                userLog("No connectivity in sync; finishing sync");
+                mExitStatus = EXIT_DONE;
+                return;
+            }
+
+            while (true) {
+                PartRequest req = null;
+                synchronized (mPartRequests) {
+                    if (mPartRequests.isEmpty()) {
+                        break;
+                    } else {
+                        req = mPartRequests.get(0);
+                    }
+                }
+                getAttachment(req);
+                synchronized(mPartRequests) {
+                    mPartRequests.remove(req);
+                }
+            }
+
+            Serializer s = new Serializer();
+            String className = target.getCollectionName();
+            String syncKey = target.getSyncKey();
+            userLog("sync, sending ", className, " syncKey: ", syncKey);
+            s.start(Tags.SYNC_SYNC)
+                .start(Tags.SYNC_COLLECTIONS)
+                .start(Tags.SYNC_COLLECTION)
+                .data(Tags.SYNC_CLASS, className)
+                .data(Tags.SYNC_SYNC_KEY, syncKey)
+                .data(Tags.SYNC_COLLECTION_ID, mailbox.mServerId)
+                .tag(Tags.SYNC_DELETES_AS_MOVES);
+
+            // EAS doesn't like GetChanges if the syncKey is "0"; not documented
+            if (!syncKey.equals("0")) {
+                s.tag(Tags.SYNC_GET_CHANGES);
+            }
+            s.data(Tags.SYNC_WINDOW_SIZE,
+                    className.equals("Email") ? EMAIL_WINDOW_SIZE : PIM_WINDOW_SIZE);
+
+            // 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());
+            }
+            // Set the truncation amount for all classes
+            if (mProtocolVersionDouble >= 12.0) {
+                s.start(Tags.BASE_BODY_PREFERENCE)
+                    // HTML for email; plain text for everything else
+                    .data(Tags.BASE_TYPE, (className.equals("Email") ? Eas.BODY_PREFERENCE_HTML
+                        : Eas.BODY_PREFERENCE_TEXT))
+                    .data(Tags.BASE_TRUNCATION_SIZE, Eas.EAS12_TRUNCATION_SIZE)
+                    .end();
+            } else {
+                s.data(Tags.SYNC_TRUNCATION, Eas.EAS2_5_TRUNCATION_SIZE);
+            }
+            s.end();
+
+            // Send our changes up to the server
+            target.sendLocalChanges(s);
+
+            s.end().end().end().done();
+            HttpResponse resp = sendHttpClientPost("Sync", s.toByteArray());
+            int code = resp.getStatusLine().getStatusCode();
+            if (code == HttpStatus.SC_OK) {
+                InputStream is = resp.getEntity().getContent();
+                if (is != null) {
+                    moreAvailable = target.parse(is);
+                    target.cleanup();
+                } else {
+                    userLog("Empty input stream in sync command response");
+                }
+            } else {
+                userLog("Sync response error: ", code);
+                if (isAuthError(code)) {
+                    mExitStatus = EXIT_LOGIN_FAILURE;
+                } else {
+                    mExitStatus = EXIT_IO_ERROR;
+                }
+                return;
+            }
+        }
+        mExitStatus = EXIT_DONE;
+    }
+
+    protected void setupService() {
+        // Make sure account and mailbox are always the latest from the database
+        mAccount = Account.restoreAccountWithId(mContext, mAccount.mId);
+        mMailbox = Mailbox.restoreMailboxWithId(mContext, mMailbox.mId);
+
+        mThread = Thread.currentThread();
+        android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_BACKGROUND);
+        TAG = mThread.getName();
+
+        HostAuth ha = HostAuth.restoreHostAuthWithId(mContext, mAccount.mHostAuthKeyRecv);
+        mHostAddress = ha.mAddress;
+        mUserName = ha.mLogin;
+        mPassword = ha.mPassword;
+    }
+
+    /* (non-Javadoc)
+     * @see java.lang.Runnable#run()
+     */
+    public void run() {
+        setupService();
+
+        try {
+            SyncManager.callback().syncMailboxStatus(mMailboxId, EmailServiceStatus.IN_PROGRESS, 0);
+        } catch (RemoteException e1) {
+            // Don't care if this fails
+        }
+
+        // Whether or not we're the account mailbox
+        try {
+            mDeviceId = SyncManager.getDeviceId();
+            if ((mMailbox == null) || (mAccount == null)) {
+                return;
+            } else if (mMailbox.mType == Mailbox.TYPE_EAS_ACCOUNT_MAILBOX) {
+                runAccountMailbox();
+            } else {
+                AbstractSyncAdapter target;
+                mProtocolVersion = mAccount.mProtocolVersion;
+                mProtocolVersionDouble = Double.parseDouble(mProtocolVersion);
+                if (mMailbox.mType == Mailbox.TYPE_CONTACTS) {
+                    target = new ContactsSyncAdapter(mMailbox, this);
+                } else {
+                    target = new EmailSyncAdapter(mMailbox, this);
+                }
+                // We loop here because someone might have put a request in while we were syncing
+                // and we've missed that opportunity...
+                do {
+                    if (mRequestTime != 0) {
+                        userLog("Looping for user request...");
+                        mRequestTime = 0;
+                    }
+                    sync(target);
+                } while (mRequestTime != 0);
+            }
+        } catch (IOException e) {
+            String message = e.getMessage();
+            userLog("Caught IOException: ", ((message == null) ? "No message" : message));
+            mExitStatus = EXIT_IO_ERROR;
+        } catch (Exception e) {
+            userLog("Uncaught exception in EasSyncService", e);
+        } finally {
+            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;
+                        break;
+                    case EXIT_LOGIN_FAILURE:
+                        status = EmailServiceStatus.LOGIN_FAILED;
+                        break;
+                    default:
+                        status = EmailServiceStatus.REMOTE_EXCEPTION;
+                        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.");
+            }
+
+            // Make sure SyncManager knows about this
+            SyncManager.kick("sync finished");
+       }
+    }
+}
diff --git a/src/com/android/exchange/EmailContent.aidl b/src/com/android/exchange/EmailContent.aidl
new file mode 100644
index 0000000..c6b4a7d
--- /dev/null
+++ b/src/com/android/exchange/EmailContent.aidl
@@ -0,0 +1,21 @@
+/*
+ * 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;
+
+parcelable EmailContent.Attachment;
+
diff --git a/src/com/android/exchange/EmailServiceStatus.java b/src/com/android/exchange/EmailServiceStatus.java
new file mode 100644
index 0000000..697548c
--- /dev/null
+++ b/src/com/android/exchange/EmailServiceStatus.java
@@ -0,0 +1,37 @@
+/*
+ *  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
new file mode 100644
index 0000000..4a44860
--- /dev/null
+++ b/src/com/android/exchange/EmailSyncAlarmReceiver.java
@@ -0,0 +1,101 @@
+/*
+ *  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.email.provider.EmailContent.Message;
+import com.android.email.provider.EmailContent.MessageColumns;
+
+import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.database.Cursor;
+import android.util.Log;
+
+import java.util.ArrayList;
+
+/**
+ * EmailSyncAlarmReceiver (USAR) is used by the SyncManager to start up-syncs of user-modified data
+ * back to the Exchange server.
+ *
+ * Here's how this works for Email, for example:
+ *
+ * 1) User modifies or deletes an email from the UI.
+ * 2) SyncManager, which has a ContentObserver watching the Message class, is alerted to a change
+ * 3) SyncManager sets an alarm (to be received by USAR) for a few seconds in the
+ * future (currently 15), the delay preventing excess syncing (think of it as a debounce mechanism).
+ * 4) ESAR Receiver's onReceive method is called
+ * 5) ESAR goes through all change and deletion records and compiles a list of mailboxes which have
+ * changes to be uploaded.
+ * 6) ESAR calls SyncManager to start syncs of those mailboxes
+ *
+ */
+public class EmailSyncAlarmReceiver extends BroadcastReceiver {
+    final String[] MAILBOX_DATA_PROJECTION = {MessageColumns.MAILBOX_KEY};
+    private static String TAG = "EmailSyncAlarm";
+
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        Log.v(TAG, "onReceive");
+        ArrayList<Long> mailboxesToNotify = new ArrayList<Long>();
+        ContentResolver cr = context.getContentResolver();
+        int messageCount = 0;
+        
+        // Get a selector for EAS accounts (we don't want to sync on changes to POP/IMAP messages)
+        String selector = SyncManager.getEasAccountSelector();
+        
+        // Find all of the deletions
+        Cursor c = cr.query(Message.DELETED_CONTENT_URI, MAILBOX_DATA_PROJECTION, selector,
+               null, null);
+        try {
+            // Keep track of which mailboxes to notify; we'll only notify each one once
+            while (c.moveToNext()) {
+                messageCount++;
+                long mailboxId = c.getLong(0);
+                if (!mailboxesToNotify.contains(mailboxId)) {
+                    mailboxesToNotify.add(mailboxId);
+                }
+            }
+        } finally {
+            c.close();
+        }
+
+        // Now, find changed messages
+        c = cr.query(Message.UPDATED_CONTENT_URI, MAILBOX_DATA_PROJECTION, selector,
+                null, null);
+        try {
+            // Keep track of which mailboxes to notify; we'll only notify each one once
+            while (c.moveToNext()) {
+                messageCount++;
+                long mailboxId = c.getLong(0);
+                if (!mailboxesToNotify.contains(mailboxId)) {
+                    mailboxesToNotify.add(mailboxId);
+                }
+            }
+        } finally {
+            c.close();
+        }
+
+        // Request service from the mailbox
+        for (Long mailboxId: mailboxesToNotify) {
+            SyncManager.serviceRequest(mailboxId, SyncManager.SYNC_UPSYNC);
+        }
+        Log.v(TAG, "Changed/Deleted messages: " + messageCount + ", mailboxes: " +
+                mailboxesToNotify.size());
+    }
+}
diff --git a/src/com/android/exchange/IEmailService.aidl b/src/com/android/exchange/IEmailService.aidl
new file mode 100644
index 0000000..ec0fd5e
--- /dev/null
+++ b/src/com/android/exchange/IEmailService.aidl
@@ -0,0 +1,43 @@
+/*
+ * 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
new file mode 100644
index 0000000..a789c4a
--- /dev/null
+++ b/src/com/android/exchange/IEmailServiceCallback.aidl
@@ -0,0 +1,68 @@
+/*
+ * 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/MailboxAlarmReceiver.java b/src/com/android/exchange/MailboxAlarmReceiver.java
new file mode 100644
index 0000000..cc4e2f4
--- /dev/null
+++ b/src/com/android/exchange/MailboxAlarmReceiver.java
@@ -0,0 +1,39 @@
+/*
+ *  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 android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+
+/**
+ * MailboxAlarmReceiver is used to "wake up" the SyncManager at the appropriate time(s).  It may
+ * also be used for individual sync adapters, but this isn't implemented at the present time.
+ *
+ */
+public class MailboxAlarmReceiver extends BroadcastReceiver {
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        long mid = intent.getLongExtra("mailbox", -1);
+        if (SyncManager.INSTANCE != null) {
+            SyncManager.INSTANCE.log("Alarm received for: " + SyncManager.alarmOwner(mid));
+        }
+        SyncManager.ping(context, mid);
+    }
+}
+
diff --git a/src/com/android/exchange/MockParserStream.java b/src/com/android/exchange/MockParserStream.java
new file mode 100644
index 0000000..a518667
--- /dev/null
+++ b/src/com/android/exchange/MockParserStream.java
@@ -0,0 +1,55 @@
+/*
+ * 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 java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * MockParserStream is an InputStream that feeds pre-generated data into various EasParser
+ * subclasses.
+ * 
+ * After parsing is done, the result can be obtained with getResult
+ *
+ */
+public class MockParserStream extends InputStream {
+    int[] array;
+    int pos = 0;
+    Object value;
+
+    MockParserStream (int[] _array) {
+        array = _array;
+    }
+
+    @Override
+    public int read() throws IOException {
+        try {
+            return array[pos++];
+        } catch (IndexOutOfBoundsException e) {
+            throw new IOException("End of stream");
+        }
+    }
+
+    public void setResult(Object _value) {
+        value = _value;
+    }
+
+    public Object getResult() {
+        return value;
+    }
+}
diff --git a/src/com/android/exchange/PartRequest.java b/src/com/android/exchange/PartRequest.java
new file mode 100644
index 0000000..72b79f4
--- /dev/null
+++ b/src/com/android/exchange/PartRequest.java
@@ -0,0 +1,47 @@
+/*
+ * 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.email.provider.EmailContent.Attachment;
+
+/**
+ * PartRequest is the EAS wrapper for attachment loading requests.  In addition to information about
+ * 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 PartRequest(Attachment _att) {
+        timeStamp = System.currentTimeMillis();
+        emailId = _att.mMessageKey;
+        att = _att;
+        loc = att.mLocation;
+    }
+
+    public PartRequest(Attachment _att, String _destination, String _contentUriString) {
+        this(_att);
+        destination = _destination;
+        contentUriString = _contentUriString;
+    }
+}
diff --git a/src/com/android/exchange/StaleFolderListException.java b/src/com/android/exchange/StaleFolderListException.java
new file mode 100644
index 0000000..70ac032
--- /dev/null
+++ b/src/com/android/exchange/StaleFolderListException.java
@@ -0,0 +1,22 @@
+/*
+ *  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;
+
+public class StaleFolderListException extends EasException {
+    private static final long serialVersionUID = 1L;
+}
diff --git a/src/com/android/exchange/SyncManager.java b/src/com/android/exchange/SyncManager.java
new file mode 100644
index 0000000..ece930c
--- /dev/null
+++ b/src/com/android/exchange/SyncManager.java
@@ -0,0 +1,1864 @@
+/*
+ * 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.email.mail.MessagingException;
+import com.android.email.mail.store.TrustManagerFactory;
+import com.android.email.provider.EmailContent;
+import com.android.email.provider.EmailContent.Account;
+import com.android.email.provider.EmailContent.Attachment;
+import com.android.email.provider.EmailContent.HostAuth;
+import com.android.email.provider.EmailContent.HostAuthColumns;
+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.provider.EmailContent.SyncColumns;
+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;
+import org.apache.http.conn.routing.HttpRoute;
+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;
+
+import android.accounts.AccountManager;
+import android.accounts.OnAccountsUpdateListener;
+import android.app.AlarmManager;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.SyncStatusObserver;
+import android.database.ContentObserver;
+import android.database.Cursor;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.net.Uri;
+import android.net.NetworkInfo.State;
+import android.os.Bundle;
+import android.os.Debug;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.PowerManager;
+import android.os.RemoteCallbackList;
+import android.os.RemoteException;
+import android.os.PowerManager.WakeLock;
+import android.provider.ContactsContract;
+import android.util.Log;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.security.KeyManagementException;
+import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.X509TrustManager;
+
+/**
+ * The SyncManager handles all aspects of starting, maintaining, and stopping the various sync
+ * adapters used by Exchange.  However, it is capable of handing any kind of email sync, and it
+ * would be appropriate to use for IMAP push, when that functionality is added to the Email
+ * application.
+ *
+ * The Email application communicates with EAS sync adapters via SyncManager's binder interface,
+ * which exposes UI-related functionality to the application (see the definitions below)
+ *
+ * SyncManager uses ContentObservers to detect changes to accounts, mailboxes, and messages in
+ * order to maintain proper 2-way syncing of data.  (More documentation to follow)
+ *
+ */
+public class SyncManager extends Service implements Runnable {
+
+    private static final String TAG = "EAS SyncManager";
+
+    // The SyncManager's mailbox "id"
+    private static final int SYNC_MANAGER_ID = -1;
+
+    private static final int SECONDS = 1000;
+    private static final int MINUTES = 60*SECONDS;
+    private static final int ONE_DAY_MINUTES = 1440;
+
+    private static final int SYNC_MANAGER_HEARTBEAT_TIME = 15*MINUTES;
+    private static final int CONNECTIVITY_WAIT_TIME = 10*MINUTES;
+
+    // Sync hold constants for services with transient errors
+    private static final int HOLD_DELAY_MAXIMUM = 4*MINUTES;
+
+    // Reason codes when SyncManager.kick is called (mainly for debugging)
+    // UI has changed data, requiring an upsync of changes
+    public static final int SYNC_UPSYNC = 0;
+    // A scheduled sync (when not using push)
+    public static final int SYNC_SCHEDULED = 1;
+    // Mailbox was marked push
+    public static final int SYNC_PUSH = 2;
+    // A ping (EAS push signal) was received
+    public static final int SYNC_PING = 3;
+    // startSync was requested of SyncManager
+    public static final int SYNC_SERVICE_START_SYNC = 4;
+    // A part request (attachment load, for now) was sent to SyncManager
+    public static final int SYNC_SERVICE_PART_REQUEST = 5;
+    // Misc.
+    public static final int SYNC_KICK = 6;
+
+    private static final String WHERE_PUSH_OR_PING_NOT_ACCOUNT_MAILBOX =
+        MailboxColumns.ACCOUNT_KEY + "=? and " + MailboxColumns.TYPE + "!=" +
+        Mailbox.TYPE_EAS_ACCOUNT_MAILBOX + " and " + MailboxColumns.SYNC_INTERVAL +
+        " IN (" + Mailbox.CHECK_INTERVAL_PING + ',' + Mailbox.CHECK_INTERVAL_PUSH + ')';
+    protected static final String WHERE_IN_ACCOUNT_AND_PUSHABLE =
+        MailboxColumns.ACCOUNT_KEY + "=? and type in (" + Mailbox.TYPE_INBOX + ','
+        + Mailbox.TYPE_EAS_ACCOUNT_MAILBOX + ',' + Mailbox.TYPE_CONTACTS + ')';
+    private static final String WHERE_MAILBOX_KEY = EmailContent.RECORD_ID + "=?";
+    private static final String WHERE_PROTOCOL_EAS = HostAuthColumns.PROTOCOL + "=\"" +
+        AbstractSyncService.EAS_PROTOCOL + "\"";
+    private static final String WHERE_NOT_INTERVAL_NEVER_AND_ACCOUNT_KEY_IN =
+        "(" + MailboxColumns.TYPE + '=' + Mailbox.TYPE_OUTBOX
+        + " or " + MailboxColumns.SYNC_INTERVAL + "!=" + Mailbox.CHECK_INTERVAL_NEVER + ')'
+        + " and " + MailboxColumns.ACCOUNT_KEY + " in (";
+    private static final String ACCOUNT_KEY_IN = MailboxColumns.ACCOUNT_KEY + " in (";
+
+    // Offsets into the syncStatus data for EAS that indicate type, exit status, and change count
+    // The format is S<type_char>:<exit_char>:<change_count>
+    public static final int STATUS_TYPE_CHAR = 1;
+    public static final int STATUS_EXIT_CHAR = 3;
+    public static final int STATUS_CHANGE_COUNT_OFFSET = 5;
+
+    // Ready for ping
+    public static final int PING_STATUS_OK = 0;
+    // Service already running (can't ping)
+    public static final int PING_STATUS_RUNNING = 1;
+    // Service waiting after I/O error (can't ping)
+    public static final int PING_STATUS_WAITING = 2;
+    // Service had a fatal error; can't run
+    public static final int PING_STATUS_UNABLE = 3;
+
+    // We synchronize on this for all actions affecting the service and error maps
+    private static Object sSyncToken = new Object();
+    // All threads can use this lock to wait for connectivity
+    public static Object sConnectivityLock = new Object();
+    public static boolean sConnectivityHold = false;
+
+    // Keeps track of running services (by mailbox id)
+    private HashMap<Long, AbstractSyncService> mServiceMap =
+        new HashMap<Long, AbstractSyncService>();
+    // Keeps track of services whose last sync ended with an error (by mailbox id)
+    private HashMap<Long, SyncError> mSyncErrorMap = new HashMap<Long, SyncError>();
+    // Keeps track of which services require a wake lock (by mailbox id)
+    private HashMap<Long, Boolean> mWakeLocks = new HashMap<Long, Boolean>();
+    // Keeps track of PendingIntents for mailbox alarms (by mailbox id)
+    private HashMap<Long, PendingIntent> mPendingIntents = new HashMap<Long, PendingIntent>();
+    // The actual WakeLock obtained by SyncManager
+    private WakeLock mWakeLock = null;
+    private static final AccountList EMPTY_ACCOUNT_LIST = new AccountList();
+
+    // Observers that we use to look for changed mail-related data
+    private Handler mHandler = new Handler();
+    private AccountObserver mAccountObserver;
+    private MailboxObserver mMailboxObserver;
+    private SyncedMessageObserver mSyncedMessageObserver;
+    private MessageObserver mMessageObserver;
+    private EasSyncStatusObserver mSyncStatusObserver;
+    private EasAccountsUpdatedListener mAccountsUpdatedListener;
+
+    private ContentResolver mResolver;
+
+    // The singleton SyncManager object, with its thread and stop flag
+    protected static SyncManager INSTANCE;
+    private static Thread sServiceThread = null;
+    // Cached unique device id
+    private static String sDeviceId = null;
+    // ConnectionManager that all EAS threads can use
+    private static ClientConnectionManager sClientConnectionManager = null;
+
+    private boolean mStop = false;
+
+    // The reason for SyncManager's next wakeup call
+    private String mNextWaitReason;
+    // Whether we have an unsatisfied "kick" pending
+    private boolean mKicked = false;
+
+    // Receiver of connectivity broadcasts
+    private ConnectivityReceiver mConnectivityReceiver = null;
+
+    // The callback sent in from the UI using setCallback
+    private IEmailServiceCallback mCallback;
+    private RemoteCallbackList<IEmailServiceCallback> mCallbackList =
+        new RemoteCallbackList<IEmailServiceCallback>();
+
+    /**
+     * Proxy that can be used by various sync adapters to tie into SyncManager's callback system.
+     * Used this way:  SyncManager.callback().callbackMethod(args...);
+     * The proxy wraps checking for existence of a SyncManager instance and an active callback.
+     * Failures of these callbacks can be safely ignored.
+     */
+    static private final IEmailServiceCallback.Stub sCallbackProxy =
+        new IEmailServiceCallback.Stub() {
+
+        public void loadAttachmentStatus(long messageId, long attachmentId, int statusCode,
+                int progress) throws RemoteException {
+            IEmailServiceCallback cb = INSTANCE == null ? null: INSTANCE.mCallback;
+            if (cb != null) {
+                cb.loadAttachmentStatus(messageId, attachmentId, statusCode, progress);
+            }
+        }
+
+        public void sendMessageStatus(long messageId, String subject, int statusCode, int progress)
+                throws RemoteException {
+            IEmailServiceCallback cb = INSTANCE == null ? null: INSTANCE.mCallback;
+            if (cb != null) {
+                cb.sendMessageStatus(messageId, subject, statusCode, progress);
+            }
+        }
+
+        public void syncMailboxListStatus(long accountId, int statusCode, int progress)
+                throws RemoteException {
+            IEmailServiceCallback cb = INSTANCE == null ? null: INSTANCE.mCallback;
+            if (cb != null) {
+                cb.syncMailboxListStatus(accountId, statusCode, progress);
+            }
+        }
+
+        public void syncMailboxStatus(long mailboxId, int statusCode, int progress)
+                throws RemoteException {
+            IEmailServiceCallback cb = INSTANCE == null ? null: INSTANCE.mCallback;
+            if (cb != null) {
+                cb.syncMailboxStatus(mailboxId, statusCode, progress);
+            } else if (INSTANCE != null) {
+                INSTANCE.log("orphan syncMailboxStatus, id=" + mailboxId + " status=" + statusCode);
+            }
+        }
+    };
+
+    /**
+     * Create our EmailService implementation here.
+     */
+    private final IEmailService.Stub mBinder = new IEmailService.Stub() {
+
+        public int validate(String protocol, String host, String userName, String password,
+                int port, boolean ssl, boolean trustCertificates) throws RemoteException {
+            try {
+                AbstractSyncService.validate(EasSyncService.class, host, userName, password, port,
+                        ssl, trustCertificates, SyncManager.this);
+                return MessagingException.NO_ERROR;
+            } catch (MessagingException e) {
+                return e.getExceptionType();
+            }
+        }
+
+        public void startSync(long mailboxId) throws RemoteException {
+            checkSyncManagerServiceRunning();
+            Mailbox m = Mailbox.restoreMailboxWithId(INSTANCE, mailboxId);
+            if (m.mType == Mailbox.TYPE_OUTBOX) {
+                // We're using SERVER_ID to indicate an error condition (it has no other use for
+                // sent mail)  Upon request to sync the Outbox, we clear this so that all messages
+                // are candidates for sending.
+                ContentValues cv = new ContentValues();
+                cv.put(SyncColumns.SERVER_ID, 0);
+                INSTANCE.getContentResolver().update(Message.CONTENT_URI,
+                    cv, WHERE_MAILBOX_KEY, new String[] {Long.toString(mailboxId)});
+
+                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
+                return;
+            }
+            startManualSync(mailboxId, SyncManager.SYNC_SERVICE_START_SYNC, null);
+        }
+
+        public void stopSync(long mailboxId) throws RemoteException {
+            stopManualSync(mailboxId);
+        }
+
+        public void loadAttachment(long attachmentId, String destinationFile,
+                String contentUriString) throws RemoteException {
+            Attachment att = Attachment.restoreAttachmentWithId(SyncManager.this, attachmentId);
+            partRequest(new PartRequest(att, destinationFile, contentUriString));
+        }
+
+        public void updateFolderList(long accountId) throws RemoteException {
+            reloadFolderList(SyncManager.this, accountId, false);
+        }
+
+        public void hostChanged(long accountId) throws RemoteException {
+            if (INSTANCE != null) {
+                synchronized (INSTANCE.mSyncErrorMap) {
+                    // Go through the various error mailboxes
+                    for (long mailboxId: INSTANCE.mSyncErrorMap.keySet()) {
+                        SyncError error = INSTANCE.mSyncErrorMap.get(mailboxId);
+                        // 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) {
+                            error.fatal = false;
+                            error.holdEndTime = 0;
+                        }
+                    }
+                }
+                // Stop any running syncs
+                INSTANCE.stopAccountSyncs(accountId, true);
+                // Kick SyncManager
+                kick("host changed");
+            }
+        }
+
+        public void setLogging(int on) throws RemoteException {
+            Eas.setUserDebug(on);
+        }
+
+        public void loadMore(long messageId) throws RemoteException {
+            // TODO Auto-generated method stub
+        }
+
+        // The following three methods are not implemented in this version
+        public boolean createFolder(long accountId, String name) throws RemoteException {
+            return false;
+        }
+
+        public boolean deleteFolder(long accountId, String name) throws RemoteException {
+            return false;
+        }
+
+        public boolean renameFolder(long accountId, String oldName, String newName)
+                throws RemoteException {
+            return false;
+        }
+
+        public void setCallback(IEmailServiceCallback cb) throws RemoteException {
+            if (mCallback != null) {
+                mCallbackList.unregister(mCallback);
+            }
+            mCallback = cb;
+            mCallbackList.register(cb);
+        }
+    };
+
+    static class AccountList extends ArrayList<Account> {
+        private static final long serialVersionUID = 1L;
+
+        public boolean contains(long id) {
+            for (Account account: this) {
+                if (account.mId == id) {
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        public Account getById(long id) {
+            for (Account account: this) {
+                if (account.mId == id) {
+                    return account;
+                }
+            }
+            return null;
+        }
+    }
+
+    class AccountObserver extends ContentObserver {
+        // mAccounts keeps track of Accounts that we care about (EAS for now)
+        AccountList mAccounts = new AccountList();
+        String mSyncableEasMailboxSelector = null;
+        String mEasAccountSelector = null;
+
+        public AccountObserver(Handler handler) {
+            super(handler);
+            // At startup, we want to see what EAS accounts exist and cache them
+            Cursor c = getContentResolver().query(Account.CONTENT_URI, Account.CONTENT_PROJECTION,
+                    null, null, null);
+            try {
+                collectEasAccounts(c, mAccounts);
+            } finally {
+                c.close();
+            }
+
+            // Create the account mailbox for any account that doesn't have one
+            Context context = getContext();
+            for (Account account: mAccounts) {
+                int cnt = Mailbox.count(context, Mailbox.CONTENT_URI, "accountKey=" + account.mId,
+                        null);
+                if (cnt == 0) {
+                    addAccountMailbox(account.mId);
+                }
+            }
+        }
+
+        /**
+         * Returns a String suitable for appending to a where clause that selects for all syncable
+         * mailboxes in all eas accounts
+         * @return a complex selection string that is not to be cached
+         */
+        public String getSyncableEasMailboxWhere() {
+            if (mSyncableEasMailboxSelector == null) {
+                StringBuilder sb = new StringBuilder(WHERE_NOT_INTERVAL_NEVER_AND_ACCOUNT_KEY_IN);
+                boolean first = true;
+                for (Account account: mAccounts) {
+                    if (!first) {
+                        sb.append(',');
+                    } else {
+                        first = false;
+                    }
+                    sb.append(account.mId);
+                }
+                sb.append(')');
+                mSyncableEasMailboxSelector = sb.toString();
+            }
+            return mSyncableEasMailboxSelector;
+        }
+
+        /**
+         * Returns a String suitable for appending to a where clause that selects for all eas
+         * accounts.
+         * @return a String in the form "accountKey in (a, b, c...)" that is not to be cached
+         */
+        public String getAccountKeyWhere() {
+            if (mEasAccountSelector == null) {
+                StringBuilder sb = new StringBuilder(ACCOUNT_KEY_IN);
+                boolean first = true;
+                for (Account account: mAccounts) {
+                    if (!first) {
+                        sb.append(',');
+                    } else {
+                        first = false;
+                    }
+                    sb.append(account.mId);
+                }
+                sb.append(')');
+                mEasAccountSelector = sb.toString();
+            }
+            return mEasAccountSelector;
+        }
+
+        private boolean syncParametersChanged(Account account) {
+            long accountId = account.mId;
+            // Reload account from database to get its current state
+            account = Account.restoreAccountWithId(getContext(), accountId);
+            for (Account oldAccount: mAccounts) {
+                if (oldAccount.mId == accountId) {
+                    return oldAccount.mSyncInterval != account.mSyncInterval ||
+                            oldAccount.mSyncLookback != account.mSyncLookback;
+                }
+            }
+            // Really, we can't get here, but we don't want the compiler to complain
+            return false;
+        }
+
+        @Override
+        public void onChange(boolean selfChange) {
+            maybeStartSyncManagerThread();
+
+            // A change to the list requires us to scan for deletions (to stop running syncs)
+            // At startup, we want to see what accounts exist and cache them
+            AccountList currentAccounts = new AccountList();
+            Cursor c = getContentResolver().query(Account.CONTENT_URI, Account.CONTENT_PROJECTION,
+                    null, null, null);
+            try {
+                collectEasAccounts(c, currentAccounts);
+                for (Account account : mAccounts) {
+                    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);
+                        AccountManager.get(SyncManager.this).removeAccount(acct, null, null);
+                        mSyncableEasMailboxSelector = null;
+                        mEasAccountSelector = null;
+                    } else {
+                        // See whether any of our accounts has changed sync interval or window
+                        if (syncParametersChanged(account)) {
+                            // Set pushable boxes' sync interval to the sync interval of the Account
+                            Account updatedAccount =
+                                Account.restoreAccountWithId(getContext(), account.mId);
+                            ContentValues cv = new ContentValues();
+                            cv.put(MailboxColumns.SYNC_INTERVAL, updatedAccount.mSyncInterval);
+                            getContentResolver().update(Mailbox.CONTENT_URI, cv,
+                                    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...");
+                            stopAccountSyncs(account.mId, true);
+                        }
+                    }
+                }
+
+                // Look for new accounts
+                for (Account account: currentAccounts) {
+                    if (!mAccounts.contains(account.mId)) {
+                        // This is an addition; create our magic hidden mailbox...
+                        addAccountMailbox(account.mId);
+                        // Don't forget to cache the HostAuth
+                        HostAuth ha =
+                            HostAuth.restoreHostAuthWithId(getContext(), account.mHostAuthKeyRecv);
+                        account.mHostAuthRecv = ha;
+                        mAccounts.add(account);
+                        mSyncableEasMailboxSelector = null;
+                        mEasAccountSelector = null;
+                    }
+                }
+
+                // Finally, make sure mAccounts is up to date
+                mAccounts = currentAccounts;
+            } finally {
+                c.close();
+            }
+
+            // See if there's anything to do...
+            kick("account changed");
+        }
+
+        private void collectEasAccounts(Cursor c, ArrayList<Account> accounts) {
+            Context context = getContext();
+            if (context == null) return;
+            while (c.moveToNext()) {
+                long hostAuthId = c.getLong(Account.CONTENT_HOST_AUTH_KEY_RECV_COLUMN);
+                if (hostAuthId > 0) {
+                    HostAuth ha = HostAuth.restoreHostAuthWithId(context, hostAuthId);
+                    if (ha != null && ha.mProtocol.equals("eas")) {
+                        Account account = new Account().restore(c);
+                        // Cache the HostAuth
+                        account.mHostAuthRecv = ha;
+                        accounts.add(account);
+                    }
+                }
+            }
+        }
+
+        private void addAccountMailbox(long acctId) {
+            Account acct = Account.restoreAccountWithId(getContext(), acctId);
+            Mailbox main = new Mailbox();
+            main.mDisplayName = Eas.ACCOUNT_MAILBOX;
+            main.mServerId = Eas.ACCOUNT_MAILBOX + System.nanoTime();
+            main.mAccountKey = acct.mId;
+            main.mType = Mailbox.TYPE_EAS_ACCOUNT_MAILBOX;
+            main.mSyncInterval = Mailbox.CHECK_INTERVAL_PUSH;
+            main.mFlagVisible = false;
+            main.save(getContext());
+            INSTANCE.log("Initializing account: " + acct.mDisplayName);
+        }
+
+    }
+
+    class MailboxObserver extends ContentObserver {
+        public MailboxObserver(Handler handler) {
+            super(handler);
+        }
+
+        @Override
+        public void onChange(boolean selfChange) {
+            // See if there's anything to do...
+            if (!selfChange) {
+                kick("mailbox changed");
+            }
+        }
+    }
+
+    class SyncedMessageObserver extends ContentObserver {
+        Intent syncAlarmIntent = new Intent(INSTANCE, EmailSyncAlarmReceiver.class);
+        PendingIntent syncAlarmPendingIntent =
+            PendingIntent.getBroadcast(INSTANCE, 0, syncAlarmIntent, 0);
+        AlarmManager alarmManager = (AlarmManager)INSTANCE.getSystemService(Context.ALARM_SERVICE);
+
+        public SyncedMessageObserver(Handler handler) {
+            super(handler);
+        }
+
+        @Override
+        public void onChange(boolean selfChange) {
+            INSTANCE.log("SyncedMessage changed: (re)setting alarm for 10s");
+            alarmManager.set(AlarmManager.RTC_WAKEUP,
+                    System.currentTimeMillis() + 10*SECONDS, syncAlarmPendingIntent);
+        }
+    }
+
+    class MessageObserver extends ContentObserver {
+
+        public MessageObserver(Handler handler) {
+            super(handler);
+        }
+
+        @Override
+        public void onChange(boolean selfChange) {
+            // A rather blunt instrument here.  But we don't have information about the URI that
+            // triggered this, though it must have been an insert
+            if (!selfChange) {
+                kick(null);
+            }
+        }
+    }
+
+    static public IEmailServiceCallback callback() {
+        return sCallbackProxy;
+    }
+
+    static public AccountList getAccountList() {
+        if (INSTANCE != null) {
+            return INSTANCE.mAccountObserver.mAccounts;
+        } else {
+            return EMPTY_ACCOUNT_LIST;
+        }
+    }
+
+    static public String getEasAccountSelector() {
+        if (INSTANCE != null) {
+            return INSTANCE.mAccountObserver.getAccountKeyWhere();
+        } else {
+            return null;
+        }
+    }
+
+    private Account getAccountById(long accountId) {
+        return mAccountObserver.mAccounts.getById(accountId);
+    }
+
+    public class SyncStatus {
+        static public final int NOT_RUNNING = 0;
+        static public final int DIED = 1;
+        static public final int SYNC = 2;
+        static public final int IDLE = 3;
+    }
+
+    class SyncError {
+        int reason;
+        boolean fatal = false;
+        long holdDelay = 15*SECONDS;
+        long holdEndTime = System.currentTimeMillis() + holdDelay;
+
+        SyncError(int _reason, boolean _fatal) {
+            reason = _reason;
+            fatal = _fatal;
+        }
+
+        /**
+         * We double the holdDelay from 15 seconds through 4 mins
+         */
+        void escalate() {
+            if (holdDelay < HOLD_DELAY_MAXIMUM) {
+                holdDelay *= 2;
+            }
+            holdEndTime = System.currentTimeMillis() + holdDelay;
+        }
+    }
+
+    public class EasSyncStatusObserver implements SyncStatusObserver {
+        public void onStatusChanged(int which) {
+            // We ignore the argument (we can only get called in one case - when settings change)
+            // TODO Go through each account and see if sync is enabled and/or automatic for
+            // the Contacts authority, and set syncInterval accordingly.  Then kick ourselves.
+            if (INSTANCE != null) {
+                checkPIMSyncSettings();
+            }
+        }
+    }
+
+    public class EasAccountsUpdatedListener implements OnAccountsUpdateListener {
+       public void onAccountsUpdated(android.accounts.Account[] accounts) {
+           checkWithAccountManager();
+       }
+    }
+
+    static public void smLog(String str) {
+        if (INSTANCE != null) {
+            INSTANCE.log(str);
+        }
+    }
+
+    protected void log(String str) {
+        if (Eas.USER_LOG) {
+            Log.d(TAG, str);
+            if (Eas.FILE_LOG) {
+                FileLogger.log(TAG, str);
+            }
+        }
+    }
+
+    protected void alwaysLog(String str) {
+        if (!Eas.USER_LOG) {
+            Log.d(TAG, str);
+        } else {
+            log(str);
+        }
+    }
+
+    /**
+     * EAS requires a unique device id, so that sync is possible from a variety of different
+     * devices (e.g. the syncKey is specific to a device)  If we're on an emulator or some other
+     * device that doesn't provide one, we can create it as droid<n> where <n> is system time.
+     * This would work on a real device as well, but it would be better to use the "real" id if
+     * it's available
+     */
+    static public String getDeviceId() throws IOException {
+        return getDeviceId(null);
+    }
+    
+    static public synchronized String getDeviceId(Context context) throws IOException {
+        if (sDeviceId != null) {
+            return sDeviceId;
+        } else if (INSTANCE == null && context == null) {
+            throw new IOException("No context for getDeviceId");
+        } else if (context == null) {
+            context = INSTANCE;
+        }
+
+        // Otherwise, we'll read the id file or create one if it's not found
+        try {
+            File f = context.getFileStreamPath("deviceName");
+            BufferedReader rdr = null;
+            String id;
+            if (f.exists() && f.canRead()) {
+                rdr = new BufferedReader(new FileReader(f), 128);
+                id = rdr.readLine();
+                rdr.close();
+                return id;
+            } else if (f.createNewFile()) {
+                BufferedWriter w = new BufferedWriter(new FileWriter(f), 128);
+                id = "droid" + System.currentTimeMillis();
+                w.write(id);
+                w.close();
+                sDeviceId = id;
+                return id;
+            }
+        } catch (IOException e) {
+        }
+        throw new IOException("Can't get device name");
+    }
+
+    @Override
+    public IBinder onBind(Intent arg0) {
+        return mBinder;
+    }
+
+    /**
+     * 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 
+     * 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)
+     *
+     * If the running background service detects that there are no EAS accounts (on boot, if none
+     * were created, or afterward if the last remaining EAS account is deleted), it will call
+     * stopSelf() to terminate operation.
+     *
+     * The goal is to ensure that the background service is running at all times when there is at
+     * least one EAS account in existence
+     *
+     * Because there are edge cases in which our process can crash (typically, this has been seen
+     * in UI crashes, ANR's, etc.), it's possible for the UI to start up again without the
+     * background service having been started.  We explicitly try to start the service in Welcome
+     * (to handle the case of the app having been reloaded).  We also start the service on any
+     * startSync call (if it isn't already running)
+     */
+    @Override
+    public void onCreate() {
+        alwaysLog("!!! EAS SyncManager, onCreate");
+        if (INSTANCE == null) {
+            INSTANCE = this;
+            mResolver = getContentResolver();
+            mAccountObserver = new AccountObserver(mHandler);
+            mResolver.registerContentObserver(Account.CONTENT_URI, true, mAccountObserver);
+            mMailboxObserver = new MailboxObserver(mHandler);
+            mSyncedMessageObserver = new SyncedMessageObserver(mHandler);
+            mMessageObserver = new MessageObserver(mHandler);
+            mSyncStatusObserver = new EasSyncStatusObserver();
+        } else {
+            alwaysLog("!!! EAS SyncManager onCreated, but INSTANCE not null??");
+        }
+        if (sDeviceId == null) {
+            try {
+                getDeviceId(this);
+            } catch (IOException e) {
+                // We can't run in this situation
+                throw new RuntimeException();
+            }
+        }
+    }
+
+    @Override
+    public int onStartCommand(Intent intent, int flags, int startId) {
+        alwaysLog("!!! EAS SyncManager, onStartCommand");
+        maybeStartSyncManagerThread();
+        if (sServiceThread == null) {
+            alwaysLog("!!! EAS SyncManager, stopping self");
+            stopSelf();
+        }
+        return Service.START_STICKY;
+    }
+
+    @Override
+    public void onDestroy() {
+        alwaysLog("!!! EAS SyncManager, onDestroy");
+        if (INSTANCE != null) {
+            INSTANCE = null;
+            mResolver.unregisterContentObserver(mAccountObserver);
+            mResolver = null;
+            mAccountObserver = null;
+            mMailboxObserver = null;
+            mSyncedMessageObserver = null;
+            mMessageObserver = null;
+            mSyncStatusObserver = null;
+        }
+    }
+
+    void maybeStartSyncManagerThread() {
+        // Start our thread...
+        // See if there are any EAS accounts; otherwise, just go away
+        if (EmailContent.count(this, HostAuth.CONTENT_URI, WHERE_PROTOCOL_EAS, null) > 0) {
+            if (sServiceThread == null || !sServiceThread.isAlive()) {
+                log(sServiceThread == null ? "Starting thread..." : "Restarting thread...");
+                sServiceThread = new Thread(this, "SyncManager");
+                sServiceThread.start();
+            }
+        }
+    }
+
+    static void checkSyncManagerServiceRunning() {
+        // Get the service thread running if it isn't
+        // This is a stopgap for cases in which SyncManager died (due to a crash somewhere in
+        // com.android.email) and hasn't been restarted
+        // See the comment for onCreate for details
+        if (INSTANCE == null) return;
+        if (sServiceThread == null) {
+            INSTANCE.alwaysLog("!!! checkSyncManagerServiceRunning; starting service...");
+            INSTANCE.startService(new Intent(INSTANCE, SyncManager.class));
+        }
+    }
+    
+    static public ConnPerRoute sConnPerRoute = new ConnPerRoute() {
+        public int getMaxForRoute(HttpRoute route) {
+            return 8;
+        }
+    };
+
+    static public synchronized ClientConnectionManager getClientConnectionManager() {
+        if (sClientConnectionManager == null) {
+            // Create a registry for our three schemes; http and https will use built-in factories
+            SchemeRegistry registry = new SchemeRegistry();
+            registry.register(new Scheme("http",
+                    PlainSocketFactory.getSocketFactory(), 80));
+            registry.register(new Scheme("https", SSLSocketFactory.getSocketFactory(), 443));
+
+            // Create a new SSLSocketFactory for our "trusted ssl"
+            // Get the unsecure trust manager from the factory
+            X509TrustManager trustManager = TrustManagerFactory.get(null, false);
+            TrustManager[] trustManagers = new TrustManager[] {trustManager};
+            SSLContext sslcontext;
+            try {
+                sslcontext = SSLContext.getInstance("TLS");
+                sslcontext.init(null, trustManagers, null);
+                SSLContextImpl sslContext = new SSLContextImpl();
+                try {
+                    sslContext.engineInit(null, trustManagers, null, null, null);
+                } catch (KeyManagementException e) {
+                    throw new AssertionError(e);
+                }
+                // Ok, now make our factory
+                SSLSocketFactory sf = new SSLSocketFactory(sslContext.engineGetSocketFactory());
+                sf.setHostnameVerifier(SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
+                // Register the httpts scheme with our factory
+                registry.register(new Scheme("httpts", sf, 443));
+                // And create a ccm with our registry
+                HttpParams params = new BasicHttpParams();
+                params.setIntParameter(ConnManagerPNames.MAX_TOTAL_CONNECTIONS, 25);
+                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
+        return sClientConnectionManager;
+    }
+
+    public static void stopAccountSyncs(long acctId) {
+        if (INSTANCE != null) {
+            INSTANCE.stopAccountSyncs(acctId, true);
+        }
+    }
+
+    private void stopAccountSyncs(long acctId, boolean includeAccountMailbox) {
+        synchronized (sSyncToken) {
+            List<Long> deletedBoxes = new ArrayList<Long>();
+            for (Long mid : INSTANCE.mServiceMap.keySet()) {
+                Mailbox box = Mailbox.restoreMailboxWithId(INSTANCE, mid);
+                if (box != null) {
+                    if (box.mAccountKey == acctId) {
+                        if (!includeAccountMailbox &&
+                                box.mType == Mailbox.TYPE_EAS_ACCOUNT_MAILBOX) {
+                            AbstractSyncService svc = INSTANCE.mServiceMap.get(mid);
+                            if (svc != null) {
+                                svc.stop();
+                            }
+                            continue;
+                        }
+                        AbstractSyncService svc = INSTANCE.mServiceMap.get(mid);
+                        if (svc != null) {
+                            svc.stop();
+                            Thread t = svc.mThread;
+                            if (t != null) {
+                                t.interrupt();
+                            }
+                        }
+                        deletedBoxes.add(mid);
+                    }
+                }
+            }
+            for (Long mid : deletedBoxes) {
+                releaseMailbox(mid);
+            }
+        }
+    }
+
+    static public void reloadFolderList(Context context, long accountId, boolean force) {
+        if (INSTANCE == null) return;
+        Cursor c = context.getContentResolver().query(Mailbox.CONTENT_URI,
+                Mailbox.CONTENT_PROJECTION, MailboxColumns.ACCOUNT_KEY + "=? AND " +
+                MailboxColumns.TYPE + "=?",
+                new String[] {Long.toString(accountId),
+                    Long.toString(Mailbox.TYPE_EAS_ACCOUNT_MAILBOX)}, null);
+        try {
+            if (c.moveToFirst()) {
+                synchronized(sSyncToken) {
+                    Mailbox m = new Mailbox().restore(c);
+                    Account acct = Account.restoreAccountWithId(context, accountId);
+                    if (acct == null) {
+                        return;
+                    }
+                    String syncKey = acct.mSyncKey;
+                    // No need to reload the list if we don't have one
+                    if (!force && (syncKey == null || syncKey.equals("0"))) {
+                        return;
+                    }
+
+                    // Change all ping/push boxes to push/hold
+                    ContentValues cv = new ContentValues();
+                    cv.put(Mailbox.SYNC_INTERVAL, Mailbox.CHECK_INTERVAL_PUSH_HOLD);
+                    context.getContentResolver().update(Mailbox.CONTENT_URI, cv,
+                            WHERE_PUSH_OR_PING_NOT_ACCOUNT_MAILBOX,
+                            new String[] {Long.toString(accountId)});
+                    INSTANCE.log("Set push/ping boxes to push/hold");
+
+                    long id = m.mId;
+                    AbstractSyncService svc = INSTANCE.mServiceMap.get(id);
+                    // Tell the service we're done
+                    if (svc != null) {
+                        synchronized (svc.getSynchronizer()) {
+                            svc.stop();
+                        }
+                        // Interrupt the thread so that it can stop
+                        Thread thread = svc.mThread;
+                        thread.setName(thread.getName() + " (Stopped)");
+                        thread.interrupt();
+                        // Abandon the service
+                        INSTANCE.releaseMailbox(id);
+                        // And have it start naturally
+                        kick("reload folder list");
+                    }
+                }
+            }
+        } finally {
+            c.close();
+        }
+    }
+
+    /**
+     * Informs SyncManager that an account has a new folder list; as a result, any existing folder
+     * might have become invalid.  Therefore, we act as if the account has been deleted, and then
+     * we reinitialize it.
+     *
+     * @param acctId
+     */
+    static public void folderListReloaded(long acctId) {
+        if (INSTANCE != null) {
+            INSTANCE.stopAccountSyncs(acctId, false);
+            kick("reload folder list");
+        }
+    }
+
+//    private void logLocks(String str) {
+//        StringBuilder sb = new StringBuilder(str);
+//        boolean first = true;
+//        for (long id: mWakeLocks.keySet()) {
+//            if (!first) {
+//                sb.append(", ");
+//            } else {
+//                first = false;
+//            }
+//            sb.append(id);
+//        }
+//        log(sb.toString());
+//    }
+
+    private void acquireWakeLock(long id) {
+        synchronized (mWakeLocks) {
+            Boolean lock = mWakeLocks.get(id);
+            if (lock == null) {
+                if (id > 0) {
+                    //log("+WakeLock requested for " + alarmOwner(id));
+                }
+                if (mWakeLock == null) {
+                    PowerManager pm = (PowerManager)getSystemService(Context.POWER_SERVICE);
+                    mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "MAIL_SERVICE");
+                    mWakeLock.acquire();
+                    log("+WAKE LOCK ACQUIRED");
+                }
+                mWakeLocks.put(id, true);
+                //logLocks("Post-acquire of WakeLock for " + alarmOwner(id) + ": ");
+            }
+        }
+    }
+
+    private void releaseWakeLock(long id) {
+        synchronized (mWakeLocks) {
+            Boolean lock = mWakeLocks.get(id);
+            if (lock != null) {
+                if (id > 0) {
+                    //log("+WakeLock not needed for " + alarmOwner(id));
+                }
+                mWakeLocks.remove(id);
+                if (mWakeLocks.isEmpty()) {
+                    if (mWakeLock != null) {
+                        mWakeLock.release();
+                    }
+                    mWakeLock = null;
+                    log("+WAKE LOCK RELEASED");
+                } else {
+                    //logLocks("Post-release of WakeLock for " + alarmOwner(id) + ": ");
+                }
+            }
+        }
+    }
+
+    static public String alarmOwner(long id) {
+        if (id == SYNC_MANAGER_ID) {
+            return "SyncManager";
+        } else {
+            String name = Long.toString(id);
+            if (Eas.USER_LOG && INSTANCE != null) {
+                Mailbox m = Mailbox.restoreMailboxWithId(INSTANCE, id);
+                if (m != null) {
+                    name = m.mDisplayName + '(' + m.mAccountKey + ')';
+                }
+            }
+            return "Mailbox " + name;
+        }
+    }
+
+    private void clearAlarm(long id) {
+        synchronized (mPendingIntents) {
+            PendingIntent pi = mPendingIntents.get(id);
+            if (pi != null) {
+                AlarmManager alarmManager = (AlarmManager)getSystemService(Context.ALARM_SERVICE);
+                alarmManager.cancel(pi);
+                log("+Alarm cleared for " + alarmOwner(id));
+                mPendingIntents.remove(id);
+            }
+        }
+    }
+
+    private void setAlarm(long id, long millis) {
+        synchronized (mPendingIntents) {
+            PendingIntent pi = mPendingIntents.get(id);
+            if (pi == null) {
+                Intent i = new Intent(this, MailboxAlarmReceiver.class);
+                i.putExtra("mailbox", id);
+                i.setData(Uri.parse("Box" + id));
+                pi = PendingIntent.getBroadcast(this, 0, i, 0);
+                mPendingIntents.put(id, pi);
+
+                AlarmManager alarmManager = (AlarmManager)getSystemService(Context.ALARM_SERVICE);
+                alarmManager.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + millis, pi);
+                log("+Alarm set for " + alarmOwner(id) + ", " + millis/1000 + "s");
+            }
+        }
+    }
+
+    private void clearAlarms() {
+        AlarmManager alarmManager = (AlarmManager)getSystemService(Context.ALARM_SERVICE);
+        synchronized (mPendingIntents) {
+            for (PendingIntent pi : mPendingIntents.values()) {
+                alarmManager.cancel(pi);
+            }
+            mPendingIntents.clear();
+        }
+    }
+
+    static public void runAwake(long id) {
+        if (INSTANCE == null) return;
+        INSTANCE.acquireWakeLock(id);
+        INSTANCE.clearAlarm(id);
+    }
+
+    static public void runAsleep(long id, long millis) {
+        if (INSTANCE == null) return;
+        INSTANCE.setAlarm(id, millis);
+        INSTANCE.releaseWakeLock(id);
+    }
+
+    static public void ping(Context context, long id) {
+        checkSyncManagerServiceRunning();
+        if (id < 0) {
+            kick("ping SyncManager");
+        } else if (INSTANCE == null) {
+            context.startService(new Intent(context, SyncManager.class));
+        } else {
+            AbstractSyncService service = INSTANCE.mServiceMap.get(id);
+            if (service != null) {
+                Mailbox m = Mailbox.restoreMailboxWithId(INSTANCE, id);
+                if (m != null) {
+                    // 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) {
+                        return;
+                    }
+                    service.mAccount = Account.restoreAccountWithId(INSTANCE, m.mAccountKey);
+                    service.mMailbox = m;
+                    service.ping();
+                }
+            }
+        }
+    }
+
+    /**
+     * See if we need to change the syncInterval for any of our PIM mailboxes based on changes
+     * to settings in the AccountManager (sync settings).
+     * This code is called 1) when SyncManager starts, and 2) when SyncManager is running and there
+     * are changes made (this is detected via a SyncStatusObserver)
+     */
+    private void checkPIMSyncSettings() {
+        ContentValues cv = new ContentValues();
+        // For now, just Contacts
+        // First, walk through our list of accounts
+        List<Account> easAccounts = getAccountList();
+        for (Account easAccount: easAccounts) {
+            // Find the contacts mailbox
+            long contactsId =
+                Mailbox.findMailboxOfType(this, easAccount.mId, Mailbox.TYPE_CONTACTS);
+            // Presumably there is one, but if not, it's ok.  Just move on...
+            if (contactsId != Mailbox.NO_MAILBOX) {
+                // Create an AccountManager style Account
+                android.accounts.Account acct =
+                    new android.accounts.Account(easAccount.mEmailAddress,
+                            Eas.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;
+                // If we're syncable, look further...
+                if (ContentResolver.getIsSyncable(acct, ContactsContract.AUTHORITY) > 0) {
+                    // If we're supposed to sync automatically (push), set to push if it's not
+                    if (ContentResolver.getSyncAutomatically(acct, ContactsContract.AUTHORITY)) {
+                        if (syncInterval == Mailbox.CHECK_INTERVAL_NEVER || syncInterval > 0) {
+                            log("Sync setting: Contacts for " + acct.name + " changed to push");
+                            cv.put(MailboxColumns.SYNC_INTERVAL, Mailbox.CHECK_INTERVAL_PUSH);
+                        }
+                        // If we're NOT supposed to push, and we're not set up that way, change it
+                        } else if (syncInterval != Mailbox.CHECK_INTERVAL_NEVER) {
+                            log("Sync setting: Contacts for " + acct.name + " changed to manual");
+                            cv.put(MailboxColumns.SYNC_INTERVAL, Mailbox.CHECK_INTERVAL_NEVER);
+                    }
+                // If not, set it to never check
+                } else if (syncInterval != Mailbox.CHECK_INTERVAL_NEVER) {
+                    log("Sync setting: Contacts for " + acct.name + " changed to manual");
+                    cv.put(MailboxColumns.SYNC_INTERVAL, Mailbox.CHECK_INTERVAL_NEVER);
+                }
+
+                // If we've made a change, update the Mailbox, and kick
+                if (cv.containsKey(MailboxColumns.SYNC_INTERVAL)) {
+                    mResolver.update(ContentUris.withAppendedId(Mailbox.CONTENT_URI, contactsId),
+                            cv,null, null);
+                    kick("sync settings change");
+                }
+            }
+        }
+    }
+
+    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;
+            boolean found = false;
+            for (android.accounts.Account acct: accts) {
+                if (acct.name.equalsIgnoreCase(accountName)) {
+                    found = true;
+                    break;
+                }
+            }
+            if (!found) {
+                // This account has been deleted in the AccountManager!
+                log("Account deleted in AccountManager; deleting from provider: " + accountName);
+                // TODO This will orphan downloaded attachments; need to handle this
+                mResolver.delete(ContentUris.withAppendedId(Account.CONTENT_URI, easAccount.mId),
+                        null, null);
+            }
+        }
+    }
+
+    private void releaseConnectivityLock(String reason) {
+        // Clear our sync error map when we get connected
+        mSyncErrorMap.clear();
+        synchronized (sConnectivityLock) {
+            sConnectivityLock.notifyAll();
+        }
+        kick(reason);
+    }
+
+    public class ConnectivityReceiver extends BroadcastReceiver {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            Bundle b = intent.getExtras();
+            if (b != null) {
+                NetworkInfo a = (NetworkInfo)b.get(ConnectivityManager.EXTRA_NETWORK_INFO);
+                String info = "Connectivity alert for " + a.getTypeName();
+                State state = a.getState();
+                if (state == State.CONNECTED) {
+                    info += " CONNECTED";
+                    log(info);
+                    releaseConnectivityLock("connected");
+                } else if (state == State.DISCONNECTED) {
+                    info += " DISCONNECTED";
+                    a = (NetworkInfo)b.get(ConnectivityManager.EXTRA_OTHER_NETWORK_INFO);
+                    if (a != null && a.getState() == State.CONNECTED) {
+                        info += " (OTHER CONNECTED)";
+                        releaseConnectivityLock("disconnect/other");
+                        ConnectivityManager cm =
+                            (ConnectivityManager)getSystemService(Context.CONNECTIVITY_SERVICE);
+                        if (cm != null) {
+                            NetworkInfo i = cm.getActiveNetworkInfo();
+                            if (i == null || i.getState() != State.CONNECTED) {
+                                log("CM says we're connected, but no active info?");
+                            }
+                        }
+                    } else {
+                        log(info);
+                        kick("disconnected");
+                    }
+                }
+            }
+        }
+    }
+
+    private void startService(AbstractSyncService service, Mailbox m) {
+        synchronized (sSyncToken) {
+            String mailboxName = m.mDisplayName;
+            String accountName = service.mAccount.mDisplayName;
+            Thread thread = new Thread(service, mailboxName + "(" + accountName + ")");
+            log("Starting thread for " + mailboxName + " in account " + accountName);
+            thread.start();
+            mServiceMap.put(m.mId, service);
+            runAwake(m.mId);
+        }
+    }
+
+    private void startService(Mailbox m, int reason, PartRequest 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);
+                }
+                startService(service, m);
+            }
+        }
+    }
+
+    private void stopServices() {
+        synchronized (sSyncToken) {
+            ArrayList<Long> toStop = new ArrayList<Long>();
+
+            // Keep track of which services to stop
+            for (Long mailboxId : mServiceMap.keySet()) {
+                toStop.add(mailboxId);
+            }
+
+            // Shut down all of those running services
+            for (Long mailboxId : toStop) {
+                AbstractSyncService svc = mServiceMap.get(mailboxId);
+                if (svc != null) {
+                    log("Stopping " + svc.mAccount.mDisplayName + '/' + svc.mMailbox.mDisplayName);
+                    svc.stop();
+                    if (svc.mThread != null) {
+                        svc.mThread.interrupt();
+                    }
+                }
+                releaseWakeLock(mailboxId);
+            }
+        }
+    }
+
+    private void waitForConnectivity() {
+        int cnt = 0;
+        while (!mStop) {
+            ConnectivityManager cm =
+                (ConnectivityManager)getSystemService(Context.CONNECTIVITY_SERVICE);
+            NetworkInfo info = cm.getActiveNetworkInfo();
+            if (info != null) {
+                //log("NetworkInfo: " + info.getTypeName() + ", " + info.getState().name());
+                return;
+            } else {
+
+                // If we're waiting for the long haul, shut down running service threads
+                if (++cnt > 1) {
+                    stopServices();
+                }
+
+                // Wait until a network is connected, but let the device sleep
+                // We'll set an alarm just in case we don't get notified (bugs happen)
+                synchronized (sConnectivityLock) {
+                    runAsleep(SYNC_MANAGER_ID, CONNECTIVITY_WAIT_TIME+5*SECONDS);
+                    try {
+                        log("Connectivity lock...");
+                        sConnectivityHold = true;
+                        sConnectivityLock.wait(CONNECTIVITY_WAIT_TIME);
+                        log("Connectivity lock released...");
+                    } catch (InterruptedException e) {
+                    } finally {
+                        sConnectivityHold = false;
+                    }
+                    runAwake(SYNC_MANAGER_ID);
+                }
+            }
+        }
+    }
+
+    public void run() {
+        mStop = false;
+
+        // If we're really debugging, turn on all logging
+        if (Eas.DEBUG) {
+            Eas.USER_LOG = true;
+            Eas.PARSER_LOG = true;
+            Eas.FILE_LOG = true;
+        }
+
+        // If we need to wait for the debugger, do so
+        if (Eas.WAIT_DEBUG) {
+            Debug.waitForDebugger();
+        }
+
+        // Set up our observers; we need them to know when to start/stop various syncs based
+        // on the insert/delete/update of mailboxes and accounts
+        // We also observe synced messages to trigger upsyncs at the appropriate time
+        mResolver.registerContentObserver(Mailbox.CONTENT_URI, false, mMailboxObserver);
+        mResolver.registerContentObserver(Message.SYNCED_CONTENT_URI, true, mSyncedMessageObserver);
+        mResolver.registerContentObserver(Message.CONTENT_URI, true, mMessageObserver);
+        ContentResolver.addStatusChangeListener(ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS,
+                mSyncStatusObserver);
+        mAccountsUpdatedListener = new EasAccountsUpdatedListener();
+        AccountManager.get(getApplication())
+            .addOnAccountsUpdatedListener(mAccountsUpdatedListener, mHandler, true);
+
+        mConnectivityReceiver = new ConnectivityReceiver();
+        registerReceiver(mConnectivityReceiver,
+                new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));
+
+        // See if any settings have changed while we weren't running...
+        checkPIMSyncSettings();
+
+        try {
+            while (!mStop) {
+                runAwake(SYNC_MANAGER_ID);
+                waitForConnectivity();
+                mNextWaitReason = "Heartbeat";
+                long nextWait = checkMailboxes();
+                try {
+                    synchronized (this) {
+                        if (!mKicked) {
+                            if (nextWait < 0) {
+                                log("Negative wait? Setting to 1s");
+                                nextWait = 1*SECONDS;
+                            }
+                            if (nextWait > 10*SECONDS) {
+                                log("Next awake in " + nextWait / 1000 + "s: " + mNextWaitReason);
+                                runAsleep(SYNC_MANAGER_ID, nextWait + (3*SECONDS));
+                            }
+                            wait(nextWait);
+                        }
+                    }
+                } catch (InterruptedException e) {
+                    // Needs to be caught, but causes no problem
+                } finally {
+                    synchronized (this) {
+                        if (mKicked) {
+                            //log("Wait deferred due to kick");
+                            mKicked = false;
+                        }
+                    }
+                }
+            }
+            stopServices();
+            log("Shutdown requested");
+        } finally {
+            // Lots of cleanup here
+            // Stop our running syncs
+            stopServices();
+
+            // Stop receivers and content observers
+            if (mConnectivityReceiver != null) {
+                unregisterReceiver(mConnectivityReceiver);
+            }
+            
+            if (INSTANCE != null) {
+                ContentResolver resolver = getContentResolver();
+                resolver.unregisterContentObserver(mAccountObserver);
+                resolver.unregisterContentObserver(mMailboxObserver);
+                resolver.unregisterContentObserver(mSyncedMessageObserver);
+                resolver.unregisterContentObserver(mMessageObserver);
+            }
+            // Don't leak the Intent associated with this listener
+            if (mAccountsUpdatedListener != null) {
+                AccountManager.get(this).removeOnAccountsUpdatedListener(mAccountsUpdatedListener);
+                mAccountsUpdatedListener = null;
+            }
+
+            // Clear pending alarms and associated Intents
+            clearAlarms();
+
+            // Release our wake lock, if we have one
+            synchronized (mWakeLocks) {
+                if (mWakeLock != null) {
+                    mWakeLock.release();
+                    mWakeLock = null;
+                }
+            }
+
+            log("Goodbye");
+        }
+
+        if (!mStop) {
+            // If this wasn't intentional, try to restart the service
+            throw new RuntimeException("EAS SyncManager crash; please restart me...");
+       }
+    }
+
+    private void releaseMailbox(long mailboxId) {
+        mServiceMap.remove(mailboxId);
+        releaseWakeLock(mailboxId);
+    }
+
+    private long checkMailboxes () {
+        // First, see if any running mailboxes have been deleted
+        ArrayList<Long> deletedMailboxes = new ArrayList<Long>();
+        synchronized (sSyncToken) {
+            for (long mailboxId: mServiceMap.keySet()) {
+                Mailbox m = Mailbox.restoreMailboxWithId(this, mailboxId);
+                if (m == null) {
+                    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");
+                    releaseMailbox(mailboxId);
+                }
+            }
+        }
+
+        long nextWait = SYNC_MANAGER_HEARTBEAT_TIME;
+        long now = System.currentTimeMillis();
+
+        // Start up threads that need it; use a query which finds eas mailboxes where the
+        // the sync interval is not "never".  This is the set of mailboxes that we control
+        Cursor c = getContentResolver().query(Mailbox.CONTENT_URI, Mailbox.CONTENT_PROJECTION,
+                mAccountObserver.getSyncableEasMailboxWhere(), null, null);
+
+        try {
+            while (c.moveToNext()) {
+                long mid = c.getLong(Mailbox.CONTENT_ID_COLUMN);
+                AbstractSyncService service = mServiceMap.get(mid);
+                if (service == null) {
+                    // Check whether we're in a hold (temporary or permanent)
+                    SyncError syncError = mSyncErrorMap.get(mid);
+                    if (syncError != null) {
+                        // Nothing we can do about fatal errors
+                        if (syncError.fatal) continue;
+                        if (now < syncError.holdEndTime) {
+                            // If release time is earlier than next wait time,
+                            // move next wait time up to the release time
+                            if (syncError.holdEndTime < now + nextWait) {
+                                nextWait = syncError.holdEndTime - now;
+                                mNextWaitReason = "Release hold";
+                            }
+                            continue;
+                        } else {
+                            // Keep the error around, but clear the end time
+                            syncError.holdEndTime = 0;
+                        }
+                    }
+
+                    // We handle a few types of mailboxes specially
+                    int type = c.getInt(Mailbox.CONTENT_TYPE_COLUMN);
+                    if (type == Mailbox.TYPE_CONTACTS) {
+                        // See if "sync automatically" is set
+                        Account account =
+                            getAccountById(c.getInt(Mailbox.CONTENT_ACCOUNT_KEY_COLUMN));
+                        if (account != null) {
+                            android.accounts.Account a =
+                                new android.accounts.Account(account.mEmailAddress,
+                                        Eas.ACCOUNT_MANAGER_TYPE);
+                            if (!ContentResolver.getSyncAutomatically(a,
+                                    ContactsContract.AUTHORITY)) {
+                                continue;
+                            }
+                        }
+                    } else if (type == Mailbox.TYPE_TRASH) {
+                        continue;
+                    }
+
+                    // Otherwise, we use the sync interval
+                    long interval = c.getInt(Mailbox.CONTENT_SYNC_INTERVAL_COLUMN);
+                    if (interval == Mailbox.CHECK_INTERVAL_PUSH) {
+                        Mailbox m = EmailContent.getContent(c, Mailbox.class);
+                        startService(m, SYNC_PUSH, null);
+                    } else if (type == Mailbox.TYPE_OUTBOX) {
+                        int cnt = EmailContent.count(this, Message.CONTENT_URI,
+                                EasOutboxService.MAILBOX_KEY_AND_NOT_SEND_FAILED,
+                                new String[] {Long.toString(mid)});
+                        if (cnt > 0) {
+                            Mailbox m = EmailContent.getContent(c, Mailbox.class);
+                            startService(new EasOutboxService(this, m), m);
+                        }
+                    } else if (interval > 0 && interval <= ONE_DAY_MINUTES) {
+                        long lastSync = c.getLong(Mailbox.CONTENT_SYNC_TIME_COLUMN);
+                        long sinceLastSync = now - lastSync;
+                        if (sinceLastSync < 0) {
+                            log("WHOA! lastSync in the future for mailbox: " + mid);
+                            sinceLastSync = interval*MINUTES;
+                        }
+                        long toNextSync = interval*MINUTES - sinceLastSync;
+                        String name = c.getString(Mailbox.CONTENT_DISPLAY_NAME_COLUMN);
+                        if (toNextSync <= 0) {
+                            Mailbox m = EmailContent.getContent(c, Mailbox.class);
+                            startService(m, SYNC_SCHEDULED, null);
+                        } else if (toNextSync < nextWait) {
+                            nextWait = toNextSync;
+                            if (Eas.USER_LOG) {
+                                log("Next sync for " + name + " in " + nextWait/1000 + "s");
+                            }
+                            mNextWaitReason = "Scheduled sync, " + name;
+                        } else if (Eas.USER_LOG) {
+                            log("Next sync for " + name + " in " + toNextSync/1000 + "s");
+                        }
+                    }
+                } else {
+                    Thread thread = service.mThread;
+                    // Look for threads that have died but aren't in an error state
+                    if (thread != null && !thread.isAlive() && !mSyncErrorMap.containsKey(mid)) {
+                        releaseMailbox(mid);
+                        // Restart this if necessary
+                        if (nextWait > 3*SECONDS) {
+                            nextWait = 3*SECONDS;
+                            mNextWaitReason = "Clean up dead thread(s)";
+                        }
+                    } else {
+                        long requestTime = service.mRequestTime;
+                        if (requestTime > 0) {
+                            long timeToRequest = requestTime - now;
+                            if (service instanceof AbstractSyncService && timeToRequest <= 0) {
+                                service.mRequestTime = 0;
+                                service.ping();
+                            } else if (requestTime > 0 && timeToRequest < nextWait) {
+                                if (timeToRequest < 11*MINUTES) {
+                                    nextWait = timeToRequest < 250 ? 250 : timeToRequest;
+                                    mNextWaitReason = "Sync data change";
+                                } else {
+                                    log("Illegal timeToRequest: " + timeToRequest);
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        } finally {
+            c.close();
+        }
+        return nextWait;
+    }
+
+    static public void serviceRequest(long mailboxId, int reason) {
+        serviceRequest(mailboxId, 5*SECONDS, reason);
+    }
+
+    static public void serviceRequest(long mailboxId, long ms, int reason) {
+        if (INSTANCE == null) return;
+        Mailbox m = Mailbox.restoreMailboxWithId(INSTANCE, mailboxId);
+        // Never allow manual start of Drafts or Outbox via serviceRequest
+        if (m == null || m.mType == Mailbox.TYPE_DRAFTS || m.mType == Mailbox.TYPE_OUTBOX) {
+            INSTANCE.log("Ignoring serviceRequest for drafts/outbox");
+            return;
+        }
+        try {
+            AbstractSyncService service = INSTANCE.mServiceMap.get(mailboxId);
+            if (service != null) {
+                service.mRequestTime = System.currentTimeMillis() + ms;
+                kick("service request");
+            } else {
+                startManualSync(mailboxId, reason, null);
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+
+    static public void serviceRequestImmediate(long mailboxId) {
+        if (INSTANCE == null) return;
+        AbstractSyncService service = INSTANCE.mServiceMap.get(mailboxId);
+        if (service != null) {
+            service.mRequestTime = System.currentTimeMillis();
+            Mailbox m = Mailbox.restoreMailboxWithId(INSTANCE, mailboxId);
+            if (m != null) {
+                service.mAccount = Account.restoreAccountWithId(INSTANCE, m.mAccountKey);
+                service.mMailbox = m;
+                kick("service request immediate");
+            }
+        }
+    }
+
+    static public void partRequest(PartRequest req) {
+        if (INSTANCE == null) return;
+        Message msg = Message.restoreMessageWithId(INSTANCE, req.emailId);
+        if (msg == null) {
+            return;
+        }
+        long mailboxId = msg.mMailboxKey;
+        AbstractSyncService service = INSTANCE.mServiceMap.get(mailboxId);
+
+        if (service == null) {
+            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);
+        }
+    }
+
+    /**
+     * Determine whether a given Mailbox can be synced, i.e. is not already syncing and is not in
+     * an error state
+     *
+     * @param mailboxId
+     * @return whether or not the Mailbox is available for syncing (i.e. is a valid push target)
+     */
+    static public int pingStatus(long mailboxId) {
+        // Already syncing...
+        if (INSTANCE.mServiceMap.get(mailboxId) != null) {
+            return PING_STATUS_RUNNING;
+        }
+        // No errors or a transient error, don't ping...
+        SyncError error = INSTANCE.mSyncErrorMap.get(mailboxId);
+        if (error != null) {
+            if (error.fatal) {
+                return PING_STATUS_UNABLE;
+            } else if (error.holdEndTime > 0) {
+                return PING_STATUS_WAITING;
+            }
+        }
+        return PING_STATUS_OK;
+    }
+
+    static public AbstractSyncService startManualSync(long mailboxId, int reason, PartRequest req) {
+        if (INSTANCE == null || INSTANCE.mServiceMap == null) return null;
+        synchronized (sSyncToken) {
+            if (INSTANCE.mServiceMap.get(mailboxId) == null) {
+                INSTANCE.mSyncErrorMap.remove(mailboxId);
+                Mailbox m = Mailbox.restoreMailboxWithId(INSTANCE, mailboxId);
+                if (m != null) {
+                    INSTANCE.log("Starting sync for " + m.mDisplayName);
+                    INSTANCE.startService(m, reason, req);
+                }
+            }
+        }
+        return INSTANCE.mServiceMap.get(mailboxId);
+    }
+
+    // DO NOT CALL THIS IN A LOOP ON THE SERVICEMAP
+    static private void stopManualSync(long mailboxId) {
+        if (INSTANCE == null || INSTANCE.mServiceMap == null) return;
+        synchronized (sSyncToken) {
+            AbstractSyncService svc = INSTANCE.mServiceMap.get(mailboxId);
+            if (svc != null) {
+                INSTANCE.log("Stopping sync for " + svc.mMailboxName);
+                svc.stop();
+                svc.mThread.interrupt();
+                INSTANCE.releaseWakeLock(mailboxId);
+            }
+        }
+    }
+
+    /**
+     * Wake up SyncManager to check for mailboxes needing service
+     */
+    static public void kick(String reason) {
+        if (INSTANCE != null) {
+            synchronized (INSTANCE) {
+                INSTANCE.mKicked = true;
+                INSTANCE.notify();
+            }
+        }
+        if (sConnectivityLock != null) {
+            synchronized (sConnectivityLock) {
+                sConnectivityLock.notify();
+            }
+        }
+    }
+
+    static public void accountUpdated(long acctId) {
+        if (INSTANCE == null) return;
+        synchronized (sSyncToken) {
+            for (AbstractSyncService svc : INSTANCE.mServiceMap.values()) {
+                if (svc.mAccount.mId == acctId) {
+                    svc.mAccount = Account.restoreAccountWithId(INSTANCE, acctId);
+                }
+            }
+        }
+    }
+
+    /**
+     * Sent by services indicating that their thread is finished; action depends on the exitStatus
+     * of the service.
+     *
+     * @param svc the service that is finished
+     */
+    static public void done(AbstractSyncService svc) {
+        if (INSTANCE == null) return;
+        synchronized(sSyncToken) {
+            long mailboxId = svc.mMailboxId;
+            HashMap<Long, SyncError> errorMap = INSTANCE.mSyncErrorMap;
+            SyncError syncError = errorMap.get(mailboxId);
+            INSTANCE.releaseMailbox(mailboxId);
+            int exitStatus = svc.mExitStatus;
+            switch (exitStatus) {
+                case AbstractSyncService.EXIT_DONE:
+                    if (!svc.mPartRequests.isEmpty()) {
+                        // TODO Handle this case
+                    }
+                    errorMap.remove(mailboxId);
+                    break;
+                case AbstractSyncService.EXIT_IO_ERROR:
+                    Mailbox m = Mailbox.restoreMailboxWithId(INSTANCE, mailboxId);
+                    if (syncError != null) {
+                        syncError.escalate();
+                        INSTANCE.log(m.mDisplayName + " held for " + syncError.holdDelay + "ms");
+                    } else {
+                        errorMap.put(mailboxId, INSTANCE.new SyncError(exitStatus, false));
+                        INSTANCE.log(m.mDisplayName + " added to syncErrorMap, hold for 15s");
+                    }
+                    break;
+                case AbstractSyncService.EXIT_LOGIN_FAILURE:
+                case AbstractSyncService.EXIT_EXCEPTION:
+                    errorMap.put(mailboxId, INSTANCE.new SyncError(exitStatus, true));
+                    break;
+            }
+            kick("sync completed");
+        }
+    }
+
+    /**
+     * Given the status string from a Mailbox, return the type code for the last sync
+     * @param status the syncStatus column of a Mailbox
+     * @return
+     */
+    static public int getStatusType(String status) {
+        if (status == null) {
+            return -1;
+        } else {
+            return status.charAt(STATUS_TYPE_CHAR) - '0';
+        }
+    }
+
+    /**
+     * Given the status string from a Mailbox, return the change count for the last sync
+     * The change count is the number of adds + deletes + changes in the last sync
+     * @param status the syncStatus column of a Mailbox
+     * @return
+     */
+    static public int getStatusChangeCount(String status) {
+        try {
+            String s = status.substring(STATUS_CHANGE_COUNT_OFFSET);
+            return Integer.parseInt(s);
+        } catch (RuntimeException e) {
+            return -1;
+        }
+    }
+
+    static public Context getContext() {
+        return INSTANCE;
+    }
+}
diff --git a/src/com/android/exchange/adapter/AbstractSyncAdapter.java b/src/com/android/exchange/adapter/AbstractSyncAdapter.java
new file mode 100644
index 0000000..d6ad165
--- /dev/null
+++ b/src/com/android/exchange/adapter/AbstractSyncAdapter.java
@@ -0,0 +1,89 @@
+/*
+ * 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.adapter;
+
+import com.android.email.provider.EmailContent.Account;
+import com.android.email.provider.EmailContent.Mailbox;
+import com.android.exchange.EasSyncService;
+
+import android.content.Context;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Parent class of all sync adapters (EasMailbox, EasCalendar, and EasContacts)
+ *
+ */
+public abstract class AbstractSyncAdapter {
+
+    public static final int SECONDS = 1000;
+    public static final int MINUTES = SECONDS*60;
+    public static final int HOURS = MINUTES*60;
+    public static final int DAYS = HOURS*24;
+    public static final int WEEKS = DAYS*7;
+
+    public Mailbox mMailbox;
+    public EasSyncService mService;
+    public Context mContext;
+    public Account mAccount;
+
+    // Create the data for local changes that need to be sent up to the server
+    public abstract boolean sendLocalChanges(Serializer s)
+        throws IOException;
+    // Parse incoming data from the EAS server, creating, modifying, and deleting objects as
+    // required through the EmailProvider
+    public abstract boolean parse(InputStream is)
+        throws IOException;
+    // The name used to specify the collection type of the target (Email, Calendar, or Contacts)
+    public abstract String getCollectionName();
+    public abstract void cleanup();
+
+    public AbstractSyncAdapter(Mailbox mailbox, EasSyncService service) {
+        mMailbox = mailbox;
+        mService = service;
+        mContext = service.mContext;
+        mAccount = service.mAccount;
+    }
+
+    public void userLog(String ...strings) {
+        mService.userLog(strings);
+    }
+
+    public void incrementChangeCount() {
+        mService.mChangeCount++;
+    }
+
+    /**
+     * Returns the current SyncKey; override if the SyncKey is stored elsewhere (as for Contacts)
+     * @return the current SyncKey for the Mailbox
+     * @throws IOException
+     */
+    public String getSyncKey() throws IOException {
+        if (mMailbox.mSyncKey == null) {
+            userLog("Reset SyncKey to 0");
+            mMailbox.mSyncKey = "0";
+        }
+        return mMailbox.mSyncKey;
+    }
+
+    public void setSyncKey(String syncKey, boolean inCommands) throws IOException {
+        mMailbox.mSyncKey = syncKey;
+    }
+}
+
diff --git a/src/com/android/exchange/adapter/AbstractSyncParser.java b/src/com/android/exchange/adapter/AbstractSyncParser.java
new file mode 100644
index 0000000..97dfb50
--- /dev/null
+++ b/src/com/android/exchange/adapter/AbstractSyncParser.java
@@ -0,0 +1,205 @@
+/*
+ * 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.adapter;
+
+import com.android.email.provider.EmailContent.Account;
+import com.android.email.provider.EmailContent.Mailbox;
+import com.android.email.provider.EmailContent.MailboxColumns;
+import com.android.exchange.EasSyncService;
+import com.android.exchange.SyncManager;
+
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Base class for the Email and PIM sync parsers
+ * Handles the basic flow of syncKeys, looping to get more data, handling errors, etc.
+ * Each subclass must implement a handful of methods that relate specifically to the data type
+ *
+ */
+public abstract class AbstractSyncParser extends Parser {
+
+    protected EasSyncService mService;
+    protected Mailbox mMailbox;
+    protected Account mAccount;
+    protected Context mContext;
+    protected ContentResolver mContentResolver;
+    protected AbstractSyncAdapter mAdapter;
+
+    public AbstractSyncParser(InputStream in, AbstractSyncAdapter adapter) throws IOException {
+        super(in);
+        mAdapter = adapter;
+        mService = adapter.mService;
+        mContext = mService.mContext;
+        mContentResolver = mContext.getContentResolver();
+        mMailbox = mService.mMailbox;
+        mAccount = mService.mAccount;
+    }
+
+    /**
+     * Read, parse, and act on incoming commands from the Exchange server
+     * @throws IOException if the connection is broken
+     */
+    public abstract void commandsParser() throws IOException;
+
+    /**
+     * Read, parse, and act on server responses
+     * @throws IOException
+     */
+    public abstract void responsesParser() throws IOException;
+
+    /**
+     * Commit any changes found during parsing
+     * @throws IOException
+     */
+    public abstract void commit() throws IOException;
+
+    /**
+     * Delete all records of this class in this account
+     */
+    public abstract void wipe();
+
+    /**
+     * Loop through the top-level structure coming from the Exchange server
+     * Sync keys and the more available flag are handled here, whereas specific data parsing
+     * is handled by abstract methods implemented for each data class (e.g. Email, Contacts, etc.)
+     */
+    @Override
+    public boolean parse() throws IOException {
+        int status;
+        boolean moreAvailable = false;
+        boolean newSyncKey = false;
+        int interval = mMailbox.mSyncInterval;
+
+        // If we're not at the top of the xml tree, throw an exception
+        if (nextTag(START_DOCUMENT) != Tags.SYNC_SYNC) {
+            throw new EasParserException();
+        }
+
+        boolean mailboxUpdated = false;
+        ContentValues cv = new ContentValues();
+
+        // Loop here through the remaining xml
+        while (nextTag(START_DOCUMENT) != END_DOCUMENT) {
+            if (tag == Tags.SYNC_COLLECTION || tag == Tags.SYNC_COLLECTIONS) {
+                // Ignore these tags, since we've only got one collection syncing in this loop
+            } else if (tag == Tags.SYNC_STATUS) {
+                // Status = 1 is success; everything else is a failure
+                status = getValueInt();
+                if (status != 1) {
+                    mService.errorLog("Sync failed: " + status);
+                    // Status = 3 means invalid sync key
+                    if (status == 3) {
+                        // Must delete all of the data and start over with syncKey of "0"
+                        mAdapter.setSyncKey("0", false);
+                        // Make this a push box through the first sync
+                        // TODO Make frequency conditional on user settings!
+                        mMailbox.mSyncInterval = Mailbox.CHECK_INTERVAL_PUSH;
+                        mService.errorLog("Bad sync key; RESET and delete data");
+                        wipe();
+                        // Indicate there's more so that we'll start syncing again
+                        moreAvailable = true;
+                    } else if (status == 8) {
+                        // This is Bad; it means the server doesn't recognize the serverId it
+                        // sent us.  What's needed is a refresh of the folder list.
+                        SyncManager.reloadFolderList(mContext, mAccount.mId, true);
+                    }
+                    // TODO Look at other error codes and consider what's to be done
+                }
+            } else if (tag == Tags.SYNC_COMMANDS) {
+                commandsParser();
+            } else if (tag == Tags.SYNC_RESPONSES) {
+                responsesParser();
+            } else if (tag == Tags.SYNC_MORE_AVAILABLE) {
+                moreAvailable = true;
+            } else if (tag == Tags.SYNC_SYNC_KEY) {
+                if (mAdapter.getSyncKey().equals("0")) {
+                    moreAvailable = true;
+                }
+                String newKey = getValue();
+                userLog("Parsed key for ", mMailbox.mDisplayName, ": ", newKey);
+                if (!newKey.equals(mMailbox.mSyncKey)) {
+                    mAdapter.setSyncKey(newKey, true);
+                    cv.put(MailboxColumns.SYNC_KEY, newKey);
+                    mailboxUpdated = true;
+                    newSyncKey = true;
+                }
+                // If we were pushing (i.e. auto-start), now we'll become ping-triggered
+                if (mMailbox.mSyncInterval == Mailbox.CHECK_INTERVAL_PUSH) {
+                    mMailbox.mSyncInterval = Mailbox.CHECK_INTERVAL_PING;
+                }
+           } else {
+                skipTag();
+           }
+        }
+
+        // If we don't have a new sync key, ignore moreAvailable (or we'll loop)
+        if (moreAvailable && !newSyncKey) {
+            userLog("!! SyncKey hasn't changed, setting moreAvailable = false");
+            moreAvailable = false;
+        }
+
+        // Commit any changes
+        commit();
+
+        boolean abortSyncs = false;
+
+        // If the sync interval has changed, we need to save it
+        if (mMailbox.mSyncInterval != interval) {
+            cv.put(MailboxColumns.SYNC_INTERVAL, mMailbox.mSyncInterval);
+            mailboxUpdated = true;
+        // If there are changes, and we were bounced from push/ping, try again
+        } else if (mService.mChangeCount > 0 &&
+                mAccount.mSyncInterval == Account.CHECK_INTERVAL_PUSH &&
+                mMailbox.mSyncInterval > 0) {
+            userLog("Changes found to ping loop mailbox ", mMailbox.mDisplayName, ": will ping.");
+            cv.put(MailboxColumns.SYNC_INTERVAL, Mailbox.CHECK_INTERVAL_PING);
+            mailboxUpdated = true;
+            abortSyncs = true;
+        }
+
+        if (mailboxUpdated) {
+             synchronized (mService.getSynchronizer()) {
+                if (!mService.isStopped()) {
+                     mMailbox.update(mContext, cv);
+                }
+            }
+        }
+
+        if (abortSyncs) {
+            userLog("Aborting account syncs due to mailbox change to ping...");
+            SyncManager.stopAccountSyncs(mAccount.mId);
+        }
+
+        // Let the caller know that there's more to do
+        userLog("Returning moreAvailable = " + moreAvailable);
+        return moreAvailable;
+    }
+
+    void userLog(String ...strings) {
+        mService.userLog(strings);
+    }
+
+    void userLog(String string, int num, String string2) {
+        mService.userLog(string, num, string2);
+    }
+}
diff --git a/src/com/android/exchange/adapter/AccountSyncAdapter.java b/src/com/android/exchange/adapter/AccountSyncAdapter.java
new file mode 100644
index 0000000..2a1c7be
--- /dev/null
+++ b/src/com/android/exchange/adapter/AccountSyncAdapter.java
@@ -0,0 +1,34 @@
+package com.android.exchange.adapter;
+
+import com.android.email.provider.EmailContent.Mailbox;
+import com.android.exchange.EasSyncService;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+public class AccountSyncAdapter extends AbstractSyncAdapter {
+
+    public AccountSyncAdapter(Mailbox mailbox, EasSyncService service) {
+        super(mailbox, service);
+     }
+
+    @Override
+    public void cleanup() {
+    }
+
+    @Override
+    public String getCollectionName() {
+        return null;
+    }
+
+    @Override
+    public boolean parse(InputStream is) throws IOException {
+        return false;
+    }
+
+    @Override
+    public boolean sendLocalChanges(Serializer s) throws IOException {
+        return false;
+    }
+
+}
diff --git a/src/com/android/exchange/adapter/CalendarSyncAdapter.java b/src/com/android/exchange/adapter/CalendarSyncAdapter.java
new file mode 100644
index 0000000..739c9e2
--- /dev/null
+++ b/src/com/android/exchange/adapter/CalendarSyncAdapter.java
@@ -0,0 +1,57 @@
+/*
+ * 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.adapter;
+
+import com.android.email.provider.EmailContent.Mailbox;
+import com.android.exchange.EasSyncService;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Sync adapter class for EAS calendars
+ *
+ */
+public class CalendarSyncAdapter extends AbstractSyncAdapter {
+
+    public CalendarSyncAdapter(Mailbox mailbox, EasSyncService service) {
+        super(mailbox, service);
+    }
+
+    @Override
+    public String getCollectionName() {
+        return "Calendar";
+    }
+
+    @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
+        return false;
+    }
+}
diff --git a/src/com/android/exchange/adapter/ContactsSyncAdapter.java b/src/com/android/exchange/adapter/ContactsSyncAdapter.java
new file mode 100644
index 0000000..fff6400
--- /dev/null
+++ b/src/com/android/exchange/adapter/ContactsSyncAdapter.java
@@ -0,0 +1,1909 @@
+/*
+ * 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.adapter;
+
+import com.android.email.codec.binary.Base64;
+import com.android.email.provider.EmailContent.Mailbox;
+import com.android.exchange.Eas;
+import com.android.exchange.EasSyncService;
+
+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.ContentProviderOperation.Builder;
+import android.content.Entity.NamedContentValues;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.RemoteException;
+import android.provider.ContactsContract;
+import android.provider.SyncStateContract;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.Groups;
+import android.provider.ContactsContract.RawContacts;
+import android.provider.ContactsContract.Settings;
+import android.provider.ContactsContract.SyncState;
+import android.provider.ContactsContract.CommonDataKinds.Email;
+import android.provider.ContactsContract.CommonDataKinds.Event;
+import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
+import android.provider.ContactsContract.CommonDataKinds.Im;
+import android.provider.ContactsContract.CommonDataKinds.Nickname;
+import android.provider.ContactsContract.CommonDataKinds.Note;
+import android.provider.ContactsContract.CommonDataKinds.Organization;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.CommonDataKinds.Photo;
+import android.provider.ContactsContract.CommonDataKinds.Relation;
+import android.provider.ContactsContract.CommonDataKinds.StructuredName;
+import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
+import android.provider.ContactsContract.CommonDataKinds.Website;
+import android.text.util.Rfc822Token;
+import android.text.util.Rfc822Tokenizer;
+import android.util.Log;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+
+/**
+ * Sync adapter for EAS Contacts
+ *
+ */
+public class ContactsSyncAdapter extends AbstractSyncAdapter {
+
+    private static final String TAG = "EasContactsSyncAdapter";
+    private static final String SERVER_ID_SELECTION = RawContacts.SOURCE_ID + "=?";
+    private static final String CLIENT_ID_SELECTION = RawContacts.SYNC1 + "=?";
+    private static final String[] ID_PROJECTION = new String[] {RawContacts._ID};
+    private static final String[] GROUP_PROJECTION = new String[] {Groups.SOURCE_ID};
+
+    private static final ArrayList<NamedContentValues> EMPTY_ARRAY_NAMEDCONTENTVALUES
+        = new ArrayList<NamedContentValues>();
+
+    private static final String FOUND_DATA_ROW = "com.android.exchange.FOUND_ROW";
+
+    private static final int[] HOME_ADDRESS_TAGS = new int[] {Tags.CONTACTS_HOME_ADDRESS_CITY,
+        Tags.CONTACTS_HOME_ADDRESS_COUNTRY,
+        Tags.CONTACTS_HOME_ADDRESS_POSTAL_CODE,
+        Tags.CONTACTS_HOME_ADDRESS_STATE,
+        Tags.CONTACTS_HOME_ADDRESS_STREET};
+
+    private static final int[] WORK_ADDRESS_TAGS = new int[] {Tags.CONTACTS_BUSINESS_ADDRESS_CITY,
+        Tags.CONTACTS_BUSINESS_ADDRESS_COUNTRY,
+        Tags.CONTACTS_BUSINESS_ADDRESS_POSTAL_CODE,
+        Tags.CONTACTS_BUSINESS_ADDRESS_STATE,
+        Tags.CONTACTS_BUSINESS_ADDRESS_STREET};
+
+    private static final int[] OTHER_ADDRESS_TAGS = new int[] {Tags.CONTACTS_HOME_ADDRESS_CITY,
+        Tags.CONTACTS_OTHER_ADDRESS_COUNTRY,
+        Tags.CONTACTS_OTHER_ADDRESS_POSTAL_CODE,
+        Tags.CONTACTS_OTHER_ADDRESS_STATE,
+        Tags.CONTACTS_OTHER_ADDRESS_STREET};
+
+    private static final int MAX_IM_ROWS = 3;
+    private static final int MAX_EMAIL_ROWS = 3;
+    private static final int MAX_PHONE_ROWS = 2;
+    private static final String COMMON_DATA_ROW = Im.DATA;  // Could have been Email.DATA, etc.
+    private static final String COMMON_TYPE_ROW = Phone.TYPE; // Could have been any typed row
+
+    private static final int[] IM_TAGS = new int[] {Tags.CONTACTS2_IM_ADDRESS,
+        Tags.CONTACTS2_IM_ADDRESS_2, Tags.CONTACTS2_IM_ADDRESS_3};
+
+    private static final int[] EMAIL_TAGS = new int[] {Tags.CONTACTS_EMAIL1_ADDRESS,
+        Tags.CONTACTS_EMAIL2_ADDRESS, Tags.CONTACTS_EMAIL3_ADDRESS};
+
+    private static final int[] WORK_PHONE_TAGS = new int[] {Tags.CONTACTS_BUSINESS_TELEPHONE_NUMBER,
+        Tags.CONTACTS_BUSINESS2_TELEPHONE_NUMBER};
+
+    private static final int[] HOME_PHONE_TAGS = new int[] {Tags.CONTACTS_HOME_TELEPHONE_NUMBER,
+        Tags.CONTACTS_HOME2_TELEPHONE_NUMBER};
+
+    ArrayList<Long> mDeletedIdList = new ArrayList<Long>();
+    ArrayList<Long> mUpdatedIdList = new ArrayList<Long>();
+
+    private boolean mGroupsUsed = false;
+
+    private android.accounts.Account mAccountManagerAccount;
+
+    public ContactsSyncAdapter(Mailbox mailbox, EasSyncService service) {
+        super(mailbox, service);
+    }
+
+    static Uri addCallerIsSyncAdapterParameter(Uri uri) {
+        return uri.buildUpon()
+                .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
+                .build();
+    }
+
+    @Override
+    public boolean parse(InputStream is) throws IOException {
+        EasContactsSyncParser p = new EasContactsSyncParser(is, this);
+        return p.parse();
+    }
+
+    interface UntypedRow {
+        public void addValues(RowBuilder builder);
+        public boolean isSameAs(int type, String value);
+    }
+
+    /**
+     * We get our SyncKey from ContactsProvider.  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(ContactsContract.AUTHORITY_URI);
+        try {
+            byte[] data = SyncStateContract.Helpers.get(client,
+                    ContactsContract.SyncState.CONTENT_URI, getAccountManagerAccount());
+            if (data == null || data.length == 0) {
+                // Initialize the SyncKey
+                setSyncKey("0", false);
+                // 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(Settings.UNGROUPED_VISIBLE, true);
+                client.insert(addCallerIsSyncAdapterParameter(Settings.CONTENT_URI), cv);
+                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 ContactOperations
+     */
+    @Override
+    public void setSyncKey(String syncKey, boolean inCommands) throws IOException {
+        if ("0".equals(syncKey) || !inCommands) {
+            ContentProviderClient client =
+                mService.mContentResolver
+                    .acquireContentProviderClient(ContactsContract.AUTHORITY_URI);
+            try {
+                SyncStateContract.Helpers.set(client, ContactsContract.SyncState.CONTENT_URI,
+                        getAccountManagerAccount(), syncKey.getBytes());
+                userLog("SyncKey set to ", syncKey, " in ContactsProvider");
+           } catch (RemoteException e) {
+                throw new IOException("Can't set SyncKey in ContactsProvider");
+            }
+        }
+        mMailbox.mSyncKey = syncKey;
+    }
+
+    public android.accounts.Account getAccountManagerAccount() {
+        if (mAccountManagerAccount == null) {
+            mAccountManagerAccount =
+                new android.accounts.Account(mAccount.mEmailAddress, Eas.ACCOUNT_MANAGER_TYPE);
+        }
+        return mAccountManagerAccount;
+    }
+
+    public static final class EasChildren {
+        private EasChildren() {}
+
+        /** MIME type used when storing this in data table. */
+        public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/eas_children";
+        public static final int MAX_CHILDREN = 8;
+        public static final String[] ROWS =
+            new String[] {"data2", "data3", "data4", "data5", "data6", "data7", "data8", "data9"};
+    }
+
+    public static final class EasPersonal {
+        String anniversary;
+        String fileAs;
+
+            /** MIME type used when storing this in data table. */
+        public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/eas_personal";
+        public static final String ANNIVERSARY = "data2";
+        public static final String FILE_AS = "data4";
+
+        boolean hasData() {
+            return anniversary != null || fileAs != null;
+        }
+    }
+
+    public static final class EasBusiness {
+        String customerId;
+        String governmentId;
+        String accountName;
+
+        /** MIME type used when storing this in data table. */
+        public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/eas_business";
+        public static final String CUSTOMER_ID = "data6";
+        public static final String GOVERNMENT_ID = "data7";
+        public static final String ACCOUNT_NAME = "data8";
+
+        boolean hasData() {
+            return customerId != null || governmentId != null || accountName != null;
+        }
+    }
+
+    public static final class Address {
+        String city;
+        String country;
+        String code;
+        String street;
+        String state;
+
+        boolean hasData() {
+            return city != null || country != null || code != null || state != null
+                || street != null;
+        }
+    }
+
+    class EmailRow implements UntypedRow {
+        String email;
+        String displayName;
+
+        public EmailRow(String _email) {
+            Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(_email);
+            // Can't happen, but belt & suspenders
+            if (tokens.length == 0) {
+                email = "";
+                displayName = "";
+            } else {
+                Rfc822Token token = tokens[0];
+                email = token.getAddress();
+                displayName = token.getName();
+            }
+        }
+
+        public void addValues(RowBuilder builder) {
+            builder.withValue(Email.DATA, email);
+            builder.withValue(Email.DISPLAY_NAME, displayName);
+        }
+
+        public boolean isSameAs(int type, String value) {
+            return email.equalsIgnoreCase(value);
+        }
+    }
+
+    class ImRow implements UntypedRow {
+        String im;
+
+        public ImRow(String _im) {
+            im = _im;
+        }
+
+        public void addValues(RowBuilder builder) {
+            builder.withValue(Im.DATA, im);
+        }
+
+        public boolean isSameAs(int type, String value) {
+            return im.equalsIgnoreCase(value);
+        }
+    }
+
+    class PhoneRow implements UntypedRow {
+        String phone;
+        int type;
+
+        public PhoneRow(String _phone, int _type) {
+            phone = _phone;
+            type = _type;
+        }
+
+        public void addValues(RowBuilder builder) {
+            builder.withValue(Im.DATA, phone);
+            builder.withValue(Phone.TYPE, type);
+        }
+
+        public boolean isSameAs(int _type, String value) {
+            return type == _type && phone.equalsIgnoreCase(value);
+        }
+    }
+
+   class EasContactsSyncParser extends AbstractSyncParser {
+
+        String[] mBindArgument = new String[1];
+        String mMailboxIdAsString;
+        Uri mAccountUri;
+        ContactOperations ops = new ContactOperations();
+
+        public EasContactsSyncParser(InputStream in, ContactsSyncAdapter adapter)
+                throws IOException {
+            super(in, adapter);
+            mAccountUri = uriWithAccountAndIsSyncAdapter(RawContacts.CONTENT_URI);
+        }
+
+        @Override
+        public void wipe() {
+            mContentResolver.delete(mAccountUri, null, null);
+        }
+
+        public void addData(String serverId, ContactOperations ops, Entity entity)
+                throws IOException {
+            String fileAs = null;
+            String prefix = null;
+            String firstName = null;
+            String lastName = null;
+            String middleName = null;
+            String suffix = null;
+            String companyName = null;
+            String yomiFirstName = null;
+            String yomiLastName = null;
+            String yomiCompanyName = null;
+            String title = null;
+            String department = null;
+            String officeLocation = null;
+            Address home = new Address();
+            Address work = new Address();
+            Address other = new Address();
+            EasBusiness business = new EasBusiness();
+            EasPersonal personal = new EasPersonal();
+            ArrayList<String> children = new ArrayList<String>();
+            ArrayList<UntypedRow> emails = new ArrayList<UntypedRow>();
+            ArrayList<UntypedRow> ims = new ArrayList<UntypedRow>();
+            ArrayList<UntypedRow> homePhones = new ArrayList<UntypedRow>();
+            ArrayList<UntypedRow> workPhones = new ArrayList<UntypedRow>();
+            if (entity == null) {
+                ops.newContact(serverId);
+            }
+
+            while (nextTag(Tags.SYNC_APPLICATION_DATA) != END) {
+                switch (tag) {
+                    case Tags.CONTACTS_FIRST_NAME:
+                        firstName = getValue();
+                        break;
+                    case Tags.CONTACTS_LAST_NAME:
+                        lastName = getValue();
+                        break;
+                    case Tags.CONTACTS_MIDDLE_NAME:
+                        middleName = getValue();
+                        break;
+                    case Tags.CONTACTS_FILE_AS:
+                        fileAs = getValue();
+                        break;
+                    case Tags.CONTACTS_SUFFIX:
+                        suffix = getValue();
+                        break;
+                    case Tags.CONTACTS_COMPANY_NAME:
+                        companyName = getValue();
+                        break;
+                    case Tags.CONTACTS_JOB_TITLE:
+                        title = getValue();
+                        break;
+                    case Tags.CONTACTS_EMAIL1_ADDRESS:
+                    case Tags.CONTACTS_EMAIL2_ADDRESS:
+                    case Tags.CONTACTS_EMAIL3_ADDRESS:
+                        emails.add(new EmailRow(getValue()));
+                        break;
+                    case Tags.CONTACTS_BUSINESS2_TELEPHONE_NUMBER:
+                    case Tags.CONTACTS_BUSINESS_TELEPHONE_NUMBER:
+                        workPhones.add(new PhoneRow(getValue(), Phone.TYPE_WORK));
+                        break;
+                    case Tags.CONTACTS2_MMS:
+                        ops.addPhone(entity, Phone.TYPE_MMS, getValue());
+                        break;
+                    case Tags.CONTACTS_BUSINESS_FAX_NUMBER:
+                        ops.addPhone(entity, Phone.TYPE_FAX_WORK, getValue());
+                        break;
+                    case Tags.CONTACTS2_COMPANY_MAIN_PHONE:
+                        ops.addPhone(entity, Phone.TYPE_COMPANY_MAIN, getValue());
+                        break;
+                    case Tags.CONTACTS_HOME_FAX_NUMBER:
+                        ops.addPhone(entity, Phone.TYPE_FAX_HOME, getValue());
+                        break;
+                    case Tags.CONTACTS_HOME_TELEPHONE_NUMBER:
+                    case Tags.CONTACTS_HOME2_TELEPHONE_NUMBER:
+                        homePhones.add(new PhoneRow(getValue(), Phone.TYPE_HOME));
+                        break;
+                    case Tags.CONTACTS_MOBILE_TELEPHONE_NUMBER:
+                        ops.addPhone(entity, Phone.TYPE_MOBILE, getValue());
+                        break;
+                    case Tags.CONTACTS_CAR_TELEPHONE_NUMBER:
+                        ops.addPhone(entity, Phone.TYPE_CAR, getValue());
+                        break;
+                    case Tags.CONTACTS_RADIO_TELEPHONE_NUMBER:
+                        ops.addPhone(entity, Phone.TYPE_RADIO, getValue());
+                        break;
+                    case Tags.CONTACTS_PAGER_NUMBER:
+                        ops.addPhone(entity, Phone.TYPE_PAGER, getValue());
+                        break;
+                    case Tags.CONTACTS_ASSISTANT_TELEPHONE_NUMBER:
+                        ops.addPhone(entity, Phone.TYPE_ASSISTANT, getValue());
+                        break;
+                    case Tags.CONTACTS2_IM_ADDRESS:
+                    case Tags.CONTACTS2_IM_ADDRESS_2:
+                    case Tags.CONTACTS2_IM_ADDRESS_3:
+                        ims.add(new ImRow(getValue()));
+                        break;
+                    case Tags.CONTACTS_BUSINESS_ADDRESS_CITY:
+                        work.city = getValue();
+                        break;
+                    case Tags.CONTACTS_BUSINESS_ADDRESS_COUNTRY:
+                        work.country = getValue();
+                        break;
+                    case Tags.CONTACTS_BUSINESS_ADDRESS_POSTAL_CODE:
+                        work.code = getValue();
+                        break;
+                    case Tags.CONTACTS_BUSINESS_ADDRESS_STATE:
+                        work.state = getValue();
+                        break;
+                    case Tags.CONTACTS_BUSINESS_ADDRESS_STREET:
+                        work.street = getValue();
+                        break;
+                    case Tags.CONTACTS_HOME_ADDRESS_CITY:
+                        home.city = getValue();
+                        break;
+                    case Tags.CONTACTS_HOME_ADDRESS_COUNTRY:
+                        home.country = getValue();
+                        break;
+                    case Tags.CONTACTS_HOME_ADDRESS_POSTAL_CODE:
+                        home.code = getValue();
+                        break;
+                    case Tags.CONTACTS_HOME_ADDRESS_STATE:
+                        home.state = getValue();
+                        break;
+                    case Tags.CONTACTS_HOME_ADDRESS_STREET:
+                        home.street = getValue();
+                        break;
+                    case Tags.CONTACTS_OTHER_ADDRESS_CITY:
+                        other.city = getValue();
+                        break;
+                    case Tags.CONTACTS_OTHER_ADDRESS_COUNTRY:
+                        other.country = getValue();
+                        break;
+                    case Tags.CONTACTS_OTHER_ADDRESS_POSTAL_CODE:
+                        other.code = getValue();
+                        break;
+                    case Tags.CONTACTS_OTHER_ADDRESS_STATE:
+                        other.state = getValue();
+                        break;
+                    case Tags.CONTACTS_OTHER_ADDRESS_STREET:
+                        other.street = getValue();
+                        break;
+
+                    case Tags.CONTACTS_CHILDREN:
+                        childrenParser(children);
+                        break;
+
+                    case Tags.CONTACTS_YOMI_COMPANY_NAME:
+                        yomiCompanyName = getValue();
+                        break;
+                    case Tags.CONTACTS_YOMI_FIRST_NAME:
+                        yomiFirstName = getValue();
+                        break;
+                    case Tags.CONTACTS_YOMI_LAST_NAME:
+                        yomiLastName = getValue();
+                        break;
+
+                    case Tags.CONTACTS2_NICKNAME:
+                        ops.addNickname(entity, getValue());
+                        break;
+
+                    case Tags.CONTACTS_ASSISTANT_NAME:
+                        ops.addRelation(entity, Relation.TYPE_ASSISTANT, getValue());
+                        break;
+                    case Tags.CONTACTS2_MANAGER_NAME:
+                        ops.addRelation(entity, Relation.TYPE_MANAGER, getValue());
+                        break;
+                    case Tags.CONTACTS_SPOUSE:
+                        ops.addRelation(entity, Relation.TYPE_SPOUSE, getValue());
+                        break;
+                    case Tags.CONTACTS_DEPARTMENT:
+                        department = getValue();
+                        break;
+                    case Tags.CONTACTS_TITLE:
+                        prefix = getValue();
+                        break;
+
+                    // EAS Business
+                    case Tags.CONTACTS_OFFICE_LOCATION:
+                        officeLocation = getValue();
+                        break;
+                    case Tags.CONTACTS2_CUSTOMER_ID:
+                        business.customerId = getValue();
+                        break;
+                    case Tags.CONTACTS2_GOVERNMENT_ID:
+                        business.governmentId = getValue();
+                        break;
+                    case Tags.CONTACTS2_ACCOUNT_NAME:
+                        business.accountName = getValue();
+                        break;
+
+                    // EAS Personal
+                    case Tags.CONTACTS_ANNIVERSARY:
+                        personal.anniversary = getValue();
+                        break;
+                    case Tags.CONTACTS_BIRTHDAY:
+                        ops.addBirthday(entity, getValue());
+                        break;
+                    case Tags.CONTACTS_WEBPAGE:
+                        ops.addWebpage(entity, getValue());
+                        break;
+
+                    case Tags.CONTACTS_PICTURE:
+                        ops.addPhoto(entity, getValue());
+                        break;
+
+                    case Tags.BASE_BODY:
+                        ops.addNote(entity, bodyParser());
+                        break;
+                    case Tags.CONTACTS_BODY:
+                        ops.addNote(entity, getValue());
+                        break;
+
+                    case Tags.CONTACTS_CATEGORIES:
+                        mGroupsUsed = true;
+                        categoriesParser(ops, entity);
+                        break;
+
+                    case Tags.CONTACTS_COMPRESSED_RTF:
+                        // We don't use this, and it isn't necessary to upload, so we'll ignore it
+                        skipTag();
+                        break;
+
+                    default:
+                        skipTag();
+                }
+            }
+
+            // We must have first name, last name, or company name
+            String name = null;
+            if (firstName != null || lastName != null) {
+                if (firstName == null) {
+                    name = lastName;
+                } else if (lastName == null) {
+                    name = firstName;
+                } else {
+                    name = firstName + ' ' + lastName;
+                }
+            } else if (companyName != null) {
+                name = companyName;
+            }
+
+            ops.addName(entity, prefix, firstName, lastName, middleName, suffix, name,
+                    yomiFirstName, yomiLastName, fileAs);
+            ops.addBusiness(entity, business);
+            ops.addPersonal(entity, personal);
+
+            ops.addUntyped(entity, emails, Email.CONTENT_ITEM_TYPE, -1, MAX_EMAIL_ROWS);
+            ops.addUntyped(entity, ims, Im.CONTENT_ITEM_TYPE, -1, MAX_IM_ROWS);
+            ops.addUntyped(entity, homePhones, Phone.CONTENT_ITEM_TYPE, Phone.TYPE_HOME,
+                    MAX_PHONE_ROWS);
+            ops.addUntyped(entity, workPhones, Phone.CONTENT_ITEM_TYPE, Phone.TYPE_WORK,
+                    MAX_PHONE_ROWS);
+
+            if (!children.isEmpty()) {
+                ops.addChildren(entity, children);
+            }
+
+            if (work.hasData()) {
+                ops.addPostal(entity, StructuredPostal.TYPE_WORK, work.street, work.city,
+                        work.state, work.country, work.code);
+            }
+            if (home.hasData()) {
+                ops.addPostal(entity, StructuredPostal.TYPE_HOME, home.street, home.city,
+                        home.state, home.country, home.code);
+            }
+            if (other.hasData()) {
+                ops.addPostal(entity, StructuredPostal.TYPE_OTHER, other.street, other.city,
+                        other.state, other.country, other.code);
+            }
+
+            if (companyName != null) {
+                ops.addOrganization(entity, Organization.TYPE_WORK, companyName, title, department,
+                        yomiCompanyName, officeLocation);
+            }
+
+            if (entity != null) {
+                // We've been removing rows from the list as they've been found in the xml
+                // Any that are left must have been deleted on the server
+                ArrayList<NamedContentValues> ncvList = entity.getSubValues();
+                for (NamedContentValues ncv: ncvList) {
+                    // These rows need to be deleted...
+                    Uri u = dataUriFromNamedContentValues(ncv);
+                    ops.add(ContentProviderOperation.newDelete(addCallerIsSyncAdapterParameter(u))
+                            .build());
+                }
+            }
+        }
+
+        private void categoriesParser(ContactOperations ops, Entity entity) throws IOException {
+            while (nextTag(Tags.CONTACTS_CATEGORIES) != END) {
+                switch (tag) {
+                    case Tags.CONTACTS_CATEGORY:
+                        ops.addGroup(entity, getValue());
+                        break;
+                    default:
+                        skipTag();
+                }
+            }
+        }
+
+        private void childrenParser(ArrayList<String> children) throws IOException {
+            while (nextTag(Tags.CONTACTS_CHILDREN) != END) {
+                switch (tag) {
+                    case Tags.CONTACTS_CHILD:
+                        if (children.size() < EasChildren.MAX_CHILDREN) {
+                            children.add(getValue());
+                        }
+                        break;
+                    default:
+                        skipTag();
+                }
+            }
+        }
+
+        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();
+                }
+            }
+            return body;
+        }
+
+        public void addParser(ContactOperations 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:
+                        addData(serverId, ops, null);
+                        break;
+                    default:
+                        skipTag();
+                }
+            }
+        }
+
+        private Cursor getServerIdCursor(String serverId) {
+            mBindArgument[0] = serverId;
+            return mContentResolver.query(mAccountUri, ID_PROJECTION, SERVER_ID_SELECTION,
+                    mBindArgument, null);
+        }
+
+        private Cursor getClientIdCursor(String clientId) {
+            mBindArgument[0] = clientId;
+            return mContentResolver.query(mAccountUri, ID_PROJECTION, CLIENT_ID_SELECTION,
+                    mBindArgument, null);
+        }
+
+        public void deleteParser(ContactOperations ops) throws IOException {
+            while (nextTag(Tags.SYNC_DELETE) != END) {
+                switch (tag) {
+                    case Tags.SYNC_SERVER_ID:
+                        String serverId = getValue();
+                        // Find the message in this mailbox 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();
+                }
+            }
+        }
+
+        class ServerChange {
+            long id;
+            boolean read;
+
+            ServerChange(long _id, boolean _read) {
+                id = _id;
+                read = _read;
+            }
+        }
+
+        /**
+         * Changes are handled row by row, and only changed/new rows are acted upon
+         * @param ops the array of pending ContactProviderOperations.
+         * @throws IOException
+         */
+        public void changeParser(ContactOperations ops) throws IOException {
+            String serverId = null;
+            Entity entity = null;
+            while (nextTag(Tags.SYNC_CHANGE) != END) {
+                switch (tag) {
+                    case Tags.SYNC_SERVER_ID:
+                        serverId = getValue();
+                        Cursor c = getServerIdCursor(serverId);
+                        try {
+                            if (c.moveToFirst()) {
+                                // TODO Handle deleted individual rows...
+                                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) {
+                                }
+                            }
+                        } finally {
+                            c.close();
+                        }
+                        break;
+                    case Tags.SYNC_APPLICATION_DATA:
+                        addData(serverId, ops, entity);
+                        break;
+                    default:
+                        skipTag();
+                }
+            }
+        }
+
+        @Override
+        public void commandsParser() throws IOException {
+            while (nextTag(Tags.SYNC_COMMANDS) != END) {
+                if (tag == Tags.SYNC_ADD) {
+                    addParser(ops);
+                    incrementChangeCount();
+                } else if (tag == Tags.SYNC_DELETE) {
+                    deleteParser(ops);
+                    incrementChangeCount();
+                } else if (tag == Tags.SYNC_CHANGE) {
+                    changeParser(ops);
+                    incrementChangeCount();
+                } else
+                    skipTag();
+            }
+        }
+
+        @Override
+        public void commit() throws IOException {
+           // Save the syncKey here, using the Helper provider by Contacts provider
+            userLog("Contacts SyncKey saved as: ", mMailbox.mSyncKey);
+            ops.add(SyncStateContract.Helpers.newSetOperation(SyncState.CONTENT_URI,
+                    getAccountManagerAccount(), mMailbox.mSyncKey.getBytes()));
+
+            // Execute these all at once...
+            ops.execute();
+
+            if (ops.mResults != null) {
+                ContentValues cv = new ContentValues();
+                cv.put(RawContacts.DIRTY, 0);
+                for (int i = 0; i < ops.mContactIndexCount; i++) {
+                    int index = ops.mContactIndexArray[i];
+                    Uri u = ops.mResults[index].uri;
+                    if (u != null) {
+                        String idString = u.getLastPathSegment();
+                        mContentResolver.update(
+                                addCallerIsSyncAdapterParameter(RawContacts.CONTENT_URI), cv,
+                                RawContacts._ID + "=" + idString, null);
+                    }
+                }
+            }
+        }
+
+        public void addResponsesParser() throws IOException {
+            String serverId = null;
+            String clientId = null;
+            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:
+                        getValue();
+                        break;
+                    default:
+                        skipTag();
+                }
+            }
+
+            // This is theoretically impossible, but...
+            if (clientId == null || serverId == null) return;
+
+            Cursor c = getClientIdCursor(clientId);
+            try {
+                if (c.moveToFirst()) {
+                    cv.put(RawContacts.SOURCE_ID, serverId);
+                    cv.put(RawContacts.DIRTY, 0);
+                    ops.add(ContentProviderOperation.newUpdate(
+                            ContentUris.withAppendedId(
+                                    addCallerIsSyncAdapterParameter(RawContacts.CONTENT_URI),
+                                    c.getLong(0)))
+                            .withValues(cv)
+                            .build());
+                    userLog("New contact " + 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 contact " + 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 Uri uriWithAccountAndIsSyncAdapter(Uri uri) {
+        return uri.buildUpon()
+            .appendQueryParameter(RawContacts.ACCOUNT_NAME, mAccount.mEmailAddress)
+            .appendQueryParameter(RawContacts.ACCOUNT_TYPE, Eas.ACCOUNT_MANAGER_TYPE)
+            .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
+            .build();
+    }
+
+    /**
+     * SmartBuilder is a wrapper for the Builder class that is used to create/update rows for a
+     * ContentProvider.  It has, in addition to the Builder, ContentValues which, if present,
+     * represent the current values of that row, that can be compared against current values to
+     * see whether an update is even necessary.  The methods on SmartBuilder are delegated to
+     * the Builder.
+     */
+    private class RowBuilder {
+        Builder builder;
+        ContentValues cv;
+
+        public RowBuilder(Builder _builder) {
+            builder = _builder;
+        }
+
+        public RowBuilder(Builder _builder, NamedContentValues _ncv) {
+            builder = _builder;
+            cv = _ncv.values;
+        }
+
+        RowBuilder withValues(ContentValues values) {
+            builder.withValues(values);
+            return this;
+        }
+
+        RowBuilder withValueBackReference(String key, int previousResult) {
+            builder.withValueBackReference(key, previousResult);
+            return this;
+        }
+
+        ContentProviderOperation build() {
+            return builder.build();
+        }
+
+        RowBuilder withValue(String key, Object value) {
+            builder.withValue(key, value);
+            return this;
+        }
+    }
+
+    private class ContactOperations extends ArrayList<ContentProviderOperation> {
+        private static final long serialVersionUID = 1L;
+        private int mCount = 0;
+        private int mContactBackValue = mCount;
+        // Make an array big enough for the PIM window (max items we can get)
+        private int[] mContactIndexArray =
+            new int[Integer.parseInt(EasSyncService.PIM_WINDOW_SIZE)];
+        private int mContactIndexCount = 0;
+        private ContentProviderResult[] mResults = null;
+
+        @Override
+        public boolean add(ContentProviderOperation op) {
+            super.add(op);
+            mCount++;
+            return true;
+        }
+
+        public void newContact(String serverId) {
+            Builder builder = ContentProviderOperation
+                .newInsert(uriWithAccountAndIsSyncAdapter(RawContacts.CONTENT_URI));
+            ContentValues values = new ContentValues();
+            values.put(RawContacts.SOURCE_ID, serverId);
+            builder.withValues(values);
+            mContactBackValue = mCount;
+            mContactIndexArray[mContactIndexCount++] = mCount;
+            add(builder.build());
+        }
+
+        public void delete(long id) {
+            add(ContentProviderOperation
+                    .newDelete(ContentUris.withAppendedId(RawContacts.CONTENT_URI, id)
+                            .buildUpon()
+                            .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
+                            .build())
+                    .build());
+        }
+
+        public void execute() {
+            synchronized (mService.getSynchronizer()) {
+                if (!mService.isStopped()) {
+                    try {
+                        if (!isEmpty()) {
+                            mService.userLog("Executing ", size(), " CPO's");
+                            mResults = mContext.getContentResolver().applyBatch(
+                                    ContactsContract.AUTHORITY, this);
+                        }
+                    } catch (RemoteException e) {
+                        // There is nothing sensible to be done here
+                        Log.e(TAG, "problem inserting contact during server update", e);
+                    } catch (OperationApplicationException e) {
+                        // There is nothing sensible to be done here
+                        Log.e(TAG, "problem inserting contact during server update", e);
+                    }
+                }
+            }
+        }
+
+        /**
+         * Given the list of NamedContentValues for an entity, a mime type, and a subtype,
+         * tries to find a match, returning it
+         * @param list the list of NCV's from the contact entity
+         * @param contentItemType the mime type we're looking for
+         * @param type the subtype (e.g. HOME, WORK, etc.)
+         * @return the matching NCV or null if not found
+         */
+        private NamedContentValues findTypedData(ArrayList<NamedContentValues> list,
+                String contentItemType, int type, String stringType) {
+            NamedContentValues result = null;
+
+            // Loop through the ncv's, looking for an existing row
+            for (NamedContentValues namedContentValues: list) {
+                Uri uri = namedContentValues.uri;
+                ContentValues cv = namedContentValues.values;
+                if (Data.CONTENT_URI.equals(uri)) {
+                    String mimeType = cv.getAsString(Data.MIMETYPE);
+                    if (mimeType.equals(contentItemType)) {
+                        if (stringType != null) {
+                            if (cv.getAsString(GroupMembership.GROUP_ROW_ID).equals(stringType)) {
+                                result = namedContentValues;
+                            }
+                        // Note Email.TYPE could be ANY type column; they are all defined in
+                        // the private CommonColumns class in ContactsContract
+                        } else if (type < 0 || cv.getAsInteger(Email.TYPE) == type) {
+                            result = namedContentValues;
+                        }
+                    }
+                }
+            }
+
+            // If we've found an existing data row, we'll delete it.  Any rows left at the
+            // end should be deleted...
+            if (result != null) {
+                list.remove(result);
+            }
+
+            // Return the row found (or null)
+            return result;
+        }
+
+        /**
+         * Given the list of NamedContentValues for an entity and a mime type
+         * gather all of the matching NCV's, returning them
+         * @param list the list of NCV's from the contact entity
+         * @param contentItemType the mime type we're looking for
+         * @param type the subtype (e.g. HOME, WORK, etc.)
+         * @return the matching NCVs
+         */
+        private ArrayList<NamedContentValues> findUntypedData(ArrayList<NamedContentValues> list,
+                int type, String contentItemType) {
+            ArrayList<NamedContentValues> result = new ArrayList<NamedContentValues>();
+
+            // Loop through the ncv's, looking for an existing row
+            for (NamedContentValues namedContentValues: list) {
+                Uri uri = namedContentValues.uri;
+                ContentValues cv = namedContentValues.values;
+                if (Data.CONTENT_URI.equals(uri)) {
+                    String mimeType = cv.getAsString(Data.MIMETYPE);
+                    if (mimeType.equals(contentItemType)) {
+                        if (type != -1) {
+                            int subtype = cv.getAsInteger(Phone.TYPE);
+                            if (type != subtype) {
+                                continue;
+                            }
+                        }
+                        result.add(namedContentValues);
+                    }
+                }
+            }
+
+            // If we've found an existing data row, we'll delete it.  Any rows left at the
+            // end should be deleted...
+            if (result != null) {
+                list.remove(result);
+            }
+
+            // Return the row found (or null)
+            return result;
+        }
+
+        /**
+         * Create a wrapper for a builder (insert or update) that also includes the NCV for
+         * an existing row of this type.   If the SmartBuilder's cv field is not null, then
+         * it represents the current (old) values of this field.  The caller can then check
+         * whether the field is now different and needs to be updated; if it's not different,
+         * the caller will simply return and not generate a new CPO.  Otherwise, the builder
+         * should have its content values set, and the built CPO should be added to the
+         * ContactOperations list.
+         *
+         * @param entity the contact entity (or null if this is a new contact)
+         * @param mimeType the mime type of this row
+         * @param type the subtype of this row
+         * @param stringType for groups, the name of the group (type will be ignored), or null
+         * @return the created SmartBuilder
+         */
+        public RowBuilder createBuilder(Entity entity, String mimeType, int type,
+                String stringType) {
+            RowBuilder builder = null;
+
+            if (entity != null) {
+                NamedContentValues ncv =
+                    findTypedData(entity.getSubValues(), mimeType, type, stringType);
+                if (ncv != null) {
+                    builder = new RowBuilder(
+                            ContentProviderOperation
+                                .newUpdate(addCallerIsSyncAdapterParameter(
+                                    dataUriFromNamedContentValues(ncv))),
+                            ncv);
+                }
+            }
+
+            if (builder == null) {
+                builder = newRowBuilder(entity, mimeType);
+            }
+
+            // Return the appropriate builder (insert or update)
+            // Caller will fill in the appropriate values; 4 MIMETYPE is already set
+            return builder;
+        }
+
+        private RowBuilder typedRowBuilder(Entity entity, String mimeType, int type) {
+            return createBuilder(entity, mimeType, type, null);
+        }
+
+        private RowBuilder untypedRowBuilder(Entity entity, String mimeType) {
+            return createBuilder(entity, mimeType, -1, null);
+        }
+
+        private RowBuilder newRowBuilder(Entity entity, String mimeType) {
+            // This is a new row; first get the contactId
+            // If the Contact is new, use the saved back value; otherwise the value in the entity
+            int contactId = mContactBackValue;
+            if (entity != null) {
+                contactId = entity.getEntityValues().getAsInteger(RawContacts._ID);
+            }
+
+            // Create an insert operation with the proper contactId reference
+            RowBuilder builder =
+                new RowBuilder(ContentProviderOperation.newInsert(
+                        addCallerIsSyncAdapterParameter(Data.CONTENT_URI)));
+            if (entity == null) {
+                builder.withValueBackReference(Data.RAW_CONTACT_ID, contactId);
+            } else {
+                builder.withValue(Data.RAW_CONTACT_ID, contactId);
+            }
+
+            // Set the mime type of the row
+            builder.withValue(Data.MIMETYPE, mimeType);
+            return builder;
+        }
+
+        /**
+         * Compare a column in a ContentValues with an (old) value, and see if they are the
+         * same.  For this purpose, null and an empty string are considered the same.
+         * @param cv a ContentValues object, from a NamedContentValues
+         * @param column a column that might be in the ContentValues
+         * @param oldValue an old value (or null) to check against
+         * @return whether the column's value in the ContentValues matches oldValue
+         */
+        private boolean cvCompareString(ContentValues cv, String column, String oldValue) {
+            if (cv.containsKey(column)) {
+                if (oldValue != null && cv.getAsString(column).equals(oldValue)) {
+                    return true;
+                }
+            } else if (oldValue == null || oldValue.length() == 0) {
+                return true;
+            }
+            return false;
+        }
+
+        public void addChildren(Entity entity, ArrayList<String> children) {
+            RowBuilder builder = untypedRowBuilder(entity, EasChildren.CONTENT_ITEM_TYPE);
+            int i = 0;
+            for (String child: children) {
+                builder.withValue(EasChildren.ROWS[i++], child);
+            }
+            add(builder.build());
+        }
+
+        public void addGroup(Entity entity, String group) {
+            RowBuilder builder =
+                createBuilder(entity, GroupMembership.CONTENT_ITEM_TYPE, -1, group);
+            builder.withValue(GroupMembership.GROUP_SOURCE_ID, group);
+            add(builder.build());
+        }
+
+        public void addBirthday(Entity entity, String birthday) {
+            RowBuilder builder =
+                    typedRowBuilder(entity, Event.CONTENT_ITEM_TYPE, Event.TYPE_BIRTHDAY);
+            ContentValues cv = builder.cv;
+            if (cv != null && cvCompareString(cv, Event.START_DATE, birthday)) {
+                return;
+            }
+            builder.withValue(Event.START_DATE, birthday);
+            builder.withValue(Event.TYPE, Event.TYPE_BIRTHDAY);
+            add(builder.build());
+        }
+
+        public void addName(Entity entity, String prefix, String givenName, String familyName,
+                String middleName, String suffix, String displayName, String yomiFirstName,
+                String yomiLastName, String fileAs) {
+            RowBuilder builder = untypedRowBuilder(entity, StructuredName.CONTENT_ITEM_TYPE);
+            ContentValues cv = builder.cv;
+            if (cv != null && cvCompareString(cv, StructuredName.GIVEN_NAME, givenName) &&
+                    cvCompareString(cv, StructuredName.FAMILY_NAME, familyName) &&
+                    cvCompareString(cv, StructuredName.MIDDLE_NAME, middleName) &&
+                    cvCompareString(cv, StructuredName.PREFIX, prefix) &&
+                    cvCompareString(cv, StructuredName.PHONETIC_GIVEN_NAME, yomiFirstName) &&
+                    cvCompareString(cv, StructuredName.PHONETIC_FAMILY_NAME, yomiLastName) &&
+                    //cvCompareString(cv, StructuredName.DISPLAY_NAME, fileAs) &&
+                    cvCompareString(cv, StructuredName.SUFFIX, suffix)) {
+                return;
+            }
+            builder.withValue(StructuredName.GIVEN_NAME, givenName);
+            builder.withValue(StructuredName.FAMILY_NAME, familyName);
+            builder.withValue(StructuredName.MIDDLE_NAME, middleName);
+            builder.withValue(StructuredName.SUFFIX, suffix);
+            builder.withValue(StructuredName.PHONETIC_GIVEN_NAME, yomiFirstName);
+            builder.withValue(StructuredName.PHONETIC_FAMILY_NAME, yomiLastName);
+            builder.withValue(StructuredName.PREFIX, prefix);
+            //builder.withValue(StructuredName.DISPLAY_NAME, fileAs);
+            add(builder.build());
+        }
+
+        public void addPersonal(Entity entity, EasPersonal personal) {
+            RowBuilder builder = untypedRowBuilder(entity, EasPersonal.CONTENT_ITEM_TYPE);
+            ContentValues cv = builder.cv;
+            if (cv != null && cvCompareString(cv, EasPersonal.ANNIVERSARY, personal.anniversary) &&
+                    cvCompareString(cv, EasPersonal.FILE_AS , personal.fileAs)) {
+                return;
+            }
+            if (!personal.hasData()) {
+                return;
+            }
+            builder.withValue(EasPersonal.FILE_AS, personal.fileAs);
+            builder.withValue(EasPersonal.ANNIVERSARY, personal.anniversary);
+            add(builder.build());
+        }
+
+        public void addBusiness(Entity entity, EasBusiness business) {
+            RowBuilder builder = untypedRowBuilder(entity, EasBusiness.CONTENT_ITEM_TYPE);
+            ContentValues cv = builder.cv;
+            if (cv != null && cvCompareString(cv, EasBusiness.ACCOUNT_NAME, business.accountName) &&
+                    cvCompareString(cv, EasBusiness.CUSTOMER_ID, business.customerId) &&
+                    cvCompareString(cv, EasBusiness.GOVERNMENT_ID, business.governmentId)) {
+                return;
+            }
+            if (!business.hasData()) {
+                return;
+            }
+            builder.withValue(EasBusiness.ACCOUNT_NAME, business.accountName);
+            builder.withValue(EasBusiness.CUSTOMER_ID, business.customerId);
+            builder.withValue(EasBusiness.GOVERNMENT_ID, business.governmentId);
+            add(builder.build());
+        }
+
+        public void addPhoto(Entity entity, String photo) {
+            RowBuilder builder = untypedRowBuilder(entity, Photo.CONTENT_ITEM_TYPE);
+            // We're always going to add this; it's not worth trying to figure out whether the
+            // picture is the same as the one stored.
+            byte[] pic = Base64.decodeBase64(photo.getBytes());
+            builder.withValue(Photo.PHOTO, pic);
+            add(builder.build());
+        }
+
+        public void addPhone(Entity entity, int type, String phone) {
+            RowBuilder builder = typedRowBuilder(entity, Phone.CONTENT_ITEM_TYPE, type);
+            ContentValues cv = builder.cv;
+            if (cv != null && cvCompareString(cv, Phone.NUMBER, phone)) {
+                return;
+            }
+            builder.withValue(Phone.TYPE, type);
+            builder.withValue(Phone.NUMBER, phone);
+            add(builder.build());
+        }
+
+        public void addWebpage(Entity entity, String url) {
+            RowBuilder builder = untypedRowBuilder(entity, Website.CONTENT_ITEM_TYPE);
+            ContentValues cv = builder.cv;
+            if (cv != null && cvCompareString(cv, Website.URL, url)) {
+                return;
+            }
+            builder.withValue(Website.TYPE, Website.TYPE_WORK);
+            builder.withValue(Website.URL, url);
+            add(builder.build());
+        }
+
+        public void addRelation(Entity entity, int type, String value) {
+            RowBuilder builder = typedRowBuilder(entity, Relation.CONTENT_ITEM_TYPE, type);
+            ContentValues cv = builder.cv;
+            if (cv != null && cvCompareString(cv, Relation.DATA, value)) {
+                return;
+            }
+            builder.withValue(Relation.TYPE, type);
+            builder.withValue(Relation.DATA, value);
+            add(builder.build());
+        }
+
+        public void addNickname(Entity entity, String name) {
+            RowBuilder builder =
+                typedRowBuilder(entity, Nickname.CONTENT_ITEM_TYPE, Nickname.TYPE_DEFAULT);
+            ContentValues cv = builder.cv;
+            if (cv != null && cvCompareString(cv, Nickname.NAME, name)) {
+                return;
+            }
+            builder.withValue(Nickname.TYPE, Nickname.TYPE_DEFAULT);
+            builder.withValue(Nickname.NAME, name);
+            add(builder.build());
+        }
+
+        public void addPostal(Entity entity, int type, String street, String city, String state,
+                String country, String code) {
+            RowBuilder builder = typedRowBuilder(entity, StructuredPostal.CONTENT_ITEM_TYPE,
+                    type);
+            ContentValues cv = builder.cv;
+            if (cv != null && cvCompareString(cv, StructuredPostal.CITY, city) &&
+                    cvCompareString(cv, StructuredPostal.STREET, street) &&
+                    cvCompareString(cv, StructuredPostal.COUNTRY, country) &&
+                    cvCompareString(cv, StructuredPostal.POSTCODE, code) &&
+                    cvCompareString(cv, StructuredPostal.REGION, state)) {
+                return;
+            }
+            builder.withValue(StructuredPostal.TYPE, type);
+            builder.withValue(StructuredPostal.CITY, city);
+            builder.withValue(StructuredPostal.STREET, street);
+            builder.withValue(StructuredPostal.COUNTRY, country);
+            builder.withValue(StructuredPostal.POSTCODE, code);
+            builder.withValue(StructuredPostal.REGION, state);
+            add(builder.build());
+        }
+
+       /**
+         * We now are dealing with up to maxRows typeless rows of mimeType data.  We need to try to
+         * match them with existing rows; if there's a match, everything's great.  Otherwise, we
+         * either need to add a new row for the data, or we have to replace an existing one
+         * that no longer matches.  This is similar to the way Emails are handled.
+         */
+        public void addUntyped(Entity entity, ArrayList<UntypedRow> rows, String mimeType,
+                int type, int maxRows) {
+            // Make a list of all same type rows in the existing entity
+            ArrayList<NamedContentValues> oldValues = EMPTY_ARRAY_NAMEDCONTENTVALUES;
+            ArrayList<NamedContentValues> entityValues = EMPTY_ARRAY_NAMEDCONTENTVALUES;
+            if (entity != null) {
+                oldValues = findUntypedData(entityValues, type, mimeType);
+                entityValues = entity.getSubValues();
+            }
+
+            // These will be rows needing replacement with new values
+            ArrayList<UntypedRow> rowsToReplace = new ArrayList<UntypedRow>();
+
+            // The count of existing rows
+            int numRows = oldValues.size();
+            for (UntypedRow row: rows) {
+                boolean found = false;
+                // If we already have this row, mark it
+                for (NamedContentValues ncv: oldValues) {
+                    ContentValues cv = ncv.values;
+                    String data = cv.getAsString(COMMON_DATA_ROW);
+                    int rowType = -1;
+                    if (cv.containsKey(COMMON_TYPE_ROW)) {
+                        rowType = cv.getAsInteger(COMMON_TYPE_ROW);
+                    }
+                    if (row.isSameAs(rowType, data)) {
+                        cv.put(FOUND_DATA_ROW, true);
+                        // Remove this to indicate it's still being used
+                        entityValues.remove(ncv);
+                        found = true;
+                        break;
+                    }
+                }
+                if (!found) {
+                    // If we don't, there are two possibilities
+                    if (numRows < maxRows) {
+                        // If there are available rows, add a new one
+                        RowBuilder builder = newRowBuilder(entity, mimeType);
+                        row.addValues(builder);
+                        add(builder.build());
+                        numRows++;
+                    } else {
+                        // Otherwise, say we need to replace a row with this
+                        rowsToReplace.add(row);
+                    }
+                }
+            }
+
+            // Go through rows needing replacement
+            for (UntypedRow row: rowsToReplace) {
+                for (NamedContentValues ncv: oldValues) {
+                    ContentValues cv = ncv.values;
+                    // Find a row that hasn't been used (i.e. doesn't match current rows)
+                    if (!cv.containsKey(FOUND_DATA_ROW)) {
+                        // And update it
+                        RowBuilder builder = new RowBuilder(
+                                ContentProviderOperation
+                                    .newUpdate(addCallerIsSyncAdapterParameter(
+                                        dataUriFromNamedContentValues(ncv))),
+                                ncv);
+                        row.addValues(builder);
+                        add(builder.build());
+                    }
+                }
+            }
+        }
+
+        public void addOrganization(Entity entity, int type, String company, String title,
+                String department, String yomiCompanyName, String officeLocation) {
+            RowBuilder builder = typedRowBuilder(entity, Organization.CONTENT_ITEM_TYPE, type);
+            ContentValues cv = builder.cv;
+            if (cv != null && cvCompareString(cv, Organization.COMPANY, company) &&
+                    cvCompareString(cv, Organization.PHONETIC_NAME, yomiCompanyName) &&
+                    cvCompareString(cv, Organization.DEPARTMENT, department) &&
+                    cvCompareString(cv, Organization.TITLE, title) &&
+                    cvCompareString(cv, Organization.OFFICE_LOCATION, officeLocation)) {
+                return;
+            }
+            builder.withValue(Organization.TYPE, type);
+            builder.withValue(Organization.COMPANY, company);
+            builder.withValue(Organization.TITLE, title);
+            builder.withValue(Organization.DEPARTMENT, department);
+            builder.withValue(Organization.PHONETIC_NAME, yomiCompanyName);
+            builder.withValue(Organization.OFFICE_LOCATION, officeLocation);
+            add(builder.build());
+        }
+
+        public void addNote(Entity entity, String note) {
+            RowBuilder builder = typedRowBuilder(entity, Note.CONTENT_ITEM_TYPE, -1);
+            ContentValues cv = builder.cv;
+            if (note == null) return;
+            note = note.replaceAll("\r\n", "\n");
+            if (cv != null && cvCompareString(cv, Note.NOTE, note)) {
+                return;
+            }
+
+            // Reject notes with nothing in them.  Often, we get something from Outlook when
+            // nothing was ever entered.  Sigh.
+            int len = note.length();
+            int i = 0;
+            for (; i < len; i++) {
+                char c = note.charAt(i);
+                if (!Character.isWhitespace(c)) {
+                    break;
+                }
+            }
+            if (i == len) return;
+
+            builder.withValue(Note.NOTE, note);
+            add(builder.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;
+    }
+
+    @Override
+    public void cleanup() {
+        // Mark the changed contacts dirty = 0
+        // Permanently delete the user deletions
+        ContactOperations ops = new ContactOperations();
+        for (Long id: mUpdatedIdList) {
+            ops.add(ContentProviderOperation
+                    .newUpdate(ContentUris.withAppendedId(RawContacts.CONTENT_URI, id)
+                            .buildUpon()
+                            .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
+                            .build())
+                    .withValue(RawContacts.DIRTY, 0).build());
+        }
+        for (Long id: mDeletedIdList) {
+            ops.add(ContentProviderOperation
+                    .newDelete(ContentUris.withAppendedId(RawContacts.CONTENT_URI, id)
+                            .buildUpon()
+                            .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
+                            .build())
+                    .build());
+        }
+        ops.execute();
+        ContentResolver cr = mContext.getContentResolver();
+        if (mGroupsUsed) {
+            // Make sure the title column is set for all of our groups
+            // And that all of our groups are visible
+            // TODO Perhaps the visible part should only happen when the group is created, but
+            // this is fine for now.
+            Uri groupsUri = uriWithAccountAndIsSyncAdapter(Groups.CONTENT_URI);
+            Cursor c = cr.query(groupsUri, new String[] {Groups.SOURCE_ID, Groups.TITLE},
+                    Groups.TITLE + " IS NULL", null, null);
+            ContentValues values = new ContentValues();
+            values.put(Groups.GROUP_VISIBLE, 1);
+            try {
+                while (c.moveToNext()) {
+                    String sourceId = c.getString(0);
+                    values.put(Groups.TITLE, sourceId);
+                    cr.update(uriWithAccountAndIsSyncAdapter(groupsUri), values,
+                            Groups.SOURCE_ID + "=?", new String[] {sourceId});
+                }
+            } finally {
+                c.close();
+            }
+        }
+    }
+
+    @Override
+    public String getCollectionName() {
+        return "Contacts";
+    }
+
+    private void sendEmail(Serializer s, ContentValues cv, int count, String displayName)
+            throws IOException {
+        // Get both parts of the email address (a newly created one in the UI won't have a name)
+        String addr = cv.getAsString(Email.DATA);
+        String name = cv.getAsString(Email.DISPLAY_NAME);
+        if (name == null) {
+            if (displayName != null) {
+                name = displayName;
+            } else {
+                name = addr;
+            }
+        }
+        // Compose address from name and addr
+        if (addr != null) {
+            String value = '\"' + name + "\" <" + addr + '>';
+            if (count < MAX_EMAIL_ROWS) {
+                s.data(EMAIL_TAGS[count], value);
+            }
+        }
+    }
+
+    private void sendIm(Serializer s, ContentValues cv, int count) throws IOException {
+        String value = cv.getAsString(Im.DATA);
+        if (value == null) return;
+        if (count < MAX_IM_ROWS) {
+            s.data(IM_TAGS[count], value);
+        }
+    }
+
+    private void sendOnePostal(Serializer s, ContentValues cv, int[] fieldNames)
+            throws IOException{
+        if (cv.containsKey(StructuredPostal.CITY)) {
+            s.data(fieldNames[0], cv.getAsString(StructuredPostal.CITY));
+        }
+        if (cv.containsKey(StructuredPostal.COUNTRY)) {
+            s.data(fieldNames[1], cv.getAsString(StructuredPostal.COUNTRY));
+        }
+        if (cv.containsKey(StructuredPostal.POSTCODE)) {
+            s.data(fieldNames[2], cv.getAsString(StructuredPostal.POSTCODE));
+        }
+        if (cv.containsKey(StructuredPostal.REGION)) {
+            s.data(fieldNames[3], cv.getAsString(StructuredPostal.REGION));
+        }
+        if (cv.containsKey(StructuredPostal.STREET)) {
+            s.data(fieldNames[4], cv.getAsString(StructuredPostal.STREET));
+        }
+    }
+
+    private void sendStructuredPostal(Serializer s, ContentValues cv) throws IOException {
+        switch (cv.getAsInteger(StructuredPostal.TYPE)) {
+            case StructuredPostal.TYPE_HOME:
+                sendOnePostal(s, cv, HOME_ADDRESS_TAGS);
+                break;
+            case StructuredPostal.TYPE_WORK:
+                sendOnePostal(s, cv, WORK_ADDRESS_TAGS);
+                break;
+            case StructuredPostal.TYPE_OTHER:
+                sendOnePostal(s, cv, OTHER_ADDRESS_TAGS);
+                break;
+            default:
+                break;
+        }
+    }
+
+    private String sendStructuredName(Serializer s, ContentValues cv) throws IOException {
+        String displayName = null;
+        if (cv.containsKey(StructuredName.FAMILY_NAME)) {
+            s.data(Tags.CONTACTS_LAST_NAME, cv.getAsString(StructuredName.FAMILY_NAME));
+        }
+        if (cv.containsKey(StructuredName.GIVEN_NAME)) {
+            s.data(Tags.CONTACTS_FIRST_NAME, cv.getAsString(StructuredName.GIVEN_NAME));
+        }
+        if (cv.containsKey(StructuredName.MIDDLE_NAME)) {
+            s.data(Tags.CONTACTS_MIDDLE_NAME, cv.getAsString(StructuredName.MIDDLE_NAME));
+        }
+        if (cv.containsKey(StructuredName.SUFFIX)) {
+            s.data(Tags.CONTACTS_SUFFIX, cv.getAsString(StructuredName.SUFFIX));
+        }
+        if (cv.containsKey(StructuredName.PHONETIC_GIVEN_NAME)) {
+            s.data(Tags.CONTACTS_YOMI_FIRST_NAME,
+                    cv.getAsString(StructuredName.PHONETIC_GIVEN_NAME));
+        }
+        if (cv.containsKey(StructuredName.PHONETIC_FAMILY_NAME)) {
+            s.data(Tags.CONTACTS_YOMI_LAST_NAME,
+                    cv.getAsString(StructuredName.PHONETIC_FAMILY_NAME));
+        }
+        if (cv.containsKey(StructuredName.PREFIX)) {
+            s.data(Tags.CONTACTS_TITLE, cv.getAsString(StructuredName.PREFIX));
+        }
+        if (cv.containsKey(StructuredName.DISPLAY_NAME)) {
+            displayName = cv.getAsString(StructuredName.DISPLAY_NAME);
+            s.data(Tags.CONTACTS_FILE_AS, displayName);
+        }
+        return displayName;
+    }
+
+    private void sendBusiness(Serializer s, ContentValues cv) throws IOException {
+        if (cv.containsKey(EasBusiness.ACCOUNT_NAME)) {
+            s.data(Tags.CONTACTS2_ACCOUNT_NAME, cv.getAsString(EasBusiness.ACCOUNT_NAME));
+        }
+        if (cv.containsKey(EasBusiness.CUSTOMER_ID)) {
+            s.data(Tags.CONTACTS2_CUSTOMER_ID, cv.getAsString(EasBusiness.CUSTOMER_ID));
+        }
+        if (cv.containsKey(EasBusiness.GOVERNMENT_ID)) {
+            s.data(Tags.CONTACTS2_GOVERNMENT_ID, cv.getAsString(EasBusiness.GOVERNMENT_ID));
+        }
+    }
+
+    private void sendPersonal(Serializer s, ContentValues cv) throws IOException {
+        if (cv.containsKey(EasPersonal.ANNIVERSARY)) {
+            s.data(Tags.CONTACTS_ANNIVERSARY, cv.getAsString(EasPersonal.ANNIVERSARY));
+        }
+        if (cv.containsKey(EasPersonal.FILE_AS)) {
+            s.data(Tags.CONTACTS_FILE_AS, cv.getAsString(EasPersonal.FILE_AS));
+        }
+    }
+
+    private void sendBirthday(Serializer s, ContentValues cv) throws IOException {
+        if (cv.containsKey(Event.START_DATE)) {
+            s.data(Tags.CONTACTS_BIRTHDAY, cv.getAsString(Event.START_DATE));
+        }
+    }
+
+    private void sendOrganization(Serializer s, ContentValues cv) throws IOException {
+        if (cv.containsKey(Organization.TITLE)) {
+            s.data(Tags.CONTACTS_JOB_TITLE, cv.getAsString(Organization.TITLE));
+        }
+        if (cv.containsKey(Organization.COMPANY)) {
+            s.data(Tags.CONTACTS_COMPANY_NAME, cv.getAsString(Organization.COMPANY));
+        }
+        if (cv.containsKey(Organization.DEPARTMENT)) {
+            s.data(Tags.CONTACTS_DEPARTMENT, cv.getAsString(Organization.DEPARTMENT));
+        }
+        if (cv.containsKey(Organization.OFFICE_LOCATION)) {
+            s.data(Tags.CONTACTS_OFFICE_LOCATION, cv.getAsString(Organization.OFFICE_LOCATION));
+        }
+    }
+
+    private void sendNickname(Serializer s, ContentValues cv) throws IOException {
+        if (cv.containsKey(Nickname.NAME)) {
+            s.data(Tags.CONTACTS2_NICKNAME, cv.getAsString(Nickname.NAME));
+        }
+    }
+
+    private void sendWebpage(Serializer s, ContentValues cv) throws IOException {
+        if (cv.containsKey(Website.URL)) {
+            s.data(Tags.CONTACTS_WEBPAGE, cv.getAsString(Website.URL));
+        }
+    }
+
+    private void sendNote(Serializer s, ContentValues cv) throws IOException {
+        if (cv.containsKey(Note.NOTE)) {
+            // EAS won't accept note data with raw newline characters
+            String note = cv.getAsString(Note.NOTE).replaceAll("\n", "\r\n");
+            // Format of upsync data depends on protocol version
+            if (mService.mProtocolVersionDouble >= 12.0) {
+                s.start(Tags.BASE_BODY);
+                s.data(Tags.BASE_TYPE, Eas.BODY_PREFERENCE_TEXT).data(Tags.BASE_DATA, note);
+                s.end();
+            } else {
+                s.data(Tags.CONTACTS_BODY, note);
+            }
+        }
+    }
+
+    private void sendChildren(Serializer s, ContentValues cv) throws IOException {
+        boolean first = true;
+        for (int i = 0; i < EasChildren.MAX_CHILDREN; i++) {
+            String row = EasChildren.ROWS[i];
+            if (cv.containsKey(row)) {
+                if (first) {
+                    s.start(Tags.CONTACTS_CHILDREN);
+                    first = false;
+                }
+                s.data(Tags.CONTACTS_CHILD, cv.getAsString(row));
+            }
+        }
+        if (!first) {
+            s.end();
+        }
+    }
+
+    private void sendPhone(Serializer s, ContentValues cv, int workCount, int homeCount)
+            throws IOException {
+        String value = cv.getAsString(Phone.NUMBER);
+        if (value == null) return;
+        switch (cv.getAsInteger(Phone.TYPE)) {
+            case Phone.TYPE_WORK:
+                if (workCount < MAX_PHONE_ROWS) {
+                    s.data(WORK_PHONE_TAGS[workCount], value);
+                }
+                break;
+            case Phone.TYPE_MMS:
+                s.data(Tags.CONTACTS2_MMS, value);
+                break;
+            case Phone.TYPE_ASSISTANT:
+                s.data(Tags.CONTACTS_ASSISTANT_TELEPHONE_NUMBER, value);
+                break;
+            case Phone.TYPE_FAX_WORK:
+                s.data(Tags.CONTACTS_BUSINESS_FAX_NUMBER, value);
+                break;
+            case Phone.TYPE_COMPANY_MAIN:
+                s.data(Tags.CONTACTS2_COMPANY_MAIN_PHONE, value);
+                break;
+            case Phone.TYPE_HOME:
+                if (homeCount < MAX_PHONE_ROWS) {
+                    s.data(HOME_PHONE_TAGS[homeCount], value);
+                }
+                break;
+            case Phone.TYPE_MOBILE:
+                s.data(Tags.CONTACTS_MOBILE_TELEPHONE_NUMBER, value);
+                break;
+            case Phone.TYPE_CAR:
+                s.data(Tags.CONTACTS_CAR_TELEPHONE_NUMBER, value);
+                break;
+            case Phone.TYPE_PAGER:
+                s.data(Tags.CONTACTS_PAGER_NUMBER, value);
+                break;
+            case Phone.TYPE_RADIO:
+                s.data(Tags.CONTACTS_RADIO_TELEPHONE_NUMBER, value);
+                break;
+            case Phone.TYPE_FAX_HOME:
+                s.data(Tags.CONTACTS_HOME_FAX_NUMBER, value);
+                break;
+            default:
+                break;
+        }
+    }
+
+    private void sendRelation(Serializer s, ContentValues cv) throws IOException {
+        String value = cv.getAsString(Relation.DATA);
+        if (value == null) return;
+        switch (cv.getAsInteger(Relation.TYPE)) {
+            case Relation.TYPE_ASSISTANT:
+                s.data(Tags.CONTACTS_ASSISTANT_NAME, value);
+                break;
+            case Relation.TYPE_MANAGER:
+                s.data(Tags.CONTACTS2_MANAGER_NAME, value);
+                break;
+            case Relation.TYPE_SPOUSE:
+                s.data(Tags.CONTACTS_SPOUSE, value);
+                break;
+            default:
+                break;
+        }
+    }
+
+    @Override
+    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()
+                .appendQueryParameter(RawContacts.ACCOUNT_NAME, mAccount.mEmailAddress)
+                .appendQueryParameter(RawContacts.ACCOUNT_TYPE, Eas.ACCOUNT_MANAGER_TYPE)
+                .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
+                .build();
+
+        if (getSyncKey().equals("0")) {
+            return false;
+        }
+
+        try {
+            // Get them all atomically
+            EntityIterator ei = cr.queryEntities(uri, RawContacts.DIRTY + "=1", null, null);
+            ContentValues cidValues = new ContentValues();
+            try {
+                boolean first = true;
+                final Uri rawContactUri = addCallerIsSyncAdapterParameter(RawContacts.CONTENT_URI);
+                while (ei.hasNext()) {
+                    Entity entity = ei.next();
+                    // For each of these entities, create the change commands
+                    ContentValues entityValues = entity.getEntityValues();
+                    String serverId = entityValues.getAsString(RawContacts.SOURCE_ID);
+                    ArrayList<Integer> groupIds = new ArrayList<Integer>();
+                    if (first) {
+                        s.start(Tags.SYNC_COMMANDS);
+                        userLog("Sending Contacts changes to the server");
+                        first = false;
+                    }
+                    if (serverId == null) {
+                        // This is a new contact; create a clientId
+                        String clientId = "new_" + mMailbox.mId + '_' + System.currentTimeMillis();
+                        userLog("Creating new contact with clientId: ", clientId);
+                        s.start(Tags.SYNC_ADD).data(Tags.SYNC_CLIENT_ID, clientId);
+                        // And save it in the raw contact
+                        cidValues.put(ContactsContract.RawContacts.SYNC1, clientId);
+                        cr.update(ContentUris.
+                                withAppendedId(rawContactUri,
+                                        entityValues.getAsLong(ContactsContract.RawContacts._ID)),
+                                        cidValues, null, null);
+                    } else {
+                        if (entityValues.getAsInteger(RawContacts.DELETED) == 1) {
+                            userLog("Deleting contact with serverId: ", serverId);
+                            s.start(Tags.SYNC_DELETE).data(Tags.SYNC_SERVER_ID, serverId).end();
+                            mDeletedIdList.add(entityValues.getAsLong(RawContacts._ID));
+                            continue;
+                        }
+                        userLog("Upsync change to contact with serverId: " + serverId);
+                        s.start(Tags.SYNC_CHANGE).data(Tags.SYNC_SERVER_ID, serverId);
+                    }
+                    s.start(Tags.SYNC_APPLICATION_DATA);
+                    // Write out the data here
+                    int imCount = 0;
+                    int emailCount = 0;
+                    int homePhoneCount = 0;
+                    int workPhoneCount = 0;
+                    String displayName = null;
+                    ArrayList<ContentValues> emailValues = new ArrayList<ContentValues>();
+                    for (NamedContentValues ncv: entity.getSubValues()) {
+                        ContentValues cv = ncv.values;
+                        String mimeType = cv.getAsString(Data.MIMETYPE);
+                        if (mimeType.equals(Email.CONTENT_ITEM_TYPE)) {
+                            emailValues.add(cv);
+                        } else if (mimeType.equals(Nickname.CONTENT_ITEM_TYPE)) {
+                            sendNickname(s, cv);
+                        } else if (mimeType.equals(EasChildren.CONTENT_ITEM_TYPE)) {
+                            sendChildren(s, cv);
+                        } else if (mimeType.equals(EasBusiness.CONTENT_ITEM_TYPE)) {
+                            sendBusiness(s, cv);
+                        } else if (mimeType.equals(Website.CONTENT_ITEM_TYPE)) {
+                            sendWebpage(s, cv);
+                        } else if (mimeType.equals(EasPersonal.CONTENT_ITEM_TYPE)) {
+                            sendPersonal(s, cv);
+                        } else if (mimeType.equals(Phone.CONTENT_ITEM_TYPE)) {
+                            sendPhone(s, cv, workPhoneCount, homePhoneCount);
+                            int type = cv.getAsInteger(Phone.TYPE);
+                            if (type == Phone.TYPE_HOME) homePhoneCount++;
+                            if (type == Phone.TYPE_WORK) workPhoneCount++;
+                        } else if (mimeType.equals(Relation.CONTENT_ITEM_TYPE)) {
+                            sendRelation(s, cv);
+                        } else if (mimeType.equals(StructuredName.CONTENT_ITEM_TYPE)) {
+                            displayName = sendStructuredName(s, cv);
+                        } else if (mimeType.equals(StructuredPostal.CONTENT_ITEM_TYPE)) {
+                            sendStructuredPostal(s, cv);
+                        } else if (mimeType.equals(Organization.CONTENT_ITEM_TYPE)) {
+                            sendOrganization(s, cv);
+                        } else if (mimeType.equals(Im.CONTENT_ITEM_TYPE)) {
+                            sendIm(s, cv, imCount++);
+                        } else if (mimeType.equals(Event.CONTENT_ITEM_TYPE)) {
+                            Integer eventType = cv.getAsInteger(Event.TYPE);
+                            if (eventType != null && eventType.equals(Event.TYPE_BIRTHDAY)) {
+                                sendBirthday(s, cv);
+                            }
+                        } else if (mimeType.equals(GroupMembership.CONTENT_ITEM_TYPE)) {
+                            // We must gather these, and send them together (below)
+                            groupIds.add(cv.getAsInteger(GroupMembership.GROUP_ROW_ID));
+                        } 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.
+                        } else {
+                            userLog("Contacts upsync, unknown data: ", mimeType);
+                        }
+                    }
+
+                    // We do the email rows last, because we need to make sure we've found the
+                    // displayName (if one exists); this would be in a StructuredName rnow
+                    for (ContentValues cv: emailValues) {
+                        sendEmail(s, cv, emailCount++, displayName);
+                    }
+                    
+                    // Now, we'll send up groups, if any
+                    if (!groupIds.isEmpty()) {
+                        boolean groupFirst = true;
+                        for (int id: groupIds) {
+                            // Since we get id's from the provider, we need to find their names
+                            Cursor c = cr.query(ContentUris.withAppendedId(Groups.CONTENT_URI, id),
+                                    GROUP_PROJECTION, null, null, null);
+                            try {
+                                // Presumably, this should always succeed, but ...
+                                if (c.moveToFirst()) {
+                                    if (groupFirst) {
+                                        s.start(Tags.CONTACTS_CATEGORIES);
+                                        groupFirst = false;
+                                    }
+                                    s.data(Tags.CONTACTS_CATEGORY, c.getString(0));
+                                }
+                            } finally {
+                                c.close();
+                            }
+                        }
+                        if (!groupFirst) {
+                            s.end();
+                        }
+                    }
+                    s.end().end(); // ApplicationData & Change
+                    mUpdatedIdList.add(entityValues.getAsLong(RawContacts._ID));
+                }
+                if (!first) {
+                    s.end(); // Commands
+                }
+            } finally {
+                ei.close();
+            }
+        } catch (RemoteException e) {
+            Log.e(TAG, "Could not read dirty contacts.");
+        }
+
+        return false;
+    }
+}
diff --git a/src/com/android/exchange/adapter/EmailSyncAdapter.java b/src/com/android/exchange/adapter/EmailSyncAdapter.java
new file mode 100644
index 0000000..6a5d2a5
--- /dev/null
+++ b/src/com/android/exchange/adapter/EmailSyncAdapter.java
@@ -0,0 +1,716 @@
+/*
+ * 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.adapter;
+
+import com.android.email.mail.Address;
+import com.android.email.provider.AttachmentProvider;
+import com.android.email.provider.EmailContent;
+import com.android.email.provider.EmailProvider;
+import com.android.email.provider.EmailContent.Account;
+import com.android.email.provider.EmailContent.AccountColumns;
+import com.android.email.provider.EmailContent.Attachment;
+import com.android.email.provider.EmailContent.Mailbox;
+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.MailService;
+import com.android.exchange.Eas;
+import com.android.exchange.EasSyncService;
+
+import android.content.ContentProviderOperation;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.OperationApplicationException;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.RemoteException;
+import android.webkit.MimeTypeMap;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.GregorianCalendar;
+import java.util.TimeZone;
+
+/**
+ * Sync adapter for EAS email
+ *
+ */
+public class EmailSyncAdapter extends AbstractSyncAdapter {
+
+    private static final int UPDATES_READ_COLUMN = 0;
+    private static final int UPDATES_MAILBOX_KEY_COLUMN = 1;
+    private static final int UPDATES_SERVER_ID_COLUMN = 2;
+    private static final int UPDATES_FLAG_COLUMN = 3;
+    private static final String[] UPDATES_PROJECTION =
+        {MessageColumns.FLAG_READ, MessageColumns.MAILBOX_KEY, SyncColumns.SERVER_ID,
+            MessageColumns.FLAG_FAVORITE};
+    private static final String[] MESSAGE_ID_SUBJECT_PROJECTION =
+        new String[] { Message.RECORD_ID, MessageColumns.SUBJECT };
+
+
+    String[] bindArguments = new String[2];
+
+    ArrayList<Long> mDeletedIdList = new ArrayList<Long>();
+    ArrayList<Long> mUpdatedIdList = new ArrayList<Long>();
+
+    public EmailSyncAdapter(Mailbox mailbox, EasSyncService service) {
+        super(mailbox, service);
+    }
+
+    @Override
+    public boolean parse(InputStream is) throws IOException {
+        EasEmailSyncParser p = new EasEmailSyncParser(is, this);
+        return p.parse();
+    }
+
+    public class EasEmailSyncParser extends AbstractSyncParser {
+
+        private static final String WHERE_SERVER_ID_AND_MAILBOX_KEY =
+            SyncColumns.SERVER_ID + "=? and " + MessageColumns.MAILBOX_KEY + "=?";
+
+        private String mMailboxIdAsString;
+
+        ArrayList<Message> newEmails = new ArrayList<Message>();
+        ArrayList<Long> deletedEmails = new ArrayList<Long>();
+        ArrayList<ServerChange> changedEmails = new ArrayList<ServerChange>();
+
+        public EasEmailSyncParser(InputStream in, EmailSyncAdapter adapter) throws IOException {
+            super(in, adapter);
+            mMailboxIdAsString = Long.toString(mMailbox.mId);
+        }
+
+        @Override
+        public void wipe() {
+            mContentResolver.delete(Message.CONTENT_URI,
+                    Message.MAILBOX_KEY + "=" + mMailbox.mId, null);
+            mContentResolver.delete(Message.DELETED_CONTENT_URI,
+                    Message.MAILBOX_KEY + "=" + mMailbox.mId, null);
+            mContentResolver.delete(Message.UPDATED_CONTENT_URI,
+                    Message.MAILBOX_KEY + "=" + mMailbox.mId, null);
+        }
+
+        public void addData (Message msg) throws IOException {
+            ArrayList<Attachment> atts = new ArrayList<Attachment>();
+
+            while (nextTag(Tags.SYNC_APPLICATION_DATA) != END) {
+                switch (tag) {
+                    case Tags.EMAIL_ATTACHMENTS:
+                    case Tags.BASE_ATTACHMENTS: // BASE_ATTACHMENTS is used in EAS 12.0 and up
+                        attachmentsParser(atts, msg);
+                        break;
+                    case Tags.EMAIL_TO:
+                        msg.mTo = Address.pack(Address.parse(getValue()));
+                        break;
+                    case Tags.EMAIL_FROM:
+                        Address[] froms = Address.parse(getValue());
+                        if (froms != null && froms.length > 0) {
+                          msg.mDisplayName = froms[0].toFriendly();
+                        }
+                        msg.mFrom = Address.pack(froms);
+                        break;
+                    case Tags.EMAIL_CC:
+                        msg.mCc = Address.pack(Address.parse(getValue()));
+                        break;
+                    case Tags.EMAIL_REPLY_TO:
+                        msg.mReplyTo = Address.pack(Address.parse(getValue()));
+                        break;
+                    case Tags.EMAIL_DATE_RECEIVED:
+                        String date = getValue();
+                        // 2009-02-11T18:03:03.627Z
+                        GregorianCalendar cal = new GregorianCalendar();
+                        cal.set(Integer.parseInt(date.substring(0, 4)), Integer.parseInt(date
+                                .substring(5, 7)) - 1, Integer.parseInt(date.substring(8, 10)),
+                                Integer.parseInt(date.substring(11, 13)), Integer.parseInt(date
+                                        .substring(14, 16)), Integer.parseInt(date
+                                                .substring(17, 19)));
+                        cal.setTimeZone(TimeZone.getTimeZone("GMT"));
+                        msg.mTimeStamp = cal.getTimeInMillis();
+                        break;
+                    case Tags.EMAIL_SUBJECT:
+                        msg.mSubject = getValue();
+                        break;
+                    case Tags.EMAIL_READ:
+                        msg.mFlagRead = getValueInt() == 1;
+                        break;
+                    case Tags.BASE_BODY:
+                        bodyParser(msg);
+                        break;
+                    case Tags.EMAIL_FLAG:
+                        msg.mFlagFavorite = flagParser();
+                        break;
+                    case Tags.EMAIL_BODY:
+                        String text = getValue();
+                        msg.mText = text;
+                        break;
+                    default:
+                        skipTag();
+                }
+            }
+
+            if (atts.size() > 0) {
+                msg.mAttachments = atts;
+            }
+        }
+
+        private void addParser(ArrayList<Message> emails) throws IOException {
+            Message msg = new Message();
+            msg.mAccountKey = mAccount.mId;
+            msg.mMailboxKey = mMailbox.mId;
+            msg.mFlagLoaded = Message.FLAG_LOADED_COMPLETE;
+
+            while (nextTag(Tags.SYNC_ADD) != END) {
+                switch (tag) {
+                    case Tags.SYNC_SERVER_ID:
+                        msg.mServerId = getValue();
+                        break;
+                    case Tags.SYNC_APPLICATION_DATA:
+                        addData(msg);
+                        break;
+                    default:
+                        skipTag();
+                }
+            }
+            emails.add(msg);
+        }
+
+        // For now, we only care about the "active" state
+        private Boolean flagParser() throws IOException {
+            Boolean state = false;
+            while (nextTag(Tags.EMAIL_FLAG) != END) {
+                switch (tag) {
+                    case Tags.EMAIL_FLAG_STATUS:
+                        state = getValueInt() == 2;
+                        break;
+                    default:
+                        skipTag();
+                }
+            }
+            return state;
+        }
+
+        private void bodyParser(Message msg) throws IOException {
+            String bodyType = Eas.BODY_PREFERENCE_TEXT;
+            String body = "";
+            while (nextTag(Tags.EMAIL_BODY) != END) {
+                switch (tag) {
+                    case Tags.BASE_TYPE:
+                        bodyType = getValue();
+                        break;
+                    case Tags.BASE_DATA:
+                        body = getValue();
+                        break;
+                    default:
+                        skipTag();
+                }
+            }
+            // We always ask for TEXT or HTML; there's no third option
+            if (bodyType.equals(Eas.BODY_PREFERENCE_HTML)) {
+                msg.mHtml = body;
+            } else {
+                msg.mText = body;
+            }
+        }
+
+        private void attachmentsParser(ArrayList<Attachment> atts, Message msg) throws IOException {
+            while (nextTag(Tags.EMAIL_ATTACHMENTS) != END) {
+                switch (tag) {
+                    case Tags.EMAIL_ATTACHMENT:
+                    case Tags.BASE_ATTACHMENT:  // BASE_ATTACHMENT is used in EAS 12.0 and up
+                        attachmentParser(atts, msg);
+                        break;
+                    default:
+                        skipTag();
+                }
+            }
+        }
+
+        private void attachmentParser(ArrayList<Attachment> atts, Message msg) throws IOException {
+            String fileName = null;
+            String length = null;
+            String location = null;
+
+            while (nextTag(Tags.EMAIL_ATTACHMENT) != END) {
+                switch (tag) {
+                    // We handle both EAS 2.5 and 12.0+ attachments here
+                    case Tags.EMAIL_DISPLAY_NAME:
+                    case Tags.BASE_DISPLAY_NAME:
+                        fileName = getValue();
+                        break;
+                    case Tags.EMAIL_ATT_NAME:
+                    case Tags.BASE_FILE_REFERENCE:
+                        location = getValue();
+                        break;
+                    case Tags.EMAIL_ATT_SIZE:
+                    case Tags.BASE_ESTIMATED_DATA_SIZE:
+                        length = getValue();
+                        break;
+                    default:
+                        skipTag();
+                }
+            }
+
+            if ((fileName != null) && (length != null) && (location != null)) {
+                Attachment att = new Attachment();
+                att.mEncoding = "base64";
+                att.mSize = Long.parseLong(length);
+                att.mFileName = fileName;
+                att.mLocation = location;
+                att.mMimeType = getMimeTypeFromFileName(fileName);
+                atts.add(att);
+                msg.mFlagAttachment = true;
+            }
+        }
+
+        /**
+         * Try to determine a mime type from a file name, defaulting to application/x, where x
+         * is either the extension or (if none) octet-stream
+         * At the moment, this is somewhat lame, since many file types aren't recognized
+         * @param fileName the file name to ponder
+         * @return
+         */
+        // Note: The MimeTypeMap method currently uses a very limited set of mime types
+        // A bug has been filed against this issue.
+        public String getMimeTypeFromFileName(String fileName) {
+            String mimeType;
+            int lastDot = fileName.lastIndexOf('.');
+            String extension = null;
+            if ((lastDot > 0) && (lastDot < fileName.length() - 1)) {
+                extension = fileName.substring(lastDot + 1).toLowerCase();
+            }
+            if (extension == null) {
+                // A reasonable default for now.
+                mimeType = "application/octet-stream";
+            } else {
+                mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
+                if (mimeType == null) {
+                    mimeType = "application/" + extension;
+                }
+            }
+            return mimeType;
+        }
+
+        private Cursor getServerIdCursor(String serverId, String[] projection) {
+            bindArguments[0] = serverId;
+            bindArguments[1] = mMailboxIdAsString;
+            return mContentResolver.query(Message.CONTENT_URI, projection,
+                    WHERE_SERVER_ID_AND_MAILBOX_KEY, bindArguments, null);
+        }
+
+        private void deleteParser(ArrayList<Long> deletes, int entryTag) throws IOException {
+            while (nextTag(entryTag) != END) {
+                switch (tag) {
+                    case Tags.SYNC_SERVER_ID:
+                        String serverId = getValue();
+                        // Find the message in this mailbox with the given serverId
+                        Cursor c = getServerIdCursor(serverId, MESSAGE_ID_SUBJECT_PROJECTION);
+                        try {
+                            if (c.moveToFirst()) {
+                                deletes.add(c.getLong(0));
+                                if (Eas.USER_LOG) {
+                                    userLog("Deleting ", serverId + ", " + c.getString(1));
+                                }
+                            }
+                        } finally {
+                            c.close();
+                        }
+                        break;
+                    default:
+                        skipTag();
+                }
+            }
+        }
+
+        class ServerChange {
+            long id;
+            Boolean read;
+            Boolean flag;
+
+            ServerChange(long _id, Boolean _read, Boolean _flag) {
+                id = _id;
+                read = _read;
+                flag = _flag;
+            }
+        }
+
+        private void changeParser(ArrayList<ServerChange> changes) throws IOException {
+            String serverId = null;
+            Boolean oldRead = false;
+            Boolean oldFlag = false;
+            long id = 0;
+            while (nextTag(Tags.SYNC_CHANGE) != END) {
+                switch (tag) {
+                    case Tags.SYNC_SERVER_ID:
+                        serverId = getValue();
+                        Cursor c = getServerIdCursor(serverId, Message.LIST_PROJECTION);
+                        try {
+                            if (c.moveToFirst()) {
+                                userLog("Changing ", serverId);
+                                oldRead = c.getInt(Message.LIST_READ_COLUMN) == Message.READ;
+                                oldFlag = c.getInt(Message.LIST_FAVORITE_COLUMN) == 1;
+                                id = c.getLong(Message.LIST_ID_COLUMN);
+                            }
+                        } finally {
+                            c.close();
+                        }
+                        break;
+                    case Tags.SYNC_APPLICATION_DATA:
+                        changeApplicationDataParser(changes, oldRead, oldFlag, id);
+                        break;
+                    default:
+                        skipTag();
+                }
+            }
+        }
+
+        private void changeApplicationDataParser(ArrayList<ServerChange> changes, Boolean oldRead,
+                Boolean oldFlag, long id) throws IOException {
+            Boolean read = null;
+            Boolean flag = null;
+            while (nextTag(Tags.SYNC_APPLICATION_DATA) != END) {
+                switch (tag) {
+                    case Tags.EMAIL_READ:
+                        read = getValueInt() == 1;
+                        break;
+                    case Tags.EMAIL_FLAG:
+                        flag = flagParser();
+                        break;
+                    default:
+                        skipTag();
+                }
+            }
+            if (((read != null) && !oldRead.equals(read)) ||
+                    ((flag != null) && !oldFlag.equals(flag))) {
+                changes.add(new ServerChange(id, read, flag));
+            }
+        }
+
+        /* (non-Javadoc)
+         * @see com.android.exchange.adapter.EasContentParser#commandsParser()
+         */
+        @Override
+        public void commandsParser() throws IOException {
+            while (nextTag(Tags.SYNC_COMMANDS) != END) {
+                if (tag == Tags.SYNC_ADD) {
+                    addParser(newEmails);
+                    incrementChangeCount();
+                } else if (tag == Tags.SYNC_DELETE || tag == Tags.SYNC_SOFT_DELETE) {
+                    deleteParser(deletedEmails, tag);
+                    incrementChangeCount();
+                } else if (tag == Tags.SYNC_CHANGE) {
+                    changeParser(changedEmails);
+                    incrementChangeCount();
+                } else
+                    skipTag();
+            }
+        }
+
+        @Override
+        public void responsesParser() {
+        }
+
+        @Override
+        public void commit() {
+            int notifyCount = 0;
+
+            // Use a batch operation to handle the changes
+            // TODO New mail notifications?  Who looks for these?
+            ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();
+            for (Message msg: newEmails) {
+                if (!msg.mFlagRead) {
+                    notifyCount++;
+                }
+                msg.addSaveOps(ops);
+            }
+            for (Long id : deletedEmails) {
+                ops.add(ContentProviderOperation.newDelete(
+                        ContentUris.withAppendedId(Message.CONTENT_URI, id)).build());
+                AttachmentProvider.deleteAllAttachmentFiles(mContext, mAccount.mId, id);
+            }
+            if (!changedEmails.isEmpty()) {
+                // Server wins in a conflict...
+                for (ServerChange change : changedEmails) {
+                     ContentValues cv = new ContentValues();
+                    if (change.read != null) {
+                        cv.put(MessageColumns.FLAG_READ, change.read);
+                    }
+                    if (change.flag != null) {
+                        cv.put(MessageColumns.FLAG_FAVORITE, change.flag);
+                    }
+                    ops.add(ContentProviderOperation.newUpdate(
+                            ContentUris.withAppendedId(Message.CONTENT_URI, change.id))
+                                .withValues(cv)
+                                .build());
+                }
+            }
+
+            // We only want to update the sync key here
+            ContentValues mailboxValues = new ContentValues();
+            mailboxValues.put(Mailbox.SYNC_KEY, mMailbox.mSyncKey);
+            ops.add(ContentProviderOperation.newUpdate(
+                    ContentUris.withAppendedId(Mailbox.CONTENT_URI, mMailbox.mId))
+                        .withValues(mailboxValues).build());
+
+            addCleanupOps(ops);
+
+            // No commits if we're stopped
+            synchronized (mService.getSynchronizer()) {
+                if (mService.isStopped()) return;
+                try {
+                    mContentResolver.applyBatch(EmailProvider.EMAIL_AUTHORITY, ops);
+                    userLog(mMailbox.mDisplayName, " SyncKey saved as: ", mMailbox.mSyncKey);
+                } catch (RemoteException e) {
+                    // There is nothing to be done here; fail by returning null
+                } catch (OperationApplicationException e) {
+                    // There is nothing to be done here; fail by returning null
+                }
+            }
+
+            if (notifyCount > 0) {
+                // Use the new atomic add URI in EmailProvider
+                // We could add this to the operations being done, but it's not strictly
+                // speaking necessary, as the previous batch preserves the integrity of the
+                // database, whereas this is purely for notification purposes, and is itself atomic
+                ContentValues cv = new ContentValues();
+                cv.put(EmailContent.FIELD_COLUMN_NAME, AccountColumns.NEW_MESSAGE_COUNT);
+                cv.put(EmailContent.ADD_COLUMN_NAME, notifyCount);
+                Uri uri = ContentUris.withAppendedId(Account.ADD_TO_FIELD_URI, mAccount.mId);
+                mContentResolver.update(uri, cv, null, null);
+                MailService.actionNotifyNewMessages(mContext, mAccount.mId);
+            }
+        }
+    }
+
+    @Override
+    public String getCollectionName() {
+        return "Email";
+    }
+
+    private void addCleanupOps(ArrayList<ContentProviderOperation> ops) {
+        // If we've sent local deletions, clear out the deleted table
+        for (Long id: mDeletedIdList) {
+            ops.add(ContentProviderOperation.newDelete(
+                    ContentUris.withAppendedId(Message.DELETED_CONTENT_URI, id)).build());
+        }
+        // And same with the updates
+        for (Long id: mUpdatedIdList) {
+            ops.add(ContentProviderOperation.newDelete(
+                    ContentUris.withAppendedId(Message.UPDATED_CONTENT_URI, id)).build());
+        }
+    }
+
+    @Override
+    public void cleanup() {
+        if (!mDeletedIdList.isEmpty() || !mUpdatedIdList.isEmpty()) {
+            ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();
+            addCleanupOps(ops);
+            try {
+                mContext.getContentResolver()
+                    .applyBatch(EmailProvider.EMAIL_AUTHORITY, ops);
+            } catch (RemoteException e) {
+                // There is nothing to be done here; fail by returning null
+            } catch (OperationApplicationException e) {
+                // There is nothing to be done here; fail by returning null
+            }
+        }
+    }
+
+    private String formatTwo(int num) {
+        if (num < 10) {
+            return "0" + (char)('0' + num);
+        } else
+            return Integer.toString(num);
+    }
+
+    /**
+     * Create date/time in RFC8601 format.  Oddly enough, for calendar date/time, Microsoft uses
+     * a different format that excludes the punctuation (this is why I'm not putting this in a
+     * parent class)
+     */
+    public String formatDateTime(Calendar calendar) {
+        StringBuilder sb = new StringBuilder();
+        //YYYY-MM-DDTHH:MM:SS.MSSZ
+        sb.append(calendar.get(Calendar.YEAR));
+        sb.append('-');
+        sb.append(formatTwo(calendar.get(Calendar.MONTH) + 1));
+        sb.append('-');
+        sb.append(formatTwo(calendar.get(Calendar.DAY_OF_MONTH)));
+        sb.append('T');
+        sb.append(formatTwo(calendar.get(Calendar.HOUR_OF_DAY)));
+        sb.append(':');
+        sb.append(formatTwo(calendar.get(Calendar.MINUTE)));
+        sb.append(':');
+        sb.append(formatTwo(calendar.get(Calendar.SECOND)));
+        sb.append(".000Z");
+        return sb.toString();
+    }
+
+    @Override
+    public boolean sendLocalChanges(Serializer s) throws IOException {
+        ContentResolver cr = mContext.getContentResolver();
+
+        // Never upsync from these folders
+        if (mMailbox.mType == Mailbox.TYPE_DRAFTS || mMailbox.mType == Mailbox.TYPE_OUTBOX) {
+            return false;
+        }
+
+        // Find any of our deleted items
+        Cursor c = cr.query(Message.DELETED_CONTENT_URI, Message.LIST_PROJECTION,
+                MessageColumns.MAILBOX_KEY + '=' + mMailbox.mId, null, null);
+        boolean first = true;
+        // We keep track of the list of deleted item id's so that we can remove them from the
+        // deleted table after the server receives our command
+        mDeletedIdList.clear();
+        try {
+            while (c.moveToNext()) {
+                String serverId = c.getString(Message.LIST_SERVER_ID_COLUMN);
+                // Keep going if there's no serverId
+                if (serverId == null) {
+                    continue;
+                } else if (first) {
+                    s.start(Tags.SYNC_COMMANDS);
+                    first = false;
+                }
+                // Send the command to delete this message
+                s.start(Tags.SYNC_DELETE).data(Tags.SYNC_SERVER_ID, serverId).end();
+                mDeletedIdList.add(c.getLong(Message.LIST_ID_COLUMN));
+            }
+        } finally {
+            c.close();
+        }
+
+        // Find our trash mailbox, since deletions will have been moved there...
+        long trashMailboxId =
+            Mailbox.findMailboxOfType(mContext, mMailbox.mAccountKey, Mailbox.TYPE_TRASH);
+
+        // Do the same now for updated items
+        c = cr.query(Message.UPDATED_CONTENT_URI, Message.LIST_PROJECTION,
+                MessageColumns.MAILBOX_KEY + '=' + mMailbox.mId, null, null);
+
+        // We keep track of the list of updated item id's as we did above with deleted items
+        mUpdatedIdList.clear();
+        try {
+            while (c.moveToNext()) {
+                long id = c.getLong(Message.LIST_ID_COLUMN);
+                // Say we've handled this update
+                mUpdatedIdList.add(id);
+                // We have the id of the changed item.  But first, we have to find out its current
+                // state, since the updated table saves the opriginal state
+                Cursor currentCursor = cr.query(ContentUris.withAppendedId(Message.CONTENT_URI, id),
+                        UPDATES_PROJECTION, null, null, null);
+                try {
+                    // If this item no longer exists (shouldn't be possible), just move along
+                    if (!currentCursor.moveToFirst()) {
+                         continue;
+                    }
+                    // Keep going if there's no serverId
+                    String serverId = currentCursor.getString(UPDATES_SERVER_ID_COLUMN);
+                    if (serverId == null) {
+                        continue;
+                    }
+                    // If the message is now in the trash folder, it has been deleted by the user
+                    if (currentCursor.getLong(UPDATES_MAILBOX_KEY_COLUMN) == trashMailboxId) {
+                         if (first) {
+                            s.start(Tags.SYNC_COMMANDS);
+                            first = false;
+                        }
+                        // Send the command to delete this message
+                        s.start(Tags.SYNC_DELETE).data(Tags.SYNC_SERVER_ID, serverId).end();
+                        continue;
+                    }
+
+                    boolean flagChange = false;
+                    boolean readChange = false;
+
+                    int flag = 0;
+
+                    // We can only send flag changes to the server in 12.0 or later
+                    if (mService.mProtocolVersionDouble >= 12.0) {
+                        flag = currentCursor.getInt(UPDATES_FLAG_COLUMN);
+                        if (flag != c.getInt(Message.LIST_FAVORITE_COLUMN)) {
+                            flagChange = true;
+                        }
+                    }
+
+                    int read = currentCursor.getInt(UPDATES_READ_COLUMN);
+                    if (read != c.getInt(Message.LIST_READ_COLUMN)) {
+                        readChange = true;
+                    }
+
+                    if (!flagChange && !readChange) {
+                        // In this case, we've got nothing to send to the server
+                        continue;
+                    }
+
+                    if (first) {
+                        s.start(Tags.SYNC_COMMANDS);
+                        first = false;
+                    }
+                    // Send the change to "read" and "favorite" (flagged)
+                    s.start(Tags.SYNC_CHANGE)
+                        .data(Tags.SYNC_SERVER_ID, c.getString(Message.LIST_SERVER_ID_COLUMN))
+                        .start(Tags.SYNC_APPLICATION_DATA);
+                    if (readChange) {
+                        s.data(Tags.EMAIL_READ, Integer.toString(read));
+                    }
+                    // "Flag" is a relatively complex concept in EAS 12.0 and above.  It is not only
+                    // the boolean "favorite" that we think of in Gmail, but it also represents a
+                    // follow up action, which can include a subject, start and due dates, and even
+                    // recurrences.  We don't support any of this as yet, but EAS 12.0 and higher
+                    // require that a flag contain a status, a type, and four date fields, two each
+                    // for start date and end (due) date.
+                    if (flagChange) {
+                        if (flag != 0) {
+                            // Status 2 = set flag
+                            s.start(Tags.EMAIL_FLAG).data(Tags.EMAIL_FLAG_STATUS, "2");
+                            // "FollowUp" is the standard type
+                            s.data(Tags.EMAIL_FLAG_TYPE, "FollowUp");
+                            long now = System.currentTimeMillis();
+                            Calendar calendar =
+                                GregorianCalendar.getInstance(TimeZone.getTimeZone("GMT"));
+                            calendar.setTimeInMillis(now);
+                            // Flags are required to have a start date and end date (duplicated)
+                            // First, we'll set the current date/time in GMT as the start time
+                            String utc = formatDateTime(calendar);
+                            s.data(Tags.TASK_START_DATE, utc).data(Tags.TASK_UTC_START_DATE, utc);
+                            // And then we'll use one week from today for completion date
+                            calendar.setTimeInMillis(now + 1*WEEKS);
+                            utc = formatDateTime(calendar);
+                            s.data(Tags.TASK_DUE_DATE, utc).data(Tags.TASK_UTC_DUE_DATE, utc);
+                            s.end();
+                        } else {
+                            s.tag(Tags.EMAIL_FLAG);
+                        }
+                    }
+                    s.end().end(); // SYNC_APPLICATION_DATA, SYNC_CHANGE
+                } finally {
+                    currentCursor.close();
+                }
+            }
+        } finally {
+            c.close();
+        }
+
+        if (!first) {
+            s.end(); // SYNC_COMMANDS
+        }
+        return false;
+    }
+}
diff --git a/src/com/android/exchange/adapter/FolderSyncParser.java b/src/com/android/exchange/adapter/FolderSyncParser.java
new file mode 100644
index 0000000..337e329
--- /dev/null
+++ b/src/com/android/exchange/adapter/FolderSyncParser.java
@@ -0,0 +1,403 @@
+/*
+ * 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.adapter;
+
+import com.android.email.provider.AttachmentProvider;
+import com.android.email.provider.EmailContent;
+import com.android.email.provider.EmailProvider;
+import com.android.email.provider.EmailContent.Account;
+import com.android.email.provider.EmailContent.AccountColumns;
+import com.android.email.provider.EmailContent.Mailbox;
+import com.android.email.provider.EmailContent.MailboxColumns;
+import com.android.exchange.Eas;
+import com.android.exchange.MockParserStream;
+import com.android.exchange.SyncManager;
+
+import android.content.ContentProviderOperation;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.OperationApplicationException;
+import android.database.Cursor;
+import android.os.RemoteException;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Parse the result of a FolderSync command
+ *
+ * Handles the addition, deletion, and changes to folders in the user's Exchange account.
+ **/
+
+public class FolderSyncParser extends AbstractSyncParser {
+
+    public static final String TAG = "FolderSyncParser";
+
+    // These are defined by the EAS protocol
+    public static final int USER_FOLDER_TYPE = 1;
+    public static final int INBOX_TYPE = 2;
+    public static final int DRAFTS_TYPE = 3;
+    public static final int DELETED_TYPE = 4;
+    public static final int SENT_TYPE = 5;
+    public static final int OUTBOX_TYPE = 6;
+    public static final int TASKS_TYPE = 7;
+    public static final int CALENDAR_TYPE = 8;
+    public static final int CONTACTS_TYPE = 9;
+    public static final int NOTES_TYPE = 10;
+    public static final int JOURNAL_TYPE = 11;
+    public static final int USER_MAILBOX_TYPE = 12;
+
+    public static final List<Integer> mValidFolderTypes = Arrays.asList(INBOX_TYPE, DRAFTS_TYPE,
+            DELETED_TYPE, SENT_TYPE, OUTBOX_TYPE, USER_MAILBOX_TYPE, CALENDAR_TYPE, CONTACTS_TYPE);
+
+    public static final String ALL_BUT_ACCOUNT_MAILBOX = MailboxColumns.ACCOUNT_KEY + "=? and " +
+        MailboxColumns.TYPE + "!=" + Mailbox.TYPE_EAS_ACCOUNT_MAILBOX;
+
+   private static final String WHERE_SERVER_ID_AND_ACCOUNT = MailboxColumns.SERVER_ID + "=? and " +
+        MailboxColumns.ACCOUNT_KEY + "=?";
+
+    private static final String WHERE_DISPLAY_NAME_AND_ACCOUNT = MailboxColumns.DISPLAY_NAME +
+        "=? and " + MailboxColumns.ACCOUNT_KEY + "=?";
+
+    private static final String WHERE_PARENT_SERVER_ID_AND_ACCOUNT =
+        MailboxColumns.PARENT_SERVER_ID +"=? and " + MailboxColumns.ACCOUNT_KEY + "=?";
+
+    private static final String[] MAILBOX_ID_COLUMNS_PROJECTION =
+        new String[] {MailboxColumns.ID, MailboxColumns.SERVER_ID};
+
+    private long mAccountId;
+    private String mAccountIdAsString;
+    private MockParserStream mMock = null;
+    private String[] mBindArguments = new String[2];
+
+    public FolderSyncParser(InputStream in, AbstractSyncAdapter adapter) throws IOException {
+        super(in, adapter);
+        mAccountId = mAccount.mId;
+        mAccountIdAsString = Long.toString(mAccountId);
+        if (in instanceof MockParserStream) {
+            mMock = (MockParserStream)in;
+        }
+    }
+
+    @Override
+    public boolean parse() throws IOException {
+        int status;
+        boolean res = false;
+        boolean resetFolders = false;
+        if (nextTag(START_DOCUMENT) != Tags.FOLDER_FOLDER_SYNC)
+            throw new EasParserException();
+        while (nextTag(START_DOCUMENT) != END_DOCUMENT) {
+            if (tag == Tags.FOLDER_STATUS) {
+                status = getValueInt();
+                if (status != Eas.FOLDER_STATUS_OK) {
+                    mService.errorLog("FolderSync failed: " + status);
+                    if (status == Eas.FOLDER_STATUS_INVALID_KEY) {
+                        mAccount.mSyncKey = "0";
+                        mService.errorLog("Bad sync key; RESET and delete all folders");
+                        mContentResolver.delete(Mailbox.CONTENT_URI, ALL_BUT_ACCOUNT_MAILBOX,
+                                new String[] {Long.toString(mAccountId)});
+                        // Stop existing syncs and reconstruct _main
+                        SyncManager.folderListReloaded(mAccountId);
+                        res = true;
+                        resetFolders = true;
+                    } else {
+                        // Other errors are at the server, so let's throw an error that will
+                        // cause this sync to be retried at a later time
+                        mService.errorLog("Throwing IOException; will retry later");
+                        throw new EasParserException("Folder status error");
+                    }
+                }
+            } else if (tag == Tags.FOLDER_SYNC_KEY) {
+                mAccount.mSyncKey = getValue();
+                userLog("New Account SyncKey: ", mAccount.mSyncKey);
+            } else if (tag == Tags.FOLDER_CHANGES) {
+                changesParser();
+            } else
+                skipTag();
+        }
+        synchronized (mService.getSynchronizer()) {
+            if (!mService.isStopped() || resetFolders) {
+                ContentValues cv = new ContentValues();
+                cv.put(AccountColumns.SYNC_KEY, mAccount.mSyncKey);
+                mAccount.update(mContext, cv);
+                userLog("Leaving FolderSyncParser with Account syncKey=", mAccount.mSyncKey);
+            }
+        }
+        return res;
+    }
+
+    private Cursor getServerIdCursor(String serverId) {
+        mBindArguments[0] = serverId;
+        mBindArguments[1] = mAccountIdAsString;
+        return mContentResolver.query(Mailbox.CONTENT_URI, EmailContent.ID_PROJECTION,
+                WHERE_SERVER_ID_AND_ACCOUNT, mBindArguments, null);
+    }
+
+    public void deleteParser(ArrayList<ContentProviderOperation> ops) throws IOException {
+        while (nextTag(Tags.FOLDER_DELETE) != END) {
+            switch (tag) {
+                case Tags.FOLDER_SERVER_ID:
+                    String serverId = getValue();
+                    // Find the mailbox in this account with the given serverId
+                    Cursor c = getServerIdCursor(serverId);
+                    try {
+                        if (c.moveToFirst()) {
+                            userLog("Deleting ", serverId);
+                            ops.add(ContentProviderOperation.newDelete(
+                                    ContentUris.withAppendedId(Mailbox.CONTENT_URI,
+                                            c.getLong(0))).build());
+                            AttachmentProvider.deleteAllMailboxAttachmentFiles(mContext,
+                                    mAccountId, mMailbox.mId);
+                        }
+                    } finally {
+                        c.close();
+                    }
+                    break;
+                default:
+                    skipTag();
+            }
+        }
+    }
+
+    public void addParser(ArrayList<ContentProviderOperation> ops) throws IOException {
+        String name = null;
+        String serverId = null;
+        String parentId = null;
+        int type = 0;
+
+        while (nextTag(Tags.FOLDER_ADD) != END) {
+            switch (tag) {
+                case Tags.FOLDER_DISPLAY_NAME: {
+                    name = getValue();
+                    break;
+                }
+                case Tags.FOLDER_TYPE: {
+                    type = getValueInt();
+                    break;
+                }
+                case Tags.FOLDER_PARENT_ID: {
+                    parentId = getValue();
+                    break;
+                }
+                case Tags.FOLDER_SERVER_ID: {
+                    serverId = getValue();
+                    break;
+                }
+                default:
+                    skipTag();
+            }
+        }
+        if (mValidFolderTypes.contains(type)) {
+            Mailbox m = new Mailbox();
+            m.mDisplayName = name;
+            m.mServerId = serverId;
+            m.mAccountKey = mAccountId;
+            m.mType = Mailbox.TYPE_MAIL;
+            // Note that all mailboxes default to checking "never" (i.e. manual sync only)
+            // We set specific intervals for inbox, contacts, and (eventually) calendar
+            m.mSyncInterval = Mailbox.CHECK_INTERVAL_NEVER;
+            switch (type) {
+                case INBOX_TYPE:
+                    m.mType = Mailbox.TYPE_INBOX;
+                    m.mSyncInterval = mAccount.mSyncInterval;
+                    break;
+                case CONTACTS_TYPE:
+                    m.mType = Mailbox.TYPE_CONTACTS;
+                    m.mSyncInterval = mAccount.mSyncInterval;
+                    break;
+                case OUTBOX_TYPE:
+                    // TYPE_OUTBOX mailboxes are known by SyncManager to sync whenever they aren't
+                    // empty.  The value of mSyncFrequency is ignored for this kind of mailbox.
+                    m.mType = Mailbox.TYPE_OUTBOX;
+                    break;
+                case SENT_TYPE:
+                    m.mType = Mailbox.TYPE_SENT;
+                    break;
+                case DRAFTS_TYPE:
+                    m.mType = Mailbox.TYPE_DRAFTS;
+                    break;
+                case DELETED_TYPE:
+                    m.mType = Mailbox.TYPE_TRASH;
+                    break;
+                case CALENDAR_TYPE:
+                    m.mType = Mailbox.TYPE_CALENDAR;
+                    // For now, no sync, since it's not yet implemented
+                    break;
+            }
+
+            // Make boxes like Contacts and Calendar invisible in the folder list
+            m.mFlagVisible = (m.mType < Mailbox.TYPE_NOT_EMAIL);
+
+            if (!parentId.equals("0")) {
+                m.mParentServerId = parentId;
+            }
+
+            userLog("Adding mailbox: ", m.mDisplayName);
+            ops.add(ContentProviderOperation
+                    .newInsert(Mailbox.CONTENT_URI).withValues(m.toContentValues()).build());
+        }
+
+        return;
+    }
+
+    public void updateParser(ArrayList<ContentProviderOperation> ops) throws IOException {
+        String serverId = null;
+        String displayName = null;
+        String parentId = null;
+        while (nextTag(Tags.FOLDER_UPDATE) != END) {
+            switch (tag) {
+                case Tags.FOLDER_SERVER_ID:
+                    serverId = getValue();
+                    break;
+                case Tags.FOLDER_DISPLAY_NAME:
+                    displayName = getValue();
+                    break;
+                case Tags.FOLDER_PARENT_ID:
+                    parentId = getValue();
+                    break;
+                default:
+                    skipTag();
+                    break;
+            }
+        }
+        // We'll make a change if one of parentId or displayName are specified
+        // serverId is required, but let's be careful just the same
+        if (serverId != null && (displayName != null || parentId != null)) {
+            Cursor c = getServerIdCursor(serverId);
+            try {
+                // If we find the mailbox (using serverId), make the change
+                if (c.moveToFirst()) {
+                    userLog("Updating ", serverId);
+                    ContentValues cv = new ContentValues();
+                    if (displayName != null) {
+                        cv.put(Mailbox.DISPLAY_NAME, displayName);
+                    }
+                    if (parentId != null) {
+                        cv.put(Mailbox.PARENT_SERVER_ID, parentId);
+                    }
+                    ops.add(ContentProviderOperation.newUpdate(
+                            ContentUris.withAppendedId(Mailbox.CONTENT_URI,
+                                    c.getLong(0))).withValues(cv).build());
+                }
+            } finally {
+                c.close();
+            }
+        }
+    }
+
+    public void changesParser() throws IOException {
+        // Keep track of new boxes, deleted boxes, updated boxes
+        ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();
+
+        while (nextTag(Tags.FOLDER_CHANGES) != END) {
+            if (tag == Tags.FOLDER_ADD) {
+                addParser(ops);
+            } else if (tag == Tags.FOLDER_DELETE) {
+                deleteParser(ops);
+            } else if (tag == Tags.FOLDER_UPDATE) {
+                updateParser(ops);
+            } else if (tag == Tags.FOLDER_COUNT) {
+                getValueInt();
+            } else
+                skipTag();
+        }
+
+        // The mock stream is used for junit tests, so that the parsing code can be tested
+        // separately from the provider code.
+        // TODO Change tests to not require this; remove references to the mock stream
+        if (mMock != null) {
+            mMock.setResult(null);
+            return;
+        }
+
+        // Create the new mailboxes in a single batch operation
+        // Don't save any data if the service has been stopped
+        synchronized (mService.getSynchronizer()) {
+            if (!ops.isEmpty() && !mService.isStopped()) {
+                userLog("Applying ", ops.size(), " mailbox operations.");
+
+                // Then, we create an update for the account (most importantly, updating the syncKey)
+                ops.add(ContentProviderOperation.newUpdate(
+                        ContentUris.withAppendedId(Account.CONTENT_URI, mAccountId)).withValues(
+                                mAccount.toContentValues()).build());
+
+                // Finally, we execute the batch
+                try {
+                    mContentResolver.applyBatch(EmailProvider.EMAIL_AUTHORITY, ops);
+                    userLog("New Account SyncKey: ", mAccount.mSyncKey);
+                } catch (RemoteException e) {
+                    // There is nothing to be done here; fail by returning null
+                } catch (OperationApplicationException e) {
+                    // There is nothing to be done here; fail by returning null
+                }
+
+                // Look for sync issues and its children and delete them
+                // I'm not aware of any other way to deal with this properly
+                mBindArguments[0] = "Sync Issues";
+                mBindArguments[1] = mAccountIdAsString;
+                Cursor c = mContentResolver.query(Mailbox.CONTENT_URI,
+                        MAILBOX_ID_COLUMNS_PROJECTION, WHERE_DISPLAY_NAME_AND_ACCOUNT,
+                        mBindArguments, null);
+                String parentServerId = null;
+                long id = 0;
+                try {
+                    if (c.moveToFirst()) {
+                        id = c.getLong(0);
+                        parentServerId = c.getString(1);
+                    }
+                } finally {
+                    c.close();
+                }
+                if (parentServerId != null) {
+                    mContentResolver.delete(ContentUris.withAppendedId(Mailbox.CONTENT_URI, id),
+                            null, null);
+                    mBindArguments[0] = parentServerId;
+                    mContentResolver.delete(Mailbox.CONTENT_URI, WHERE_PARENT_SERVER_ID_AND_ACCOUNT,
+                            mBindArguments);
+                }
+            }
+        }
+    }
+
+    /**
+     * Not needed for FolderSync parsing; everything is done within changesParser
+     */
+    @Override
+    public void commandsParser() throws IOException {
+    }
+
+    /**
+     * We don't need to implement commit() because all operations take place atomically within
+     * changesParser
+     */
+    @Override
+    public void commit() throws IOException {
+    }
+
+    @Override
+    public void wipe() {
+    }
+
+    @Override
+    public void responsesParser() throws IOException {
+    }
+
+}
diff --git a/src/com/android/exchange/adapter/Parser.java b/src/com/android/exchange/adapter/Parser.java
new file mode 100644
index 0000000..ffe8039
--- /dev/null
+++ b/src/com/android/exchange/adapter/Parser.java
@@ -0,0 +1,502 @@
+/*
+ * 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.adapter;
+
+import com.android.exchange.Eas;
+import com.android.exchange.EasException;
+import com.android.exchange.utility.FileLogger;
+
+import org.kxml2.wap.Wbxml;
+
+import android.content.Context;
+import android.util.Log;
+
+import java.io.ByteArrayOutputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+
+/**
+ * Extremely fast and lightweight WBXML parser, implementing only the subset of WBXML that
+ * EAS uses (as defined in the EAS specification)
+ *
+ */
+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;
+    public static final int START = 2;
+    public static final int END = 3;
+    public static final int TEXT = 4;
+    public static final int END_DOCUMENT = 3;
+    private static final int NOT_FETCHED = Integer.MIN_VALUE;
+    private static final int NOT_ENDED = Integer.MIN_VALUE;
+    private static final int EOF_BYTE = -1;
+    private boolean logging = false;
+    private boolean capture = false;
+
+    private ArrayList<Integer> captureArray;
+
+    // The input stream for this parser
+    private InputStream in;
+
+    // The current tag depth
+    private int depth;
+
+    // The upcoming (saved) id from the stream
+    private int nextId = NOT_FETCHED;
+
+    // The current tag table (i.e. the tag table for the current page)
+    private String[] tagTable;
+
+    // An array of tag tables, as defined in EasTags
+    static private String[][] tagTables = new String[24][];
+
+    // The stack of names of tags being processed; used when debug = true
+    private String[] nameArray = new String[32];
+
+    // The stack of tags being processed
+    private int[] startTagArray = new int[32];
+
+    // The following vars are available to all to avoid method calls that represent the state of
+    // the parser at any given time
+    public int endTag = NOT_ENDED;
+
+    public int startTag;
+
+    // The type of the last token read
+    public int type;
+
+    // The current page
+    public int page;
+
+    // The current tag
+    public int tag;
+
+    // The name of the current tag
+    public String name;
+
+    // Whether the current tag is associated with content (a value)
+    private boolean noContent;
+
+    // The value read, as a String.  Only one of text or num will be valid, depending on whether the
+    // value was requested as a String or an int (to avoid wasted effort in parsing)
+    public String text;
+
+    // The value read, as an int
+    public int num;
+
+    public class EofException extends IOException {
+        private static final long serialVersionUID = 1L;
+    }
+
+    public class EodException extends IOException {
+        private static final long serialVersionUID = 1L;
+    }
+
+    public class EasParserException extends IOException {
+        private static final long serialVersionUID = 1L;
+
+        EasParserException() {
+            super("WBXML format error");
+        }
+
+        EasParserException(String reason) {
+            super(reason);
+        }
+    }
+
+    public boolean parse() throws IOException, EasException {
+        return false;
+    }
+
+    /**
+     * Initialize the tag tables; they are constant
+     *
+     */
+    {
+        String[][] pages = Tags.pages;
+        for (int i = 0; i < pages.length; i++) {
+            String[] page = pages[i];
+            if (page.length > 0) {
+                tagTables[i] = page;
+            }
+        }
+    }
+
+    public Parser(InputStream in) throws IOException {
+        setInput(in);
+        logging = Eas.PARSER_LOG;
+    }
+
+    /**
+     * Set the debug state of the parser.  When debugging is on, every token is logged (Log.v) to
+     * the console.
+     *
+     * @param val the desired state for debug output
+     */
+    public void setDebug(boolean val) {
+        logging = val;
+    }
+
+    /**
+     * Turns on data capture; this is used to create test streams that represent "live" data and
+     * can be used against the various parsers.
+     */
+    public void captureOn() {
+        capture = true;
+        captureArray = new ArrayList<Integer>();
+    }
+
+    /**
+     * Turns off data capture; writes the captured data to a specified file.
+     */
+    public void captureOff(Context context, String file) {
+        try {
+            FileOutputStream out = context.openFileOutput(file, Context.MODE_WORLD_WRITEABLE);
+            out.write(captureArray.toString().getBytes());
+            out.close();
+        } catch (FileNotFoundException e) {
+            // This is debug code; exceptions aren't interesting.
+        } catch (IOException e) {
+            // This is debug code; exceptions aren't interesting.
+        }
+    }
+
+    /**
+     * Return the value of the current tag, as a String
+     *
+     * @return the String value of the current tag
+     * @throws IOException
+     */
+    public String getValue() throws IOException {
+        // The false argument tells getNext to return the value as a String
+        getNext(false);
+        // Save the value
+        String val = text;
+        // Read the next token; it had better be the end of the current tag
+        getNext(false);
+        // If not, throw an exception
+        if (type != END) {
+            throw new IOException("No END found!");
+        }
+        endTag = startTag;
+        return val;
+    }
+
+    /**
+     * Return the value of the current tag, as an integer
+     *
+     * @return the integer value of the current tag
+     * @throws IOException
+     */
+   public int getValueInt() throws IOException {
+        // The true argument to getNext indicates the desire for an integer return value
+        getNext(true);
+        // Save the value
+        int val = num;
+        // Read the next token; it had better be the end of the current tag
+        getNext(false);
+        // If not, throw an exception
+        if (type != END) {
+            throw new IOException("No END found!");
+        }
+        endTag = startTag;
+        return val;
+    }
+
+    /**
+     * Return the next tag found in the stream; special tags END and END_DOCUMENT are used to
+     * mark the end of the current tag and end of document.  If we hit end of document without
+     * looking for it, generate an EodException.  The tag returned consists of the page number
+     * shifted PAGE_SHIFT bits OR'd with the tag retrieved from the stream.  Thus, all tags returned
+     * are unique.
+     *
+     * @param endingTag the tag that would represent the end of the tag we're processing
+     * @return the next tag found
+     * @throws IOException
+     */
+    public int nextTag(int endingTag) throws IOException {
+        // Lose the page information
+        endTag = endingTag &= Tags.PAGE_MASK;
+        while (getNext(false) != DONE) {
+            // If we're a start, set tag to include the page and return it
+            if (type == START) {
+                tag = page | startTag;
+                return tag;
+            // If we're at the ending tag we're looking for, return the END signal
+            } else if (type == END && startTag == endTag) {
+                return END;
+            }
+        }
+        // We're at end of document here.  If we're looking for it, return END_DOCUMENT
+        if (endTag == START_DOCUMENT) {
+            return END_DOCUMENT;
+        }
+        // Otherwise, we've prematurely hit end of document, so exception out
+        // EodException is a subclass of IOException; this will be treated as an IO error by
+        // SyncManager.
+        throw new EodException();
+    }
+
+    /**
+     * Skip anything found in the stream until the end of the current tag is reached.  This can be
+     * used to ignore stretches of xml that aren't needed by the parser.
+     *
+     * @throws IOException
+     */
+    public void skipTag() throws IOException {
+        int thisTag = startTag;
+        // Just loop until we hit the end of the current tag
+        while (getNext(false) != DONE) {
+            if (type == END && startTag == thisTag) {
+                return;
+            }
+        }
+
+        // If we're at end of document, that's bad
+        throw new EofException();
+    }
+
+    /**
+     * Retrieve the next token from the input stream
+     *
+     * @return the token found
+     * @throws IOException
+     */
+    public int nextToken() throws IOException {
+        getNext(false);
+        return type;
+    }
+
+    /**
+     * Initializes the parser with an input stream; reads the first 4 bytes (which are always the
+     * same in EAS, and then sets the tag table to point to page 0 (by definition, the starting
+     * page).
+     *
+     * @param in the InputStream associated with this parser
+     * @throws IOException
+     */
+    public void setInput(InputStream in) throws IOException {
+        this.in = in;
+        readByte(); // version
+        readInt();  // ?
+        readInt();  // 106 (UTF-8)
+        readInt();  // string table length
+        tagTable = tagTables[0];
+    }
+
+    void log(String str) {
+        int cr = str.indexOf('\n');
+        if (cr > 0) {
+            str = str.substring(0, cr);
+        }
+        Log.v(TAG, str);
+        if (Eas.FILE_LOG) {
+            FileLogger.log(TAG, str);
+        }
+    }
+
+    /**
+     * Return the next piece of data from the stream.  The return value indicates the type of data
+     * that has been retrieved - START (start of tag), END (end of tag), DONE (end of stream), or
+     * TEXT (the value of a tag)
+     *
+     * @param asInt whether a TEXT value should be parsed as a String or an int.
+     * @return the type of data retrieved
+     * @throws IOException
+     */
+    private final int getNext(boolean asInt) throws IOException {
+        int savedEndTag = endTag;
+        if (type == END) {
+            depth--;
+        } else {
+            endTag = NOT_ENDED;
+        }
+
+        if (noContent) {
+            type = END;
+            noContent = false;
+            endTag = savedEndTag;
+            return type;
+        }
+
+        text = null;
+        name = null;
+
+        int id = nextId ();
+        while (id == Wbxml.SWITCH_PAGE) {
+            nextId = NOT_FETCHED;
+            // Get the new page number
+            int pg = readByte();
+            // Save the shifted page to add into the startTag in nextTag
+            page = pg << Tags.PAGE_SHIFT;
+            // Retrieve the current tag table
+            tagTable = tagTables[pg];
+            id = nextId();
+        }
+        nextId = NOT_FETCHED;
+
+        switch (id) {
+            case EOF_BYTE:
+                // End of document
+                type = DONE;
+                break;
+
+            case Wbxml.END:
+                // End of tag
+                type = END;
+                if (logging) {
+                    name = nameArray[depth];
+                    //log("</" + name + '>');
+                }
+                // Retrieve the now-current startTag from our stack
+                startTag = endTag = startTagArray[depth];
+                break;
+
+            case Wbxml.STR_I:
+                // Inline string
+                type = TEXT;
+                if (asInt) {
+                    num = readInlineInt();
+                } else {
+                    text = readInlineString();
+                }
+                if (logging) {
+                    name = tagTable[startTag - 5];
+                    log(name + ": " + (asInt ? Integer.toString(num) : text));
+                }
+                break;
+
+            default:
+                // Start of tag
+                type = START;
+                // The tag is in the low 6 bits
+                startTag = id & 0x3F;
+                // If the high bit is set, there is content (a value) to be read
+                noContent = (id & 0x40) == 0;
+                depth++;
+                if (logging) {
+                    name = tagTable[startTag - 5];
+                    //log('<' + name + '>');
+                    nameArray[depth] = name;
+                }
+                // Save the startTag to our stack
+                startTagArray[depth] = startTag;
+        }
+
+        // Return the type of data we're dealing with
+        return type;
+    }
+
+    /**
+     * Read an int from the input stream, and capture it if necessary for debugging.  Seems a small
+     * price to pay...
+     *
+     * @return the int read
+     * @throws IOException
+     */
+    private int read() throws IOException {
+        int i;
+        i = in.read();
+        if (capture) {
+            captureArray.add(i);
+        }
+        return i;
+    }
+
+    private int nextId() throws IOException {
+        if (nextId == NOT_FETCHED) {
+            nextId = read();
+        }
+        return nextId;
+    }
+
+    private int readByte() throws IOException {
+        int i = read();
+        if (i == EOF_BYTE) {
+            throw new EofException();
+        }
+        return i;
+    }
+
+    /**
+     * Read an integer from the stream; this is called when the parser knows that what follows is
+     * an inline string representing an integer (e.g. the Read tag in Email has a value known to
+     * be either "0" or "1")
+     *
+     * @return the integer as parsed from the stream
+     * @throws IOException
+     */
+    private int readInlineInt() throws IOException {
+        int result = 0;
+
+        while (true) {
+            int i = readByte();
+            // Inline strings are always terminated with a zero byte
+            if (i == 0) {
+                return result;
+            }
+            if (i >= '0' && i <= '9') {
+                result = (result * 10) + (i - '0');
+            } else {
+                throw new IOException("Non integer");
+            }
+        }
+    }
+
+    private int readInt() throws IOException {
+        int result = 0;
+        int i;
+
+        do {
+            i = readByte();
+            result = (result << 7) | (i & 0x7f);
+        } while ((i & 0x80) != 0);
+
+        return result;
+    }
+
+    /**
+     * Read an inline string from the stream
+     *
+     * @return the String as parsed from the stream
+     * @throws IOException
+     */
+    private String readInlineString() throws IOException {
+        ByteArrayOutputStream outputStream = new ByteArrayOutputStream(256);
+        while (true) {
+            int i = read();
+            if (i == 0) {
+                break;
+            } else if (i == EOF_BYTE) {
+                throw new EofException();
+            }
+            outputStream.write(i);
+        }
+        outputStream.flush();
+        String res = outputStream.toString("UTF-8");
+        outputStream.close();
+        return res;
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/exchange/adapter/PingParser.java b/src/com/android/exchange/adapter/PingParser.java
new file mode 100644
index 0000000..0731ac0
--- /dev/null
+++ b/src/com/android/exchange/adapter/PingParser.java
@@ -0,0 +1,90 @@
+/* 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.adapter;
+
+import com.android.exchange.EasSyncService;
+import com.android.exchange.StaleFolderListException;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+
+/**
+ * Parse the result of a Ping command.
+ *
+ * If there are folders with changes, add the serverId of those folders to the syncList array.
+ * If the folder list needs to be reloaded, throw a StaleFolderListException, which will be caught
+ * by the sync server, which will sync the updated folder list.
+ */
+public class PingParser extends Parser {
+    private ArrayList<String> syncList = new ArrayList<String>();
+    private EasSyncService mService;
+    private int mSyncStatus = 0;
+
+    public ArrayList<String> getSyncList() {
+        return syncList;
+    }
+
+    public int getSyncStatus() {
+        return mSyncStatus;
+    }
+
+    public PingParser(InputStream in, EasSyncService service) throws IOException {
+        super(in);
+        mService = service;
+    }
+
+    public void parsePingFolders(ArrayList<String> syncList) throws IOException {
+        while (nextTag(Tags.PING_FOLDERS) != END) {
+            if (tag == Tags.PING_FOLDER) {
+                // Here we'll keep track of which mailboxes need syncing
+                String serverId = getValue();
+                syncList.add(serverId);
+                mService.userLog("Changes found in: ", serverId);
+            } else {
+                skipTag();
+            }
+        }
+    }
+
+    @Override
+    public boolean parse() throws IOException, StaleFolderListException {
+        boolean res = false;
+        if (nextTag(START_DOCUMENT) != Tags.PING_PING) {
+            throw new IOException();
+        }
+        while (nextTag(START_DOCUMENT) != END_DOCUMENT) {
+            if (tag == Tags.PING_STATUS) {
+                int status = getValueInt();
+                mSyncStatus = status;
+                mService.userLog("Ping completed, status = ", status);
+                if (status == 2) {
+                    res = true;
+                } else if (status == 7 || status == 4) {
+                    // Status of 7 or 4 indicate a stale folder list
+                    throw new StaleFolderListException();
+                }
+            } else if (tag == Tags.PING_FOLDERS) {
+                parsePingFolders(syncList);
+            } else {
+                skipTag();
+            }
+        }
+        return res;
+    }
+}
+
diff --git a/src/com/android/exchange/adapter/Serializer.java b/src/com/android/exchange/adapter/Serializer.java
new file mode 100644
index 0000000..32909bf
--- /dev/null
+++ b/src/com/android/exchange/adapter/Serializer.java
@@ -0,0 +1,190 @@
+/* Copyright (c) 2002,2003, Stefan Haustein, Oberhausen, Rhld., Germany
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The  above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE. */
+
+//Contributors: Jonathan Cox, Bogdan Onoiu, Jerry Tian
+//Simplified for Google, Inc. by Marc Blank
+
+package com.android.exchange.adapter;
+
+import com.android.exchange.Eas;
+import com.android.exchange.utility.FileLogger;
+
+import org.kxml2.wap.Wbxml;
+
+import android.util.Log;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.Hashtable;
+
+public class Serializer {
+
+    private static final String TAG = "Serializer";
+    private static final boolean logging = false;    // DO NOT CHECK IN WITH THIS TRUE!
+
+    private static final int NOT_PENDING = -1;
+
+    ByteArrayOutputStream out = new ByteArrayOutputStream();
+    ByteArrayOutputStream buf = new ByteArrayOutputStream();
+
+    String pending;
+    int pendingTag = NOT_PENDING;
+    int depth;
+    String name;
+    String[] nameStack = new String[20];
+
+    Hashtable<String, Object> tagTable = new Hashtable<String, Object>();
+
+    private int tagPage;
+
+    public Serializer() {
+        super();
+        try {
+            startDocument();
+            //logging = Eas.PARSER_LOG;
+        } catch (IOException e) {
+            // Nothing to be done
+        }
+    }
+
+    void log(String str) {
+        int cr = str.indexOf('\n');
+        if (cr > 0) {
+            str = str.substring(0, cr);
+        }
+        Log.v(TAG, str);
+        if (Eas.FILE_LOG) {
+            FileLogger.log(TAG, str);
+        }
+    }
+
+    public void done() throws IOException {
+        if (depth != 0) {
+            throw new IOException("Done received with unclosed tags");
+        }
+        writeInteger(out, 0);
+        out.write(buf.toByteArray());
+        out.flush();
+    }
+
+    public void startDocument() throws IOException{
+        out.write(0x03); // version 1.3
+        out.write(0x01); // unknown or missing public identifier
+        out.write(106);
+    }
+
+    public void checkPendingTag(boolean degenerated) throws IOException {
+        if (pendingTag == NOT_PENDING)
+            return;
+
+        int page = pendingTag >> Tags.PAGE_SHIFT;
+        int tag = pendingTag & Tags.PAGE_MASK;
+        if (page != tagPage) {
+            tagPage = page;
+            buf.write(Wbxml.SWITCH_PAGE);
+            buf.write(page);
+        }
+
+        buf.write(degenerated ? tag : tag | 64);
+        if (logging) {
+            String name = Tags.pages[page][tag - 5];
+            nameStack[depth] = name;
+            log("<" + name + '>');
+        }
+        pendingTag = NOT_PENDING;
+    }
+
+    public Serializer start(int tag) throws IOException {
+        checkPendingTag(false);
+        pendingTag = tag;
+        depth++;
+        return this;
+    }
+
+    public Serializer end() throws IOException {
+        if (pendingTag >= 0) {
+            checkPendingTag(true);
+        } else {
+            buf.write(Wbxml.END);
+            if (logging) {
+                log("</" + nameStack[depth] + '>');
+            }
+        }
+        depth--;
+        return this;
+    }
+
+    public Serializer tag(int t) throws IOException {
+        start(t);
+        end();
+        return this;
+    }
+
+    public Serializer data(int tag, String value) throws IOException {
+        start(tag);
+        text(value);
+        end();
+        return this;
+    }
+
+    @Override
+    public String toString() {
+        return out.toString();
+    }
+
+    public byte[] toByteArray() {
+        return out.toByteArray();
+    }
+
+    public Serializer text(String text) throws IOException {
+        checkPendingTag(false);
+        buf.write(Wbxml.STR_I);
+        writeLiteralString(buf, text);
+        if (logging) {
+            log(text);
+        }
+        return this;
+    }
+
+    void writeInteger(OutputStream out, int i) throws IOException {
+        byte[] buf = new byte[5];
+        int idx = 0;
+
+        do {
+            buf[idx++] = (byte) (i & 0x7f);
+            i = i >> 7;
+        } while (i != 0);
+
+        while (idx > 1) {
+            out.write(buf[--idx] | 0x80);
+        }
+        out.write(buf[0]);
+        if (logging) {
+            log(Integer.toString(i));
+        }
+    }
+
+    void writeLiteralString(OutputStream out, String s) throws IOException {
+        byte[] data = s.getBytes("UTF-8");
+        out.write(data);
+        out.write(0);
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/exchange/adapter/Tags.java b/src/com/android/exchange/adapter/Tags.java
new file mode 100644
index 0000000..d822181
--- /dev/null
+++ b/src/com/android/exchange/adapter/Tags.java
@@ -0,0 +1,518 @@
+/*
+ * 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.adapter;
+
+/**
+ * The wbxml tags for EAS are all defined here.
+ *
+ * The static final int's, of the form <page>_<tag> = <constant> are used in parsing incoming
+ * responses from the server (i.e. EasParser and its subclasses).
+ *
+ * The array of String arrays is used to construct server requests with EasSerializer.  One thing
+ * we might do eventually is to "precompile" these requests, in part, although they should be
+ * fairly fast to begin with (each tag requires one HashMap lookup, and there aren't all that many
+ * of them in a given command).
+ *
+ */
+public class Tags {
+
+    // Wbxml page definitions for EAS
+    public static final int AIRSYNC = 0x00;
+    public static final int CONTACTS = 0x01;
+    public static final int EMAIL = 0x02;
+    public static final int CALENDAR = 0x04;
+    public static final int MOVE = 0x05;
+    public static final int GIE = 0x06;
+    public static final int FOLDER = 0x07;
+    public static final int TASK = 0x09;
+    public static final int CONTACTS2 = 0x0C;
+    public static final int PING = 0x0D;
+    public static final int GAL = 0x10;
+    public static final int BASE = 0x11;
+
+    // Shift applied to page numbers to generate tag
+    public static final int PAGE_SHIFT = 6;
+    public static final int PAGE_MASK = 0x3F;  // 6 bits
+
+    public static final int SYNC_PAGE = 0 << PAGE_SHIFT;
+    public static final int SYNC_SYNC = SYNC_PAGE + 5;
+    public static final int SYNC_RESPONSES = SYNC_PAGE + 6;
+    public static final int SYNC_ADD = SYNC_PAGE + 7;
+    public static final int SYNC_CHANGE = SYNC_PAGE + 8;
+    public static final int SYNC_DELETE = SYNC_PAGE + 9;
+    public static final int SYNC_FETCH = SYNC_PAGE + 0xA;
+    public static final int SYNC_SYNC_KEY = SYNC_PAGE + 0xB;
+    public static final int SYNC_CLIENT_ID = SYNC_PAGE + 0xC;
+    public static final int SYNC_SERVER_ID = SYNC_PAGE + 0xD;
+    public static final int SYNC_STATUS = SYNC_PAGE + 0xE;
+    public static final int SYNC_COLLECTION = SYNC_PAGE + 0xF;
+    public static final int SYNC_CLASS = SYNC_PAGE + 0x10;
+    public static final int SYNC_VERSION = SYNC_PAGE + 0x11;
+    public static final int SYNC_COLLECTION_ID = SYNC_PAGE + 0x12;
+    public static final int SYNC_GET_CHANGES = SYNC_PAGE + 0x13;
+    public static final int SYNC_MORE_AVAILABLE = SYNC_PAGE + 0x14;
+    public static final int SYNC_WINDOW_SIZE = SYNC_PAGE + 0x15;
+    public static final int SYNC_COMMANDS = SYNC_PAGE + 0x16;
+    public static final int SYNC_OPTIONS = SYNC_PAGE + 0x17;
+    public static final int SYNC_FILTER_TYPE = SYNC_PAGE + 0x18;
+    public static final int SYNC_TRUNCATION = SYNC_PAGE + 0x19;
+    public static final int SYNC_RTF_TRUNCATION = SYNC_PAGE + 0x1A;
+    public static final int SYNC_CONFLICT = SYNC_PAGE + 0x1B;
+    public static final int SYNC_COLLECTIONS = SYNC_PAGE + 0x1C;
+    public static final int SYNC_APPLICATION_DATA = SYNC_PAGE + 0x1D;
+    public static final int SYNC_DELETES_AS_MOVES = SYNC_PAGE + 0x1E;
+    public static final int SYNC_NOTIFY_GUID = SYNC_PAGE + 0x1F;
+    public static final int SYNC_SUPPORTED = SYNC_PAGE + 0x20;
+    public static final int SYNC_SOFT_DELETE = SYNC_PAGE + 0x21;
+    public static final int SYNC_MIME_SUPPORT = SYNC_PAGE + 0x22;
+    public static final int SYNC_MIME_TRUNCATION = SYNC_PAGE + 0x23;
+    public static final int SYNC_WAIT = SYNC_PAGE + 0x24;
+    public static final int SYNC_LIMIT = SYNC_PAGE + 0x25;
+    public static final int SYNC_PARTIAL = SYNC_PAGE + 0x26;
+
+
+    public static final int GIE_PAGE = GIE << PAGE_SHIFT;
+    public static final int GIE_GET_ITEM_ESTIMATE = GIE_PAGE + 5;
+    public static final int GIE_VERSION = GIE_PAGE + 6;
+    public static final int GIE_COLLECTIONS = GIE_PAGE + 7;
+    public static final int GIE_COLLECTION = GIE_PAGE + 8;
+    public static final int GIE_CLASS = GIE_PAGE + 9;
+    public static final int GIE_COLLECTION_ID = GIE_PAGE + 0xA;
+    public static final int GIE_DATE_TIME = GIE_PAGE + 0xB;
+    public static final int GIE_ESTIMATE = GIE_PAGE + 0xC;
+    public static final int GIE_RESPONSE = GIE_PAGE + 0xD;
+    public static final int GIE_STATUS = GIE_PAGE + 0xE;
+
+    public static final int CONTACTS_PAGE = CONTACTS << PAGE_SHIFT;
+    public static final int CONTACTS_ANNIVERSARY = CONTACTS_PAGE + 5;
+    public static final int CONTACTS_ASSISTANT_NAME = CONTACTS_PAGE + 6;
+    public static final int CONTACTS_ASSISTANT_TELEPHONE_NUMBER = CONTACTS_PAGE + 7;
+    public static final int CONTACTS_BIRTHDAY = CONTACTS_PAGE + 8;
+    public static final int CONTACTS_BODY = CONTACTS_PAGE + 9;
+    public static final int CONTACTS_BODY_SIZE = CONTACTS_PAGE + 0xA;
+    public static final int CONTACTS_BODY_TRUNCATED = CONTACTS_PAGE + 0xB;
+    public static final int CONTACTS_BUSINESS2_TELEPHONE_NUMBER = CONTACTS_PAGE + 0xC;
+    public static final int CONTACTS_BUSINESS_ADDRESS_CITY = CONTACTS_PAGE + 0xD;
+    public static final int CONTACTS_BUSINESS_ADDRESS_COUNTRY = CONTACTS_PAGE + 0xE;
+    public static final int CONTACTS_BUSINESS_ADDRESS_POSTAL_CODE = CONTACTS_PAGE + 0xF;
+    public static final int CONTACTS_BUSINESS_ADDRESS_STATE = CONTACTS_PAGE + 0x10;
+    public static final int CONTACTS_BUSINESS_ADDRESS_STREET = CONTACTS_PAGE + 0x11;
+    public static final int CONTACTS_BUSINESS_FAX_NUMBER = CONTACTS_PAGE + 0x12;
+    public static final int CONTACTS_BUSINESS_TELEPHONE_NUMBER = CONTACTS_PAGE + 0x13;
+    public static final int CONTACTS_CAR_TELEPHONE_NUMBER = CONTACTS_PAGE + 0x14;
+    public static final int CONTACTS_CATEGORIES = CONTACTS_PAGE + 0x15;
+    public static final int CONTACTS_CATEGORY = CONTACTS_PAGE + 0x16;
+    public static final int CONTACTS_CHILDREN = CONTACTS_PAGE + 0x17;
+    public static final int CONTACTS_CHILD = CONTACTS_PAGE + 0x18;
+    public static final int CONTACTS_COMPANY_NAME = CONTACTS_PAGE + 0x19;
+    public static final int CONTACTS_DEPARTMENT = CONTACTS_PAGE + 0x1A;
+    public static final int CONTACTS_EMAIL1_ADDRESS = CONTACTS_PAGE + 0x1B;
+    public static final int CONTACTS_EMAIL2_ADDRESS = CONTACTS_PAGE + 0x1C;
+    public static final int CONTACTS_EMAIL3_ADDRESS = CONTACTS_PAGE + 0x1D;
+    public static final int CONTACTS_FILE_AS = CONTACTS_PAGE + 0x1E;
+    public static final int CONTACTS_FIRST_NAME = CONTACTS_PAGE + 0x1F;
+    public static final int CONTACTS_HOME2_TELEPHONE_NUMBER = CONTACTS_PAGE + 0x20;
+    public static final int CONTACTS_HOME_ADDRESS_CITY = CONTACTS_PAGE + 0x21;
+    public static final int CONTACTS_HOME_ADDRESS_COUNTRY = CONTACTS_PAGE + 0x22;
+    public static final int CONTACTS_HOME_ADDRESS_POSTAL_CODE = CONTACTS_PAGE + 0x23;
+    public static final int CONTACTS_HOME_ADDRESS_STATE = CONTACTS_PAGE + 0x24;
+    public static final int CONTACTS_HOME_ADDRESS_STREET = CONTACTS_PAGE + 0x25;
+    public static final int CONTACTS_HOME_FAX_NUMBER = CONTACTS_PAGE + 0x26;
+    public static final int CONTACTS_HOME_TELEPHONE_NUMBER = CONTACTS_PAGE + 0x27;
+    public static final int CONTACTS_JOB_TITLE = CONTACTS_PAGE + 0x28;
+    public static final int CONTACTS_LAST_NAME = CONTACTS_PAGE + 0x29;
+    public static final int CONTACTS_MIDDLE_NAME = CONTACTS_PAGE + 0x2A;
+    public static final int CONTACTS_MOBILE_TELEPHONE_NUMBER = CONTACTS_PAGE + 0x2B;
+    public static final int CONTACTS_OFFICE_LOCATION = CONTACTS_PAGE + 0x2C;
+    public static final int CONTACTS_OTHER_ADDRESS_CITY = CONTACTS_PAGE + 0x2D;
+    public static final int CONTACTS_OTHER_ADDRESS_COUNTRY = CONTACTS_PAGE + 0x2E;
+    public static final int CONTACTS_OTHER_ADDRESS_POSTAL_CODE = CONTACTS_PAGE + 0x2F;
+    public static final int CONTACTS_OTHER_ADDRESS_STATE = CONTACTS_PAGE + 0x30;
+    public static final int CONTACTS_OTHER_ADDRESS_STREET = CONTACTS_PAGE + 0x31;
+    public static final int CONTACTS_PAGER_NUMBER = CONTACTS_PAGE + 0x32;
+    public static final int CONTACTS_RADIO_TELEPHONE_NUMBER = CONTACTS_PAGE + 0x33;
+    public static final int CONTACTS_SPOUSE = CONTACTS_PAGE + 0x34;
+    public static final int CONTACTS_SUFFIX = CONTACTS_PAGE + 0x35;
+    public static final int CONTACTS_TITLE = CONTACTS_PAGE + 0x36;
+    public static final int CONTACTS_WEBPAGE = CONTACTS_PAGE + 0x37;
+    public static final int CONTACTS_YOMI_COMPANY_NAME = CONTACTS_PAGE + 0x38;
+    public static final int CONTACTS_YOMI_FIRST_NAME = CONTACTS_PAGE + 0x39;
+    public static final int CONTACTS_YOMI_LAST_NAME = CONTACTS_PAGE + 0x3A;
+    public static final int CONTACTS_COMPRESSED_RTF = CONTACTS_PAGE + 0x3B;
+    public static final int CONTACTS_PICTURE = CONTACTS_PAGE + 0x3C;
+
+    public static final int CALENDAR_PAGE = CALENDAR << PAGE_SHIFT;
+    public static final int CALENDAR_TIME_ZONE = CALENDAR_PAGE + 5;
+    public static final int CALENDAR_ALL_DAY_EVENT = CALENDAR_PAGE + 6;
+    public static final int CALENDAR_ATTENDEES = CALENDAR_PAGE + 7;
+    public static final int CALENDAR_ATTENDEE = CALENDAR_PAGE + 8;
+    public static final int CALENDAR_ATTENDEE_EMAIL = CALENDAR_PAGE + 9;
+    public static final int CALENDAR_ATTENDEE_NAME = CALENDAR_PAGE + 0xA;
+    public static final int CALENDAR_BODY = CALENDAR_PAGE + 0xB;
+    public static final int CALENDAR_BODY_TRUNCATED = CALENDAR_PAGE + 0xC;
+    public static final int CALENDAR_BUSY_STATUS = CALENDAR_PAGE + 0xD;
+    public static final int CALENDAR_CATEGORIES = CALENDAR_PAGE + 0xE;
+    public static final int CALENDAR_CATEGORY = CALENDAR_PAGE + 0xF;
+    public static final int CALENDAR_COMPRESSED_RTF = CALENDAR_PAGE + 0x10;
+    public static final int CALENDAR_DTSTAMP = CALENDAR_PAGE + 0x11;
+    public static final int CALENDAR_END_TIME = CALENDAR_PAGE + 0x12;
+    public static final int CALENDAR_EXCEPTION = CALENDAR_PAGE + 0x13;
+    public static final int CALENDAR_EXCEPTIONS = CALENDAR_PAGE + 0x14;
+    public static final int CALENDAR_EXCEPTION_IS_DELETED = CALENDAR_PAGE + 0x15;
+    public static final int CALENDAR_EXCEPTION_START_TIME = CALENDAR_PAGE + 0x16;
+    public static final int CALENDAR_LOCATION = CALENDAR_PAGE + 0x17;
+    public static final int CALENDAR_MEETING_STATUS = CALENDAR_PAGE + 0x18;
+    public static final int CALENDAR_ORGANIZER_EMAIL = CALENDAR_PAGE + 0x19;
+    public static final int CALENDAR_ORGANIZER_NAME = CALENDAR_PAGE + 0x1A;
+    public static final int CALENDAR_RECURRENCE = CALENDAR_PAGE + 0x1B;
+    public static final int CALENDAR_RECURRENCE_TYPE = CALENDAR_PAGE + 0x1C;
+    public static final int CALENDAR_RECURRENCE_UNTIL = CALENDAR_PAGE + 0x1D;
+    public static final int CALENDAR_RECURRENCE_OCCURRENCES = CALENDAR_PAGE + 0x1E;
+    public static final int CALENDAR_RECURRENCE_INTERVAL = CALENDAR_PAGE + 0x1F;
+    public static final int CALENDAR_RECURRENCE_DAYOFWEEK = CALENDAR_PAGE + 0x20;
+    public static final int CALENDAR_RECURRENCE_DAYOFMONTH = CALENDAR_PAGE + 0x21;
+    public static final int CALENDAR_RECURRENCE_WEEKOFMONTH = CALENDAR_PAGE + 0x22;
+    public static final int CALENDAR_RECURRENCE_MONTHOFYEAR = CALENDAR_PAGE + 0x23;
+    public static final int CALENDAR_REMINDER_MINS_BEFORE = CALENDAR_PAGE + 0x24;
+    public static final int CALENDAR_SENSITIVITY = CALENDAR_PAGE + 0x25;
+    public static final int CALENDAR_SUBJECT = CALENDAR_PAGE + 0x26;
+    public static final int CALENDAR_START_TIME = CALENDAR_PAGE + 0x27;
+    public static final int CALENDAR_UID = CALENDAR_PAGE + 0x28;
+    public static final int CALENDAR_ATTENDEE_STATUS = CALENDAR_PAGE + 0x29;
+    public static final int CALENDAR_ATTENDEE_TYPE = CALENDAR_PAGE + 0x2A;
+
+    public static final int FOLDER_PAGE = FOLDER << PAGE_SHIFT;
+    public static final int FOLDER_FOLDERS = FOLDER_PAGE + 5;
+    public static final int FOLDER_FOLDER = FOLDER_PAGE + 6;
+    public static final int FOLDER_DISPLAY_NAME = FOLDER_PAGE + 7;
+    public static final int FOLDER_SERVER_ID = FOLDER_PAGE + 8;
+    public static final int FOLDER_PARENT_ID = FOLDER_PAGE + 9;
+    public static final int FOLDER_TYPE = FOLDER_PAGE + 0xA;
+    public static final int FOLDER_RESPONSE = FOLDER_PAGE + 0xB;
+    public static final int FOLDER_STATUS = FOLDER_PAGE + 0xC;
+    public static final int FOLDER_CONTENT_CLASS = FOLDER_PAGE + 0xD;
+    public static final int FOLDER_CHANGES = FOLDER_PAGE + 0xE;
+    public static final int FOLDER_ADD = FOLDER_PAGE + 0xF;
+    public static final int FOLDER_DELETE = FOLDER_PAGE + 0x10;
+    public static final int FOLDER_UPDATE = FOLDER_PAGE + 0x11;
+    public static final int FOLDER_SYNC_KEY = FOLDER_PAGE + 0x12;
+    public static final int FOLDER_FOLDER_CREATE = FOLDER_PAGE + 0x13;
+    public static final int FOLDER_FOLDER_DELETE= FOLDER_PAGE + 0x14;
+    public static final int FOLDER_FOLDER_UPDATE = FOLDER_PAGE + 0x15;
+    public static final int FOLDER_FOLDER_SYNC = FOLDER_PAGE + 0x16;
+    public static final int FOLDER_COUNT = FOLDER_PAGE + 0x17;
+    public static final int FOLDER_VERSION = FOLDER_PAGE + 0x18;
+
+    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;
+    public static final int EMAIL_ATT_NAME = EMAIL_PAGE + 7;
+    public static final int EMAIL_ATT_SIZE = EMAIL_PAGE + 8;
+    public static final int EMAIL_ATT0ID = EMAIL_PAGE + 9;
+    public static final int EMAIL_ATT_METHOD = EMAIL_PAGE + 0xA;
+    public static final int EMAIL_ATT_REMOVED = EMAIL_PAGE + 0xB;
+    public static final int EMAIL_BODY = EMAIL_PAGE + 0xC;
+    public static final int EMAIL_BODY_SIZE = EMAIL_PAGE + 0xD;
+    public static final int EMAIL_BODY_TRUNCATED = EMAIL_PAGE + 0xE;
+    public static final int EMAIL_DATE_RECEIVED = EMAIL_PAGE + 0xF;
+    public static final int EMAIL_DISPLAY_NAME = EMAIL_PAGE + 0x10;
+    public static final int EMAIL_DISPLAY_TO = EMAIL_PAGE + 0x11;
+    public static final int EMAIL_IMPORTANCE = EMAIL_PAGE + 0x12;
+    public static final int EMAIL_MESSAGE_CLASS = EMAIL_PAGE + 0x13;
+    public static final int EMAIL_SUBJECT = EMAIL_PAGE + 0x14;
+    public static final int EMAIL_READ = EMAIL_PAGE + 0x15;
+    public static final int EMAIL_TO = EMAIL_PAGE + 0x16;
+    public static final int EMAIL_CC = EMAIL_PAGE + 0x17;
+    public static final int EMAIL_FROM = EMAIL_PAGE + 0x18;
+    public static final int EMAIL_REPLY_TO = EMAIL_PAGE + 0x19;
+    public static final int EMAIL_ALL_DAY_EVENT = EMAIL_PAGE + 0x1A;
+    public static final int EMAIL_CATEGORIES = EMAIL_PAGE + 0x1B;
+    public static final int EMAIL_CATEGORY = EMAIL_PAGE + 0x1C;
+    public static final int EMAIL_DTSTAMP = EMAIL_PAGE + 0x1D;
+    public static final int EMAIL_END_TIME = EMAIL_PAGE + 0x1E;
+    public static final int EMAIL_INSTANCE_TYPE = EMAIL_PAGE + 0x1F;
+    public static final int EMAIL_INTD_BUSY_STATUS = EMAIL_PAGE + 0x20;
+    public static final int EMAIL_LOCATION = EMAIL_PAGE + 0x21;
+    public static final int EMAIL_MEETING_REQUEST = EMAIL_PAGE + 0x22;
+    public static final int EMAIL_ORGANIZER = EMAIL_PAGE + 0x23;
+    public static final int EMAIL_RECURRENCE_ID = EMAIL_PAGE + 0x24;
+    public static final int EMAIL_REMINDER = EMAIL_PAGE + 0x25;
+    public static final int EMAIL_RESPONSE_REQUESTED = EMAIL_PAGE + 0x26;
+    public static final int EMAIL_RECURRENCES = EMAIL_PAGE + 0x27;
+    public static final int EMAIL_RECURRENCE = EMAIL_PAGE + 0x28;
+    public static final int EMAIL_RECURRENCE_TYPE = EMAIL_PAGE + 0x29;
+    public static final int EMAIL_RECURRENCE_UNTIL = EMAIL_PAGE + 0x2A;
+    public static final int EMAIL_RECURRENCE_OCCURRENCES = EMAIL_PAGE + 0x2B;
+    public static final int EMAIL_RECURRENCE_INTERVAL = EMAIL_PAGE + 0x2C;
+    public static final int EMAIL_RECURRENCE_DAYOFWEEK = EMAIL_PAGE + 0x2D;
+    public static final int EMAIL_RECURRENCE_DAYOFMONTH = EMAIL_PAGE + 0x2E;
+    public static final int EMAIL_RECURRENCE_WEEKOFMONTH = EMAIL_PAGE + 0x2F;
+    public static final int EMAIL_RECURRENCE_MONTHOFYEAR = EMAIL_PAGE + 0x30;
+    public static final int EMAIL_START_TIME = EMAIL_PAGE + 0x31;
+    public static final int EMAIL_SENSITIVITY = EMAIL_PAGE + 0x32;
+    public static final int EMAIL_TIME_ZONE = EMAIL_PAGE + 0x33;
+    public static final int EMAIL_GLOBAL_OBJID = EMAIL_PAGE + 0x34;
+    public static final int EMAIL_THREAD_TOPIC = EMAIL_PAGE + 0x35;
+    public static final int EMAIL_MIME_DATA = EMAIL_PAGE + 0x36;
+    public static final int EMAIL_MIME_TRUNCATED = EMAIL_PAGE + 0x37;
+    public static final int EMAIL_MIME_SIZE = EMAIL_PAGE + 0x38;
+    public static final int EMAIL_INTERNET_CPID = EMAIL_PAGE + 0x39;
+    public static final int EMAIL_FLAG = EMAIL_PAGE + 0x3A;
+    public static final int EMAIL_FLAG_STATUS = EMAIL_PAGE + 0x3B;
+    public static final int EMAIL_CONTENT_CLASS = EMAIL_PAGE + 0x3C;
+    public static final int EMAIL_FLAG_TYPE = EMAIL_PAGE + 0x3D;
+    public static final int EMAIL_COMPLETE_TIME = EMAIL_PAGE + 0x3E;
+
+    public static final int TASK_PAGE = TASK << PAGE_SHIFT;
+    public static final int TASK_BODY = TASK_PAGE + 5;
+    public static final int TASK_BODY_SIZE = TASK_PAGE + 6;
+    public static final int TASK_BODY_TRUNCATED = TASK_PAGE + 7;
+    public static final int TASK_CATEGORIES = TASK_PAGE + 8;
+    public static final int TASK_CATEGORY = TASK_PAGE + 9;
+    public static final int TASK_COMPLETE = TASK_PAGE + 0xA;
+    public static final int TASK_DATE_COMPLETED = TASK_PAGE + 0xB;
+    public static final int TASK_DUE_DATE = TASK_PAGE + 0xC;
+    public static final int TASK_UTC_DUE_DATE = TASK_PAGE + 0xD;
+    public static final int TASK_IMPORTANCE = TASK_PAGE + 0xE;
+    public static final int TASK_RECURRENCE = TASK_PAGE + 0xF;
+    public static final int TASK_RECURRENCE_TYPE = TASK_PAGE + 0x10;
+    public static final int TASK_RECURRENCE_START = TASK_PAGE + 0x11;
+    public static final int TASK_RECURRENCE_UNTIL = TASK_PAGE + 0x12;
+    public static final int TASK_RECURRENCE_OCCURRENCES = TASK_PAGE + 0x13;
+    public static final int TASK_RECURRENCE_INTERVAL = TASK_PAGE + 0x14;
+    public static final int TASK_RECURRENCE_DAY_OF_MONTH = TASK_PAGE + 0x15;
+    public static final int TASK_RECURRENCE_DAY_OF_WEEK = TASK_PAGE + 0x16;
+    public static final int TASK_RECURRENCE_WEEK_OF_MONTH = TASK_PAGE + 0x17;
+    public static final int TASK_RECURRENCE_MONTH_OF_YEAR = TASK_PAGE + 0x18;
+    public static final int TASK_RECURRENCE_REGENERATE = TASK_PAGE + 0x19;
+    public static final int TASK_RECURRENCE_DEAD_OCCUR = TASK_PAGE + 0x1A;
+    public static final int TASK_REMINDER_SET = TASK_PAGE + 0x1B;
+    public static final int TASK_REMINDER_TIME = TASK_PAGE + 0x1C;
+    public static final int TASK_SENSITIVITY = TASK_PAGE + 0x1D;
+    public static final int TASK_START_DATE = TASK_PAGE + 0x1E;
+    public static final int TASK_UTC_START_DATE = TASK_PAGE + 0x1F;
+    public static final int TASK_SUBJECT = TASK_PAGE + 0x20;
+    public static final int COMPRESSED_RTF = TASK_PAGE + 0x21;
+    public static final int ORDINAL_DATE = TASK_PAGE + 0x22;
+    public static final int SUBORDINAL_DATE = TASK_PAGE + 0x23;
+
+    public static final int MOVE_PAGE = MOVE << PAGE_SHIFT;
+    public static final int MOVE_MOVE_ITEMS = MOVE_PAGE + 5;
+    public static final int MOVE_MOVE = MOVE_PAGE + 6;
+    public static final int MOVE_SRCMSGID = MOVE_PAGE + 7;
+    public static final int MOVE_SRCFLDID = MOVE_PAGE + 8;
+    public static final int MOVE_DSTFLDID = MOVE_PAGE + 9;
+    public static final int MOVE_RESPONSE = MOVE_PAGE + 0xA;
+    public static final int MOVE_STATUS = MOVE_PAGE + 0xB;
+    public static final int MOVE_DSTMSGID = MOVE_PAGE + 0xC;
+
+    public static final int CONTACTS2_PAGE = CONTACTS2 << PAGE_SHIFT;
+    public static final int CONTACTS2_CUSTOMER_ID = CONTACTS2_PAGE + 5;
+    public static final int CONTACTS2_GOVERNMENT_ID = CONTACTS2_PAGE + 6;
+    public static final int CONTACTS2_IM_ADDRESS = CONTACTS2_PAGE + 7;
+    public static final int CONTACTS2_IM_ADDRESS_2 = CONTACTS2_PAGE + 8;
+    public static final int CONTACTS2_IM_ADDRESS_3 = CONTACTS2_PAGE + 9;
+    public static final int CONTACTS2_MANAGER_NAME = CONTACTS2_PAGE + 0xA;
+    public static final int CONTACTS2_COMPANY_MAIN_PHONE = CONTACTS2_PAGE + 0xB;
+    public static final int CONTACTS2_ACCOUNT_NAME = CONTACTS2_PAGE + 0xC;
+    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;
+    public static final int PING_STATUS = PING_PAGE + 7;
+    public static final int PING_HEARTBEAT_INTERVAL = PING_PAGE + 8;
+    public static final int PING_FOLDERS = PING_PAGE + 9;
+    public static final int PING_FOLDER = PING_PAGE + 0xA;
+    public static final int PING_ID = PING_PAGE + 0xB;
+    public static final int PING_CLASS = PING_PAGE + 0xC;
+    public static final int PING_MAX_FOLDERS = PING_PAGE + 0xD;
+
+    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;
+    public static final int BASE_TRUNCATION_SIZE = BASE_PAGE + 7;
+    public static final int BASE_ALL_OR_NONE = BASE_PAGE + 8;
+    public static final int BASE_RESERVED = BASE_PAGE + 9;
+    public static final int BASE_BODY = BASE_PAGE + 0xA;
+    public static final int BASE_DATA = BASE_PAGE + 0xB;
+    public static final int BASE_ESTIMATED_DATA_SIZE = BASE_PAGE + 0xC;
+    public static final int BASE_TRUNCATED = BASE_PAGE + 0xD;
+    public static final int BASE_ATTACHMENTS = BASE_PAGE + 0xE;
+    public static final int BASE_ATTACHMENT = BASE_PAGE + 0xF;
+    public static final int BASE_DISPLAY_NAME = BASE_PAGE + 0x10;
+    public static final int BASE_FILE_REFERENCE = BASE_PAGE + 0x11;
+    public static final int BASE_METHOD = BASE_PAGE + 0x12;
+    public static final int BASE_CONTENT_ID = BASE_PAGE + 0x13;
+    public static final int BASE_CONTENT_LOCATION = BASE_PAGE + 0x14;
+    public static final int BASE_IS_INLINE = BASE_PAGE + 0x15;
+    public static final int BASE_NATIVE_BODY_TYPE = BASE_PAGE + 0x16;
+    public static final int BASE_CONTENT_TYPE = BASE_PAGE + 0x17;
+
+    static public String[][] pages = {
+        {    // 0x00 AirSync
+            "Sync", "Responses", "Add", "Change", "Delete", "Fetch", "SyncKey", "ClientId",
+            "ServerId", "Status", "Collection", "Class", "Version", "CollectionId", "GetChanges",
+            "MoreAvailable", "WindowSize", "Commands", "Options", "FilterType", "Truncation",
+            "RTFTruncation", "Conflict", "Collections", "ApplicationData", "DeletesAsMoves",
+            "NotifyGUID", "Supported", "SoftDelete", "MIMESupport", "MIMETruncation", "Wait",
+            "Limit", "Partial"
+        },
+        {
+            // 0x01 Contacts
+            "Anniversary", "AssistantName", "AssistantTelephoneNumber", "Birthday", "ContactsBody",
+            "ContactsBodySize", "ContactsBodyTruncated", "Business2TelephoneNumber",
+            "BusinessAddressCity",
+            "BusinessAddressCountry", "BusinessAddressPostalCode", "BusinessAddressState",
+            "BusinessAddressStreet", "BusinessFaxNumber", "BusinessTelephoneNumber",
+            "CarTelephoneNumber", "ContactsCategories", "ContactsCategory", "Children", "Child",
+            "CompanyName", "Department", "Email1Address", "Email2Address", "Email3Address",
+            "FileAs", "FirstName", "Home2TelephoneNumber", "HomeAddressCity", "HomeAddressCountry",
+            "HomeAddressPostalCode", "HomeAddressState", "HomeAddressStreet", "HomeFaxNumber",
+            "HomeTelephoneNumber", "JobTitle", "LastName", "MiddleName", "MobileTelephoneNumber",
+            "OfficeLocation", "OtherAddressCity", "OtherAddressCountry",
+            "OtherAddressPostalCode", "OtherAddressState", "OtherAddressStreet", "PagerNumber",
+            "RadioTelephoneNumber", "Spouse", "Suffix", "Title", "Webpage", "YomiCompanyName",
+            "YomiFirstName", "YomiLastName", "CompressedRTF", "Picture"
+        },
+        {
+            // 0x02 Email
+            "Attachment", "Attachments", "AttName", "AttSize", "Add0Id", "AttMethod", "AttRemoved",
+            "Body", "BodySize", "BodyTruncated", "DateReceived", "DisplayName", "DisplayTo",
+            "Importance", "MessageClass", "Subject", "Read", "To", "CC", "From", "ReplyTo",
+            "AllDayEvent", "Categories", "Category", "DTStamp", "EndTime", "InstanceType",
+            "IntDBusyStatus", "Location", "MeetingRequest", "Organizer", "RecurrenceId", "Reminder",
+            "ResponseRequested", "Recurrences", "Recurence", "Recurrence_Type", "Recurrence_Until",
+            "Recurrence_Occurrences", "Recurrence_Interval", "Recurrence_DayOfWeek",
+            "Recurrence_DayOfMonth", "Recurrence_WeekOfMonth", "Recurrence_MonthOfYear",
+            "StartTime", "Sensitivity", "TimeZone", "GlobalObjId", "ThreadTopic", "MIMEData",
+            "MIMETruncated", "MIMESize", "InternetCPID", "Flag", "FlagStatus", "EmailContentClass",
+            "FlagType", "CompleteTime"
+        },
+        {
+            // 0x03 AirNotify
+        },
+        {
+            // 0x04 Calendar
+            "CalTimeZone", "CalAllDayEvent", "CalAttendees", "CalAttendee", "CalAttendee_Email",
+            "CalAttendee_Name", "CalBody", "CalBodyTruncated", "CalBusyStatus", "CalCategories",
+            "CalCategory", "CalCompressed_RTF", "CalDTStamp", "CalEndTime", "CalExeption",
+            "CalExceptions", "CalException_IsDeleted", "CalException_StartTime", "CalLocation",
+            "CalMeetingStatus", "CalOrganizer_Email", "CalOrganizer_Name", "CalRecurrence",
+            "CalRecurrence_Type", "CalRecurrence_Until", "CalRecurrence_Occurrences",
+            "CalRecurrence_Interval", "CalRecurrence_DayOfWeek", "CalRecurrence_DayOfMonth",
+            "CalRecurrence_WeekOfMonth", "CalRecurrence_MonthOfYear", "CalReminder_MinsBefore",
+            "CalSensitivity", "CalSubject", "CalStartTime", "CalUID", "CalAttendee_Status",
+            "CalAttendee_Type"
+        },
+        {
+            // 0x05 Move
+            "MoveItems", "Move", "SrcMsgId", "SrcFldId", "DstFldId", "MoveResponse", "MoveStatus",
+            "DstMsgId"
+        },
+        {
+            // 0x06 ItemEstimate
+            "GetItemEstimate", "Version", "Collection", "Collection", "Class", "CollectionId",
+            "DateTime", "Estimate", "Response", "Status"
+        },
+        {
+            // 0x07 FolderHierarchy
+            "Folders", "Folder", "FolderDisplayName", "FolderServerId", "FolderParentId", "Type",
+            "FolderResponse", "FolderStatus", "FolderContentClass", "Changes", "FolderAdd",
+            "FolderDelete", "FolderUpdate", "FolderSyncKey", "FolderFolderCreate",
+            "FolderFolderDelete", "FolderFolderUpdate", "FolderSync", "Count", "FolderVersion"
+        },
+        {
+            // 0x08 MeetingResponse
+        },
+        {
+            // 0x09 Tasks
+            "Body", "BodySize", "BodyTruncated", "Categories", "Category", "Complete",
+            "DateCompleted", "DueDate", "UTCDueDate", "Importance", "Recurrence", "RecurrenceType",
+            "RecurrenceStart", "RecurrenceUntil", "RecurrenceOccurrences", "RecurrenceInterval",
+            "RecurrenceDOM", "RecurrenceDOW", "RecurrenceWOM", "RecurrenceMOY",
+            "RecurrenceRegenerate", "RecurrenceDeadOccur", "ReminderSet", "ReminderTime",
+            "Sensitivity", "StartDate", "UTCStartDate", "Subject", "CompressedRTF", "OrdinalDate",
+            "SubordinalDate"
+        },
+        {
+            // 0x0A ResolveRecipients
+        },
+        {
+            // 0x0B ValidateCert
+        },
+        {
+            // 0x0C Contacts2
+            "CustomerId", "GovernmentId", "IMAddress", "IMAddress2", "IMAddress3", "ManagerName",
+            "CompanyMainPhone", "AccountName", "NickName", "MMS"
+        },
+        {
+            // 0x0D Ping
+            "Ping", "AutdState", "PingStatus", "HeartbeatInterval", "PingFolders", "PingFolder",
+            "PingId", "PingClass", "MaxFolders"
+        },
+        {
+            // 0x0E Provision
+            "Provision", "Policies", "Policy", "PolicyType", "PolicyKey", "Data", "ProvisionStatus",
+            "RemoteWipe", "EASProvidionDoc", "DevicePasswordEnabled",
+            "AlphanumericDevicePasswordRequired",
+            "DeviceEncryptionEnabled", "-unused-", "AttachmentsEnabled", "MinDevicePasswordLength",
+            "MaxInactivityTimeDeviceLock", "MaxDevicePasswordFailedAttempts", "MaxAttachmentSize",
+            "AllowSimpleDevicePassword", "DevicePasswordExpiration", "DevicePasswordHistory",
+            "AllowStorageCard", "AllowCamera", "RequireDeviceEncryption",
+            "AllowUnsignedApplications", "AllowUnsignedInstallationPackages",
+            "MinDevicePasswordComplexCharacters", "AllowWiFi", "AllowTextMessaging",
+            "AllowPOPIMAPEmail", "AllowBluetooth", "AllowIrDA", "RequireManualSyncWhenRoaming",
+            "AllowDesktopSync",
+            "MaxCalendarAgeFilder", "AllowHTMLEmail", "MaxEmailAgeFilder",
+            "MaxEmailBodyTruncationSize", "MaxEmailHTMLBodyTruncationSize",
+            "RequireSignedSMIMEMessages", "RequireEncryptedSMIMEMessages",
+            "RequireSignedSMIMEAlgorithm", "RequireEncryptionSMIMEAlgorithm",
+            "AllowSMIMEEncryptionAlgorithmNegotiation", "AllowSMIMESoftCerts", "AllowBrowser",
+            "AllowConsumerEmail", "AllowRemoteDesktop", "AllowInternetSharing",
+            "UnapprovedInROMApplicationList", "ApplicationName", "ApprovedApplicationList", "Hash"
+        },
+        {
+            // 0x0F Search
+        },
+        {
+            // 0x10 Gal
+            "GalDisplayName", "GalPhone", "GalOffice", "GalTitle", "GalCompany", "GalAlias",
+            "GalFirstName", "GalLastName", "GalHomePhone", "GalMobilePhone", "GalEmailAddress"
+        },
+        {
+            // 0x11 AirSyncBase
+            "BodyPreference", "BodyPreferenceType", "BodyPreferenceTruncationSize", "AllOrNone",
+            "--unused--", "BaseBody", "BaseData", "BaseEstimatedDataSize", "BaseTruncated",
+            "BaseAttachments", "BaseAttachment", "BaseDisplayName", "FileReference", "BaseMethod",
+            "BaseContentId", "BaseContentLocation", "BaseIsInline", "BaseNativeBodyType",
+            "BaseContentType"
+        },
+        {
+            // 0x12 Settings
+        },
+        {
+            // 0x13 DocumentLibrary
+        },
+        {
+            // 0x14 ItemOperations
+        }
+    };
+}
diff --git a/src/com/android/exchange/adapter/patent_disclaimer.txt b/src/com/android/exchange/adapter/patent_disclaimer.txt
new file mode 100644
index 0000000..8a715a3
--- /dev/null
+++ b/src/com/android/exchange/adapter/patent_disclaimer.txt
@@ -0,0 +1,9 @@
+-----------------------------
+THIS IS NOT A GRANT OF PATENT RIGHTS.
+
+Google makes no representation or warranty that the source code made available hereunder is
+unencumbered by third-party patents.  Those intending to use this source code in hardware or
+software products are advised that implementations of this code, including in open source software
+or shareware, may require patent licenses from the relevant patent holders.
+
+-----------------------------
diff --git a/src/com/android/exchange/patent_disclaimer.txt b/src/com/android/exchange/patent_disclaimer.txt
new file mode 100644
index 0000000..8a715a3
--- /dev/null
+++ b/src/com/android/exchange/patent_disclaimer.txt
@@ -0,0 +1,9 @@
+-----------------------------
+THIS IS NOT A GRANT OF PATENT RIGHTS.
+
+Google makes no representation or warranty that the source code made available hereunder is
+unencumbered by third-party patents.  Those intending to use this source code in hardware or
+software products are advised that implementations of this code, including in open source software
+or shareware, may require patent licenses from the relevant patent holders.
+
+-----------------------------
diff --git a/src/com/android/exchange/utility/FileLogger.java b/src/com/android/exchange/utility/FileLogger.java
new file mode 100644
index 0000000..c5f4635
--- /dev/null
+++ b/src/com/android/exchange/utility/FileLogger.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2009 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.content.Context;
+
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.Date;
+
+public class FileLogger {
+    private static FileLogger LOGGER = null;
+    private static FileWriter mLogWriter = null;
+    public static String LOG_FILE_NAME = "/sdcard/emaillog.txt";
+
+    public synchronized static FileLogger getLogger (Context c) {
+        LOGGER = new FileLogger();
+        return LOGGER;
+    }
+
+    private FileLogger() {
+        try {
+            mLogWriter = new FileWriter(LOG_FILE_NAME, true);
+        } catch (IOException e) {
+            // Doesn't matter
+        }
+    }
+
+    static public synchronized void close() {
+        if (mLogWriter != null) {
+            try {
+                mLogWriter.close();
+            } catch (IOException e) {
+                // Doesn't matter
+            }
+            mLogWriter = null;
+        }
+    }
+
+    static public synchronized void log(Exception e) {
+        if (mLogWriter != null) {
+            log("Exception", "Stack trace follows...");
+            PrintWriter pw = new PrintWriter(mLogWriter);
+            e.printStackTrace(pw);
+            pw.flush();
+        }
+    }
+
+    @SuppressWarnings("deprecation")
+    static public synchronized void log(String prefix, String str) {
+        if (LOGGER == null) {
+            LOGGER = new FileLogger();
+            log("Logger", "\r\n\r\n --- New Log ---");
+        }
+        Date d = new Date();
+        int hr = d.getHours();
+        int min = d.getMinutes();
+        int sec = d.getSeconds();
+
+        // I don't use DateFormat here because (in my experience), it's much slower
+        StringBuffer sb = new StringBuffer(256);
+        sb.append('[');
+        sb.append(hr);
+        sb.append(':');
+        if (min < 10)
+            sb.append('0');
+        sb.append(min);
+        sb.append(':');
+        if (sec < 10) {
+            sb.append('0');
+        }
+        sb.append(sec);
+        sb.append("] ");
+        if (prefix != null) {
+            sb.append(prefix);
+            sb.append("| ");
+        }
+        sb.append(str);
+        sb.append("\r\n");
+        String s = sb.toString();
+
+        if (mLogWriter != null) {
+            try {
+                mLogWriter.write(s);
+                mLogWriter.flush();
+            } catch (IOException e) {
+                // Doesn't matter
+            }
+        }
+    }
+}
diff --git a/src/com/android/exchange/utility/patent_disclaimer.txt b/src/com/android/exchange/utility/patent_disclaimer.txt
new file mode 100644
index 0000000..8a715a3
--- /dev/null
+++ b/src/com/android/exchange/utility/patent_disclaimer.txt
@@ -0,0 +1,9 @@
+-----------------------------
+THIS IS NOT A GRANT OF PATENT RIGHTS.
+
+Google makes no representation or warranty that the source code made available hereunder is
+unencumbered by third-party patents.  Those intending to use this source code in hardware or
+software products are advised that implementations of this code, including in open source software
+or shareware, may require patent licenses from the relevant patent holders.
+
+-----------------------------
diff --git a/tests/src/com/android/exchange/EasEmailSyncAdapterTests.java b/tests/src/com/android/exchange/EasEmailSyncAdapterTests.java
new file mode 100644
index 0000000..a3e7dae
--- /dev/null
+++ b/tests/src/com/android/exchange/EasEmailSyncAdapterTests.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2009 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.Account;
+import com.android.email.provider.EmailContent.Mailbox;
+import com.android.exchange.adapter.EmailSyncAdapter;
+import com.android.exchange.adapter.EmailSyncAdapter.EasEmailSyncParser;
+
+import android.test.AndroidTestCase;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.GregorianCalendar;
+import java.util.TimeZone;
+
+public class EasEmailSyncAdapterTests extends AndroidTestCase {
+
+    /**
+     * 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;
+    }
+
+    /**
+     * Check functionality for getting mime type from a file name (using its extension)
+     * The default for all unknown files is application/octet-stream
+     */
+    public void testGetMimeTypeFromFileName() throws IOException {
+        EasSyncService service = getTestService();
+        EmailSyncAdapter adapter = new EmailSyncAdapter(service.mMailbox, service);
+        EasEmailSyncParser p = adapter.new EasEmailSyncParser(getTestInputStream(), adapter);
+        // Test a few known types
+        String mimeType = p.getMimeTypeFromFileName("foo.jpg");
+        assertEquals("image/jpeg", mimeType);
+        // Make sure this is case insensitive
+        mimeType = p.getMimeTypeFromFileName("foo.JPG");
+        assertEquals("image/jpeg", mimeType);
+        mimeType = p.getMimeTypeFromFileName("this_is_a_weird_filename.gif");
+        assertEquals("image/gif", mimeType);
+        // Test an illegal file name ending with the extension prefix
+        mimeType = p.getMimeTypeFromFileName("foo.");
+        assertEquals("application/octet-stream", mimeType);
+        // Test a really awful name
+        mimeType = p.getMimeTypeFromFileName(".....");
+        assertEquals("application/octet-stream", mimeType);
+        // Test a bare file name (no extension)
+        mimeType = p.getMimeTypeFromFileName("foo");
+        assertEquals("application/octet-stream", mimeType);
+        // And no name at all (null isn't a valid input)
+        mimeType = p.getMimeTypeFromFileName("");
+        assertEquals("application/octet-stream", mimeType);
+    }
+
+    public void testFormatDateTime() throws IOException {
+        EmailSyncAdapter adapter = getTestSyncAdapter();
+        GregorianCalendar calendar = new GregorianCalendar(TimeZone.getTimeZone("GMT"));
+        // Calendar is odd, months are zero based, so the first 11 below is December...
+        calendar.set(2008, 11, 11, 18, 19, 20);
+        String date = adapter.formatDateTime(calendar);
+        assertEquals("2008-12-11T18:19:20.000Z", date);
+        calendar.clear();
+        calendar.set(2012, 0, 2, 23, 0, 1);
+        date = adapter.formatDateTime(calendar);
+        assertEquals("2012-01-02T23:00:01.000Z", date);
+    }
+}
diff --git a/tests/src/com/android/exchange/EasSyncServiceTests.java b/tests/src/com/android/exchange/EasSyncServiceTests.java
new file mode 100644
index 0000000..45f4882
--- /dev/null
+++ b/tests/src/com/android/exchange/EasSyncServiceTests.java
@@ -0,0 +1,78 @@
+/*
+ * 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 android.content.Context;
+import android.test.AndroidTestCase;
+
+import java.io.File;
+import java.io.IOException;
+
+public class EasSyncServiceTests extends AndroidTestCase {
+    Context mMockContext;
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        mMockContext = getContext();
+    }
+
+   /**
+     * Test that our unique file name algorithm works as expected.
+     * @throws IOException
+     */
+    public void testCreateUniqueFile() throws IOException {
+        // Delete existing files, if they exist
+        EasSyncService svc = new EasSyncService();
+        svc.mContext = mMockContext;
+        try {
+            String fileName = "A11achm3n1.doc";
+            File uniqueFile = svc.createUniqueFileInternal(null, fileName);
+            assertEquals(fileName, uniqueFile.getName());
+            if (uniqueFile.createNewFile()) {
+                uniqueFile = svc.createUniqueFileInternal(null, fileName);
+                assertEquals("A11achm3n1-2.doc", uniqueFile.getName());
+                if (uniqueFile.createNewFile()) {
+                    uniqueFile = svc.createUniqueFileInternal(null, fileName);
+                    assertEquals("A11achm3n1-3.doc", uniqueFile.getName());
+                }
+           }
+            fileName = "A11achm3n1";
+            uniqueFile = svc.createUniqueFileInternal(null, fileName);
+            assertEquals(fileName, uniqueFile.getName());
+            if (uniqueFile.createNewFile()) {
+                uniqueFile = svc.createUniqueFileInternal(null, fileName);
+                assertEquals("A11achm3n1-2", uniqueFile.getName());
+            }
+        } finally {
+            // These are the files that should be created earlier in the test.  Make sure
+            // they are deleted for the next go-around
+            File directory = getContext().getFilesDir();
+            String[] fileNames = new String[] {"A11achm3n1.doc", "A11achm3n1-2.doc", "A11achm3n1"};
+            int length = fileNames.length;
+            for (int i = 0; i < length; i++) {
+                File file = new File(directory, fileNames[i]);
+                if (file.exists()) {
+                    file.delete();
+                }
+            }
+        }
+    }
+
+
+}
diff --git a/tests/src/com/android/exchange/TagsTests.java b/tests/src/com/android/exchange/TagsTests.java
new file mode 100644
index 0000000..1a87c5c
--- /dev/null
+++ b/tests/src/com/android/exchange/TagsTests.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2009 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.adapter.Tags;
+
+import android.test.AndroidTestCase;
+
+import java.util.HashMap;
+
+public class TagsTests extends AndroidTestCase {
+
+    // Make sure there are no duplicates in the tags table
+    // This test is no longer required - tags can be duplicated
+    public void disable_testNoDuplicates() {
+        String[][] allTags = Tags.pages;
+        HashMap<String, Boolean> map = new HashMap<String, Boolean>();
+        for (String[] page: allTags) {
+            for (String tag: page) {
+                assertTrue(tag, !map.containsKey(tag));
+                map.put(tag, true);
+            }
+        }
+    }
+}