am 62ee3f4c: (-s ours) am ec988c2f: DO NOT MERGE Added/updated Yahoo provider settings

* commit '62ee3f4cac15e94dccb7795334c0355aa0e80228':
  DO NOT MERGE Added/updated Yahoo provider settings
diff --git a/Android.mk b/Android.mk
index 7295eed..104fa3e 100644
--- a/Android.mk
+++ b/Android.mk
@@ -24,7 +24,9 @@
     src/com/android/email/service/IEmailServiceCallback.aidl
 # EXCHANGE-REMOVE-SECTION-END
 
-LOCAL_JAVA_STATIC_LIBRARIES := android-common
+LOCAL_STATIC_JAVA_LIBRARIES := android-common
+# Revive this when the app is unbundled.
+# LOCAL_SDK_VERSION := current
 
 LOCAL_PACKAGE_NAME := Email
 
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index d50817f..dd2bef4 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -4,9 +4,9 @@
      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.
@@ -14,73 +14,114 @@
      limitations under the License.
 -->
 
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+<manifest
+    xmlns:android="http://schemas.android.com/apk/res/android"
     package="com.android.email"
-    android:versionCode="230000"
-    android:versionName="2.3"
+    android:versionCode="300000"
+    android:versionName="3.0"
     >
 
-    <original-package android:name="com.android.email" />
+    <original-package
+        android:name="com.android.email" />
 
-    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
-    <uses-permission android:name="android.permission.READ_CONTACTS"/>
-    <uses-permission android:name="android.permission.READ_OWNER_DATA"/>
-    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
-    <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" />
+    <uses-permission
+        android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
+    <uses-permission
+        android:name="android.permission.READ_CONTACTS"/>
+    <uses-permission
+        android:name="android.permission.READ_OWNER_DATA"/>
+    <uses-permission
+        android:name="android.permission.ACCESS_NETWORK_STATE"/>
+    <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"/>
-    <uses-permission android:name="android.permission.WRITE_CALENDAR"/>
-    <uses-permission android:name="android.permission.READ_CALENDAR"/>
+    <uses-permission
+        android:name="android.permission.WRITE_CONTACTS"/>
+    <uses-permission
+        android:name="android.permission.WRITE_CALENDAR"/>
+    <uses-permission
+        android:name="android.permission.READ_CALENDAR"/>
 
     <!-- Only required if a store implements push mail and needs to keep network open -->
-    <uses-permission android:name="android.permission.WAKE_LOCK"/>
-    <uses-permission android:name="android.permission.READ_PHONE_STATE"/>
+    <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"/>
+    <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"/>
+    <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">
+    <application
+        android:icon="@mipmap/icon"
+        android:label="@string/app_name"
+        android:name="Email"
+        android:theme="@android:style/Theme.Holo.Light"
+        android:hardwareAccelerated="false"
+        >
+        <!-- STOPSHIP android:hardwareAccelerated should be "true" -->
         <activity
-            android:name=".activity.Welcome">
+            android:name=".activity.Welcome"
+            >
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action
+                    android:name="android.intent.action.MAIN" />
+                <category
+                    android:name="android.intent.category.DEFAULT" />
+                <category
+                    android:name="android.intent.category.LAUNCHER" />
             </intent-filter>
         </activity>
         <activity
             android:name=".activity.UpgradeAccounts"
             android:label="@string/upgrade_accounts_title"
             android:theme="@android:style/Theme.NoTitleBar"
-            android:configChanges="keyboardHidden|orientation" >
+            android:configChanges="keyboardHidden|orientation"
+            >
         </activity>
         <!-- Must be exported in order for the AccountManager to launch it -->
+        <!-- Also available for continuous test systems to force account creation -->
         <activity
             android:name=".activity.setup.AccountSetupBasics"
             android:label="@string/account_setup_basics_title"
             android:exported="true"
             >
+            <intent-filter>
+                <action
+                    android:name="com.android.email.CREATE_ACCOUNT" />
+                <category
+                    android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
         </activity>
         <activity
             android:name=".activity.setup.AccountSetupAccountType"
@@ -98,12 +139,9 @@
             >
         </activity>
         <!--EXCHANGE-REMOVE-SECTION-START-->
-        <!-- This activity ignores configuration changes (e.g. rotation) so that it will
-             not make multiple calls to AccountSetupCheckSettings. -->
         <activity
             android:name=".activity.setup.AccountSetupExchange"
             android:label="@string/account_setup_exchange_title"
-            android:configChanges="keyboardHidden|orientation"
             >
         </activity>
         <!--EXCHANGE-REMOVE-SECTION-END-->
@@ -117,21 +155,15 @@
             android:label="@string/account_setup_names_title"
             >
         </activity>
-        <!-- XXX Note: this activity is hacked to ignore config changes,
-             since it doesn't currently handle them correctly in code. -->
         <activity
-            android:name=".activity.setup.AccountSetupCheckSettings"
-            android:label="@string/account_setup_check_settings_title"
-            android:configChanges="keyboardHidden|orientation"
-            >
-        </activity>
-        <activity
-            android:name=".activity.setup.AccountSettings"
+            android:name=".activity.setup.AccountSettingsXL"
             android:label="@string/account_settings_action"
             >
             <intent-filter>
-                <action android:name="com.android.email.activity.setup.ACCOUNT_MANAGER_ENTRY" />
-                <category android:name="android.intent.category.DEFAULT" />
+                <action
+                    android:name="com.android.email.activity.setup.ACCOUNT_MANAGER_ENTRY" />
+                <category
+                    android:name="android.intent.category.DEFAULT" />
             </intent-filter>
         </activity>
         <activity
@@ -142,36 +174,42 @@
 
         <activity
             android:name=".activity.Debug"
-            android:label="@string/debug_title">
+            android:label="@string/debug_title"
+            >
         </activity>
         <activity
             android:name=".activity.AccountFolderList"
-            android:launchMode="singleTop" >
-        </activity>
-        
-        <activity 
-            android:name=".activity.AccountShortcutPicker"
-            android:label="@string/app_name"
-            android:enabled="false"
+            android:launchMode="singleTop"
             >
-            <intent-filter>
-                <action android:name="android.intent.action.CREATE_SHORTCUT" />
-                <category android:name="android.intent.category.DEFAULT" />
+        </activity>
+
+        <activity
+            android:name=".activity.AccountShortcutPicker"
+            android:label="@string/account_shortcut_picker_title"
+            android:enabled="false"
+            android:theme="@android:style/Theme.Holo.DialogWhenLarge"
+            >
+            <intent-filter
+                android:label="@string/account_shortcut_picker_name">
+                <action
+                    android:name="android.intent.action.CREATE_SHORTCUT" />
+                <category
+                    android:name="android.intent.category.DEFAULT" />
             </intent-filter>
         </activity>
-        
+
         <activity
             android:name=".activity.MailboxList"
-            android:theme="@style/ThemeNoTitleBar">
+            >
         </activity>
-        
+
         <activity
             android:name=".activity.MessageList"
-            android:theme="@style/ThemeNoTitleBar">
-            <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.MessageListXL"
+            >
         </activity>
 
         <!--
@@ -187,50 +225,104 @@
             >
             <intent-filter>
                 <!-- This action is only to allow an entry point for launcher shortcuts -->
-                <action android:name="android.intent.action.MAIN" />
+                <action
+                    android:name="android.intent.action.MAIN" />
             </intent-filter>
         </activity>
-                
+
         <activity
             android:name=".activity.MessageView"
-            android:theme="@android:style/Theme.NoTitleBar" >
+            >
+        </activity>
+        <activity
+            android:name=".activity.MessageFileView"
+            >
+            <intent-filter
+                android:label="@string/app_name">
+                <action
+                    android:name="android.intent.action.VIEW" />
+                <data
+                    android:mimeType="application/eml" />
+                <data
+                    android:mimeType="message/rfc822" />
+                <category
+                    android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
         </activity>
         <activity
             android:name=".activity.MessageCompose"
-            android:label="@string/app_name"
+            android:label="@string/compose_title"
             android:enabled="false"
             >
             <intent-filter>
-                <action android:name="android.intent.action.VIEW" />
-                <action android:name="android.intent.action.SENDTO" />
-                <data android:scheme="mailto" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <category android:name="android.intent.category.BROWSABLE" />
+                <action
+                    android:name="android.intent.action.VIEW" />
+                <action
+                    android:name="android.intent.action.SENDTO" />
+                <data
+                    android:scheme="mailto" />
+                <category
+                    android:name="android.intent.category.DEFAULT" />
+                <category
+                    android:name="android.intent.category.BROWSABLE" />
             </intent-filter>
-            <intent-filter android:label="@string/app_name">
-                <action android:name="android.intent.action.SEND" />
-                <data android:mimeType="*/*" />
-                <category android:name="android.intent.category.DEFAULT" />
+            <intent-filter
+                android:label="@string/app_name">
+                <action
+                    android:name="android.intent.action.SEND" />
+                <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
+                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>
+            <intent-filter>
+                <action
+                    android:name="com.android.email.intent.action.REPLY" />
             </intent-filter>
         </activity>
         <!--EXCHANGE-REMOVE-SECTION-START-->
-        <receiver android:name="com.android.exchange.EmailSyncAlarmReceiver"/>
-        <receiver android:name="com.android.exchange.MailboxAlarmReceiver"/>
+        <receiver
+            android:name="com.android.exchange.EmailSyncAlarmReceiver"/>
+        <receiver
+            android:name="com.android.exchange.MailboxAlarmReceiver"/>
         <!--EXCHANGE-REMOVE-SECTION-END-->
 
-        <receiver android:name=".service.EmailBroadcastReceiver" android:enabled="true">
+        <receiver 
+            android:name=".service.AttachmentDownloadService$Watchdog"
+            android:enabled="true"/>
+
+        <receiver
+            android:name=".service.EmailBroadcastReceiver"
+            android:enabled="true">
             <intent-filter>
-                <action android:name="android.intent.action.BOOT_COMPLETED" />
-                <action android:name="android.intent.action.DEVICE_STORAGE_LOW" />
-                <action android:name="android.intent.action.DEVICE_STORAGE_OK" />
+                <action
+                    android:name="android.intent.action.BOOT_COMPLETED" />
+                <action
+                    android:name="android.intent.action.DEVICE_STORAGE_LOW" />
+                <action
+                    android:name="android.intent.action.DEVICE_STORAGE_OK" />
+            </intent-filter>
+            <!-- To handle secret code to activate the debug screen. -->
+            <intent-filter>
+                <action
+                    android:name="android.provider.Telephony.SECRET_CODE" />
+                <!-- "36245" = "email" -->
+                <data
+                    android:scheme="android_secret_code"
+                    android:host="36245" />
             </intent-filter>
         </receiver>
-        <service android:name=".service.EmailBroadcastProcessorService" />
+        <service
+            android:name=".service.EmailBroadcastProcessorService" />
 
         <!-- Support for DeviceAdmin / DevicePolicyManager.  See SecurityPolicy class for impl. -->
         <receiver
@@ -242,7 +334,8 @@
                 android:name="android.app.device_admin"
                 android:resource="@xml/device_admin" />
             <intent-filter>
-                <action android:name="android.app.action.DEVICE_ADMIN_ENABLED" />
+                <action
+                    android:name="android.app.action.DEVICE_ADMIN_ENABLED" />
             </intent-filter>
         </receiver>
 
@@ -251,33 +344,88 @@
             android:enabled="false"
             >
         </service>
-        
+
+        <service
+            android:name=".Controller$ControllerService"
+            android:enabled="true"
+            >
+        </service>
+
+        <service
+            android:name=".service.AttachmentDownloadService"
+            android:enabled="false"
+            >
+        </service>
+
         <!--EXCHANGE-REMOVE-SECTION-START-->
-        <!--Required stanza to register the ContactsSyncAdapterService with SyncManager -->
-        <service 
-            android:name="com.android.exchange.ContactsSyncAdapterService" 
-        	android:exported="true">
+        <!--Required stanza to register the PopImapAuthenticatorService with AccountManager -->
+        <service
+            android:name=".service.PopImapAuthenticatorService"
+            android:exported="true"
+            android:enabled="true"
+            >
             <intent-filter>
-                <action android:name="android.content.SyncAdapter" />
+                <action 
+                    android:name="android.accounts.AccountAuthenticator" />
+            </intent-filter>
+            <meta-data
+                android:name="android.accounts.AccountAuthenticator"
+                android:resource="@xml/pop_imap_authenticator"
+                />
+        </service>
+
+        <!--Required stanza to register the PopImapSyncAdapterService with SyncManager -->
+        <service
+            android:name="com.android.email.service.PopImapSyncAdapterService"
+            android:exported="true">
+            <intent-filter>
+                <action
+                    android:name="android.content.SyncAdapter" />
+            </intent-filter>
+            <meta-data android:name="android.content.SyncAdapter"
+                       android:resource="@xml/syncadapter_pop_imap" />
+        </service>
+
+        <!--EXCHANGE-REMOVE-SECTION-START-->
+        <!--Required stanza to register the EAS EmailSyncAdapterService with SyncManager -->
+        <service
+            android:name="com.android.exchange.EmailSyncAdapterService"
+            android:exported="true">
+            <intent-filter>
+                <action
+                    android:name="android.content.SyncAdapter" />
+            </intent-filter>
+            <meta-data android:name="android.content.SyncAdapter"
+                       android:resource="@xml/syncadapter_email" />
+        </service>
+
+        <!--Required stanza to register the EAS 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>
 
-        <!--Required stanza to register the CalendarSyncAdapterService with SyncManager -->
+        <!--Required stanza to register the EAS CalendarSyncAdapterService with SyncManager -->
         <service
             android:name="com.android.exchange.CalendarSyncAdapterService"
             android:exported="true">
             <intent-filter>
-                <action android:name="android.content.SyncAdapter" />
+                <action
+                    android:name="android.content.SyncAdapter" />
             </intent-filter>
             <meta-data android:name="android.content.SyncAdapter"
                        android:resource="@xml/syncadapter_calendar" />
         </service>
 
-        <!-- Add android:process=":remote" below to enable SyncManager as a separate process -->
+        <!-- Add android:process=":remote" below to enable ExchangeService as a separate process -->
         <service
-            android:name="com.android.exchange.SyncManager"
+            android:name="com.android.exchange.ExchangeService"
             android:enabled="true"
             >
         </service>
@@ -289,15 +437,16 @@
             android:enabled="true"
             >
             <intent-filter>
-                <action android:name="android.accounts.AccountAuthenticator" />
+                <action
+                    android:name="android.accounts.AccountAuthenticator" />
             </intent-filter>
             <meta-data
                 android:name="android.accounts.AccountAuthenticator"
-                android:resource="@xml/authenticator"
+                android:resource="@xml/eas_authenticator"
                 />
         </service>
         <!--
-            EasAuthenticatorService with the altenative label.  Disabled by default,
+            EasAuthenticatorService with the alternative label.  Disabled by default,
             and OneTimeInitializer enables it if the vendor policy tells so.
         -->
         <service
@@ -306,7 +455,8 @@
             android:enabled="false"
             >
             <intent-filter>
-                <action android:name="android.accounts.AccountAuthenticator" />
+                <action
+                    android:name="android.accounts.AccountAuthenticator" />
             </intent-filter>
             <meta-data
                 android:name="android.accounts.AccountAuthenticator"
@@ -327,21 +477,40 @@
              it exposes user passwords and other confidential information. -->
         <provider
             android:name=".provider.EmailProvider"
-            android:authorities="com.android.email.provider"
+            android:authorities="com.android.email.provider;com.android.email.notifier"
             android:multiprocess="true"
             android:permission="com.android.email.permission.ACCESS_PROVIDER"
+            android:label="@string/app_name"
             />
 
         <!--EXCHANGE-REMOVE-SECTION-START-->
-        <!-- In this release, GAL information is used locally only, so we used the same
-             strict permissions. -->
         <provider
-            android:name="com.android.exchange.provider.ExchangeProvider"
-            android:authorities="com.android.exchange.provider"
-            android:multiprocess="true"
-            android:permission="com.android.email.permission.ACCESS_PROVIDER"
-            />
+            android:name="com.android.exchange.provider.ExchangeDirectoryProvider"
+            android:authorities="com.android.exchange.directory.provider"
+            android:readPermission="android.permission.READ_CONTACTS"
+            android:multiprocess="false"
+            >
+          <meta-data 
+              android:name="android.content.ContactDirectory"
+              android:value="true"/>
+        </provider>
         <!--EXCHANGE-REMOVE-SECTION-END-->
 
+        <!-- Email AppWidget definitions -->
+        <service
+            android:name=".provider.WidgetProvider$WidgetService"
+            android:enabled="true"
+            android:exported="true"
+            />
+        <receiver
+            android:name=".provider.WidgetProvider" >
+            <intent-filter>
+                <action 
+                    android:name="android.appwidget.action.APPWIDGET_UPDATE" />
+            </intent-filter>
+            <meta-data 
+                android:name="android.appwidget.provider"
+                android:resource="@xml/widget_info" />
+        </receiver>
     </application>
 </manifest>
diff --git a/proguard.flags b/proguard.flags
index f3b7e57..747aed2 100644
--- a/proguard.flags
+++ b/proguard.flags
@@ -1,11 +1,11 @@
 # keep names that are used by reflection.
 -keep class com.android.email.provider.EmailContent$Account
 -keepclasseswithmembers class * {
-  public static void actionEditIncomingSettings(android.app.Activity, com.android.email.provider.EmailContent$Account);
+  public static void actionEditIncomingSettings(android.app.Activity, int, com.android.email.provider.EmailContent$Account);
 }
 
 -keepclasseswithmembers class * {
-  public static void actionEditOutgoingSettings(android.app.Activity, com.android.email.provider.EmailContent$Account);
+  public static void actionEditOutgoingSettings(android.app.Activity, int, com.android.email.provider.EmailContent$Account);
 }
 
 -keepclasseswithmembers class * {
@@ -23,11 +23,10 @@
 
 -keep class * extends org.apache.james.mime4j.util.TempStorage
 
-# Keep names that are used only by unit tests
-
-# Any methods whose name is '*ForTest' are preserved.
+# Keep names that are used only by unit tests or by animators
 -keep class ** {
   *** *ForTest(...);
+  *** *Anim(...);
 }
 
 -keepclasseswithmembers class com.android.email.GroupMessagingListener {
diff --git a/res/drawable-hdpi/drag_background_holo.9.png b/res/drawable-hdpi/drag_background_holo.9.png
new file mode 100644
index 0000000..a5f3814
--- /dev/null
+++ b/res/drawable-hdpi/drag_background_holo.9.png
Binary files differ
diff --git a/res/drawable-hdpi/stat_notify_calendar.png b/res/drawable-hdpi/stat_notify_calendar.png
index 7e7cd60..71ceef0 100644
--- a/res/drawable-hdpi/stat_notify_calendar.png
+++ b/res/drawable-hdpi/stat_notify_calendar.png
Binary files differ
diff --git a/res/drawable-mdpi/drag_background_holo.9.png b/res/drawable-mdpi/drag_background_holo.9.png
new file mode 100644
index 0000000..63a1f5a
--- /dev/null
+++ b/res/drawable-mdpi/drag_background_holo.9.png
Binary files differ
diff --git a/res/drawable-mdpi/stat_notify_calendar.png b/res/drawable-mdpi/stat_notify_calendar.png
old mode 100755
new mode 100644
index 4433a16..6bff5f1
--- a/res/drawable-mdpi/stat_notify_calendar.png
+++ b/res/drawable-mdpi/stat_notify_calendar.png
Binary files differ
diff --git a/res/drawable/divider.xml b/res/drawable/divider.xml
new file mode 100644
index 0000000..c6789e8
--- /dev/null
+++ b/res/drawable/divider.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2010 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<!-- Drawable for a divider.  Intended to be used with LinearLayouts. -->
+
+<shape
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle" >
+    <size
+        android:width="1dip"
+        android:height="1dip"
+        />
+    <solid
+        android:color="@color/divider_color"
+        />
+</shape>
\ No newline at end of file
diff --git a/res/drawable/drag_background.xml b/res/drawable/drag_background.xml
new file mode 100644
index 0000000..6bd40fe
--- /dev/null
+++ b/res/drawable/drag_background.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2010 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<!-- STOPSHIP Temporary UI -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:state_activated="true"
+          android:drawable="@drawable/drag_background_holo" />
+    <item android:drawable="@drawable/drag_background_holo" />
+</selector>
diff --git a/res/drawable/widget_conversation_selector.xml b/res/drawable/widget_conversation_selector.xml
new file mode 100644
index 0000000..9376172
--- /dev/null
+++ b/res/drawable/widget_conversation_selector.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (c) 2010, Google Inc. -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:state_focused="true"
+          android:drawable="@drawable/header_row_focused_email_widget_holo" />
+    <item android:state_pressed="true"
+          android:drawable="@drawable/header_row_press_email_widget_holo" />
+    <item android:drawable="@drawable/bg_row_email_widget_holo" />
+</selector>
diff --git a/res/drawable-hdpi/icon.png b/res/mipmap-hdpi/icon.png
similarity index 100%
rename from res/drawable-hdpi/icon.png
rename to res/mipmap-hdpi/icon.png
Binary files differ
diff --git a/res/drawable-mdpi/icon.png b/res/mipmap-mdpi/icon.png
similarity index 100%
rename from res/drawable-mdpi/icon.png
rename to res/mipmap-mdpi/icon.png
Binary files differ
diff --git a/res/values/strings.xml b/res/values/strings.xml
index e242774..4356ed4 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -4,9 +4,9 @@
      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.
@@ -19,40 +19,41 @@
     <!-- 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="meeting_invitation"></string>
+    <string name="account_setup_basics_email_hint" translatable="false"></string>
     <!-- Do Not Translate.  Unused string. -->
-    <string name="account_setup_failed_security_policies_required"></string>
+    <string name="account_setup_basics_password_hint" translatable="false"></string>
     <!-- Do Not Translate.  Unused string. -->
-    <string name="account_folder_list_summary_unread"></string>    
+    <string name="account_settings_exchange_summary" translatable="false"></string>
     <!-- Do Not Translate.  Unused string. -->
-    <string name="account_settings_add_account_label"></string>
+    <string name="message_compose_attachments_skipped_toast" translatable="false"></string>
     <!-- Do Not Translate.  Unused string. -->
-    <string name="account_setup_basics_instructions"></string>
+    <string name="status_loading_more" translatable="false"></string>
     <!-- Do Not Translate.  Unused string. -->
-    <string name="account_setup_exchange_domain_hint"></string>
+    <string name="account_setup_check_settings_canceling_msg" translatable="false"></string>
     <!-- Do Not Translate.  Unused string. -->
-    <string name="account_setup_exchange_domain_label"></string>
+    <string name="message_view_show_pictures_instructions" translatable="false"></string>
     <!-- Do Not Translate.  Unused string. -->
-    <string name="account_setup_options_mail_window_all"></string>
+    <string name="provider_note_yahoo" translatable="false"></string>
     <!-- Do Not Translate.  Unused string. -->
-    <string name="accounts_context_menu_title"></string>
+    <string name="provider_note_yahoo_uk" translatable="false"></string>
     <!-- Do Not Translate.  Unused string. -->
-    <string name="accounts_title"></string>
+    <plurals name="notification_sender_name_multi_messages" translatable="false" />
     <!-- Do Not Translate.  Unused string. -->
-    <string name="add_contact_dlg_message_fmt"></string>
+    <plurals name="notification_num_new_messages_single_account" translatable="false" />
     <!-- Do Not Translate.  Unused string. -->
-    <string name="add_contact_dlg_title"></string>
-    <!-- Do Not Translate.  Unused string. -->
-    <string name="message_list_title"></string>
-    <!-- Do Not Translate.  Unused string. -->
-    <string name="status_loading_more_failed"></string>
-    <!-- Do Not Translate.  Unused string. -->
-    <string name="status_sending_messages_failed"></string>
-    <!-- Do Not Translate.  Unused string. -->
-    <string name="provider_note_yahoo"></string>
-    <!-- Do Not Translate.  Unused string. -->
-    <string name="provider_note_yahoo_uk"></string>
+    <plurals name="notification_num_new_messages_multi_account" translatable="false" />
 
+    <!-- Messages that are not currently used, but will probably reused in the near future -->
+    <!-- STOPSHIP Remove them if they're not used after all -->
+    <!-- Appears in message list view of outbox while messages are being sent -->
+    <string name="status_sending_messages">Sending messages\u2026</string>
+    <!-- Command shown on Outbox to send all pending messages [CHAR_LIMIT=15] -->
+    <string name="message_list_send_pending_messages_action">Send all</string>
+    <!-- Toast while fetching attachment -->
+    <string name="message_view_fetching_attachment_toast">Fetching attachment.</string>
+    <!-- Appears progress dialog for fetching attachment -->
+    <string name="message_view_fetching_attachment_progress">Fetching attachment <xliff:g
+        id="filename">%s</xliff:g></string>
     <!-- Permissions label for reading attachments -->
     <string name="read_attachment_label">Read Email attachments</string>
     <!-- Permissions description for reading attachments -->
@@ -73,13 +74,15 @@
 
     <!-- Actions will be used as buttons and in menu items -->
     <skip />
-    
-    <!-- Used as part of a multi-step process -->
-    <string name="next_action">Next</string> 
-    <!--  Button name used to confirm acceptance of dialog boxes, warnings, errors, etc. -->
-    <string name="okay_action">OK</string> 
-    <!--  Button name used to cancel out of dialog boxes -->
+
+    <!-- Button name used as part of a multi-step process -->
+    <string name="next_action">Next</string>
+    <!-- Button name used to confirm acceptance of dialog boxes, warnings, errors, etc. -->
+    <string name="okay_action">OK</string>
+    <!-- Button name used to cancel out of dialog boxes -->
     <string name="cancel_action">Cancel</string>
+    <!-- Button name used to move to previous step during setup sequence [CHAR_LIMIT=16] -->
+    <string name="previous_action">Previous</string>
     <!-- Menu item/button name -->
     <string name="send_action">Send</string>
     <!-- Menu item/button name -->
@@ -91,11 +94,11 @@
     <!-- Menu item/button name -->
     <string name="forward_action">Forward</string>
     <!--  Button name used to complete a multi-step process -->
-    <string name="done_action">Done</string> 
+    <string name="done_action">Done</string>
     <!-- Menu item/button name -->
     <string name="discard_action">Discard</string>
-    <!-- Menu item/button name -->
-    <string name="save_draft_action">Save as draft</string>
+    <!-- Menu item/button name [CHAR_LIMIT=16] -->
+    <string name="save_draft_action">Save draft</string>
     <!-- Menu item/button name -->
     <string name="read_unread_action">Read/Unread</string>
     <!-- Menu item/button name -->
@@ -132,18 +135,20 @@
     <string name="mark_as_read_action">Mark as read</string>
     <!-- Menu item -->
     <string name="mark_as_unread_action">Mark as unread</string>
-    <!-- Menu item -->
-    <string name="add_cc_bcc_action">Add Cc/Bcc</string>
+    <!-- Menu item for moving messages to folders [CHAR LIMIT=10] -->
+    <string name="move_action">Move</string>
+    <!-- Menu item / button label for adding CC/BCC fields on message compose. [CHAR LIMIT=16] -->
+    <string name="add_cc_bcc_action">+ Cc/Bcc</string>
     <!-- Menu item -->
     <string name="add_attachment_action">Add attachment</string>
     <!-- Menu item (debug screen) -->
     <string name="dump_settings_action">Dump settings</string>
     <!-- Appears in choose attachment dialog title -->
     <string name="choose_attachment_dialog_title">Choose attachment</string>
-    <!-- Appears in message list view while messages are being loaded -->
-    <string name="status_loading_more">Loading messages\u2026</string>
-    <!-- Appears in message list view of outbox while messages are being sent -->
-    <string name="status_sending_messages">Sending messages\u2026</string>
+    <!-- Dialog title to select a mailbox to which the user moves messages [CHAR LIMIT=20] -->
+    <string name="move_to_folder_dialog_title">Move to</string>
+    <!-- Appears in message list view while messages are being loaded [CHAR LIMIT=260dip] -->
+    <string name="status_loading_messages">Loading messages\u2026</string>
     <!-- Appears in message list view when there's a network error. -->
     <!-- Also appears in a toast, in the message viewer, when there's a network error. -->
     <string name="status_network_error">Connection error</string>
@@ -152,15 +157,22 @@
     <!-- 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>
+    <!-- Text shown with dragged messages to indicate how many are being dragged [CHAR LIMIT=40]-->
+    <plurals name="move_messages">
+        <item quantity="one">Move <xliff:g id="num_message" example="1">%1$d</xliff:g> message
+        </item>
+        <item quantity="other">Move <xliff:g id="num_message" example="3">%1$d</xliff:g> messages
+        </item>
+    </plurals>
 
     <!-- Notification message in notifications window when one account has
          one or more new messages; e.g, "279 unread (someone@google.com)". -->
     <plurals name="notification_new_one_account_fmt">
         <!-- Case of one new message. -->
-        <item quantity="one"><xliff:g id="unread_message_count" example="1">%1$d</xliff:g> unread (<xliff:g id="account">%2$s</xliff:g>)</item> 
+        <item quantity="one"><xliff:g id="unread_message_count" example="1">%1$d</xliff:g> unread (<xliff:g id="account">%2$s</xliff:g>)</item>
 
         <!-- Case of "few" (small number of) new messages. -->
-        <item quantity="few"><xliff:g id="unread_message_count" example="2">%1$d</xliff:g> unread (<xliff:g id="account">%2$s</xliff:g>)</item> 
+        <item quantity="few"><xliff:g id="unread_message_count" example="2">%1$d</xliff:g> unread (<xliff:g id="account">%2$s</xliff:g>)</item>
 
         <!-- Case of a plural number of new messages. -->
         <item quantity="other"><xliff:g id="unread_message_count" example="279">%1$d</xliff:g> unread (<xliff:g id="account">%2$s</xliff:g>)</item>
@@ -174,20 +186,28 @@
         <!-- Case of plural number of accounts with unread messages. -->
         <item quantity="other">in <xliff:g id="number_accounts" example="10">%d</xliff:g> accounts</item>
     </plurals>
+    <!-- String to indicate which account received a new message. [CHAR LIMIT=none] -->
+    <string name="notification_to_account">to <xliff:g id="receiver_name"
+            example="Main">%1$s</xliff:g></string>
 
+    <!-- The number of accounts configured. [CHAR LIMIT=16] -->
+    <plurals name="number_of_accounts">
+        <item quantity="one"><xliff:g id="num_accounts" example="1">%1$d</xliff:g> account</item>
+        <item quantity="other"><xliff:g id="num_accounts" example="2">%1$d</xliff:g> accounts</item>
+    </plurals>
     <!-- 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>
+    <string name="mailbox_name_server_inbox" translatable="false">Inbox</string>
     <!-- Do Not Translate.  This is the name of the "outbox" folder, on the server. -->
-    <string name="mailbox_name_server_outbox">Outbox</string>
+    <string name="mailbox_name_server_outbox" translatable="false">Outbox</string>
     <!-- Do Not Translate.  This is the name of the "drafts" folder, on the server. -->
-    <string name="mailbox_name_server_drafts">Drafts</string>
+    <string name="mailbox_name_server_drafts" translatable="false">Drafts</string>
     <!-- Do Not Translate.  This is the name of the "trash" folder, on the server. -->
-    <string name="mailbox_name_server_trash">Trash</string>
+    <string name="mailbox_name_server_trash" translatable="false">Trash</string>
     <!-- Do Not Translate.  This is the name of the "sent" folder, on the server. -->
-    <string name="mailbox_name_server_sent">Sent</string>
+    <string name="mailbox_name_server_sent" translatable="false">Sent</string>
     <!-- Do Not Translate.  This is the name of the "junk" folder, on the server. -->
-    <string name="mailbox_name_server_junk">Junk</string>
+    <string name="mailbox_name_server_junk" translatable="false">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 -->
@@ -206,35 +226,57 @@
     <!-- Version number, shown only on debug screen -->
     <string name="debug_version_fmt">Version: <xliff:g id="version">%s</xliff:g></string>
     <!-- Do Not Translate.  Checkbox label, shown only on debug screen -->
-    <string name="debug_enable_debug_logging_label" translatable="false">Enable extra debug logging?</string>
+    <string name="debug_enable_debug_logging_label" translatable="false">
+        Enable extra debug logging?</string>
     <!-- Do Not Translate.  Checkbox label, shown only on debug screen -->
-    <string name="debug_enable_sensitive_logging_label" translatable="false">Enable sensitive information debug logging? (May show passwords in logs.)</string>
+    <string name="debug_enable_sensitive_logging_label" translatable="false">
+        Enable sensitive information debug logging? (May show passwords in logs.)</string>
     <!-- Do Not Translate.  Checkbox label, shown only on debug screen -->
-    <string name="debug_enable_exchange_logging_label" translatable="false">Enable exchange parser logging? (Extremely verbose)</string>
+    <string name="debug_enable_exchange_logging_label" translatable="false">
+        Enable exchange parser logging? (Extremely verbose)</string>
     <!-- Do Not Translate.  Checkbox label, shown only on debug screen -->
-    <string name="debug_enable_exchange_file_logging_label" translatable="false">Enable exchange sd card logging?</string>
+    <string name="debug_enable_exchange_file_logging_label" translatable="false">
+        Enable exchange sd card logging?</string>
+    <!-- Do Not Translate.  Button label, shown only on debug screen -->
+    <string name="debug_clear_webview_cache" translatable="false">
+        Clear WebView cache</string>
+    <!-- Do Not Translate.  Checkbox label, shown on debug screen. -->
+    <string name="debug_disable_graphics_acceleration_label" translatable="false">
+        Disable hardware graphics acceleration</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 starred -->
+    <string name="account_folder_list_summary_inbox">Inbox</string>
+    <!-- The summary section entry in the AccountFolder list to display all starred
+        [CHAR LIMIT=200dip] -->
     <string name="account_folder_list_summary_starred">Starred</string>
-    <!-- The summary section entry in the AccountFolder list to display all drafts -->
+    <!-- The summary section entry in the AccountFolder list to display all drafts
+        [CHAR LIMIT=200dip] -->
     <string name="account_folder_list_summary_drafts">Drafts</string>
-    <!-- The summary section entry in the AccountFolder list to display all outboxes -->
+    <!-- The summary section entry in the AccountFolder list to display all outboxes
+        [CHAR LIMIT=200dip] -->
     <string name="account_folder_list_summary_outbox">Outbox</string>
     <!-- Toast that appears when you select "Refresh" menu option -->
     <string name="account_folder_list_refresh_toast">Please longpress an account to refresh it</string>
 
     <!-- Title of the screen that shows a list of mailboxes for an account -->
     <string name="mailbox_list_title">Mailbox</string>
+    <!-- Label shown in the account selector to select "Combined view", which contains
+         Combined Inbox, Combined Outbox, etc. [CHAR LIMIT=30] -->
+    <string name="mailbox_list_account_selector_combined_view">Combined view</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>
+    <!--  The number of messages that are currently selected for various operations such as
+          "delete message(s)" [CHAR LIMIT=32] -->
+    <plurals name="message_view_selected_message_count">
+        <item quantity="one"  ><xliff:g id="message_count" example="1">%d</xliff:g> selected</item>
+        <item quantity="other"><xliff:g id="message_count" example="9">%d</xliff:g> selected</item>
+    </plurals>
+    <!-- Message to show when there are no messages (emails) in the selected mailbox.
+         [CHAR LIMIT=26] -->
+    <string name="message_list_no_messages">No messages</string>
     <!-- Hint text in To field -->
     <string name="message_compose_to_hint">To</string>
     <!-- Hint text in Cc field -->
@@ -243,20 +285,31 @@
     <string name="message_compose_bcc_hint">Bcc</string>
     <!-- Hint text in Subject field -->
     <string name="message_compose_subject_hint">Subject</string>
+    <!-- Label for From field [CHAR LIMIT=12] -->
+    <string name="message_compose_from_label">From:</string>
+    <!-- Label for To field [CHAR LIMIT=12] -->
+    <string name="message_compose_to_label">To:</string>
+    <!-- Label for Cc field [CHAR LIMIT=12] -->
+    <string name="message_compose_cc_label">Cc:</string>
+    <!-- Label for Bcc field [CHAR LIMIT=12] -->
+    <string name="message_compose_bcc_label">Bcc:</string>
+    <!-- Label for Subject field [CHAR LIMIT=12] -->
+    <string name="message_compose_subject_label">Subject:</string>
     <!-- Hint text in Message composer body field -->
-    <string name="message_compose_body_hint">Compose Mail</string>    
+    <string name="message_compose_body_hint">Compose Mail</string>
     <!-- Header for forwarded original messages -->
     <string name="message_compose_fwd_header_fmt">\n\n-------- Original Message --------\nSubject: <xliff:g id="subject">%1$s</xliff:g>\nFrom: <xliff:g id="sender">%2$s</xliff:g>\nTo: <xliff:g id="to">%3$s</xliff:g>\nCC: <xliff:g id="cc">%4$s</xliff:g>\n\n</string>
     <!-- Header for replied-to messages -->
     <string name="message_compose_reply_header_fmt">\n\n<xliff:g id="sender">%s</xliff:g> wrote:\n\n</string>
     <!-- Heading that appears before forwarded text -->
     <string name="message_compose_quoted_text_label">Quoted text</string>
+    <!-- Label of checkbox to include original message in a forwarded/replied message
+         [CHAR_LIMIT=32] -->
+    <string name="message_compose_include_quoted_text_checkbox_label">Include 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.
@@ -266,27 +319,40 @@
     <string name="message_view_to_label">To:</string>
     <!-- Label for CC field in read message view -->
     <string name="message_view_cc_label">Cc:</string>
-    <!-- Menu item -->
-    <string name="message_view_attachment_view_action">Open</string>
-    <!-- Button name -->
-    <string name="message_view_attachment_download_action">Save</string>
+    <string name="message_view_bcc_label">Bcc:</string>
+    <!-- Button name to view an attachment (in another activity) [CHAR LIMIT=10]-->
+    <string name="message_view_attachment_view_action">View</string>
+    <!-- Button name, to load an attachment from the mail server [CHAR LIMIT=10]-->
+    <string name="message_view_attachment_load_action">Load</string>
+    <!-- Button name, to save the attachment to SD card [CHAR LIMIT=10]-->
+    <string name="message_view_attachment_save_action">Save</string>
+    <!-- Button name, to cancel a queued attachment download [CHAR LIMIT=10]-->
+    <string name="message_view_attachment_cancel_action">Stop</string>
     <!-- Toast after saving attachment [CHAR LIMIT=30] -->
     <string name="message_view_status_attachment_saved">Attachment saved
 as <xliff:g id="filename">%s</xliff:g>.</string>
     <!-- Toast after attachment could not be saved [CHAR LIMIT=30] -->
     <string name="message_view_status_attachment_not_saved">Unable to
 save attachment.</string>
-    <!-- Displayed in message view -->
-    <string name="message_view_show_pictures_instructions">Select \"Show pictures\" to display embedded pictures.</string>
-    <!-- Button on email opened for reading -->
+    <!-- Toast upon using "send" when one or more attachments will need to be background loaded
+      [CHAR LIMIT=none]-->
+    <string name="message_view_attachment_background_load">Note: One or more attachments in your
+        forwarded message will be downloaded prior to sending.</string>
+    <!--Button on the message view screen to show the message content [CHAR LIMIT=16] -->
+    <string name="message_view_show_message_action">Message</string>
+    <!--Button on the message view screen to show the calendar invite [CHAR LIMIT=16] -->
+    <string name="message_view_show_invite_action">Invite</string>
+    <!--Button on the message view screen to show the attachments [CHAR LIMIT=16] -->
+    <plurals name="message_view_show_attachments_action">
+        <item quantity="one">Attachment <xliff:g id="num_attachment" example="1" >%1$d</xliff:g>
+            </item>
+        <item quantity="other">Attachments <xliff:g id="num_attachment" example="3" >%1$d</xliff:g>
+            </item>
+    </plurals>
+    <!--Button on the message view screen to show the embedded pictures [CHAR LIMIT=16] -->
     <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 attachment -->
-    <string name="message_view_fetching_attachment_progress">Fetching attachment <xliff:g id="filename">%s</xliff:g></string>
-    <!-- Calendar invitation, link to calendar.
-         Preserve the chevron (unicode &#xbb;) untranslated -->
-    <string name="message_view_invite_view">View in Calendar &#xbb;</string>
+    <!-- Calendar invitation, label of the button to open in calendar [CHAR LIMIT=24] -->
+    <string name="message_view_invite_view">View in Calendar</string>
     <!-- String shown with a calendar invitation. -->
     <string name="message_view_invite_title">Calendar Invite</string>
     <!-- String shown with a calendar invitation. -->
@@ -304,6 +370,18 @@
         You have replied \"maybe\" to this invitation</string>
     <!-- Toast shown following a meeting invite reply, declined -->
     <string name="message_view_invite_toast_no">You have declined this invitation</string>
+    <!--Confirmation dialog title shown when user tries to delete messages.  [CHAR LIMIT=16] -->
+
+    <!-- Title of the EML viewer activity. [CHAR LIMIT=32] -->
+    <string name="eml_view_title">Viewing
+            <xliff:g id="filename" example="test.eml">%s</xliff:g></string>
+
+    <string name="message_delete_dialog_title">Discard message</string>
+    <!--Confirmation dialog text shown when user tries to delete messages.  [CHAR LIMIT=32] -->
+    <plurals name="message_delete_confirm">
+        <item quantity="one">Discard this message?</item>
+        <item quantity="other">Discard these messages?</item>
+    </plurals>
     <!-- Toast shown briefly while deleting a message -->
     <plurals name="message_deleted_toast">
         <item quantity="one">Message deleted.</item>
@@ -314,16 +392,72 @@
     <!-- Toast shown briefly while saving a draft -->
     <string name="message_saved_toast">Message saved as draft.</string>
     <!-- String that is displayed when the attachment could not be displayed. -->
-    <string name="message_view_display_attachment_toast">This attachment cannot be displayed.</string>
+    <string name="message_view_display_attachment_toast">This attachment cannot be
+        displayed.</string>
+    <!-- String that is displayed when the attachment could not be loaded.[CHAR LIMIT=none] -->
+    <string name="message_view_load_attachment_failed_toast">The attachment \"
+        <xliff:g id="filename">%s</xliff:g>\" could not be loaded.</string>
+    <!-- String that is displayed when a long message is being parsed. [CHAR LIMIT=none] -->
+    <string name="message_view_parse_message_toast">Opening message\u2026</string>
+    <!-- Toast shown after moving a message to a different mailbox. [CHAR LIMIT=none]-->
+    <plurals name="message_moved_toast">
+        <item quantity="one"  ><xliff:g id="num_message" example="1" >%1$d</xliff:g>
+        message moved to <xliff:g id="mailbox_name" example="Inbox" >%2$s</xliff:g></item>
+        <item quantity="other"  ><xliff:g id="num_message" example="5" >%1$d</xliff:g>
+        messages moved to <xliff:g id="mailbox_name" example="Inbox" >%2$s</xliff:g></item>
+    </plurals>
+    <!-- Notification ticker when a forwarded attachment couldn't be sent [CHAR LIMIT=none] -->
+    <string name="forward_download_failed_ticker">Could not forward one or more attachments</string>
+    <!-- Notification text when a forwarded attachment couldn't be sent [CHAR LIMIT=30]-->
+    <string name="forward_download_failed_notification">Could not forward <xliff:g id="filename">
+        %s</xliff:g></string>
+    <!-- Notification ticker when email account authentication fails [CHAR LIMIT=20] -->
+    <string name="login_failed_ticker"><xliff:g id="account_name">%s
+        </xliff:g> sign-in failed</string>
+    <!-- Notification text when email account authentication fails [CHAR LIMIT=75]-->
+    <string name="login_failed_notification">Touch to change account settings</string>
 
-    <!-- Title of screen when setting up new email account -->
-    <string name="account_setup_basics_title">Set up email</string>
-    <!-- Title of the screen when adding exchange account -->
+    <!-- Size unit for bytes for attachments [CHAR LIMIT=10] -->
+    <plurals name="message_view_attachment_bytes">
+        <item quantity="one"  ><xliff:g id="size_in_bytes" example="1"  >%d</xliff:g>B</item>
+        <item quantity="other"><xliff:g id="size_in_bytes" example="279">%d</xliff:g>B</item>
+    </plurals>
+    <!-- Size unit for kilo bytes for attachments [CHAR LIMIT=10] -->
+    <plurals name="message_view_attachment_kilobytes">
+        <item quantity="one"  ><xliff:g id="size_in_kilobytes" example="1"  >%d</xliff:g>KB</item>
+        <item quantity="other"><xliff:g id="size_in_kilobytes" example="279">%d</xliff:g>KB</item>
+    </plurals>
+    <!-- Size unit for mega bytes for attachments [CHAR LIMIT=10] -->
+    <plurals name="message_view_attachment_megabytes">
+        <item quantity="one"  ><xliff:g id="size_in_megabytes" example="1"  >%d</xliff:g>MB</item>
+        <item quantity="other"><xliff:g id="size_in_megabytes" example="279">%d</xliff:g>MB</item>
+    </plurals>
+    <!-- Size unit for giga bytes for attachments [CHAR LIMIT=10] -->
+    <plurals name="message_view_attachment_gigabytes">
+        <item quantity="one"  ><xliff:g id="size_in_gigabytes" example="1"  >%d</xliff:g>GB</item>
+        <item quantity="other"><xliff:g id="size_in_gigabytes" example="279">%d</xliff:g>GB</item>
+    </plurals>
+
+    <!-- The label of the next button on the message view screen. -->
+    <string name="message_view_move_to_newer">Newer</string>
+    <!-- The label of the previous button on the message view screen. -->
+    <string name="message_view_move_to_older">Older</string>
+
+    <!-- A simple divider between subject and message snippet in the message list view
+        [CHAR LIMIT=4]-->
+    <string name="message_list_subject_snippet_divider">\u0020\u2014\u0020</string>
+
+    <!-- Title of screen when setting up new email account [CHAR LIMIT=45] -->
+    <string name="account_setup_basics_title">Account setup</string>
+    <!-- Title of the screen when adding exchange account [CHAR LIMIT=45] -->
     <string name="account_setup_basics_exchange_title">
         Add an Exchange account</string>
-    <!-- Title of the screen when adding exchange account -->
+    <!-- Title of the screen when adding exchange account [CHAR LIMIT=45] -->
     <string name="account_setup_basics_exchange_title_alternate">
         Add an Exchange ActiveSync account</string>
+    <!-- Headline of screen when setting up new email account (large text over divider)
+        [CHAR LIMIT=none] -->
+    <string name="account_setup_basics_headline">Email account</string>
     <!-- On "Set up email" screen, enthusiastic welcome message. -->
     <string name="accounts_welcome">You can configure Email for most accounts in just a few steps.
         </string>
@@ -334,16 +468,19 @@
     <string name="accounts_welcome_exchange_alternate">
         You can configure an Exchange ActiveSync account in just a few steps.</string>
     <!-- On "Set up email" screen, hint for account email address text field -->
-    <string name="account_setup_basics_email_hint">Email address</string>
+    <string name="account_setup_basics_email_label">Email address</string>
     <!-- On "Set up email" screen, hint for account email password text field -->
-    <string name="account_setup_basics_password_hint">Password</string>
-    <!-- On "Set up email" screen, checkbox label for making this the new account be the default account -->
-    <string name="account_setup_basics_default_label">Send email from this account by default.</string>
+    <string name="account_setup_basics_password_label">Password</string>
+    <!-- On "Set up email" screen, checkbox label for making the new account the default account -->
+    <string name="account_setup_basics_default_label">
+        Send email from this account by default.</string>
     <!-- Button name on "Set up email" screen -->
     <string name="account_setup_basics_manual_setup_action">Manual setup</string>
     <!-- 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>
+    <!-- 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
@@ -352,46 +489,54 @@
         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>
     <!-- On check-settings screen, this is the initially-displayed message. -->
-    <string name="account_setup_check_settings_retr_info_msg">Retrieving account information\u2026</string>
+    <string name="account_setup_check_settings_retr_info_msg">
+    Retrieving account information\u2026</string>
     <!-- Appears on screen while system is checking incoming server settings -->
-    <string name="account_setup_check_settings_check_incoming_msg">Checking incoming server settings\u2026</string>
+    <string name="account_setup_check_settings_check_incoming_msg">
+    Checking incoming server settings\u2026</string>
     <!-- Appears on screen while system is checking outgoing server settings -->
-    <string name="account_setup_check_settings_check_outgoing_msg">Checking outgoing server settings\u2026</string>
-    <!-- On check-settings screen, displayed briefly when user cancels the operation. -->
-    <string name="account_setup_check_settings_canceling_msg">Canceling\u2026</string>
-    <!-- Title of "Set up email" screen after success -->
-    <string name="account_setup_names_title">Set up email</string>
-    <!-- Text that appears on top of "Set up email" screen after successfully setting up an account -->
-    <string name="account_setup_names_instructions">Your account is set up, and email is on its way!</string>
-    <!-- On "Set up email" screen, label of text field -->
-    <string name="account_setup_names_account_name_label">Give this account a name (optional)</string>
-    <!-- On "Set up email" screen, label of text field -->
-    <string name="account_setup_names_user_name_label">Your name (displayed on outgoing messages)</string>
+    <string name="account_setup_check_settings_check_outgoing_msg">
+    Checking outgoing server settings\u2026</string>
 
-    <!-- Activity Title for the first screen in manual setup (where you select IMAP or POP3) -->
-    <string name="account_setup_account_type_title">Add new email account</string>
+    <!-- Title of "Set up email" screen after success [CHAR LIMIT=45] -->
+    <string name="account_setup_names_title">Account setup</string>
+    <!-- Text that appears on "Set up email" screen after successfully setting up an account
+        [CHAR LIMIT=none] -->
+    <string name="account_setup_names_headline">
+        Your account is set up, and email is on its way!</string>
+    <!-- On "Set up email" screen, label of text field -->
+    <string name="account_setup_names_account_name_label">
+        Give this account a name (optional)</string>
+    <!-- On "Set up email" screen, label of text field -->
+    <string name="account_setup_names_user_name_label">
+        Your name (displayed on outgoing messages)</string>
+
+    <!-- Activity Title for the account type selector (IMAP or POP3 or EAS) [CHAR LIMIT=45] -->
+    <string name="account_setup_account_type_title">Account setup</string>
+    <!-- Headline for the the account type selector (IMAP or POP3 or EAS) [CHAR LIMIT=none] -->
+    <string name="account_setup_account_type_headline">Account type</string>
     <!-- "Add new email account" screen, text that appears on screen -->
     <string name="account_setup_account_type_instructions">What type of account is this?</string>
 
     <!-- Do Not Translate. "Add new email account" screen, button name in response to what
          type of account this is -->
-    <string name="account_setup_account_type_pop_action">POP3</string>
+    <string name="account_setup_account_type_pop_action" translatable="false">POP3</string>
     <!-- Do Not Translate. "Add new email account" screen, button name in response to what
          type of account this is -->
-    <string name="account_setup_account_type_imap_action">IMAP</string>
+    <string name="account_setup_account_type_imap_action" translatable="false">IMAP</string>
     <!-- Do Not Translate. "Add new email account" screen, button name in response to what
          type of account this is -->
-    <string name="account_setup_account_type_exchange_action">Exchange</string>
+    <string name="account_setup_account_type_exchange_action" translatable="false">Exchange</string>
     <!-- Do Not Translate. "Add new email account" screen, button name in
          response to what type of account this is -->
-    <string name="account_setup_account_type_exchange_action_alternate">
-        Microsoft Exchange ActiveSync</string>
+    <string name="account_setup_account_type_exchange_action_alternate"
+         translatable="false">Microsoft Exchange ActiveSync</string>
 
-    <!-- "Incoming server settings" screen, label for text field -->
-    <string name="account_setup_incoming_title">Incoming server settings</string>
+    <!-- "Incoming server settings" screen, title [CHAR LIMIT=45] -->
+    <string name="account_setup_incoming_title">Account setup</string>
+    <!-- "Incoming server settings" screen, headline (text over divider) [CHAR LIMIT=none] -->
+    <string name="account_setup_incoming_headline">Incoming server settings</string>
     <!-- "Incoming server settings" screen, label for text field -->
     <string name="account_setup_incoming_username_label">Username</string>
     <!-- "Incoming server settings" screen, label for text field -->
@@ -407,11 +552,13 @@
     <!-- "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_trust_certificates_label">SSL (Accept all certificates)</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</string>
     <!-- "Incoming server settings" screen, options for "Security type" pop-up menu -->
-    <string name="account_setup_incoming_security_tls_trust_certificates_label">TLS (Accept all certificates)</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</string>
     <!-- "Incoming server settings" screen, label for pop-up menu -->
@@ -420,14 +567,17 @@
     <!-- "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_delete_label">When I delete from Inbox</string>
-    
+    <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: -->
     <string name="account_setup_incoming_imap_path_prefix_label">IMAP path prefix</string>
     <!-- "Incoming server settings" screen, hint for setting IMAP path prefix: -->
     <string name="account_setup_incoming_imap_path_prefix_hint">Optional</string>
-    <!-- Title of "Outgoing server settings" screen -->
-    <string name="account_setup_outgoing_title">Outgoing server settings</string>
+
+    <!-- "Outgoing server settings" screen, title [CHAR LIMIT=45] -->
+    <string name="account_setup_outgoing_title">Account setup</string>
+    <!-- "Outgoing server settings" screen, headline (text over divider) [CHAR LIMIT=none] -->
+    <string name="account_setup_outgoing_headline">Outgoing server settings</string>
     <!-- On "Outgoing server settings" screen, label for text field -->
     <string name="account_setup_outgoing_smtp_server_label">SMTP server</string>
     <!-- On "Outgoing server settings" screen, label for text field -->
@@ -440,9 +590,11 @@
     <string name="account_setup_outgoing_username_label">Username</string>
     <!-- On "Outgoing server settings" screen, label for text field -->
     <string name="account_setup_outgoing_password_label">Password</string>
-    
-    <!-- Title of "Exchange server settings" screen -->
-    <string name="account_setup_exchange_title">Server settings</string>
+
+    <!-- Title of "Exchange server settings" screen [CHAR LIMIT=45] -->
+    <string name="account_setup_exchange_title">Account setup</string>
+    <!-- Headline of "Exchange server settings" screen (text over divider) [CHAR LIMIT=none] -->
+    <string name="account_setup_exchange_headline">Server settings</string>
     <!-- On "Exchange" setup screen, the name of the server -->
     <string name="account_setup_exchange_server_label">Server</string>
     <!-- On "Exchange" setup screen, the domain\\username -->
@@ -451,9 +603,13 @@
     <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>
+    <!-- On "Exchange" setup screen, the exchange device-id label [CHAR LIMIT=30] -->
+    <string name="account_setup_exchange_device_id_label">Mobile Device ID</string>
 
-    <!-- In Account setup options screen, Activity title -->
-    <string name="account_setup_options_title">Account options</string>
+    <!-- In Account setup options screen, Activity title [CHAR LIMIT=45] -->
+    <string name="account_setup_options_title">Account settings</string>
+    <!-- In Account setup options screen, Activity headline [CHAR LIMIT=none] -->
+    <string name="account_setup_options_headline">Account options</string>
     <!-- In Account setup options screen, label for email check frequency selector -->
     <string name="account_setup_options_mail_check_frequency_label">Inbox checking frequency</string>
     <!-- In Account setup options & Account Settings screens, label for email check frequency option -->
@@ -481,6 +637,9 @@
     <!-- In Account setup options screen, optional check box to also sync contacts -->
     <string name="account_setup_options_sync_calendar_label">Sync calendar from this account.
     </string>
+    <!-- In Account setup options screen, check box to sync email -->
+    <string name="account_setup_options_sync_email_label">Sync email 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 -->
@@ -497,19 +656,24 @@
     <string name="account_setup_options_mail_window_1month">One month</string>
 
     <!-- "Setup could not finish" dialog text; e.g., Username or password incorrect -->
-    <string name="account_setup_failed_dlg_auth_message">Username or password incorrect.</string> 
+    <string name="account_setup_failed_dlg_auth_message">Username or password incorrect.</string>
     <!-- "Setup could not finish" dialog text; e.g., Username or password incorrect\n(ERR01 Account does not exist) -->
-    <string name="account_setup_failed_dlg_auth_message_fmt">Username or password incorrect.\n(<xliff:g id="error">%s</xliff:g>)</string> 
+    <string name="account_setup_failed_dlg_auth_message_fmt">Username or password incorrect.\n(<xliff:g id="error">%s</xliff:g>)</string>
 
     <!-- "Setup could not finish" dialog text; e.g., Cannot safely connect to server -->
-    <string name="account_setup_failed_dlg_certificate_message">Cannot safely connect to server.</string> 
+    <string name="account_setup_failed_dlg_certificate_message">Cannot safely connect to server.</string>
     <!-- "Setup could not finish" dialog text; e.g., Cannot safely connect to server\n(TLS Not Supported) -->
-    <string name="account_setup_failed_dlg_certificate_message_fmt">Cannot safely connect to server.\n(<xliff:g id="error">%s</xliff:g>)</string> 
+    <string name="account_setup_failed_dlg_certificate_message_fmt">Cannot safely connect to server.\n(<xliff:g id="error">%s</xliff:g>)</string>
+
+    <!-- Dialog text for ambiguous setup failure; server error/bad credentials [CHAR LIMIT=none] -->
+    <string name="account_setup_failed_check_credentials_message">
+         The server responded with an error; please check your username and password
+         and try again.</string>
 
     <!-- "Setup could not finish" dialog text; e.g., Cannot connect to server -->
-    <string name="account_setup_failed_dlg_server_message">Cannot connect to server.</string> 
+    <string name="account_setup_failed_dlg_server_message">Cannot connect to server.</string>
     <!-- "Setup could not finish" dialog text; e.g., Cannot connect to server\n(Connection timed out) -->
-    <string name="account_setup_failed_dlg_server_message_fmt">Cannot connect to server.\n(<xliff:g id="error">%s</xliff:g>)</string> 
+    <string name="account_setup_failed_dlg_server_message_fmt">Cannot connect to server.\n(<xliff:g id="error">%s</xliff:g>)</string>
 
     <!-- Additional diagnostic text when TLS was required but the server doesn't support it -->
     <string name="account_setup_failed_tls_required">TLS required but not supported by server.</string>
@@ -519,6 +683,11 @@
     <string name="account_setup_failed_security">Unable to open connection to server due to security error.</string>
     <!-- Additional diagnostic text when server connection failed due to io error (connection) -->
     <string name="account_setup_failed_ioerror">Unable to open connection to server.</string>
+    <!-- Additional diagnostic text when server connection failed due to our inability to support a
+         required EAS protocol version [CHAR LIMIT=none] -->
+    <string name="account_setup_failed_protocol_unsupported">
+         You entered an incorrect server address or the server requires a protocol version that
+         Email does not support.</string>
 
     <!-- Dialog title when validation requires security provisioning (e.g. support
          for device lock PIN, or remote wipe.) and we ask the user permission before continuing -->
@@ -533,6 +702,11 @@
          being supported -->
     <string name="account_setup_failed_security_policies_unsupported">
          This server requires security features your phone does not support.</string>
+    <!-- Warning given to users when they request disabling device administration (i.e. that their
+         administered accounts will be deleted) [CHAR LIMIT=none] -->
+    <string name="disable_admin_warning">WARNING: Deactivating the Email application\'s authority
+         to administer your device will delete all Email accounts that require it, along with their
+         email, contacts, calendar events, and other data.</string>
 
     <!-- Notification ticker  when device security required -->
     <string name="security_notification_ticker_fmt">
@@ -549,6 +723,32 @@
 
     <!-- "Setup could not finish" dialog action button -->
     <string name="account_setup_failed_dlg_edit_details_action">Edit details</string>
+
+    <!-- Notification ticker when device password is getting ready to expire [CHAR_LIMIT=80] -->
+    <string name="password_expire_warning_ticker_fmt">
+            Account \"<xliff:g id="account">%s</xliff:g>\" requires you to update your screen
+            unlock code.</string>
+    <!-- Notification content title when device password is getting ready to expire
+            [CHAR_LIMIT=28] -->
+    <string name="password_expire_warning_content_title">New screen unlock required</string>
+    <!-- Notification content text when device password is getting ready to expire
+            [CHAR_LIMIT=2 lines] -->
+    <string name="password_expire_warning_content_text_fmt">
+            Account \"<xliff:g id="account">%s</xliff:g>\" requires you to update your screen
+            unlock code.  Touch here to update it.</string>
+
+    <!-- Notification ticker when device password has expired [CHAR_LIMIT=80] -->
+    <string name="password_expired_ticker">Your screen unlock code has expired.</string>
+    <!-- Notification content title when device password has expired [CHAR_LIMIT=28] -->
+    <string name="password_expired_content_title">New screen unlock required</string>
+    <!-- Notification content text when device password has expired [CHAR_LIMIT=2 lines] -->
+    <string name="password_expired_content_text">
+            Your screen unlock code has expired.  Touch here to update it.</string>
+
+    <!-- On AccountSettingsXL, dialog text if you try to exit in/out/eas fragment (server settings)
+         without checking/saving [CHAR LIMIT=none]-->
+    <string name="account_settings_exit_server_settings">Discard unsaved changes?</string>
+
     <!-- On Settings screen, section heading -->
     <string name="account_settings_title_fmt">General settings</string>
     <!-- On Settings screen, setting option name -->
@@ -558,8 +758,9 @@
     <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, summary line when called via AccountManager for Exchange accounts
+        [CHAR LIMIT=50] -->
+    <string name="account_settings_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 -->
@@ -579,16 +780,18 @@
     <!-- 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, sync calendar check box label -->
-        <string name="account_settings_sync_calendar_enable">Sync calendar</string>
-    <!-- On settings screen, sync calendar summary text -->
-    <string name="account_settings_sync_calendar_summary">Also sync calendar from this account
-        </string>
+    <!-- On settings screen, sync contacts check box label [CHAR LIMIT=20]-->
+    <string name="account_settings_sync_contacts_enable">Sync Contacts</string>
+    <!-- On settings screen, sync contacts summary text [CHAR LIMIT=35] -->
+    <string name="account_settings_sync_contacts_summary">Sync contacts for this account</string>
+    <!-- On settings screen, sync calendar check box label [CHAR LIMIT=20]-->
+        <string name="account_settings_sync_calendar_enable">Sync Calendar</string>
+    <!-- On settings screen, sync calendar summary text [CHAR LIMIT=35] -->
+    <string name="account_settings_sync_calendar_summary">Sync calendar for this account</string>
+    <!-- On settings screen, sync email check box label [CHAR LIMIT=20]-->
+    <string name="account_settings_sync_email_enable">Sync Email</string>
+    <!-- On settings screen, sync email summary text [CHAR LIMIT=35] -->
+    <string name="account_settings_sync_email_summary">Sync email for this account</string>
 
     <!-- On Settings screen, vibrate pop-up menu label -->
     <string name="account_settings_vibrate_when_label">Vibrate</string>
@@ -611,10 +814,23 @@
     <!-- Title of Remove account confirmation dialog box -->
     <string name="account_delete_dlg_title">Remove account</string>
     <!-- Message of Remove account confirmation dialog box -->
-    <string name="account_delete_dlg_instructions_fmt">The account \"<xliff:g id="account">%s</xliff:g>\" will be removed from Email.</string>
+    <string name="account_delete_dlg_instructions_fmt">
+    The account \"<xliff:g id="account">%s</xliff:g>\" will be removed from Email.</string>
+
+    <!-- On Settings screen, section heading for delete account [CHAR LIMIT=none] -->
+    <string name="account_settings_category_delete_account">Remove account</string>
+    <!-- On Settings screen, settings option for delete account [CHAR LIMIT=none] -->
+    <string name="account_settings_delete_account_label">Remove account</string>
+
+    <!-- Strings used for account shortcut picker -->
+    <!-- String displayed in launcher [CHAR_LIMIT=10] -->
+    <string name="account_shortcut_picker_name">Email account</string>
+    <!-- Title of activity/dialog containing shortcut picker (list of accounts) [CHAR_LIMIT=20] -->
+    <string name="account_shortcut_picker_title">Select an account</string>
 
     <!-- Title of Upgrade Accounts activity -->
     <string name="upgrade_accounts_title">Upgrade accounts</string>
+    <!-- Error shown when Upgrade Accounts fails (shown below name of that account) -->
     <string name="upgrade_accounts_error">Unable to upgrade account</string>
 
     <!-- Message that appears when adding a Windows Live Hotmail Plus account -->
@@ -707,4 +923,104 @@
          always a larger value because it represents the limit of displayed results.  -->
     <string name="gal_completed_limited_fmt">First <xliff:g id="results" example="20">%1$d</xliff:g>
         results from <xliff:g id="domain">%2$s</xliff:g></string>
+
+    <!-- General Preferences Screen -->
+    <!-- Label in preferences header to describe general preferences -->
+    <string name="header_label_general_preferences">Email Preferences</string>
+    <!-- First category in general preferences -->
+    <string name="category_general_preferences">Application Preferences</string>
+
+    <!-- General preference: Label of the setting for the direction to move to
+         when deleting the current message.
+         Options contain "newer message","older message", etc. [CHAR LIMIT=32] -->
+    <string name="general_preference_auto_advance_label">Auto-advance</string>
+    <!-- General preference: Description of the setting for the direction to move to
+         when deleting the current message.
+         Options contain "newer message","older message", etc. [CHAR LIMIT=64] -->
+    <string name="general_preference_auto_advance_summary">
+         Select which screen to show after you delete a message</string>
+    <!-- General preference: Title of the dialog box containing options for setting for
+         the direction to move to when deleting the current message.
+         Options contain "newer message","older message", etc. [CHAR LIMIT=32] -->
+    <string name="general_preference_auto_advance_dialog_title">Advance to</string>
+    <!-- General preference: Option for the setting for
+         the direction to move to when deleting the current message.
+         This option is for "move to the newer message" [CHAR LIMIT=32] -->
+    <string name="general_preference_auto_advance_newer">Newer message</string>
+    <!-- General preference: Option for the setting for
+         the direction to move to when deleting the current message.
+         This option is for "move to the older message" [CHAR LIMIT=32] -->
+    <string name="general_preference_auto_advance_older">Older message</string>
+    <!-- General preference: Option for the setting for
+         the direction to move to when deleting the current message.
+         This option is for "move back to the message list" [CHAR LIMIT=32] -->
+    <string name="general_preference_auto_advance_message_list">Message list</string>
+
+    <!-- General preference: Label of the setting for the text zoom.  [CHAR LIMIT=32] -->
+    <string name="general_preference_text_zoom_label">Message text size</string>
+    <!-- General preference: Description of each setting for text zoom.  The entries here must
+         correspond to the strings general_preference_text_zoom_tiny,
+         general_preference_text_zoom_small, general_preference_text_zoom_normal, etc.
+         [CHAR LIMIT=64] -->
+    <string-array name="general_preference_text_zoom_summary_array">
+        <item>Display content of messages in tiny sized text</item>
+        <item>Display content of messages in small sized text</item>
+        <item>Display content of messages in normal sized text</item>
+        <item>Display content of messages in large sized text</item>
+        <item>Display content of messages in huge sized text</item>
+    </string-array>
+    <!-- General preference: Title of the dialog box with options for text zoom. [CHAR LIMIT=32] -->
+    <string name="general_preference_text_zoom_dialog_title">Message text size</string>
+    <!-- General preference:  Text zoom.  Value is "tiny" (-2) [CHAR LIMIT=32] -->
+    <string name="general_preference_text_zoom_tiny">Tiny</string>
+    <!-- General preference:  Text zoom.  Value is "small" (-1) [CHAR LIMIT=32] -->
+    <string name="general_preference_text_zoom_small">Small</string>
+    <!-- General preference:  Text zoom.  Value is "normal" (0) [CHAR LIMIT=32] -->
+    <string name="general_preference_text_zoom_normal">Normal</string>
+    <!-- General preference:  Text zoom.  Value is "large" (+1) [CHAR LIMIT=32] -->
+    <string name="general_preference_text_zoom_large">Large</string>
+    <!-- General preference:  Text zoom.  Value is "huge" (+2) [CHAR LIMIT=32] -->
+    <string name="general_preference_text_zoom_huge">Huge</string>
+
+    <!-- General preference:  Allow downloading of attachments in background -->
+    <!-- Title of general preference for downloading attachments in background [CHAR LIMIT=32] -->
+    <string name="general_preference_background_attachments_label">
+        Automatically fetch attachments</string>
+    <!-- Summary of general preference for downloading attachments in background [CHAR LIMIT=64] -->
+    <string name="general_preference_background_attachments_summary">
+        Download attachments for Inbox messages.  (Not available for POP3 accounts.)
+    </string>
+
+    <!-- Generic string for "current position" / "total number" [CHAR LIMIT=12] -->
+    <string name="position_of_count"><xliff:g example="1">%1$d</xliff:g> of <xliff:g
+            example="12">%2$s</xliff:g></string>
+
+    <!-- Widget -->
+    <!-- Instruction for how to move to different widget views [CHAR LIMIT=20] -->
+    <string name="widget_other_views">Tap icon to change</string>
+    <!-- Header for the "All Mail" widget view (showing all of the user's mail) [CHAR LIMIT=20] -->
+    <string name="widget_all_mail">Combined Inbox</string>
+    <!-- Header for the "Unread" widget view (showing all unread mail) [CHAR LIMIT=20] -->
+    <string name="widget_unread">Unread</string>
+    <!-- Header for the "Starred" widget view (showing all starred mail) [CHAR LIMIT=20] -->
+    <string name="widget_starred">Starred</string>
+    <!-- Shown when waiting for mail data to be loaded into the widget list view [CHAR LIMIT=20] -->
+    <string name="widget_loading">Loading\u2026</string>
+
+    <!--
+        Strings for temporary UI
+        STOPSHIP Remove them or move them up and make translatable
+    -->
+    <!-- Toast shown when a message(s) can't be moved because it's not supported
+         on the POP3 protocol. [CHAR LIMIT=none]-->
+    <string name="cannot_move_protocol_not_supported_toast" translatable="false">
+        Move is not supported on POP3 accounts.</string>
+    <!-- Toast shown when messages can't be moved because the selection contains
+         multiple accounts' messages. [CHAR LIMIT=none]-->
+    <string name="cannot_move_multiple_accounts_toast" translatable="false">
+        Cannot move.  Selection contains multiple accounts.</string>
+    <!-- Toast shown when messages can't be moved because they're in a special
+         mailbox.  Do not translate "Drafts", "Outbox" and "Sent". [CHAR LIMIT=none]-->
+    <string name="cannot_move_special_messages" translatable="false">
+        Messages in Drafts, Outbox and Sent cannot be moved.</string>
 </resources>
diff --git a/res/xml/authenticator.xml b/res/xml/syncadapter_email.xml
similarity index 65%
rename from res/xml/authenticator.xml
rename to res/xml/syncadapter_email.xml
index ae82c17..c7ca850 100644
--- a/res/xml/authenticator.xml
+++ b/res/xml/syncadapter_email.xml
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
 /**
- * Copyright (c) 2009, The Android Open Source Project
+ * Copyright (c) 2010, The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -18,12 +18,10 @@
 -->
 
 <!-- The attributes in this XML file provide configuration information -->
-<!-- for the Account Manager. -->
+<!-- for the SyncAdapter. -->
 
-<account-authenticator xmlns:android="http://schemas.android.com/apk/res/android"
+<sync-adapter xmlns:android="http://schemas.android.com/apk/res/android"
+    android:contentAuthority="com.android.email.provider"
     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"
+    android:supportsUploading="false"
 />
diff --git a/src/com/android/exchange/AbstractSyncService.java b/src/com/android/exchange/AbstractSyncService.java
index f1fe834..586baf7 100644
--- a/src/com/android/exchange/AbstractSyncService.java
+++ b/src/com/android/exchange/AbstractSyncService.java
@@ -22,17 +22,14 @@
 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.os.Bundle;
 import android.util.Log;
 
-import java.util.ArrayList;
+import java.util.concurrent.LinkedBlockingQueue;
 
 /**
  * Base class for all protocol services SyncManager (extends Service, implements
@@ -68,12 +65,12 @@
     public Account mAccount;
     public Context mContext;
     public int mChangeCount = 0;
-    public int mSyncReason = 0;
+    public volatile int mSyncReason = 0;
     protected volatile boolean mStop = false;
     protected Object mSynchronizer = new Object();
 
     protected volatile long mRequestTime = 0;
-    protected ArrayList<Request> mRequests = new ArrayList<Request>();
+    protected LinkedBlockingQueue<Request> mRequestQueue = new LinkedBlockingQueue<Request>();
     protected PartRequest mPendingRequest = null;
 
     /**
@@ -109,10 +106,12 @@
      * @param port
      * @param ssl
      * @param context
+     * @return a Bundle containing a result code and, depending on the result, a PolicySet or an
+     * error message
      * @throws MessagingException
      */
-    public abstract void validateAccount(String host, String userName, String password, int port,
-            boolean ssl, boolean trustCertificates, Context context) throws MessagingException;
+    public abstract Bundle validateAccount(String host, String userName, String password, int port,
+            boolean ssl, boolean trustCertificates, Context context);
 
     public AbstractSyncService(Context _context, Mailbox _mailbox) {
         mContext = _context;
@@ -137,21 +136,22 @@
      * @param port
      * @param ssl
      * @param context
+     * @return a Bundle containing a result code and, depending on the result, a PolicySet or an
+     * error message
      * @throws MessagingException
      */
-    static public void validate(Class<? extends AbstractSyncService> klass, String host,
+    static public Bundle validate(Class<? extends AbstractSyncService> klass, String host,
             String userName, String password, int port, boolean ssl, boolean trustCertificates,
-            Context context)
-            throws MessagingException {
+            Context context) {
         AbstractSyncService svc;
         try {
             svc = klass.newInstance();
-            svc.validateAccount(host, userName, password, port, ssl, trustCertificates, context);
+            return 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);
         }
+        return null;
     }
 
     public static class ValidationResult {
@@ -294,48 +294,18 @@
      */
 
     public void addRequest(Request req) {
-        synchronized (mRequests) {
-            mRequests.add(req);
-            mRequestTime = System.currentTimeMillis();
-        }
+        mRequestQueue.offer(req);
     }
 
     public void removeRequest(Request req) {
-        synchronized (mRequests) {
-            mRequests.remove(req);
-        }
+        mRequestQueue.remove(req);
     }
 
-    /**
-     * 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;
+    public boolean hasPendingRequests() {
+        return !mRequestQueue.isEmpty();
     }
 
-    /**
-     * 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);
+    public void clearRequests() {
+        mRequestQueue.clear();
     }
 }
diff --git a/src/com/android/exchange/CalendarSyncAdapterService.java b/src/com/android/exchange/CalendarSyncAdapterService.java
index 7941a0c..876f4a0 100644
--- a/src/com/android/exchange/CalendarSyncAdapterService.java
+++ b/src/com/android/exchange/CalendarSyncAdapterService.java
@@ -89,8 +89,8 @@
     }
 
     /**
-     * Partial integration with system SyncManager; we tell our EAS SyncManager to start a calendar
-     * sync when we get the signal from the system SyncManager.
+     * Partial integration with system SyncManager; we tell our EAS ExchangeService to start a
+     * calendar sync when we get the signal from 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.
      */
@@ -138,8 +138,8 @@
                             return;
                         }
                         // Ask for a sync from our sync manager
-                        SyncManager.serviceRequest(mailboxCursor.getLong(ID_SYNC_KEY_MAILBOX_ID),
-                                SyncManager.SYNC_UPSYNC);
+                        ExchangeService.serviceRequest(mailboxCursor.getLong(
+                                ID_SYNC_KEY_MAILBOX_ID), ExchangeService.SYNC_UPSYNC);
                     }
                 } finally {
                     mailboxCursor.close();
diff --git a/src/com/android/exchange/CalendarSyncEnabler.java b/src/com/android/exchange/CalendarSyncEnabler.java
index 4eaad8e..5837bb2 100644
--- a/src/com/android/exchange/CalendarSyncEnabler.java
+++ b/src/com/android/exchange/CalendarSyncEnabler.java
@@ -17,8 +17,8 @@
 package com.android.exchange;
 
 import com.android.email.Email;
+import com.android.email.NotificationController;
 import com.android.email.R;
-import com.android.email.service.MailService;
 
 import android.accounts.Account;
 import android.accounts.AccountManager;
@@ -30,7 +30,6 @@
 import android.content.Intent;
 import android.net.Uri;
 import android.provider.Calendar;
-import android.provider.ContactsContract;
 import android.util.Log;
 
 /**
@@ -106,7 +105,7 @@
 
         NotificationManager nm =
                 (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
-        nm.notify(MailService.NOTIFICATION_ID_EXCHANGE_CALENDAR_ADDED, n);
+        nm.notify(NotificationController.NOTIFICATION_ID_EXCHANGE_CALENDAR_ADDED, n);
     }
 
     /** @return {@link Intent} to launch the Calendar app. */
diff --git a/src/com/android/exchange/ContactsSyncAdapterService.java b/src/com/android/exchange/ContactsSyncAdapterService.java
index b39d789..311dd4f 100644
--- a/src/com/android/exchange/ContactsSyncAdapterService.java
+++ b/src/com/android/exchange/ContactsSyncAdapterService.java
@@ -17,7 +17,6 @@
 package com.android.exchange;
 
 import com.android.email.Email;
-import com.android.email.provider.EmailContent;
 import com.android.email.provider.EmailContent.AccountColumns;
 import com.android.email.provider.EmailContent.Mailbox;
 import com.android.email.provider.EmailContent.MailboxColumns;
@@ -35,6 +34,7 @@
 import android.net.Uri;
 import android.os.Bundle;
 import android.os.IBinder;
+import android.provider.ContactsContract.Groups;
 import android.provider.ContactsContract.RawContacts;
 import android.util.Log;
 
@@ -43,7 +43,7 @@
     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[] ID_PROJECTION = new String[] {"_id"};
     private static final String ACCOUNT_AND_TYPE_CONTACTS =
         MailboxColumns.ACCOUNT_KEY + "=? AND " + MailboxColumns.TYPE + '=' + Mailbox.TYPE_CONTACTS;
 
@@ -85,9 +85,18 @@
         return sSyncAdapter.getSyncAdapterBinder();
     }
 
+    private static boolean hasDirtyRows(ContentResolver resolver, Uri uri, String dirtyColumn) {
+        Cursor c = resolver.query(uri, ID_PROJECTION, dirtyColumn + "=1", null, null);
+        try {
+            return c.getCount() > 0;
+        } finally {
+            c.close();
+        }
+    }
+
     /**
-     * Partial integration with system SyncManager; we tell our EAS SyncManager to start a contacts
-     * sync when we get the signal from the system SyncManager.
+     * Partial integration with system SyncManager; we tell our EAS ExchangeService to start a
+     * contacts sync when we get the signal from 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.
      */
@@ -95,21 +104,29 @@
             String authority, ContentProviderClient provider, SyncResult syncResult)
             throws OperationCanceledException {
         ContentResolver cr = context.getContentResolver();
-        Log.i(TAG, "performSync");
+        if (Email.DEBUG) {
+            Log.d(TAG, "performSync");
+        }
+
+        // If we've been asked to do an upload, make sure we've got work to do
         if (extras.getBoolean(ContentResolver.SYNC_EXTRAS_UPLOAD)) {
             Uri uri = RawContacts.CONTENT_URI.buildUpon()
                 .appendQueryParameter(RawContacts.ACCOUNT_NAME, account.name)
                 .appendQueryParameter(RawContacts.ACCOUNT_TYPE, Email.EXCHANGE_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();
+            // See if we've got dirty contacts or dirty groups containing our contacts
+            boolean changed = hasDirtyRows(cr, uri, RawContacts.DIRTY);
+            if (!changed) {
+                uri = Groups.CONTENT_URI.buildUpon()
+                    .appendQueryParameter(RawContacts.ACCOUNT_NAME, account.name)
+                    .appendQueryParameter(RawContacts.ACCOUNT_TYPE,
+                            Email.EXCHANGE_ACCOUNT_MANAGER_TYPE)
+                    .build();
+                changed = hasDirtyRows(cr, uri, Groups.DIRTY);
+            }
+            if (!changed) {
+                Log.i(TAG, "Upload sync; no changes");
+                return;
             }
         }
 
@@ -127,8 +144,8 @@
                      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);
+                        ExchangeService.serviceRequest(mailboxCursor.getLong(0),
+                                ExchangeService.SYNC_UPSYNC);
                     }
                 } finally {
                     mailboxCursor.close();
diff --git a/src/com/android/exchange/Eas.java b/src/com/android/exchange/Eas.java
index 8e561e7..6ab8779 100644
--- a/src/com/android/exchange/Eas.java
+++ b/src/com/android/exchange/Eas.java
@@ -37,7 +37,7 @@
     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 CLIENT_VERSION = "EAS-1.2";
     public static final String ACCOUNT_MAILBOX_PREFIX = "__eas";
 
     // Define our default protocol version as 2.5 (Exchange 2003)
@@ -45,6 +45,8 @@
     public static final double SUPPORTED_PROTOCOL_EX2003_DOUBLE = 2.5;
     public static final String SUPPORTED_PROTOCOL_EX2007 = "12.0";
     public static final double SUPPORTED_PROTOCOL_EX2007_DOUBLE = 12.0;
+    public static final String SUPPORTED_PROTOCOL_EX2007_SP1 = "12.1";
+    public static final double SUPPORTED_PROTOCOL_EX2007_SP1_DOUBLE = 12.1;
     public static final String DEFAULT_PROTOCOL_VERSION = SUPPORTED_PROTOCOL_EX2003;
 
     // From EAS spec
@@ -68,6 +70,8 @@
     public static final String FILTER_6_MONTHS = "7";
     public static final String BODY_PREFERENCE_TEXT = "1";
     public static final String BODY_PREFERENCE_HTML = "2";
+    public static final String MIME_BODY_PREFERENCE_TEXT = "0";
+    public static final String MIME_BODY_PREFERENCE_MIME = "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";
@@ -91,5 +95,16 @@
             Log.d("Eas Debug", "Logging: " + (USER_LOG ? "User " : "") +
                     (PARSER_LOG ? "Parser " : "") + (FILE_LOG ? "File" : ""));
         }
-     }
+    }
+
+    static public Double getProtocolVersionDouble(String version) {
+        if (SUPPORTED_PROTOCOL_EX2003.equals(version)) {
+            return SUPPORTED_PROTOCOL_EX2003_DOUBLE;
+        } else if (SUPPORTED_PROTOCOL_EX2007.equals(version)) {
+            return SUPPORTED_PROTOCOL_EX2007_DOUBLE;
+        } if (SUPPORTED_PROTOCOL_EX2007_SP1.equals(version)) {
+            return SUPPORTED_PROTOCOL_EX2007_SP1_DOUBLE;
+        }
+        throw new IllegalArgumentException("illegal protocol version");
+    }
 }
diff --git a/src/com/android/exchange/EasOutboxService.java b/src/com/android/exchange/EasOutboxService.java
index 34b6d7f..a4b043c 100644
--- a/src/com/android/exchange/EasOutboxService.java
+++ b/src/com/android/exchange/EasOutboxService.java
@@ -17,8 +17,10 @@
 
 package com.android.exchange;
 
+import com.android.email.Utility;
 import com.android.email.mail.MessagingException;
 import com.android.email.mail.transport.Rfc822Output;
+import com.android.email.provider.EmailContent.Account;
 import com.android.email.provider.EmailContent.Body;
 import com.android.email.provider.EmailContent.BodyColumns;
 import com.android.email.provider.EmailContent.Mailbox;
@@ -36,6 +38,7 @@
 import android.content.ContentValues;
 import android.content.Context;
 import android.database.Cursor;
+import android.net.Uri;
 import android.os.RemoteException;
 
 import java.io.File;
@@ -65,16 +68,21 @@
 
     private void sendCallback(long msgId, String subject, int status) {
         try {
-            SyncManager.callback().sendMessageStatus(msgId, subject, status, 0);
+            ExchangeService.callback().sendMessageStatus(msgId, subject, status, 0);
         } catch (RemoteException e) {
             // It's all good
         }
     }
 
+    /*package*/ String generateSmartSendCmd(boolean reply, String itemId, String collectionId) {
+        return (reply ? "SmartReply" : "SmartForward") + "&ItemId=" + Uri.encode(itemId, ":") +
+            "&CollectionId=" + Uri.encode(collectionId, ":");
+    }
+
     /**
      * 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.
+     * IOException, which is handled by ExchangeService with retries, backoffs, etc.
      *
      * @param cacheDir the cache directory for this context
      * @param msgId the _id of the message to send
@@ -86,8 +94,8 @@
         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);
+            String[] cols = Utility.getRowColumns(mContext, Message.CONTENT_URI, msgId,
+                    MessageColumns.FLAGS, MessageColumns.SUBJECT);
             int flags = Integer.parseInt(cols[0]);
             String subject = cols[1];
 
@@ -98,18 +106,19 @@
             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,
+                cols = Utility.getRowColumns(mContext, 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);
+                    cols = Utility.getRowColumns(mContext, 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);
+                        cols = Utility.getRowColumns(mContext, Mailbox.CONTENT_URI, boxId,
+                                MailboxColumns.SERVER_ID);
                         if (cols != null) {
                             collectionId = cols[0];
                         }
@@ -117,7 +126,14 @@
                 }
             }
 
+            // Generally, we use SmartReply/SmartForward if we've got a good reference
             boolean smartSend = itemId != null && collectionId != null;
+            // But we won't use SmartForward if the account isn't set up for it (currently, we only
+            // use SmartForward for EAS 12.0 or later to avoid creating eml files that are
+            // potentially difficult for the recipient to handle)
+            if (forward && ((mAccount.mFlags & Account.FLAGS_SUPPORTS_SMART_FORWARD) == 0)) {
+                smartSend = false;
+            }
 
             // Write the message in rfc822 format to the temporary file
             FileOutputStream fileStream = new FileOutputStream(tmpFile);
@@ -130,11 +146,12 @@
                 new InputStreamEntity(inputStream, tmpFile.length());
 
             // Create the appropriate command and POST it to the server
-            String cmd = "SendMail&SaveInSent=T";
+            String cmd = "SendMail";
             if (smartSend) {
-                cmd = reply ? "SmartReply" : "SmartForward";
-                cmd += "&ItemId=" + itemId + "&CollectionId=" + collectionId + "&SaveInSent=T";
+                cmd = generateSmartSendCmd(reply, itemId, collectionId);
             }
+            cmd += "&SaveInSent=T";
+
             userLog("Send cmd: " + cmd);
             HttpResponse resp = sendHttpClientPost(cmd, inputEntity, SEND_MAIL_TIMEOUT);
 
@@ -180,7 +197,7 @@
         setupService();
         File cacheDir = mContext.getCacheDir();
         try {
-            mDeviceId = SyncManager.getDeviceId();
+            mDeviceId = ExchangeService.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);
@@ -188,6 +205,10 @@
                 while (c.moveToNext()) {
                     long msgId = c.getLong(0);
                     if (msgId != 0) {
+                        if (Utility.hasUnloadedAttachments(mContext, msgId)) {
+                            // We'll just have to wait on this...
+                            continue;
+                        }
                         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
@@ -212,7 +233,7 @@
         } finally {
             userLog(mMailbox.mDisplayName, ": sync finished");
             userLog("Outbox exited with status ", mExitStatus);
-            SyncManager.done(this);
+            ExchangeService.done(this);
         }
     }
 
diff --git a/src/com/android/exchange/EasSyncService.java b/src/com/android/exchange/EasSyncService.java
index 20e1ea8..3b0c173 100644
--- a/src/com/android/exchange/EasSyncService.java
+++ b/src/com/android/exchange/EasSyncService.java
@@ -18,10 +18,9 @@
 package com.android.exchange;
 
 import com.android.email.SecurityPolicy;
-import com.android.email.Utility;
 import com.android.email.SecurityPolicy.PolicySet;
+import com.android.email.Utility;
 import com.android.email.mail.Address;
-import com.android.email.mail.AuthenticationFailedException;
 import com.android.email.mail.MeetingInfo;
 import com.android.email.mail.MessagingException;
 import com.android.email.mail.PackedString;
@@ -33,6 +32,8 @@
 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 com.android.email.service.EmailServiceConstants;
 import com.android.email.service.EmailServiceProxy;
 import com.android.email.service.EmailServiceStatus;
@@ -44,11 +45,12 @@
 import com.android.exchange.adapter.FolderSyncParser;
 import com.android.exchange.adapter.GalParser;
 import com.android.exchange.adapter.MeetingResponseParser;
+import com.android.exchange.adapter.MoveItemsParser;
+import com.android.exchange.adapter.Parser.EasParserException;
 import com.android.exchange.adapter.PingParser;
 import com.android.exchange.adapter.ProvisionParser;
 import com.android.exchange.adapter.Serializer;
 import com.android.exchange.adapter.Tags;
-import com.android.exchange.adapter.Parser.EasParserException;
 import com.android.exchange.provider.GalResult;
 import com.android.exchange.utility.CalendarUtilities;
 
@@ -78,6 +80,8 @@
 import android.content.Context;
 import android.content.Entity;
 import android.database.Cursor;
+import android.net.Uri;
+import android.os.Build;
 import android.os.Bundle;
 import android.os.RemoteException;
 import android.os.SystemClock;
@@ -95,7 +99,6 @@
 import java.io.InputStream;
 import java.lang.Thread.State;
 import java.net.URI;
-import java.net.URLEncoder;
 import java.security.cert.CertificateException;
 import java.util.ArrayList;
 import java.util.HashMap;
@@ -104,8 +107,6 @@
     // DO NOT CHECK IN SET TO TRUE
     public static final boolean DEBUG_GAL_SERVICE = false;
 
-    private static final String EMAIL_WINDOW_SIZE = "5";
-    public static final String PIM_WINDOW_SIZE = "4";
     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 =
@@ -142,9 +143,13 @@
     static private final String AUTO_DISCOVER_PAGE = "/autodiscover/autodiscover.xml";
     static private final int AUTO_DISCOVER_REDIRECT_CODE = 451;
 
+    static private final int INTERNAL_SERVER_ERROR_CODE = 500;
+
     static public final String EAS_12_POLICY_TYPE = "MS-EAS-Provisioning-WBXML";
     static public final String EAS_2_POLICY_TYPE = "MS-WAP-Provisioning-XML";
 
+    static public final int MESSAGE_FLAG_MOVED_MESSAGE = 1 << Message.FLAG_SYNC_ADAPTER_SHIFT;
+
     /**
      * 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
@@ -184,13 +189,16 @@
     // The EAS protocol Provision status meaning "we partially implement the policies"
     static private final String PROVISION_STATUS_PARTIAL = "2";
 
+    static /*package*/ final String DEVICE_TYPE = "Android";
+    static private final String USER_AGENT = DEVICE_TYPE + '/' + Build.VERSION.RELEASE + '-' +
+        Eas.CLIENT_VERSION;
+
     // Reasonable default
     public String mProtocolVersion = Eas.DEFAULT_PROTOCOL_VERSION;
     public Double mProtocolVersionDouble;
     protected String mDeviceId = null;
-    /*package*/ String mDeviceType = "Android";
     /*package*/ String mAuthString = null;
-    private String mCmdString = null;
+    /*package*/ String mCmdString = null;
     public String mHostAddress;
     public String mUserName;
     public String mPassword;
@@ -367,7 +375,8 @@
         // Find the most recent version we support
         for (String version: supportedVersionsArray) {
             if (version.equals(Eas.SUPPORTED_PROTOCOL_EX2003) ||
-                    version.equals(Eas.SUPPORTED_PROTOCOL_EX2007)) {
+                    version.equals(Eas.SUPPORTED_PROTOCOL_EX2007) ||
+                    version.equals(Eas.SUPPORTED_PROTOCOL_EX2007_SP1)) {
                 ourVersion = version;
             }
         }
@@ -378,7 +387,7 @@
             throw new MessagingException(MessagingException.PROTOCOL_VERSION_UNSUPPORTED);
         } else {
             service.mProtocolVersion = ourVersion;
-            service.mProtocolVersionDouble = Double.parseDouble(ourVersion);
+            service.mProtocolVersionDouble = Eas.getProtocolVersionDouble(ourVersion);
             if (service.mAccount != null) {
                 service.mAccount.mProtocolVersion = ourVersion;
             }
@@ -386,8 +395,10 @@
     }
 
     @Override
-    public void validateAccount(String hostAddress, String userName, String password, int port,
-            boolean ssl, boolean trustCertificates, Context context) throws MessagingException {
+    public Bundle validateAccount(String hostAddress, String userName, String password, int port,
+            boolean ssl, boolean trustCertificates, Context context) {
+        Bundle bundle = new Bundle();
+        int resultCode = MessagingException.NO_ERROR;
         try {
             userLog("Testing EAS: ", hostAddress, ", ", userName, ", ssl = ", ssl ? "1" : "0");
             EasSyncService svc = new EasSyncService("%TestAccount%");
@@ -401,70 +412,92 @@
             // Any string will do, but we'll go for "validate"
             svc.mDeviceId = "validate";
             HttpResponse resp = svc.sendHttpClientOptions();
-            int code = resp.getStatusLine().getStatusCode();
-            userLog("Validation (OPTIONS) response: " + code);
-            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);
-                }
+            HttpEntity entity = resp.getEntity();
+            try {
+                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");
+                    // Make sure we've got the right protocol version set up
+                    try {
+                        if (commands == null || versions == null) {
+                            userLog("OPTIONS response without commands or versions");
+                            // We'll treat this as a protocol exception
+                            throw new MessagingException(0);
+                        }
+                        setupProtocolVersion(svc, versions);
+                    } catch (MessagingException e) {
+                        bundle.putInt(EmailServiceProxy.VALIDATE_BUNDLE_RESULT_CODE,
+                                MessagingException.PROTOCOL_VERSION_UNSUPPORTED);
+                        return bundle;
+                    }
 
-                // Make sure we've got the right protocol version set up
-                setupProtocolVersion(svc, versions);
-
-                // Run second test here for provisioning failures...
-                Serializer s = new Serializer();
-                userLog("Validate: try folder sync");
-                s.start(Tags.FOLDER_FOLDER_SYNC).start(Tags.FOLDER_SYNC_KEY).text("0")
-                    .end().end().done();
-                resp = svc.sendHttpClientPost("FolderSync", s.toByteArray());
-                code = resp.getStatusLine().getStatusCode();
-                // We'll get one of the following responses if policies are required by the server
-                if (code == HttpStatus.SC_FORBIDDEN || code == HTTP_NEED_PROVISIONING) {
-                    // Get the policies and see if we are able to support them
-                    userLog("Validate: provisioning required");
-                    if (svc.canProvision() != null) {
-                        // If so, send the advisory Exception (the account may be created later)
-                        userLog("Validate: provisioning is possible");
-                        throw new MessagingException(MessagingException.SECURITY_POLICIES_REQUIRED);
-                    } else
-                        userLog("Validate: provisioning not possible");
-                        // If not, send the unsupported Exception (the account won't be created)
-                        throw new MessagingException(
-                                MessagingException.SECURITY_POLICIES_UNSUPPORTED);
-                } else if (code == HttpStatus.SC_NOT_FOUND) {
-                    userLog("Wrong address or bad protocol version");
-                    // We get a 404 from OWA addresses (which are NOT EAS addresses)
-                    throw new MessagingException(MessagingException.PROTOCOL_VERSION_UNSUPPORTED);
-                } else if (code != HttpStatus.SC_OK) {
-                    // Fail generically with anything other than success
-                    userLog("Unexpected response for FolderSync: ", code);
-                    throw new MessagingException(MessagingException.UNSPECIFIED_EXCEPTION);
+                    // Run second test here for provisioning failures using FolderSync
+                    userLog("Try folder sync");
+                    // Send "0" as the sync key for new accounts; otherwise, use the current key
+                    String syncKey = "0";
+                    Account existingAccount =
+                        Utility.findExistingAccount(context, -1L, hostAddress, userName);
+                    if (existingAccount != null && existingAccount.mSyncKey != null) {
+                        syncKey = existingAccount.mSyncKey;
+                    }
+                    Serializer s = new Serializer();
+                    s.start(Tags.FOLDER_FOLDER_SYNC).start(Tags.FOLDER_SYNC_KEY).text(syncKey)
+                        .end().end().done();
+                    resp = svc.sendHttpClientPost("FolderSync", s.toByteArray());
+                    code = resp.getStatusLine().getStatusCode();
+                    // We'll get one of the following responses if policies are required
+                    if (code == HttpStatus.SC_FORBIDDEN || code == HTTP_NEED_PROVISIONING) {
+                        // Get the policies and see if we are able to support them
+                        ProvisionParser pp = svc.canProvision();
+                        if (pp != null) {
+                            // Set the proper result code and save the PolicySet in our Bundle
+                            resultCode = MessagingException.SECURITY_POLICIES_REQUIRED;
+                            bundle.putParcelable(EmailServiceProxy.VALIDATE_BUNDLE_POLICY_SET,
+                                    pp.getPolicySet());
+                        } else
+                            // If not, set the proper code (the account will not be created)
+                            resultCode = MessagingException.SECURITY_POLICIES_UNSUPPORTED;
+                    } else if (code == HttpStatus.SC_NOT_FOUND) {
+                        // We get a 404 from OWA addresses (which are NOT EAS addresses)
+                        resultCode = MessagingException.PROTOCOL_VERSION_UNSUPPORTED;
+                    } else if (code != HttpStatus.SC_OK) {
+                        // Fail generically with anything other than success
+                        userLog("Unexpected response for FolderSync: ", code);
+                        resultCode = MessagingException.UNSPECIFIED_EXCEPTION;
+                    } else {
+                        userLog("Validation successful");
+                    }
+                } else if (isAuthError(code)) {
+                    userLog("Authentication failed");
+                    resultCode = MessagingException.AUTHENTICATION_FAILED;
+                } else if (code == INTERNAL_SERVER_ERROR_CODE) {
+                    // For Exchange 2003, this could mean an authentication failure OR server error
+                    userLog("Internal server error");
+                    resultCode = MessagingException.AUTHENTICATION_FAILED_OR_SERVER_ERROR;
+                } else {
+                    // TODO Need to catch other kinds of errors (e.g. policy) For now, report code.
+                    userLog("Validation failed, reporting I/O error: ", code);
+                    resultCode = 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);
+            } finally {
+                if (entity != null) {
+                    entity.consumeContent();
+                }
             }
         } catch (IOException e) {
             Throwable cause = e.getCause();
             if (cause != null && cause instanceof CertificateException) {
                 userLog("CertificateException caught: ", e.getMessage());
-                throw new MessagingException(MessagingException.GENERAL_SECURITY);
+                resultCode = MessagingException.GENERAL_SECURITY;
             }
             userLog("IOException caught: ", e.getMessage());
-            throw new MessagingException(MessagingException.IOERROR);
+            resultCode = MessagingException.IOERROR;
         }
-
+        bundle.putInt(EmailServiceProxy.VALIDATE_BUNDLE_RESULT_CODE, resultCode);
+        return bundle;
     }
 
     /**
@@ -605,15 +638,14 @@
                 resp = postAutodiscover(client, post, true /*canRetry*/);
             }
 
-            // Get the "final" code; if it's not 200, just return null
-            int code = resp.getStatusLine().getStatusCode();
-            userLog("Code: " + code);
-            if (code != HttpStatus.SC_OK) return null;
-
-            // At this point, we have a 200 response (SC_OK)
-            HttpEntity e = resp.getEntity();
-            InputStream is = e.getContent();
+            HttpEntity entity = resp.getEntity();
             try {
+                // Get the "final" code; if it's not 200, just return null
+                int code = resp.getStatusLine().getStatusCode();
+                userLog("Code: " + code);
+                if (code != HttpStatus.SC_OK) return null;
+
+                InputStream is = entity.getContent();
                 // The response to Autodiscover is regular XML (not WBXML)
                 // If we ever get an error in this process, we'll just punt and return null
                 XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
@@ -650,6 +682,10 @@
             } catch (XmlPullParserException e1) {
                 // This would indicate an I/O error of some sort
                 // We will simply return null and user can configure manually
+            } finally {
+               if (entity != null) {
+                   entity.consumeContent();
+               }
             }
         // There's no reason at all for exceptions to be thrown, and it's ok if so.
         // We just won't do auto-discover; user can configure manually
@@ -785,46 +821,71 @@
      * @param context caller's context
      * @param accountId the account Id to search
      * @param filter the characters entered so far
-     * @return a result record
+     * @return a result record or null for no data
      *
      * TODO: shorter timeout for interactive lookup
      * TODO: make watchdog actually work (it doesn't understand our service w/Mailbox == 0)
      * TODO: figure out why sendHttpClientPost() hangs - possibly pool exhaustion
      */
-    static public GalResult searchGal(Context context, long accountId, String filter) {
-        Account acct = SyncManager.getAccountById(accountId);
+    static public GalResult searchGal(Context context, long accountId, String filter, int limit) {
+        // Try to get the cached account from ExchangeService (saves a database access)
+        Account acct = ExchangeService.getAccountById(accountId);
+        if (acct == null) {
+            // ExchangeService isn't running; get the account directly from EmailProvider
+            acct = Account.restoreAccountWithId(context, accountId);
+        }
         if (acct != null) {
             HostAuth ha = HostAuth.restoreHostAuthWithId(context, acct.mHostAuthKeyRecv);
             EasSyncService svc = new EasSyncService("%GalLookupk%");
             try {
+                // If there's no protocol version set up, we haven't successfully started syncing
+                // so we can't use GAL yet
+                String protocolVersion = acct.mProtocolVersion;
+                if (protocolVersion == null) {
+                    return null;
+                } else {
+                    svc.mProtocolVersion = protocolVersion;
+                    svc.mProtocolVersionDouble = Eas.getProtocolVersionDouble(protocolVersion);
+                }
                 svc.mContext = context;
                 svc.mHostAddress = ha.mAddress;
                 svc.mUserName = ha.mLogin;
                 svc.mPassword = ha.mPassword;
                 svc.mSsl = (ha.mFlags & HostAuth.FLAG_SSL) != 0;
                 svc.mTrustSsl = (ha.mFlags & HostAuth.FLAG_TRUST_ALL_CERTIFICATES) != 0;
-                svc.mDeviceId = SyncManager.getDeviceId();
+                svc.mDeviceId = ExchangeService.getDeviceId();
                 svc.mAccount = acct;
                 Serializer s = new Serializer();
                 s.start(Tags.SEARCH_SEARCH).start(Tags.SEARCH_STORE);
                 s.data(Tags.SEARCH_NAME, "GAL").data(Tags.SEARCH_QUERY, filter);
                 s.start(Tags.SEARCH_OPTIONS);
-                s.data(Tags.SEARCH_RANGE, "0-19");  // Return 0..20 results
+                s.data(Tags.SEARCH_RANGE, "0-" + Integer.toString(limit - 1));
                 s.end().end().end().done();
                 if (DEBUG_GAL_SERVICE) svc.userLog("GAL lookup starting for " + ha.mAddress);
                 HttpResponse resp = svc.sendHttpClientPost("Search", s.toByteArray());
-                int code = resp.getStatusLine().getStatusCode();
-                if (code == HttpStatus.SC_OK) {
-                    InputStream is = resp.getEntity().getContent();
-                    GalParser gp = new GalParser(is, svc);
-                    if (gp.parse()) {
-                        if (DEBUG_GAL_SERVICE) svc.userLog("GAL lookup OK for " + ha.mAddress);
-                        return gp.getGalResult();
+                HttpEntity entity = resp.getEntity();
+                try {
+                    int code = resp.getStatusLine().getStatusCode();
+                    if (code == HttpStatus.SC_OK) {
+                        InputStream is = entity.getContent();
+                        try {
+                            GalParser gp = new GalParser(is, svc);
+                            if (gp.parse()) {
+                                if (DEBUG_GAL_SERVICE) svc.userLog("GAL lookup OK: " + ha.mAddress);
+                                return gp.getGalResult();
+                            } else {
+                                if (DEBUG_GAL_SERVICE) svc.userLog("GAL lookup: no matches");
+                            }
+                        } finally {
+                            is.close();
+                        }
                     } else {
-                        if (DEBUG_GAL_SERVICE) svc.userLog("GAL lookup returned no matches");
+                        svc.userLog("GAL lookup returned " + code);
                     }
-                } else {
-                    svc.userLog("GAL lookup returned " + code);
+                } finally {
+                    if (entity != null) {
+                        entity.consumeContent();
+                    }
                 }
             } catch (IOException e) {
                 // GAL is non-critical; we'll just go on
@@ -836,7 +897,7 @@
 
     private void doStatusCallback(long messageId, long attachmentId, int status) {
         try {
-            SyncManager.callback().loadAttachmentStatus(messageId, attachmentId, status, 0);
+            ExchangeService.callback().loadAttachmentStatus(messageId, attachmentId, status, 0);
         } catch (RemoteException e) {
             // No danger if the client is no longer around
         }
@@ -844,7 +905,7 @@
 
     private void doProgressCallback(long messageId, long attachmentId, int progress) {
         try {
-            SyncManager.callback().loadAttachmentStatus(messageId, attachmentId,
+            ExchangeService.callback().loadAttachmentStatus(messageId, attachmentId,
                     EmailServiceStatus.IN_PROGRESS, progress);
         } catch (RemoteException e) {
             // No danger if the client is no longer around
@@ -888,87 +949,97 @@
      * @param req the part (attachment) to be retrieved
      * @throws IOException
      */
-    protected void getAttachment(PartRequest req) throws IOException {
+    protected void loadAttachment(PartRequest req) throws IOException {
         Attachment att = req.mAttachment;
         Message msg = Message.restoreMessageWithId(mContext, att.mMessageKey);
-        doProgressCallback(msg.mId, att.mId, 0);
+        if (msg == null) {
+            doStatusCallback(att.mMessageKey, att.mId, EmailServiceStatus.MESSAGE_NOT_FOUND);
+            return;
+        } else {
+            doProgressCallback(msg.mId, att.mId, 0);
+        }
 
         String cmd = "GetAttachment&AttachmentName=" + att.mLocation;
         HttpResponse res = sendHttpClientPost(cmd, null, COMMAND_TIMEOUT);
+        HttpEntity entity = res.getEntity();
 
-        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.mDestination != null)
-                    ? new File(req.mDestination)
-                    : createUniqueFileInternal(req.mDestination, 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 {
-                        mPendingRequest = 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);
+        try {
+            int status = res.getStatusLine().getStatusCode();
+            if (status == HttpStatus.SC_OK) {
+                int len = (int)entity.getContentLength();
+                InputStream is = entity.getContent();
+                File f = (req.mDestination != null) ? new File(req.mDestination) :
+                    createUniqueFileInternal(req.mDestination, att.mFileName);
+                if (f != null) {
+                    // Ensure that the target directory exists
+                    File destDir = f.getParentFile();
+                    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 {
+                            mPendingRequest = req;
+                            byte[] bytes = new byte[CHUNK_SIZE];
+                            int length = len;
+                            // Loop terminates 1) when EOF is reached or 2) 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?");
+                                // read < 0 means that EOF was reached
+                                if (read < 0) {
+                                    userLog("Attachment load reached EOF, totalRead: ", totalRead);
                                     break;
                                 }
-                                int pct = (totalRead * 100) / length;
-                                doProgressCallback(msg.mId, att.mId, pct);
+
+                                // 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; 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 {
-                        mPendingRequest = null;
+                        } finally {
+                            mPendingRequest = null;
+                        }
+                    }
+                    os.flush();
+                    os.close();
+
+                    // EmailProvider will throw an exception if update an unsaved attachment
+                    if (att.isSaved()) {
+                        String contentUriString = (req.mContentUriString != null) ?
+                            req.mContentUriString :
+                            "file://" + f.getAbsolutePath();
+                        ContentValues cv = new ContentValues();
+                        cv.put(AttachmentColumns.CONTENT_URI, contentUriString);
+                        att.update(mContext, cv);
+                        doStatusCallback(msg.mId, att.mId, EmailServiceStatus.SUCCESS);
                     }
                 }
-                os.flush();
-                os.close();
-
-                // EmailProvider will throw an exception if we try to update an unsaved attachment
-                if (att.isSaved()) {
-                    String contentUriString = (req.mContentUriString != null)
-                            ? req.mContentUriString
-                            : "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);
             }
-        } else {
-            doStatusCallback(msg.mId, att.mId, EmailServiceStatus.MESSAGE_NOT_FOUND);
+        } finally {
+            if (entity != null) {
+                entity.consumeContent();
+            }
         }
     }
 
@@ -1047,6 +1118,84 @@
     }
 
     /**
+     * Responds to a move request.  The MessageMoveRequest is basically our
+     * wrapper for the MoveItems service call
+     * @param req the request (message id and "to" mailbox id)
+     * @throws IOException
+     */
+    protected void messageMoveRequest(MessageMoveRequest req) throws IOException {
+        // Retrieve the message and mailbox; punt if either are null
+        Message msg = Message.restoreMessageWithId(mContext, req.mMessageId);
+        if (msg == null) return;
+        Cursor c = mContentResolver.query(ContentUris.withAppendedId(Message.UPDATED_CONTENT_URI,
+                msg.mId), new String[] {MessageColumns.MAILBOX_KEY}, null, null, null);
+        Mailbox srcMailbox = null;
+        try {
+            if (!c.moveToNext()) return;
+            srcMailbox = Mailbox.restoreMailboxWithId(mContext, c.getLong(0));
+        } finally {
+            c.close();
+        }
+        if (srcMailbox == null) return;
+        Mailbox dstMailbox = Mailbox.restoreMailboxWithId(mContext, req.mMailboxId);
+        if (dstMailbox == null) return;
+        Serializer s = new Serializer();
+        s.start(Tags.MOVE_MOVE_ITEMS).start(Tags.MOVE_MOVE);
+        s.data(Tags.MOVE_SRCMSGID, msg.mServerId);
+        s.data(Tags.MOVE_SRCFLDID, srcMailbox.mServerId);
+        s.data(Tags.MOVE_DSTFLDID, dstMailbox.mServerId);
+        s.end().end().done();
+        HttpResponse res = sendHttpClientPost("MoveItems", s.toByteArray());
+        HttpEntity entity = res.getEntity();
+        try {
+            int status = res.getStatusLine().getStatusCode();
+            if (status == HttpStatus.SC_OK) {
+                    int len = (int) entity.getContentLength();
+                    InputStream is = res.getEntity().getContent();
+                    if (len != 0) {
+                        MoveItemsParser p = new MoveItemsParser(is, this);
+                        p.parse();
+                        int statusCode = p.getStatusCode();
+                        ContentValues cv = new ContentValues();
+                        if (statusCode == MoveItemsParser.STATUS_CODE_REVERT) {
+                            // Restore the old mailbox id
+                            cv.put(MessageColumns.MAILBOX_KEY, srcMailbox.mServerId);
+                            mContentResolver.update(
+                                    ContentUris.withAppendedId(Message.CONTENT_URI, req.mMessageId),
+                                    cv, null, null);
+                        } else if (statusCode == MoveItemsParser.STATUS_CODE_SUCCESS) {
+                            // Update with the new server id
+                            cv.put(SyncColumns.SERVER_ID, p.getNewServerId());
+                            cv.put(Message.FLAGS, msg.mFlags | MESSAGE_FLAG_MOVED_MESSAGE);
+                            mContentResolver.update(
+                                    ContentUris.withAppendedId(Message.CONTENT_URI, req.mMessageId),
+                                    cv, null, null);
+                        }
+                        if (statusCode == MoveItemsParser.STATUS_CODE_SUCCESS
+                                || statusCode == MoveItemsParser.STATUS_CODE_REVERT) {
+                            // If we revert or succeed, we no longer need the update information
+                            // OR the now-duplicate email (the new copy will be synced down)
+                            mContentResolver.delete(ContentUris.withAppendedId(
+                                    Message.UPDATED_CONTENT_URI, req.mMessageId), null, null);
+                        } else {
+                            // In this case, we're retrying, so do nothing.  The request will be
+                            // handled next sync
+                        }
+                    }
+            } else if (isAuthError(status)) {
+                throw new EasAuthenticationException();
+            } else {
+                userLog("Move items request failed, code: " + status);
+                throw new IOException();
+            }
+        } finally {
+            if (entity != null) {
+                entity.consumeContent();
+            }
+        }
+    }
+
+    /**
      * Responds to a meeting request.  The MeetingResponseRequest is basically our
      * wrapper for the meetingResponse service call
      * @param req the request (message id and response code)
@@ -1065,21 +1214,36 @@
         s.data(Tags.MREQ_REQ_ID, msg.mServerId);
         s.end().end().done();
         HttpResponse res = sendHttpClientPost("MeetingResponse", s.toByteArray());
-        int status = res.getStatusLine().getStatusCode();
-        if (status == HttpStatus.SC_OK) {
-            HttpEntity e = res.getEntity();
-            int len = (int)e.getContentLength();
-            InputStream is = res.getEntity().getContent();
-            if (len != 0) {
-                new MeetingResponseParser(is, this).parse();
-                sendMeetingResponseMail(msg, req.mResponse);
+        HttpEntity entity = res.getEntity();
+        try {
+            int status = res.getStatusLine().getStatusCode();
+            if (status == HttpStatus.SC_OK) {
+                int len = (int)entity.getContentLength();
+                if (len != 0) {
+                    InputStream is = res.getEntity().getContent();
+                    new MeetingResponseParser(is, this).parse();
+                    String meetingInfo = msg.mMeetingInfo;
+                    if (meetingInfo != null) {
+                        String responseRequested = new PackedString(meetingInfo).get(
+                                MeetingInfo.MEETING_RESPONSE_REQUESTED);
+                        // If there's no tag, or a non-zero tag, we send the response mail
+                        if ("0".equals(responseRequested)) {
+                            return;
+                        }
+                    }
+                    sendMeetingResponseMail(msg, req.mResponse);
+                }
+            } else if (isAuthError(status)) {
+                throw new EasAuthenticationException();
+            } else {
+                userLog("Meeting response request failed, code: " + status);
+                throw new IOException();
             }
-        } else if (isAuthError(status)) {
-            throw new EasAuthenticationException();
-        } else {
-            userLog("Meeting response request failed, code: " + status);
-            throw new IOException();
-        }
+        } finally {
+            if (entity != null) {
+                entity.consumeContent();
+            }
+       }
     }
 
     /**
@@ -1087,17 +1251,16 @@
      * in all HttpPost commands.  This should be called if these strings are null, or if mUserName
      * and/or mPassword are changed
      */
-    @SuppressWarnings("deprecation")
     private void cacheAuthAndCmdString() {
-        String safeUserName = URLEncoder.encode(mUserName);
+        String safeUserName = Uri.encode(mUserName);
         String cs = mUserName + ':' + mPassword;
         mAuthString = "Basic " + Base64.encodeToString(cs.getBytes(), Base64.NO_WRAP);
         mCmdString = "&User=" + safeUserName + "&DeviceId=" + mDeviceId +
-            "&DeviceType=" + mDeviceType;
+            "&DeviceType=" + DEVICE_TYPE;
     }
 
-    private String makeUriString(String cmd, String extra) throws IOException {
-         // Cache the authentication string and the command string
+    /*package*/ String makeUriString(String cmd, String extra) throws IOException {
+        // Cache the authentication string and the command string
         if (mAuthString == null || mCmdString == null) {
             cacheAuthAndCmdString();
         }
@@ -1121,7 +1284,7 @@
         method.setHeader("Authorization", mAuthString);
         method.setHeader("MS-ASProtocolVersion", mProtocolVersion);
         method.setHeader("Connection", "keep-alive");
-        method.setHeader("User-Agent", mDeviceType + '/' + Eas.VERSION);
+        method.setHeader("User-Agent", USER_AGENT);
         if (usePolicyKey) {
             // If there's an account in existence, use its key; otherwise (we're creating the
             // account), send "0".  The server will respond with code 449 if there are policies
@@ -1138,7 +1301,7 @@
     }
 
     private ClientConnectionManager getClientConnectionManager() {
-        return SyncManager.getClientConnectionManager();
+        return ExchangeService.getClientConnectionManager();
     }
 
     private HttpClient getHttpClient(int timeout) {
@@ -1189,9 +1352,9 @@
             mPendingPost = method;
             long alarmTime = timeout + WATCHDOG_TIMEOUT_ALLOWANCE;
             if (isPingCommand) {
-                SyncManager.runAsleep(mMailboxId, alarmTime);
+                ExchangeService.runAsleep(mMailboxId, alarmTime);
             } else {
-                SyncManager.setWatchdogAlarm(mMailboxId, alarmTime);
+                ExchangeService.setWatchdogAlarm(mMailboxId, alarmTime);
             }
         }
         try {
@@ -1199,9 +1362,9 @@
         } finally {
             synchronized(getSynchronizer()) {
                 if (isPingCommand) {
-                    SyncManager.runAwake(mMailboxId);
+                    ExchangeService.runAwake(mMailboxId);
                 } else {
-                    SyncManager.clearWatchdogAlarm(mMailboxId);
+                    ExchangeService.clearWatchdogAlarm(mMailboxId);
                 }
                 mPendingPost = null;
             }
@@ -1277,33 +1440,34 @@
             PolicySet ps = pp.getPolicySet();
             // Update the account with a null policyKey (the key we've gotten is
             // temporary and cannot be used for syncing)
-            if (ps.writeAccount(mAccount, null, true, mContext)) {
-                sp.updatePolicies(mAccount.mId);
-            }
+            ps.writeAccount(mAccount, null, true, mContext);
+            // Make sure that SecurityPolicy is up-to-date
+            sp.updatePolicies(mAccount.mId);
             if (pp.getRemoteWipe()) {
                 // We've gotten a remote wipe command
-                SyncManager.alwaysLog("!!! Remote wipe request received");
+                ExchangeService.alwaysLog("!!! Remote wipe request received");
                 // Start by setting the account to security hold
-                sp.setAccountHoldFlag(mAccount, true);
+                sp.setAccountHoldFlag(mContext, mAccount, true);
                 // Force a stop to any running syncs for this account (except this one)
-                SyncManager.stopNonAccountMailboxSyncsForAccount(mAccount.mId);
+                ExchangeService.stopNonAccountMailboxSyncsForAccount(mAccount.mId);
 
                 // If we're not the admin, we can't do the wipe, so just return
                 if (!sp.isActiveAdmin()) {
-                    SyncManager.alwaysLog("!!! Not device admin; can't wipe");
+                    ExchangeService.alwaysLog("!!! Not device admin; can't wipe");
                     return false;
                 }
+
                 // First, we've got to acknowledge it, but wrap the wipe in try/catch so that
                 // we wipe the device regardless of any errors in acknowledgment
                 try {
-                    SyncManager.alwaysLog("!!! Acknowledging remote wipe to server");
+                    ExchangeService.alwaysLog("!!! Acknowledging remote wipe to server");
                     acknowledgeRemoteWipe(pp.getPolicyKey());
                 } catch (Exception e) {
                     // Because remote wipe is such a high priority task, we don't want to
                     // circumvent it if there's an exception in acknowledgment
                 }
                 // Then, tell SecurityPolicy to wipe the device
-                SyncManager.alwaysLog("!!! Executing remote wipe");
+                ExchangeService.alwaysLog("!!! Executing remote wipe");
                 sp.remoteWipe();
                 return false;
             } else if (sp.isActive(ps)) {
@@ -1314,7 +1478,7 @@
                     // Write the final policy key to the Account and say we've been successful
                     ps.writeAccount(mAccount, policyKey, true, mContext);
                     // Release any mailboxes that might be in a security hold
-                    SyncManager.releaseSecurityHold(mAccount);
+                    ExchangeService.releaseSecurityHold(mAccount);
                     return true;
                 }
             } else {
@@ -1343,27 +1507,34 @@
         s.start(Tags.PROVISION_POLICY).data(Tags.PROVISION_POLICY_TYPE, getPolicyType())
             .end().end().end().done();
         HttpResponse resp = sendHttpClientPost("Provision", s.toByteArray());
-        int code = resp.getStatusLine().getStatusCode();
-        if (code == HttpStatus.SC_OK) {
-            InputStream is = resp.getEntity().getContent();
-            ProvisionParser pp = new ProvisionParser(is, this);
-            if (pp.parse()) {
-                // The PolicySet in the ProvisionParser will have the requirements for all KNOWN
-                // policies.  If others are required, hasSupportablePolicySet will be false
-                if (pp.hasSupportablePolicySet()) {
-                    // If the policies are supportable (in this context, meaning that there are no
-                    // completely unimplemented policies required), just return the parser itself
-                    return pp;
-                } else {
-                    // Try to acknowledge using the "partial" status (i.e. we can partially
-                    // accommodate the required policies).  The server will agree to this if the
-                    // "allow non-provisionable devices" setting is enabled on the server
-                    String policyKey = acknowledgeProvision(pp.getPolicyKey(),
-                            PROVISION_STATUS_PARTIAL);
-                    // Return either the parser (success) or null (failure)
-                    return (policyKey != null) ? pp : null;
+        HttpEntity entity = resp.getEntity();
+        try {
+            int code = resp.getStatusLine().getStatusCode();
+            if (code == HttpStatus.SC_OK) {
+                InputStream is = entity.getContent();
+                ProvisionParser pp = new ProvisionParser(is, this);
+                if (pp.parse()) {
+                    // The PolicySet in the ProvisionParser will have the requirements for all KNOWN
+                    // policies.  If others are required, hasSupportablePolicySet will be false
+                    if (pp.hasSupportablePolicySet()) {
+                        // If the policies are supportable (in this context, meaning there are no
+                        // completely unimplemented policies required), return the parser itself
+                        return pp;
+                    } else {
+                        // Try to acknowledge using the "partial" status (i.e. we can partially
+                        // accommodate the required policies).  The server will agree to this if the
+                        // "allow non-provisionable devices" setting is enabled on the server
+                        String policyKey = acknowledgeProvision(pp.getPolicyKey(),
+                                PROVISION_STATUS_PARTIAL);
+                        // Return either the parser (success) or null (failure)
+                        return (policyKey != null) ? pp : null;
+                    }
                 }
             }
+        } finally {
+            if (entity != null) {
+                entity.consumeContent();
+            }
         }
         // On failures, simply return null
         return null;
@@ -1403,13 +1574,20 @@
         }
         s.end().done(); // PROVISION_PROVISION
         HttpResponse resp = sendHttpClientPost("Provision", s.toByteArray());
-        int code = resp.getStatusLine().getStatusCode();
-        if (code == HttpStatus.SC_OK) {
-            InputStream is = resp.getEntity().getContent();
-            ProvisionParser pp = new ProvisionParser(is, this);
-            if (pp.parse()) {
-                // Return the final policy key from the ProvisionParser
-                return pp.getPolicyKey();
+        HttpEntity entity = resp.getEntity();
+        try {
+            int code = resp.getStatusLine().getStatusCode();
+            if (code == HttpStatus.SC_OK) {
+                InputStream is = entity.getContent();
+                ProvisionParser pp = new ProvisionParser(is, this);
+                if (pp.parse()) {
+                    // Return the final policy key from the ProvisionParser
+                    return pp.getPolicyKey();
+                }
+            }
+        } finally {
+            if (entity != null) {
+                entity.consumeContent();
             }
         }
         // On failures, return null
@@ -1424,15 +1602,21 @@
      */
     public void runAccountMailbox() throws IOException, EasParserException {
         // Initialize exit status to success
-        mExitStatus = EmailServiceStatus.SUCCESS;
+        mExitStatus = EXIT_DONE;
         try {
             try {
-                SyncManager.callback()
+                ExchangeService.callback()
                     .syncMailboxListStatus(mAccount.mId, EmailServiceStatus.IN_PROGRESS, 0);
             } catch (RemoteException e1) {
                 // Don't care if this fails
             }
 
+            // Don't run if we're being held
+            if ((mAccount.mFlags & Account.FLAGS_SECURITY_HOLD) != 0) {
+                mExitStatus = EXIT_SECURITY_FAILURE;
+                return;
+            }
+
             if (mAccount.mSyncKey == null) {
                 mAccount.mSyncKey = "0";
                 userLog("Account syncKey INIT to 0");
@@ -1452,7 +1636,7 @@
             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");
+                ExchangeService.kick("change ping boxes to push");
             }
 
             // Determine our protocol version, if we haven't already and save it in the Account
@@ -1476,8 +1660,13 @@
                     }
                     // Save the protocol version
                     cv.clear();
-                    // Save the protocol version in the account
+                    // Save the protocol version in the account; if we're using 12.0 or greater,
+                    // set the flag for support of SmartForward
                     cv.put(Account.PROTOCOL_VERSION, mProtocolVersion);
+                    if (mProtocolVersionDouble >= 12.0) {
+                        cv.put(Account.FLAGS,
+                                mAccount.mFlags | Account.FLAGS_SUPPORTS_SMART_FORWARD);
+                    }
                     mAccount.update(mContext, cv);
                     cv.clear();
                     // Save the sync time of the account mailbox to current time
@@ -1494,7 +1683,7 @@
                 cv.clear();
                 cv.put(Mailbox.SYNC_INTERVAL, Mailbox.CHECK_INTERVAL_PUSH);
                 if (mContentResolver.update(Mailbox.CONTENT_URI, cv,
-                        SyncManager.WHERE_IN_ACCOUNT_AND_PUSHABLE,
+                        ExchangeService.WHERE_IN_ACCOUNT_AND_PUSHABLE,
                         new String[] {Long.toString(mAccount.mId)}) > 0) {
                     userLog("Push account; set pushable boxes to push...");
                 }
@@ -1506,38 +1695,44 @@
                 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()) {
+                HttpEntity entity = resp.getEntity();
+                try {
+                    if (mStop) break;
+                    int code = resp.getStatusLine().getStatusCode();
+                    if (code == HttpStatus.SC_OK) {
+                            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(this))
+                                        .parse()) {
+                                    continue;
+                                }
+                            }
+                    } else if (isProvisionError(code)) {
+                        // If the sync error is a provisioning failure (perhaps policies changed),
+                        // let's try the provisioning procedure
+                        // Provisioning must only be attempted for the account mailbox - trying to
+                        // provision any other mailbox may result in race conditions and the
+                        // creation of multiple policy keys.
+                        if (!tryProvision()) {
+                            // Set the appropriate failure status
+                            mExitStatus = EXIT_SECURITY_FAILURE;
+                            return;
+                        } else {
+                            // If we succeeded, try again...
                             continue;
                         }
-                    }
-                } else if (isProvisionError(code)) {
-                    // If the sync error is a provisioning failure (perhaps the policies changed),
-                    // let's try the provisioning procedure
-                    // Provisioning must only be attempted for the account mailbox - trying to
-                    // provision any other mailbox may result in race conditions and the creation
-                    // of multiple policy keys.
-                    if (!tryProvision()) {
-                        // Set the appropriate failure status
-                        mExitStatus = EXIT_SECURITY_FAILURE;
+                    } else if (isAuthError(code)) {
+                        mExitStatus = EXIT_LOGIN_FAILURE;
                         return;
                     } else {
-                        // If we succeeded, try again...
-                        continue;
+                        userLog("FolderSync response error: ", code);
                     }
-                } else if (isAuthError(code)) {
-                    mExitStatus = EXIT_LOGIN_FAILURE;
-                    return;
-                } else {
-                    userLog("FolderSync response error: ", code);
+                } finally {
+                    if (entity != null) {
+                        entity.consumeContent();
+                    }
                 }
 
                 // Change all push/hold boxes to push
@@ -1550,7 +1745,7 @@
                 }
 
                 try {
-                    SyncManager.callback()
+                    ExchangeService.callback()
                         .syncMailboxListStatus(mAccount.mId, mExitStatus, 0);
                 } catch (RemoteException e1) {
                     // Don't care if this fails
@@ -1594,9 +1789,12 @@
             // A folder sync failed callback will get sent from run()
             try {
                 if (!mStop) {
-                    SyncManager.callback()
+                    // NOTE: The correct status is CONNECTION_ERROR, but the UI displays this, and
+                    // it's not really appropriate for EAS as this is not unexpected for a ping and
+                    // connection errors are retried in any case
+                    ExchangeService.callback()
                         .syncMailboxListStatus(mAccount.mId,
-                                EmailServiceStatus.CONNECTION_ERROR, 0);
+                                EmailServiceStatus.SUCCESS, 0);
                 }
             } catch (RemoteException e1) {
                 // Don't care if this fails
@@ -1657,7 +1855,7 @@
         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");
+        ExchangeService.kick("push fallback");
     }
 
     /**
@@ -1705,12 +1903,12 @@
                 while (c.moveToNext()) {
                     pushCount++;
                     // Two requirements for push:
-                    // 1) SyncManager tells us the mailbox is syncable (not running, not stopped)
+                    // 1) ExchangeService 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);
+                    int pingStatus = ExchangeService.pingStatus(mailboxId);
                     String mailboxName = c.getString(Mailbox.CONTENT_DISPLAY_NAME_COLUMN);
-                    if (pingStatus == SyncManager.PING_STATUS_OK) {
+                    if (pingStatus == ExchangeService.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
@@ -1733,10 +1931,10 @@
                             .data(Tags.PING_CLASS, folderClass)
                             .end();
                         readyMailboxes.add(mailboxName);
-                    } else if ((pingStatus == SyncManager.PING_STATUS_RUNNING) ||
-                            (pingStatus == SyncManager.PING_STATUS_WAITING)) {
+                    } else if ((pingStatus == ExchangeService.PING_STATUS_RUNNING) ||
+                            (pingStatus == ExchangeService.PING_STATUS_WAITING)) {
                         notReadyMailboxes.add(mailboxName);
-                    } else if (pingStatus == SyncManager.PING_STATUS_UNABLE) {
+                    } else if (pingStatus == ExchangeService.PING_STATUS_UNABLE) {
                         pushCount--;
                         userLog(mailboxName, " in error state; ignore");
                         continue;
@@ -1777,48 +1975,55 @@
                     }
                     HttpResponse res =
                         sendPing(s.toByteArray(), forcePing ? mPingForceHeartbeat : pingHeartbeat);
+                    HttpEntity entity = res.getEntity();
 
-                    int code = res.getStatusLine().getStatusCode();
-                    userLog("Ping response: ", code);
+                    try {
+                        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;
-                    }
+                        // Return immediately if we've been asked to stop during the ping
+                        if (mStop) {
+                            userLog("Stopping pingLoop");
+                            return;
+                        }
 
-                    if (code == HttpStatus.SC_OK) {
-                        // Make sure to clear out any pending sync errors
-                        SyncManager.removeFromSyncErrorMap(mMailboxId);
-                        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 < mPingMaxHeartbeat) &&
-                                        !mPingHeartbeatDropped) {
-                                    pingHeartbeat += PING_HEARTBEAT_INCREMENT;
-                                    if (pingHeartbeat > mPingMaxHeartbeat) {
-                                        pingHeartbeat = mPingMaxHeartbeat;
+                        if (code == HttpStatus.SC_OK) {
+                            // Make sure to clear out any pending sync errors
+                            ExchangeService.removeFromSyncErrorMap(mMailboxId);
+                            int len = (int)entity.getContentLength();
+                            InputStream is = entity.getContent();
+                            if (len != 0) {
+                                int pingResult = parsePingResult(is, mContentResolver,
+                                        pingErrorMap);
+                                // If our ping completed (status = 1), and wasn'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);
                                     }
-                                    userLog("Increasing ping heartbeat to ", pingHeartbeat, "s");
+                                    if ((pingHeartbeat < mPingMaxHeartbeat) &&
+                                            !mPingHeartbeatDropped) {
+                                        pingHeartbeat += PING_HEARTBEAT_INCREMENT;
+                                        if (pingHeartbeat > mPingMaxHeartbeat) {
+                                            pingHeartbeat = mPingMaxHeartbeat;
+                                        }
+                                        userLog("Increase ping heartbeat to ", pingHeartbeat, "s");
+                                    }
                                 }
+                            } else {
+                                userLog("Ping returned empty result; throwing IOException");
+                                throw new IOException();
                             }
-                        } else {
-                            userLog("Ping returned empty result; throwing IOException");
+                        } else if (isAuthError(code)) {
+                            mExitStatus = EXIT_LOGIN_FAILURE;
+                            userLog("Authorization error during Ping: ", code);
                             throw new IOException();
                         }
-                    } else if (isAuthError(code)) {
-                        mExitStatus = EXIT_LOGIN_FAILURE;
-                        userLog("Authorization error during Ping: ", code);
-                        throw new IOException();
+                    } finally {
+                        if (entity != null) {
+                            entity.consumeContent();
+                        }
                     }
                 } catch (IOException e) {
                     String message = e.getMessage();
@@ -1827,8 +2032,8 @@
                     boolean hasMessage = message != null;
                     userLog("IOException runPingLoop: " + (hasMessage ? message : "[no message]"));
                     if (mPostReset) {
-                        // Nothing to do in this case; this is SyncManager telling us to try another
-                        // ping.
+                        // Nothing to do in this case; this is ExchangeService telling us to try
+                        // another ping.
                     } else if (mPostAborted || isLikelyNatFailure(message)) {
                         long pingLength = SystemClock.elapsedRealtime() - pingTime;
                         if ((pingHeartbeat > mPingMinHeartbeat) &&
@@ -1843,7 +2048,7 @@
                             // There's no point in throwing here; this can happen in two cases
                             // 1) An alarm, which indicates minutes without activity; no sense
                             //    backing off
-                            // 2) SyncManager abort, due to sync of mailbox.  Again, we want to
+                            // 2) ExchangeService abort, due to sync of mailbox.  Again, we want to
                             //    keep on trying to ping
                             userLog("Ping aborted; retry");
                         } else if (pingLength < 2000) {
@@ -1867,7 +2072,7 @@
                 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
+                // TODO Change sleep to wait and use notify from ExchangeService when a sync ends
                 sleep(2*SECONDS, false);
                 pingWaitCount++;
                 //userLog("pingLoop waited 2s for: ", (pushCount - canPushCount), " box(es)");
@@ -1892,7 +2097,7 @@
 
     private void sleep(long ms, boolean runAsleep) {
         if (runAsleep) {
-            SyncManager.runAsleep(mMailboxId, ms+(5*SECONDS));
+            ExchangeService.runAsleep(mMailboxId, ms+(5*SECONDS));
         }
         try {
             Thread.sleep(ms);
@@ -1900,7 +2105,7 @@
             // Doesn't matter whether we stop early; it's the thought that counts
         } finally {
             if (runAsleep) {
-                SyncManager.runAwake(mMailboxId);
+                ExchangeService.runAwake(mMailboxId);
             }
         }
     }
@@ -1937,10 +2142,10 @@
 
                         // Check the status of the last sync
                         String status = c.getString(Mailbox.CONTENT_SYNC_STATUS_COLUMN);
-                        int type = SyncManager.getStatusType(status);
+                        int type = ExchangeService.getStatusType(status);
                         // This check should always be true...
-                        if (type == SyncManager.SYNC_PING) {
-                            int changeCount = SyncManager.getStatusChangeCount(status);
+                        if (type == ExchangeService.SYNC_PING) {
+                            int changeCount = ExchangeService.getStatusChangeCount(status);
                             if (changeCount > 0) {
                                 errorMap.remove(serverId);
                             } else if (changeCount == 0) {
@@ -1963,8 +2168,8 @@
                         }
 
                         // 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);
+                        ExchangeService.startManualSync(c.getLong(Mailbox.CONTENT_ID_COLUMN),
+                                ExchangeService.SYNC_PING, null);
                     }
                 } finally {
                     c.close();
@@ -1974,37 +2179,6 @@
         return pp.getSyncStatus();
     }
 
-    private String getEmailFilter() {
-        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
      *
@@ -2015,8 +2189,8 @@
 
         boolean moreAvailable = true;
         int loopingCount = 0;
-        while (!mStop && moreAvailable) {
-            // If we have no connectivity, just exit cleanly.  SyncManager will start us up again
+        while (!mStop && (moreAvailable || hasPendingRequests())) {
+            // If we have no connectivity, just exit cleanly. ExchangeService will start us up again
             // when connectivity has returned
             if (!hasConnectivity()) {
                 userLog("No connectivity in sync; finishing sync");
@@ -2033,27 +2207,31 @@
             // Now, handle various requests
             while (true) {
                 Request req = null;
-                synchronized (mRequests) {
-                    if (mRequests.isEmpty()) {
-                        break;
-                    } else {
-                        req = mRequests.get(0);
-                    }
+
+                if (mRequestQueue.isEmpty()) {
+                    break;
+                } else {
+                    req = mRequestQueue.peek();
                 }
 
                 // Our two request types are PartRequest (loading attachment) and
                 // MeetingResponseRequest (respond to a meeting request)
                 if (req instanceof PartRequest) {
-                    getAttachment((PartRequest)req);
+                    loadAttachment((PartRequest)req);
                 } else if (req instanceof MeetingResponseRequest) {
                     sendMeetingResponse((MeetingResponseRequest)req);
+                } else if (req instanceof MessageMoveRequest) {
+                    messageMoveRequest((MessageMoveRequest)req);
                 }
 
                 // If there's an exception handling the request, we'll throw it
                 // Otherwise, we remove the request
-                synchronized(mRequests) {
-                    mRequests.remove(req);
-                }
+                mRequestQueue.remove();
+            }
+
+            // Don't sync if we've got nothing to do
+            if (!moreAvailable) {
+                continue;
             }
 
             Serializer s = new Serializer();
@@ -2063,9 +2241,12 @@
             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)
+                .start(Tags.SYNC_COLLECTION);
+            // The "Class" element is removed in EAS 12.1 and later versions
+            if (mProtocolVersionDouble < Eas.SUPPORTED_PROTOCOL_EX2007_SP1_DOUBLE) {
+                s.data(Tags.SYNC_CLASS, className);
+            }
+            s.data(Tags.SYNC_SYNC_KEY, syncKey)
                 .data(Tags.SYNC_COLLECTION_ID, mailbox.mServerId);
 
             // Start with the default timeout
@@ -2075,31 +2256,8 @@
                 // appears to cause the server to delay its response in some cases, and this delay
                 // can be long enough to result in an IOException and total failure to sync.
                 // Therefore, we don't send any options with the initial sync.
-                s.tag(Tags.SYNC_DELETES_AS_MOVES);
-                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("Email")) {
-                    s.data(Tags.SYNC_FILTER_TYPE, getEmailFilter());
-                } else if (className.equals("Calendar")) {
-                    // TODO Force two weeks for calendar until we can set this!
-                    s.data(Tags.SYNC_FILTER_TYPE, Eas.FILTER_2_WEEKS);
-                }
-                // Set the truncation amount for all classes
-                if (mProtocolVersionDouble >= Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE) {
-                    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();
+                // Set the truncation amount, body preference, lookback, etc.
+                target.sendSyncOptions(mProtocolVersionDouble, s);
             } else {
                 // Use enormous timeout for initial sync, which empirically can take a while longer
                 timeout = 120*SECONDS;
@@ -2110,37 +2268,66 @@
             s.end().end().end().done();
             HttpResponse resp = sendHttpClientPost("Sync", new ByteArrayEntity(s.toByteArray()),
                     timeout);
-            int code = resp.getStatusLine().getStatusCode();
-            if (code == HttpStatus.SC_OK) {
-                InputStream is = resp.getEntity().getContent();
-                if (is != null) {
-                    moreAvailable = target.parse(is);
-                    if (target.isLooping()) {
-                        loopingCount++;
-                        userLog("** Looping: " + loopingCount);
-                        // After the maximum number of loops, we'll set moreAvailable to false and
-                        // allow the sync loop to terminate
-                        if (moreAvailable && (loopingCount > MAX_LOOPING_COUNT)) {
-                            userLog("** Looping force stopped");
-                            moreAvailable = false;
+            HttpEntity entity = resp.getEntity();
+            try {
+                int code = resp.getStatusLine().getStatusCode();
+                if (code == HttpStatus.SC_OK) {
+                    // In EAS 12.1, we can get "empty" sync responses, which indicate that there are
+                    // no changes in the mailbox; handle that case here
+                    Header header = resp.getFirstHeader("content-length");
+                    if (header != null && header.getValue().equals("0")) {
+                        // If this happens, exit cleanly, and change the interval from push to ping
+                        // if necessary
+                        userLog("Empty sync response; finishing");
+                        if (mMailbox.mSyncInterval == Mailbox.CHECK_INTERVAL_PUSH) {
+                            userLog("Changing mailbox from push to ping");
+                            ContentValues cv = new ContentValues();
+                            cv.put(Mailbox.SYNC_INTERVAL, Mailbox.CHECK_INTERVAL_PING);
+                            mContentResolver.update(
+                                    ContentUris.withAppendedId(Mailbox.CONTENT_URI, mMailbox.mId),
+                                    cv, null, null);
                         }
-                    } else {
-                        loopingCount = 0;
+                        if (mRequestQueue.isEmpty()) {
+                            mExitStatus = EXIT_DONE;
+                            return;
+                        } else {
+                            continue;
+                        }
                     }
-                    target.cleanup();
+                    InputStream is = entity.getContent();
+                    if (is != null) {
+                        moreAvailable = target.parse(is);
+                        if (target.isLooping()) {
+                            loopingCount++;
+                            userLog("** Looping: " + loopingCount);
+                            // After the maximum number of loops, we'll set moreAvailable to false
+                            // and allow the sync loop to terminate
+                            if (moreAvailable && (loopingCount > MAX_LOOPING_COUNT)) {
+                                userLog("** Looping force stopped");
+                                moreAvailable = false;
+                            }
+                        } else {
+                            loopingCount = 0;
+                        }
+                        target.cleanup();
+                    } else {
+                        userLog("Empty input stream in sync command response");
+                    }
                 } else {
-                    userLog("Empty input stream in sync command response");
+                    userLog("Sync response error: ", code);
+                    if (isProvisionError(code)) {
+                        mExitStatus = EXIT_SECURITY_FAILURE;
+                    } else if (isAuthError(code)) {
+                        mExitStatus = EXIT_LOGIN_FAILURE;
+                    } else {
+                        mExitStatus = EXIT_IO_ERROR;
+                    }
+                    return;
                 }
-            } else {
-                userLog("Sync response error: ", code);
-                if (isProvisionError(code)) {
-                    mExitStatus = EXIT_SECURITY_FAILURE;
-                } else if (isAuthError(code)) {
-                    mExitStatus = EXIT_LOGIN_FAILURE;
-                } else {
-                    mExitStatus = EXIT_IO_ERROR;
+            } finally {
+                if (entity != null) {
+                    entity.consumeContent();
                 }
-                return;
             }
         }
         mExitStatus = EXIT_DONE;
@@ -2168,7 +2355,7 @@
         if (mProtocolVersion == null) {
             mProtocolVersion = Eas.DEFAULT_PROTOCOL_VERSION;
         }
-        mProtocolVersionDouble = Double.parseDouble(mProtocolVersion);
+        mProtocolVersionDouble = Eas.getProtocolVersionDouble(mProtocolVersion);
         return true;
     }
 
@@ -2177,16 +2364,18 @@
      */
     public void run() {
         if (!setupService()) return;
-
-        try {
-            SyncManager.callback().syncMailboxStatus(mMailboxId, EmailServiceStatus.IN_PROGRESS, 0);
-        } catch (RemoteException e1) {
-            // Don't care if this fails
+        if (mSyncReason >= ExchangeService.SYNC_CALLBACK_START) {
+            try {
+                ExchangeService.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();
+            mDeviceId = ExchangeService.getDeviceId();
             if ((mMailbox == null) || (mAccount == null)) {
                 return;
             } else if (mMailbox.mType == Mailbox.TYPE_EAS_ACCOUNT_MAILBOX) {
@@ -2194,11 +2383,11 @@
             } else {
                 AbstractSyncAdapter target;
                 if (mMailbox.mType == Mailbox.TYPE_CONTACTS) {
-                    target = new ContactsSyncAdapter(mMailbox, this);
+                    target = new ContactsSyncAdapter( this);
                 } else if (mMailbox.mType == Mailbox.TYPE_CALENDAR) {
-                    target = new CalendarSyncAdapter(mMailbox, this);
+                    target = new CalendarSyncAdapter(this);
                 } else {
-                    target = new EmailSyncAdapter(mMailbox, this);
+                    target = new EmailSyncAdapter(this);
                 }
                 // We loop here because someone might have put a request in while we were syncing
                 // and we've missed that opportunity...
@@ -2224,7 +2413,7 @@
 
             if (!mStop) {
                 userLog("Sync finished");
-                SyncManager.done(this);
+                ExchangeService.done(this);
                 switch (mExitStatus) {
                     case EXIT_IO_ERROR:
                         status = EmailServiceStatus.CONNECTION_ERROR;
@@ -2245,7 +2434,7 @@
                         status = EmailServiceStatus.SECURITY_FAILURE;
                         // Ask for a new folder list.  This should wake up the account mailbox; a
                         // security error in account mailbox should start the provisioning process
-                        SyncManager.reloadFolderList(mContext, mAccount.mId, true);
+                        ExchangeService.reloadFolderList(mContext, mAccount.mId, true);
                         break;
                     default:
                         status = EmailServiceStatus.REMOTE_EXCEPTION;
@@ -2257,14 +2446,25 @@
                 status = EmailServiceStatus.SUCCESS;
             }
 
-            try {
-                SyncManager.callback().syncMailboxStatus(mMailboxId, status, 0);
-            } catch (RemoteException e1) {
-                // Don't care if this fails
+            // Send a callback if this run was initiated by a service call
+            if (mSyncReason >= ExchangeService.SYNC_CALLBACK_START) {
+                try {
+                    // Unless the user specifically asked for a sync, we really don't want to report
+                    // connection issues, as they are likely to be transient.  In this case, we
+                    // simply report success, so that the progress indicator terminates without
+                    // putting up an error banner
+                    if (mSyncReason != ExchangeService.SYNC_UI_REQUEST &&
+                            status == EmailServiceStatus.CONNECTION_ERROR) {
+                        status = EmailServiceStatus.SUCCESS;
+                    }
+                    ExchangeService.callback().syncMailboxStatus(mMailboxId, status, 0);
+                } catch (RemoteException e1) {
+                    // Don't care if this fails
+                }
             }
 
-            // Make sure SyncManager knows about this
-            SyncManager.kick("sync finished");
+            // Make sure ExchangeService knows about this
+            ExchangeService.kick("sync finished");
        }
     }
 }
diff --git a/src/com/android/exchange/EmailSyncAdapterService.java b/src/com/android/exchange/EmailSyncAdapterService.java
new file mode 100644
index 0000000..d3df136
--- /dev/null
+++ b/src/com/android/exchange/EmailSyncAdapterService.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.exchange;
+
+import com.android.email.provider.EmailContent;
+import com.android.email.provider.EmailContent.AccountColumns;
+import com.android.email.provider.EmailContent.Mailbox;
+import com.android.email.provider.EmailContent.MailboxColumns;
+
+import android.accounts.Account;
+import android.accounts.OperationCanceledException;
+import android.app.Service;
+import android.content.AbstractThreadedSyncAdapter;
+import android.content.ContentProviderClient;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SyncResult;
+import android.database.Cursor;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.util.Log;
+
+public class EmailSyncAdapterService extends Service {
+    private static final String TAG = "EAS EmailSyncAdapterService";
+    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_INBOX =
+        MailboxColumns.ACCOUNT_KEY + "=? AND " + MailboxColumns.TYPE + '=' + Mailbox.TYPE_INBOX;
+
+    public EmailSyncAdapterService() {
+        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 {
+                EmailSyncAdapterService.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 ExchangeService to start an
+     * inbox sync when we get the signal from the system SyncManager.
+     */
+    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");
+
+        // 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 inbox associated with the account
+                Cursor mailboxCursor = cr.query(Mailbox.CONTENT_URI, ID_PROJECTION,
+                        ACCOUNT_AND_TYPE_INBOX, new String[] {Long.toString(accountId)}, null);
+                try {
+                     if (mailboxCursor.moveToFirst()) {
+                        Log.i(TAG, "Mail sync requested for " + account.name);
+                        // Ask for a sync from our sync manager
+                        ExchangeService.serviceRequest(mailboxCursor.getLong(0),
+                                ExchangeService.SYNC_KICK);
+                    }
+                } finally {
+                    mailboxCursor.close();
+                }
+            }
+        } finally {
+            accountCursor.close();
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/exchange/EmailSyncAlarmReceiver.java b/src/com/android/exchange/EmailSyncAlarmReceiver.java
index ea36781..5f3d6c6 100644
--- a/src/com/android/exchange/EmailSyncAlarmReceiver.java
+++ b/src/com/android/exchange/EmailSyncAlarmReceiver.java
@@ -25,7 +25,6 @@
 import android.content.Context;
 import android.content.Intent;
 import android.database.Cursor;
-import android.util.Log;
 
 import java.util.ArrayList;
 
@@ -47,11 +46,9 @@
  */
 public class EmailSyncAlarmReceiver extends BroadcastReceiver {
     final String[] MAILBOX_DATA_PROJECTION = {MessageColumns.MAILBOX_KEY};
-    private static String TAG = "EmailSyncAlarm";
 
     @Override
     public void onReceive(final Context context, Intent intent) {
-        Log.v(TAG, "onReceive");
         new Thread(new Runnable() {
             public void run() {
                 handleReceive(context);
@@ -65,7 +62,7 @@
         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();
+        String selector = ExchangeService.getEasAccountSelector();
         
         // Find all of the deletions
         Cursor c = cr.query(Message.DELETED_CONTENT_URI, MAILBOX_DATA_PROJECTION, selector,
@@ -101,9 +98,7 @@
 
         // Request service from the mailbox
         for (Long mailboxId: mailboxesToNotify) {
-            SyncManager.serviceRequest(mailboxId, SyncManager.SYNC_UPSYNC);
+            ExchangeService.serviceRequest(mailboxId, ExchangeService.SYNC_UPSYNC);
         }
-        Log.v(TAG, "Changed/Deleted messages: " + messageCount + ", mailboxes: " +
-                mailboxesToNotify.size());
     }
 }
diff --git a/src/com/android/exchange/SyncManager.java b/src/com/android/exchange/ExchangeService.java
similarity index 78%
rename from src/com/android/exchange/SyncManager.java
rename to src/com/android/exchange/ExchangeService.java
index 3defdc5..9b9ed58 100644
--- a/src/com/android/exchange/SyncManager.java
+++ b/src/com/android/exchange/ExchangeService.java
@@ -19,9 +19,8 @@
 
 import com.android.email.AccountBackupRestore;
 import com.android.email.Email;
-import com.android.email.SecurityPolicy;
+import com.android.email.NotificationController;
 import com.android.email.Utility;
-import com.android.email.mail.MessagingException;
 import com.android.email.mail.transport.SSLUtils;
 import com.android.email.provider.EmailContent;
 import com.android.email.provider.EmailContent.Account;
@@ -35,7 +34,9 @@
 import com.android.email.service.EmailServiceStatus;
 import com.android.email.service.IEmailService;
 import com.android.email.service.IEmailServiceCallback;
+import com.android.email.service.MailService;
 import com.android.exchange.adapter.CalendarSyncAdapter;
+import com.android.exchange.adapter.ContactsSyncAdapter;
 import com.android.exchange.utility.FileLogger;
 
 import org.apache.http.conn.ClientConnectionManager;
@@ -50,10 +51,7 @@
 import org.apache.http.params.HttpParams;
 
 import android.accounts.AccountManager;
-import android.accounts.AccountManagerFuture;
-import android.accounts.AuthenticatorException;
 import android.accounts.OnAccountsUpdateListener;
-import android.accounts.OperationCanceledException;
 import android.app.AlarmManager;
 import android.app.PendingIntent;
 import android.app.Service;
@@ -69,21 +67,21 @@
 import android.database.Cursor;
 import android.net.ConnectivityManager;
 import android.net.NetworkInfo;
-import android.net.Uri;
 import android.net.NetworkInfo.State;
+import android.net.Uri;
 import android.os.Bundle;
 import android.os.Debug;
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.PowerManager;
+import android.os.PowerManager.WakeLock;
 import android.os.Process;
 import android.os.RemoteCallbackList;
 import android.os.RemoteException;
-import android.os.PowerManager.WakeLock;
 import android.provider.Calendar;
-import android.provider.ContactsContract;
 import android.provider.Calendar.Calendars;
 import android.provider.Calendar.Events;
+import android.provider.ContactsContract;
 import android.util.Log;
 
 import java.io.BufferedReader;
@@ -97,37 +95,37 @@
 import java.util.List;
 
 /**
- * The SyncManager handles all aspects of starting, maintaining, and stopping the various sync
+ * The ExchangeService 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,
+ * The Email application communicates with EAS sync adapters via ExchangeService'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
+ * ExchangeService 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 {
+public class ExchangeService extends Service implements Runnable {
 
-    private static final String TAG = "EAS SyncManager";
+    private static final String TAG = "ExchangeService";
 
-    // The SyncManager's mailbox "id"
-    protected static final int SYNC_MANAGER_ID = -1;
-    protected static final int SYNC_MANAGER_SERVICE_ID = 0;
+    // The ExchangeService's mailbox "id"
+    public static final int EXTRA_MAILBOX_ID = -1;
+    public static final int EXCHANGE_SERVICE_MAILBOX_ID = 0;
 
     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 EXCHANGE_SERVICE_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)
+    // Reason codes when ExchangeService.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)
@@ -136,12 +134,17 @@
     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;
+    public static final int SYNC_KICK = 4;
+
+    // Requests >= SYNC_CALLBACK_START generate callbacks to the UI
+    public static final int SYNC_CALLBACK_START = 5;
+    // startSync was requested of ExchangeService (other than due to user request)
+    public static final int SYNC_SERVICE_START_SYNC = SYNC_CALLBACK_START + 0;
+    // startSync was requested of ExchangeService (due to user request)
+    public static final int SYNC_UI_REQUEST = SYNC_CALLBACK_START + 1;
+    // A part request (attachment load, for now) was sent to ExchangeService
+    public static final int SYNC_SERVICE_PART_REQUEST = SYNC_CALLBACK_START + 2;
 
     private static final String WHERE_PUSH_OR_PING_NOT_ACCOUNT_MAILBOX =
         MailboxColumns.ACCOUNT_KEY + "=? and " + MailboxColumns.TYPE + "!=" +
@@ -195,7 +198,7 @@
     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
+    // The actual WakeLock obtained by ExchangeService
     private WakeLock mWakeLock = null;
     // Keep our cached list of active Accounts here
     public final AccountList mAccountList = new AccountList();
@@ -205,7 +208,6 @@
     private AccountObserver mAccountObserver;
     private MailboxObserver mMailboxObserver;
     private SyncedMessageObserver mSyncedMessageObserver;
-    private MessageObserver mMessageObserver;
     private EasSyncStatusObserver mSyncStatusObserver;
     private Object mStatusChangeListener;
     private EasAccountsUpdatedListener mAccountsUpdatedListener;
@@ -215,8 +217,8 @@
 
     private ContentResolver mResolver;
 
-    // The singleton SyncManager object, with its thread and stop flag
-    protected static SyncManager INSTANCE;
+    // The singleton ExchangeService object, with its thread and stop flag
+    protected static ExchangeService INSTANCE;
     private static Thread sServiceThread = null;
     // Cached unique device id
     private static String sDeviceId = null;
@@ -227,7 +229,7 @@
 
     private static volatile boolean sStop = false;
 
-    // The reason for SyncManager's next wakeup call
+    // The reason for ExchangeService's next wakeup call
     private String mNextWaitReason;
     // Whether we have an unsatisfied "kick" pending
     private boolean mKicked = false;
@@ -237,50 +239,91 @@
     private ConnectivityReceiver mBackgroundDataSettingReceiver = null;
     private volatile boolean mBackgroundData = true;
 
-    // The callback sent in from the UI using setCallback
-    private IEmailServiceCallback mCallback;
+    // Callbacks as set up via setCallback
     private RemoteCallbackList<IEmailServiceCallback> mCallbackList =
         new RemoteCallbackList<IEmailServiceCallback>();
 
+    private interface ServiceCallbackWrapper {
+        public void call(IEmailServiceCallback cb) throws RemoteException;
+    }
+
     /**
-     * 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.
+     * Proxy that can be used by various sync adapters to tie into ExchangeService's callback system
+     * Used this way:  ExchangeService.callback().callbackMethod(args...);
+     * The proxy wraps checking for existence of a ExchangeService instance
      * 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);
+        /**
+         * Broadcast a callback to the everyone that's registered
+         *
+         * @param wrapper the ServiceCallbackWrapper used in the broadcast
+         */
+        private synchronized void broadcastCallback(ServiceCallbackWrapper wrapper) {
+            RemoteCallbackList<IEmailServiceCallback> callbackList =
+                (INSTANCE == null) ? null: INSTANCE.mCallbackList;
+            if (callbackList != null) {
+                // Call everyone on our callback list
+                int count = callbackList.beginBroadcast();
+                try {
+                    for (int i = 0; i < count; i++) {
+                        try {
+                            wrapper.call(callbackList.getBroadcastItem(i));
+                        } catch (RemoteException e) {
+                            // Safe to ignore
+                        } catch (RuntimeException e) {
+                            // We don't want an exception in one call to prevent other calls, so
+                            // we'll just log this and continue
+                            Log.e(TAG, "Caught RuntimeException in broadcast", e);
+                        }
+                    }
+                } finally {
+                    // No matter what, we need to finish the broadcast
+                    callbackList.finishBroadcast();
+                }
             }
         }
 
-        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 loadAttachmentStatus(final long messageId, final long attachmentId,
+                final int status, final int progress) {
+            broadcastCallback(new ServiceCallbackWrapper() {
+                @Override
+                public void call(IEmailServiceCallback cb) throws RemoteException {
+                    cb.loadAttachmentStatus(messageId, attachmentId, status, 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 sendMessageStatus(final long messageId, final String subject, final int status,
+                final int progress) {
+            broadcastCallback(new ServiceCallbackWrapper() {
+                @Override
+                public void call(IEmailServiceCallback cb) throws RemoteException {
+                    cb.sendMessageStatus(messageId, subject, status, 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);
-            }
+        public void syncMailboxListStatus(final long accountId, final int status,
+                final int progress) {
+            broadcastCallback(new ServiceCallbackWrapper() {
+                @Override
+                public void call(IEmailServiceCallback cb) throws RemoteException {
+                    cb.syncMailboxListStatus(accountId, status, progress);
+                }
+            });
+        }
+
+        public void syncMailboxStatus(final long mailboxId, final int status,
+                final int progress) {
+            broadcastCallback(new ServiceCallbackWrapper() {
+                @Override
+                public void call(IEmailServiceCallback cb) throws RemoteException {
+                    cb.syncMailboxStatus(mailboxId, status, progress);
+                }
+            });
         }
     };
 
@@ -289,26 +332,21 @@
      */
     private final IEmailService.Stub mBinder = new IEmailService.Stub() {
 
-        public int validate(String protocol, String host, String userName, String password,
+        public Bundle 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();
-            }
+            return AbstractSyncService.validate(EasSyncService.class, host, userName, password,
+                    port, ssl, trustCertificates, ExchangeService.this);
         }
 
         public Bundle autoDiscover(String userName, String password) throws RemoteException {
             return new EasSyncService().tryAutodiscover(userName, password);
         }
 
-        public void startSync(long mailboxId) throws RemoteException {
-            SyncManager syncManager = INSTANCE;
-            if (syncManager == null) return;
-            checkSyncManagerServiceRunning();
-            Mailbox m = Mailbox.restoreMailboxWithId(syncManager, mailboxId);
+        public void startSync(long mailboxId, boolean userRequest) throws RemoteException {
+            ExchangeService exchangeService = INSTANCE;
+            if (exchangeService == null) return;
+            checkExchangeServiceServiceRunning();
+            Mailbox m = Mailbox.restoreMailboxWithId(exchangeService, mailboxId);
             if (m == null) return;
             if (m.mType == Mailbox.TYPE_OUTBOX) {
                 // We're using SERVER_ID to indicate an error condition (it has no other use for
@@ -316,18 +354,25 @@
                 // are candidates for sending.
                 ContentValues cv = new ContentValues();
                 cv.put(SyncColumns.SERVER_ID, 0);
-                syncManager.getContentResolver().update(Message.CONTENT_URI,
+                exchangeService.getContentResolver().update(Message.CONTENT_URI,
                     cv, WHERE_MAILBOX_KEY, new String[] {Long.toString(mailboxId)});
                 // Clear the error state; the Outbox sync will be started from checkMailboxes
-                syncManager.mSyncErrorMap.remove(mailboxId);
+                exchangeService.mSyncErrorMap.remove(mailboxId);
                 kick("start outbox");
                 // Outbox can't be synced in EAS
                 return;
             } else if (m.mType == Mailbox.TYPE_DRAFTS || m.mType == Mailbox.TYPE_TRASH) {
                 // Drafts & Trash can't be synced in EAS
+                try {
+                    // UI is expecting the callbacks....
+                    sCallbackProxy.syncMailboxStatus(mailboxId, EmailServiceStatus.IN_PROGRESS, 0);
+                    sCallbackProxy.syncMailboxStatus(mailboxId, EmailServiceStatus.SUCCESS, 0);
+                } catch (RemoteException ignore) {
+                }
                 return;
             }
-            startManualSync(mailboxId, SyncManager.SYNC_SERVICE_START_SYNC, null);
+            startManualSync(mailboxId, userRequest ? ExchangeService.SYNC_UI_REQUEST :
+                ExchangeService.SYNC_SERVICE_START_SYNC, null);
         }
 
         public void stopSync(long mailboxId) throws RemoteException {
@@ -336,25 +381,28 @@
 
         public void loadAttachment(long attachmentId, String destinationFile,
                 String contentUriString) throws RemoteException {
-            Attachment att = Attachment.restoreAttachmentWithId(SyncManager.this, attachmentId);
+            if (Email.DEBUG) {
+                Log.d(TAG, "loadAttachment: " + attachmentId + " to " + destinationFile);
+            }
+            Attachment att = Attachment.restoreAttachmentWithId(ExchangeService.this, attachmentId);
             sendMessageRequest(new PartRequest(att, destinationFile, contentUriString));
         }
 
         public void updateFolderList(long accountId) throws RemoteException {
-            reloadFolderList(SyncManager.this, accountId, false);
+            reloadFolderList(ExchangeService.this, accountId, false);
         }
 
         public void hostChanged(long accountId) throws RemoteException {
-            SyncManager syncManager = INSTANCE;
-            if (syncManager == null) return;
+            ExchangeService exchangeService = INSTANCE;
+            if (exchangeService == null) return;
             synchronized (sSyncLock) {
-                HashMap<Long, SyncError> syncErrorMap = syncManager.mSyncErrorMap;
+                HashMap<Long, SyncError> syncErrorMap = exchangeService.mSyncErrorMap;
                 ArrayList<Long> deletedMailboxes = new ArrayList<Long>();
                 // Go through the various error mailboxes
                 for (long mailboxId: syncErrorMap.keySet()) {
                     SyncError error = syncErrorMap.get(mailboxId);
                     // If it's a login failure, look a little harder
-                    Mailbox m = Mailbox.restoreMailboxWithId(syncManager, mailboxId);
+                    Mailbox m = Mailbox.restoreMailboxWithId(exchangeService, mailboxId);
                     // If it's for the account whose host has changed, clear the error
                     // If the mailbox is no longer around, remove the entry in the map
                     if (m == null) {
@@ -369,8 +417,8 @@
                 }
             }
             // Stop any running syncs
-            syncManager.stopAccountSyncs(accountId, true);
-            // Kick SyncManager
+            exchangeService.stopAccountSyncs(accountId, true);
+            // Kick ExchangeService
             kick("host changed");
         }
 
@@ -400,12 +448,37 @@
         }
 
         public void setCallback(IEmailServiceCallback cb) throws RemoteException {
-            if (mCallback != null) {
-                mCallbackList.unregister(mCallback);
-            }
-            mCallback = cb;
             mCallbackList.register(cb);
         }
+
+        public void moveMessage(long messageId, long mailboxId) throws RemoteException {
+            sendMessageRequest(new MessageMoveRequest(messageId, mailboxId));
+        }
+
+        /**
+         * Delete PIM (calendar, contacts) data for the specified account
+         *
+         * @param accountId the account whose data should be deleted
+         * @throws RemoteException
+         */
+        public void deleteAccountPIMData(long accountId) throws RemoteException {
+            ExchangeService exchangeService = INSTANCE;
+            if (exchangeService == null) return;
+            Mailbox mailbox =
+                Mailbox.restoreMailboxOfType(exchangeService, accountId, Mailbox.TYPE_CONTACTS);
+            if (mailbox != null) {
+                EasSyncService service = new EasSyncService(exchangeService, mailbox);
+                ContactsSyncAdapter adapter = new ContactsSyncAdapter(service);
+                adapter.wipe();
+            }
+            mailbox =
+                Mailbox.restoreMailboxOfType(exchangeService, accountId, Mailbox.TYPE_CALENDAR);
+            if (mailbox != null) {
+                EasSyncService service = new EasSyncService(exchangeService, mailbox);
+                CalendarSyncAdapter adapter = new CalendarSyncAdapter(service);
+                adapter.wipe();
+            }
+        }
     };
 
     static class AccountList extends ArrayList<Account> {
@@ -428,6 +501,15 @@
             }
             return null;
         }
+
+        public Account getByName(String accountName) {
+            for (Account account : this) {
+                if (account.mEmailAddress.equalsIgnoreCase(accountName)) {
+                    return account;
+                }
+            }
+            return null;
+        }
     }
 
     class AccountObserver extends ContentObserver {
@@ -514,7 +596,7 @@
         }
 
         private void onAccountChanged() {
-            maybeStartSyncManagerThread();
+            maybeStartExchangeServiceThread();
             Context context = getContext();
 
             // A change to the list requires us to scan for deletions (stop running syncs)
@@ -526,23 +608,24 @@
                 collectEasAccounts(c, currentAccounts);
                 synchronized (mAccountList) {
                     for (Account account : mAccountList) {
-                        // Ignore accounts not fully created
-                        if ((account.mFlags & Account.FLAGS_INCOMPLETE) != 0) {
-                            log("Account observer noticed incomplete account; ignoring");
-                            continue;
-                        } else if (!currentAccounts.contains(account.mId)) {
-                            // This is a deletion; shut down any account-related syncs
+                        boolean accountIncomplete =
+                            (account.mFlags & Account.FLAGS_INCOMPLETE) != 0;
+                        // If the current list doesn't include this account and the account wasn't
+                        // incomplete, then this is a deletion
+                        if (!currentAccounts.contains(account.mId) && !accountIncomplete) {
+                            // Shut down any account-related syncs
                             stopAccountSyncs(account.mId, true);
                             // Delete this from AccountManager...
                             android.accounts.Account acct = new android.accounts.Account(
                                     account.mEmailAddress, Email.EXCHANGE_ACCOUNT_MANAGER_TYPE);
-                            AccountManager.get(SyncManager.this).removeAccount(acct, null, null);
+                            AccountManager.get(ExchangeService.this)
+                                .removeAccount(acct, null, null);
                             mSyncableEasMailboxSelector = null;
                             mEasAccountSelector = null;
                         } else {
-                            // An account has changed
-                            Account updatedAccount = Account.restoreAccountWithId(context,
-                                    account.mId);
+                            // Get the newest version of this account
+                            Account updatedAccount =
+                                Account.restoreAccountWithId(context, account.mId);
                             if (updatedAccount == null) continue;
                             if (account.mSyncInterval != updatedAccount.mSyncInterval
                                     || account.mSyncLookback != updatedAccount.mSyncLookback) {
@@ -561,7 +644,7 @@
 
                             // See if this account is no longer on security hold
                             if (onSecurityHold(account) && !onSecurityHold(updatedAccount)) {
-                                releaseSyncHolds(SyncManager.this,
+                                releaseSyncHolds(ExchangeService.this,
                                         AbstractSyncService.EXIT_SECURITY_FAILURE, account);
                             }
 
@@ -744,7 +827,7 @@
                                         EasSyncService service =
                                             new EasSyncService(INSTANCE, mailbox);
                                         CalendarSyncAdapter adapter =
-                                            new CalendarSyncAdapter(mailbox, service);
+                                            new CalendarSyncAdapter(service);
                                         try {
                                             adapter.setSyncKey("0", false);
                                         } catch (IOException e) {
@@ -809,36 +892,19 @@
 
         @Override
         public void onChange(boolean selfChange) {
-            log("SyncedMessage changed: (re)setting alarm for 10s");
             alarmManager.set(AlarmManager.RTC_WAKEUP,
                     System.currentTimeMillis() + 10*SECONDS, syncAlarmPendingIntent);
         }
     }
 
-    private 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 Account getAccountById(long accountId) {
-        SyncManager syncManager = INSTANCE;
-        if (syncManager != null) {
-            AccountList accountList = syncManager.mAccountList;
+        ExchangeService exchangeService = INSTANCE;
+        if (exchangeService != null) {
+            AccountList accountList = exchangeService.mAccountList;
             synchronized (accountList) {
                 return accountList.getById(accountId);
             }
@@ -846,10 +912,21 @@
         return null;
     }
 
+    static public Account getAccountByName(String accountName) {
+        ExchangeService exchangeService = INSTANCE;
+        if (exchangeService != null) {
+            AccountList accountList = exchangeService.mAccountList;
+            synchronized (accountList) {
+                return accountList.getByName(accountName);
+            }
+        }
+        return null;
+    }
+
     static public String getEasAccountSelector() {
-        SyncManager syncManager = INSTANCE;
-        if (syncManager != null && syncManager.mAccountObserver != null) {
-            return syncManager.mAccountObserver.getAccountKeyWhere();
+        ExchangeService exchangeService = INSTANCE;
+        if (exchangeService != null && exchangeService.mAccountObserver != null) {
+            return exchangeService.mAccountObserver.getAccountKeyWhere();
         }
         return null;
     }
@@ -910,9 +987,9 @@
      * @param account the account whose Mailboxes should be released from security hold
      */
     static public void releaseSecurityHold(Account account) {
-        SyncManager syncManager = INSTANCE;
-        if (syncManager != null) {
-            syncManager.releaseSyncHolds(INSTANCE, AbstractSyncService.EXIT_SECURITY_FAILURE,
+        ExchangeService exchangeService = INSTANCE;
+        if (exchangeService != null) {
+            exchangeService.releaseSyncHolds(INSTANCE, AbstractSyncService.EXIT_SECURITY_FAILURE,
                     account);
         }
     }
@@ -922,14 +999,17 @@
      * is null, mailboxes from all accounts with the specified hold will be released
      * @param reason the reason for the SyncError (AbstractSyncService.EXIT_XXX)
      * @param account an Account whose mailboxes should be released (or all if null)
+     * @return whether or not any mailboxes were released
      */
-    /*package*/ void releaseSyncHolds(Context context, int reason, Account account) {
-        releaseSyncHoldsImpl(context, reason, account);
+    /*package*/ boolean releaseSyncHolds(Context context, int reason, Account account) {
+        boolean holdWasReleased = releaseSyncHoldsImpl(context, reason, account);
         kick("security release");
+        return holdWasReleased;
     }
 
-    private void releaseSyncHoldsImpl(Context context, int reason, Account account) {
+    private boolean releaseSyncHoldsImpl(Context context, int reason, Account account) {
         synchronized(sSyncLock) {
+            boolean holdWasReleased = false;
             ArrayList<Long> releaseList = new ArrayList<Long>();
             for (long mailboxId: mSyncErrorMap.keySet()) {
                 if (account != null) {
@@ -947,7 +1027,9 @@
             }
             for (long mailboxId: releaseList) {
                 mSyncErrorMap.remove(mailboxId);
+                holdWasReleased = true;
             }
+            return holdWasReleased;
         }
     }
 
@@ -967,36 +1049,34 @@
      */
     public class EasAccountsUpdatedListener implements OnAccountsUpdateListener {
         public void onAccountsUpdated(android.accounts.Account[] accounts) {
-            SyncManager syncManager = INSTANCE;
-            if (syncManager != null) {
-                syncManager.runAccountReconciler();
+            final ExchangeService exchangeService = INSTANCE;
+            if (exchangeService != null) {
+                Utility.runAsync(new Runnable() {
+                    @Override
+                    public void run() {
+                        exchangeService.runAccountReconcilerSync(exchangeService);
+                    }
+                });
             }
         }
     }
 
     /**
-     * Non-blocking call to run the account reconciler.
-     * Launches a worker thread, so it may be called from UI thread.
+     * Blocking call to the account reconciler
      */
-    private void runAccountReconciler() {
-        final SyncManager syncManager = this;
-        new Thread() {
-            @Override
-            public void run() {
-                android.accounts.Account[] accountMgrList = AccountManager.get(syncManager)
-                        .getAccountsByType(Email.EXCHANGE_ACCOUNT_MANAGER_TYPE);
-                synchronized (mAccountList) {
-                    // Make sure we have an up-to-date sAccountList.  If not (for example, if the
-                    // service has been destroyed), we would be reconciling against an empty account
-                    // list, which would cause the deletion of all of our accounts
-                    if (mAccountObserver != null) {
-                        mAccountObserver.onAccountChanged();
-                        reconcileAccountsWithAccountManager(syncManager, mAccountList,
-                                accountMgrList, false, mResolver);
-                    }
-                }
+    private void runAccountReconcilerSync(Context context) {
+        android.accounts.Account[] accountMgrList = AccountManager.get(context)
+                .getAccountsByType(Email.EXCHANGE_ACCOUNT_MANAGER_TYPE);
+        synchronized (mAccountList) {
+            // Make sure we have an up-to-date sAccountList.  If not (for example, if the
+            // service has been destroyed), we would be reconciling against an empty account
+            // list, which would cause the deletion of all of our accounts
+            if (mAccountObserver != null) {
+                mAccountObserver.onAccountChanged();
+                MailService.reconcileAccountsWithAccountManager(context,
+                        mAccountList, accountMgrList, false, mResolver);
             }
-        }.start();
+        }
     }
 
     public static void log(String str) {
@@ -1045,31 +1125,34 @@
             context = INSTANCE;
         }
 
-        try {
-            File f = context.getFileStreamPath("deviceName");
-            BufferedReader rdr = null;
-            String id;
-            if (f.exists() && f.canRead()) {
+        File f = context.getFileStreamPath("deviceName");
+        BufferedReader rdr = null;
+        String id;
+        if (f.exists()) {
+            if (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);
-                final String consistentDeviceId = Utility.getConsistentDeviceId(context);
-                if (consistentDeviceId != null) {
-                    // Use different prefix from random IDs.
-                    id = "androidc" + consistentDeviceId;
-                } else {
-                    id = "android" + System.currentTimeMillis();
+            } else {
+                Log.w(Email.LOG_TAG, f.getAbsolutePath() + ": File exists, but can't read?" +
+                        "  Trying to remove.");
+                if (!f.delete()) {
+                    Log.w(Email.LOG_TAG, "Remove failed. Tring to overwrite.");
                 }
-                w.write(id);
-                w.close();
-                return id;
             }
-        } catch (IOException e) {
         }
-        throw new IOException("Can't get device name");
+        BufferedWriter w = new BufferedWriter(new FileWriter(f), 128);
+        final String consistentDeviceId = Utility.getConsistentDeviceId(context);
+        if (consistentDeviceId != null) {
+            // Use different prefix from random IDs.
+            id = "androidc" + consistentDeviceId;
+        } else {
+            id = "android" + System.currentTimeMillis();
+        }
+        w.write(id);
+        w.close();
+        return id;
     }
 
     @Override
@@ -1122,9 +1205,9 @@
     }
 
     public static void stopAccountSyncs(long acctId) {
-        SyncManager syncManager = INSTANCE;
-        if (syncManager != null) {
-            syncManager.stopAccountSyncs(acctId, true);
+        ExchangeService exchangeService = INSTANCE;
+        if (exchangeService != null) {
+            exchangeService.stopAccountSyncs(acctId, true);
         }
     }
 
@@ -1171,8 +1254,8 @@
     }
 
     static public void reloadFolderList(Context context, long accountId, boolean force) {
-        SyncManager syncManager = INSTANCE;
-        if (syncManager == null) return;
+        ExchangeService exchangeService = INSTANCE;
+        if (exchangeService == null) return;
         Cursor c = context.getContentResolver().query(Mailbox.CONTENT_URI,
                 Mailbox.CONTENT_PROJECTION, MailboxColumns.ACCOUNT_KEY + "=? AND " +
                 MailboxColumns.TYPE + "=?",
@@ -1203,7 +1286,7 @@
                     log("Set push/ping boxes to push/hold");
 
                     long id = m.mId;
-                    AbstractSyncService svc = syncManager.mServiceMap.get(id);
+                    AbstractSyncService svc = exchangeService.mServiceMap.get(id);
                     // Tell the service we're done
                     if (svc != null) {
                         synchronized (svc.getSynchronizer()) {
@@ -1214,7 +1297,7 @@
                         thread.setName(thread.getName() + " (Stopped)");
                         thread.interrupt();
                         // Abandon the service
-                        syncManager.releaseMailbox(id);
+                        exchangeService.releaseMailbox(id);
                         // And have it start naturally
                         kick("reload folder list");
                     }
@@ -1226,16 +1309,16 @@
     }
 
     /**
-     * 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.
+     * Informs ExchangeService 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 stopNonAccountMailboxSyncsForAccount(long acctId) {
-        SyncManager syncManager = INSTANCE;
-        if (syncManager != null) {
-            syncManager.stopAccountSyncs(acctId, false);
+        ExchangeService exchangeService = INSTANCE;
+        if (exchangeService != null) {
+            exchangeService.stopAccountSyncs(acctId, false);
             kick("reload folder list");
         }
     }
@@ -1273,8 +1356,8 @@
     }
 
     static public String alarmOwner(long id) {
-        if (id == SYNC_MANAGER_ID) {
-            return "SyncManager";
+        if (id == EXTRA_MAILBOX_ID) {
+            return "ExchangeService";
         } else {
             String name = Long.toString(id);
             if (Eas.USER_LOG && INSTANCE != null) {
@@ -1327,57 +1410,61 @@
     }
 
     static public void runAwake(long id) {
-        SyncManager syncManager = INSTANCE;
-        if (syncManager != null) {
-            syncManager.acquireWakeLock(id);
-            syncManager.clearAlarm(id);
+        ExchangeService exchangeService = INSTANCE;
+        if (exchangeService != null) {
+            exchangeService.acquireWakeLock(id);
+            exchangeService.clearAlarm(id);
         }
     }
 
     static public void runAsleep(long id, long millis) {
-        SyncManager syncManager = INSTANCE;
-        if (syncManager != null) {
-            syncManager.setAlarm(id, millis);
-            syncManager.releaseWakeLock(id);
+        ExchangeService exchangeService = INSTANCE;
+        if (exchangeService != null) {
+            exchangeService.setAlarm(id, millis);
+            exchangeService.releaseWakeLock(id);
         }
     }
 
     static public void clearWatchdogAlarm(long id) {
-        SyncManager syncManager = INSTANCE;
-        if (syncManager != null) {
-            syncManager.clearAlarm(id);
+        ExchangeService exchangeService = INSTANCE;
+        if (exchangeService != null) {
+            exchangeService.clearAlarm(id);
         }
     }
 
     static public void setWatchdogAlarm(long id, long millis) {
-        SyncManager syncManager = INSTANCE;
-        if (syncManager != null) {
-            syncManager.setAlarm(id, millis);
+        ExchangeService exchangeService = INSTANCE;
+        if (exchangeService != null) {
+            exchangeService.setAlarm(id, millis);
         }
     }
 
     static public void alert(Context context, final long id) {
-        final SyncManager syncManager = INSTANCE;
-        checkSyncManagerServiceRunning();
+        final ExchangeService exchangeService = INSTANCE;
+        checkExchangeServiceServiceRunning();
         if (id < 0) {
-            kick("ping SyncManager");
-        } else if (syncManager == null) {
-            context.startService(new Intent(context, SyncManager.class));
+            log("ExchangeService alert");
+            kick("ping ExchangeService");
+        } else if (exchangeService == null) {
+            context.startService(new Intent(context, ExchangeService.class));
         } else {
-            final AbstractSyncService service = syncManager.mServiceMap.get(id);
+            final AbstractSyncService service = exchangeService.mServiceMap.get(id);
             if (service != null) {
                 // Handle alerts in a background thread, as we are typically called from a
                 // broadcast receiver, and are therefore running in the UI thread
-                String threadName = "SyncManager Alert: ";
+                String threadName = "ExchangeService Alert: ";
                 if (service.mMailbox != null) {
                     threadName += service.mMailbox.mDisplayName;
                 }
                 new Thread(new Runnable() {
                    public void run() {
-                       Mailbox m = Mailbox.restoreMailboxWithId(syncManager, id);
+                       Mailbox m = Mailbox.restoreMailboxWithId(exchangeService, 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 (Eas.DEBUG) {
+                               Log.d(TAG, "Alert for mailbox " + id + " (" + m.mDisplayName + ")");
+                           }
                            if (m.mType == Mailbox.TYPE_DRAFTS || m.mType == Mailbox.TYPE_OUTBOX) {
                                String[] args = new String[] {Long.toString(m.mId)};
                                ContentResolver resolver = INSTANCE.mResolver;
@@ -1396,11 +1483,11 @@
                                // thread to do the work
                                log("Alarm failed; releasing mailbox");
                                synchronized(sSyncLock) {
-                                   syncManager.releaseMailbox(id);
+                                   exchangeService.releaseMailbox(id);
                                }
                                // Shutdown the connection manager; this should close all of our
                                // sockets and generate IOExceptions all around.
-                               SyncManager.shutdownConnectionManager();
+                               ExchangeService.shutdownConnectionManager();
                            }
                        }
                     }}, threadName).start();
@@ -1411,8 +1498,8 @@
     /**
      * 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)
+     * This code is called 1) when ExchangeService starts, and 2) when ExchangeService is running
+     * and there are changes made (this is detected via a SyncStatusObserver)
      */
     private void updatePIMSyncSettings(Account providerAccount, int mailboxType, String authority) {
         ContentValues cv = new ContentValues();
@@ -1468,91 +1555,6 @@
         }
     }
 
-    /**
-     * Compare our account list (obtained from EmailProvider) with the account list owned by
-     * AccountManager.  If there are any orphans (an account in one list without a corresponding
-     * account in the other list), delete the orphan, as these must remain in sync.
-     *
-     * Note that the duplication of account information is caused by the Email application's
-     * incomplete integration with AccountManager.
-     *
-     * This function may not be called from the main/UI thread, because it makes blocking calls
-     * into the account manager.
-     *
-     * @param context The context in which to operate
-     * @param cachedEasAccounts the exchange provider accounts to work from
-     * @param accountManagerAccounts The account manager accounts to work from
-     * @param blockExternalChanges FOR TESTING ONLY - block backups, security changes, etc.
-     * @param resolver the content resolver for making provider updates (injected for testability)
-     */
-    /* package */ static void reconcileAccountsWithAccountManager(Context context,
-            List<Account> cachedEasAccounts, android.accounts.Account[] accountManagerAccounts,
-            boolean blockExternalChanges, ContentResolver resolver) {
-        // First, look through our cached EAS Accounts (from EmailProvider) to make sure there's a
-        // corresponding AccountManager account
-        boolean accountsDeleted = false;
-        for (Account providerAccount: cachedEasAccounts) {
-            String providerAccountName = providerAccount.mEmailAddress;
-            boolean found = false;
-            for (android.accounts.Account accountManagerAccount: accountManagerAccounts) {
-                if (accountManagerAccount.name.equalsIgnoreCase(providerAccountName)) {
-                    found = true;
-                    break;
-                }
-            }
-            if (!found) {
-                if ((providerAccount.mFlags & Account.FLAGS_INCOMPLETE) != 0) {
-                    log("Account reconciler noticed incomplete account; ignoring");
-                    continue;
-                }
-                // This account has been deleted in the AccountManager!
-                alwaysLog("Account deleted in AccountManager; deleting from provider: " +
-                        providerAccountName);
-                // TODO This will orphan downloaded attachments; need to handle this
-                resolver.delete(ContentUris.withAppendedId(Account.CONTENT_URI,
-                        providerAccount.mId), null, null);
-                accountsDeleted = true;
-            }
-        }
-        // Now, look through AccountManager accounts to make sure we have a corresponding cached EAS
-        // account from EmailProvider
-        for (android.accounts.Account accountManagerAccount: accountManagerAccounts) {
-            String accountManagerAccountName = accountManagerAccount.name;
-            boolean found = false;
-            for (Account cachedEasAccount: cachedEasAccounts) {
-                if (cachedEasAccount.mEmailAddress.equalsIgnoreCase(accountManagerAccountName)) {
-                    found = true;
-                }
-            }
-            if (!found) {
-                // This account has been deleted from the EmailProvider database
-                alwaysLog("Account deleted from provider; deleting from AccountManager: " +
-                        accountManagerAccountName);
-                // Delete the account
-                AccountManagerFuture<Boolean> blockingResult = AccountManager.get(context)
-                        .removeAccount(accountManagerAccount, null, null);
-                try {
-                    // Note: All of the potential errors from removeAccount() are simply logged
-                    // here, as there is nothing to actually do about them.
-                    blockingResult.getResult();
-                } catch (OperationCanceledException e) {
-                    Log.w(Email.LOG_TAG, e.toString());
-                } catch (AuthenticatorException e) {
-                    Log.w(Email.LOG_TAG, e.toString());
-                } catch (IOException e) {
-                    Log.w(Email.LOG_TAG, e.toString());
-                }
-                accountsDeleted = true;
-            }
-        }
-        // If we changed the list of accounts, refresh the backup & security settings
-        if (!blockExternalChanges && accountsDeleted) {
-            AccountBackupRestore.backupAccounts(context);
-            SecurityPolicy.getInstance(context).reducePolicies();
-            Email.setNotifyUiAccountsChanged(true);
-        }
-    }
-
     public class ConnectivityReceiver extends BroadcastReceiver {
         @Override
         public void onReceive(Context context, Intent intent) {
@@ -1577,10 +1579,10 @@
                 }
             } else if (intent.getAction().equals(
                     ConnectivityManager.ACTION_BACKGROUND_DATA_SETTING_CHANGED)) {
-                ConnectivityManager cm = (ConnectivityManager)SyncManager.this
+                ConnectivityManager cm = (ConnectivityManager)ExchangeService.this
                     .getSystemService(Context.CONNECTIVITY_SERVICE);
                 mBackgroundData = cm.getBackgroundDataSetting();
-                // If background data is now on, we want to kick SyncManager
+                // If background data is now on, we want to kick ExchangeService
                 if (mBackgroundData) {
                     kick("background data on");
                     log("Background data on; restart syncs");
@@ -1589,7 +1591,7 @@
                     log("Background data off: stop all syncs");
                     synchronized (mAccountList) {
                         for (Account account : mAccountList)
-                            SyncManager.stopAccountSyncs(account.mId);
+                            ExchangeService.stopAccountSyncs(account.mId);
                     }
                 }
             }
@@ -1709,7 +1711,7 @@
                 // Wait until a network is connected (or 10 mins), 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);
+                    runAsleep(EXTRA_MAILBOX_ID, CONNECTIVITY_WAIT_TIME+5*SECONDS);
                     try {
                         log("Connectivity lock...");
                         sConnectivityHold = true;
@@ -1720,14 +1722,14 @@
                     } finally {
                         sConnectivityHold = false;
                     }
-                    runAwake(SYNC_MANAGER_ID);
+                    runAwake(EXTRA_MAILBOX_ID);
                 }
             }
         }
     }
 
     /**
-     * Note that there are two ways the EAS SyncManager service can be created:
+     * Note that there are two ways the EAS ExchangeService 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
@@ -1751,10 +1753,8 @@
     @Override
     public void onCreate() {
         synchronized (sSyncLock) {
-            alwaysLog("!!! EAS SyncManager, onCreate");
-            // If we're in the process of shutting down, try again in 5 seconds
+            alwaysLog("!!! EAS ExchangeService, onCreate");
             if (sStop) {
-                setAlarm(SYNC_MANAGER_SERVICE_ID, 5*SECONDS);
                 return;
             }
             if (sDeviceId == null) {
@@ -1762,26 +1762,38 @@
                     getDeviceId(this);
                 } catch (IOException e) {
                     // We can't run in this situation
-                    throw new RuntimeException();
+                    throw new RuntimeException(e);
                 }
             }
-            // Run the reconciler and clean up any mismatched accounts - if we weren't running when
-            // accounts were deleted, it won't have been called.
-            runAccountReconciler();
+            // Finally, run some setup activities off the UI thread
+            Utility.runAsync(new Runnable() {
+                @Override
+                public void run() {
+                    // Run the reconciler and clean up any mismatched accounts - if we weren't
+                    // running when accounts were deleted, it won't have been called.
+                    runAccountReconcilerSync(ExchangeService.this);
+                    // Update other services depending on final account configuration
+                    Email.setServicesEnabledSync(ExchangeService.this);
+                }
+            });
         }
     }
 
     @Override
     public int onStartCommand(Intent intent, int flags, int startId) {
         synchronized (sSyncLock) {
-            alwaysLog("!!! EAS SyncManager, onStartCommand");
+            alwaysLog("!!! EAS ExchangeService, onStartCommand");
             // Restore accounts, if it has not happened already
             AccountBackupRestore.restoreAccountsIfNeeded(this);
-            maybeStartSyncManagerThread();
+            maybeStartExchangeServiceThread();
             if (sServiceThread == null) {
-                alwaysLog("!!! EAS SyncManager, stopping self");
+                alwaysLog("!!! EAS ExchangeService, stopping self");
                 stopSelf();
+            } else if (sStop) {
+                // If we were in the middle of trying to stop, attempt a restart in 5 seconds
+                setAlarm(EXCHANGE_SERVICE_MAILBOX_ID, 5*SECONDS);
             }
+            // If we're running, we want the download service running
             return Service.START_STICKY;
         }
     }
@@ -1789,7 +1801,7 @@
     @Override
     public void onDestroy() {
         synchronized(sSyncLock) {
-            alwaysLog("!!! EAS SyncManager, onDestroy");
+            alwaysLog("!!! EAS ExchangeService, onDestroy");
             // Stop the sync manager thread and return
             synchronized (sSyncLock) {
                 if (sServiceThread != null) {
@@ -1800,13 +1812,13 @@
         }
     }
 
-    void maybeStartSyncManagerThread() {
+    void maybeStartExchangeServiceThread() {
         // 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()) {
+        if (sServiceThread == null || !sServiceThread.isAlive()) {
+            if (EmailContent.count(this, HostAuth.CONTENT_URI, WHERE_PROTOCOL_EAS, null) > 0) {
                 log(sServiceThread == null ? "Starting thread..." : "Restarting thread...");
-                sServiceThread = new Thread(this, "SyncManager");
+                sServiceThread = new Thread(this, "ExchangeService");
                 INSTANCE = this;
                 sServiceThread.start();
             }
@@ -1814,22 +1826,22 @@
     }
 
     /**
-     * Start up the SyncManager service if it's not already running
-     * This is a stopgap for cases in which SyncManager died (due to a crash somewhere in
+     * Start up the ExchangeService service if it's not already running
+     * This is a stopgap for cases in which ExchangeService died (due to a crash somewhere in
      * com.android.email) and hasn't been restarted. See the comment for onCreate for details
      */
-    static void checkSyncManagerServiceRunning() {
-        SyncManager syncManager = INSTANCE;
-        if (syncManager == null) return;
+    static void checkExchangeServiceServiceRunning() {
+        ExchangeService exchangeService = INSTANCE;
+        if (exchangeService == null) return;
         if (sServiceThread == null) {
-            alwaysLog("!!! checkSyncManagerServiceRunning; starting service...");
-            syncManager.startService(new Intent(syncManager, SyncManager.class));
+            alwaysLog("!!! checkExchangeServiceServiceRunning; starting service...");
+            exchangeService.startService(new Intent(exchangeService, ExchangeService.class));
         }
     }
 
     public void run() {
         sStop = false;
-        alwaysLog("!!! SyncManager thread running");
+        alwaysLog("!!! ExchangeService thread running");
         // If we're really debugging, turn on all logging
         if (Eas.DEBUG) {
             Eas.USER_LOG = true;
@@ -1858,8 +1870,6 @@
                 mSyncedMessageObserver = new SyncedMessageObserver(mHandler);
                 mResolver.registerContentObserver(Message.SYNCED_CONTENT_URI, true,
                         mSyncedMessageObserver);
-                mMessageObserver = new MessageObserver(mHandler);
-                mResolver.registerContentObserver(Message.CONTENT_URI, true, mMessageObserver);
                 mSyncStatusObserver = new EasSyncStatusObserver();
                 mStatusChangeListener =
                     ContentResolver.addStatusChangeListener(
@@ -1892,9 +1902,9 @@
         try {
             // Loop indefinitely until we're shut down
             while (!sStop) {
-                runAwake(SYNC_MANAGER_ID);
+                runAwake(EXTRA_MAILBOX_ID);
                 waitForConnectivity();
-                mNextWaitReason = "Heartbeat";
+                mNextWaitReason = null;
                 long nextWait = checkMailboxes();
                 try {
                     synchronized (this) {
@@ -1904,15 +1914,17 @@
                                 nextWait = 1*SECONDS;
                             }
                             if (nextWait > 10*SECONDS) {
-                                log("Next awake in " + nextWait / 1000 + "s: " + mNextWaitReason);
-                                runAsleep(SYNC_MANAGER_ID, nextWait + (3*SECONDS));
+                                if (mNextWaitReason != null) {
+                                    log("Next awake " + nextWait / 1000 + "s: " + mNextWaitReason);
+                                }
+                                runAsleep(EXTRA_MAILBOX_ID, nextWait + (3*SECONDS));
                             }
                             wait(nextWait);
                         }
                     }
                 } catch (InterruptedException e) {
                     // Needs to be caught, but causes no problem
-                    log("SyncManager interrupted");
+                    log("ExchangeService interrupted");
                 } finally {
                     synchronized (this) {
                         if (mKicked) {
@@ -1924,7 +1936,7 @@
             }
             log("Shutdown requested");
         } catch (RuntimeException e) {
-            Log.e(TAG, "RuntimeException in SyncManager", e);
+            Log.e(TAG, "RuntimeException in ExchangeService", e);
             throw e;
         } finally {
             shutdown();
@@ -1935,7 +1947,7 @@
         synchronized (sSyncLock) {
             // If INSTANCE is null, we've already been shut down
             if (INSTANCE != null) {
-                log("SyncManager shutting down...");
+                log("ExchangeService shutting down...");
 
                 // Stop our running syncs
                 stopServiceThreads();
@@ -1954,10 +1966,6 @@
                     resolver.unregisterContentObserver(mSyncedMessageObserver);
                     mSyncedMessageObserver = null;
                 }
-                if (mMessageObserver != null) {
-                    resolver.unregisterContentObserver(mMessageObserver);
-                    mMessageObserver = null;
-                }
                 if (mAccountObserver != null) {
                     resolver.unregisterContentObserver(mAccountObserver);
                     mAccountObserver = null;
@@ -2006,6 +2014,28 @@
         releaseWakeLock(mailboxId);
     }
 
+    /**
+     * Check whether an Outbox (referenced by a Cursor) has any messages that can be sent
+     * @param c the cursor to an Outbox
+     * @return true if there is mail to be sent
+     */
+    private boolean hasSendableMessages(Cursor outboxCursor) {
+        Cursor c = mResolver.query(Message.CONTENT_URI, Message.ID_COLUMN_PROJECTION,
+                EasOutboxService.MAILBOX_KEY_AND_NOT_SEND_FAILED,
+                new String[] {Long.toString(outboxCursor.getLong(Mailbox.CONTENT_ID_COLUMN))},
+                null);
+        try {
+            while (c.moveToNext()) {
+                if (!Utility.hasUnloadedAttachments(this, c.getLong(Message.CONTENT_ID_COLUMN))) {
+                    return true;
+                }
+            }
+        } finally {
+            c.close();
+        }
+        return false;
+    }
+
     private long checkMailboxes () {
         // First, see if any running mailboxes have been deleted
         ArrayList<Long> deletedMailboxes = new ArrayList<Long>();
@@ -2035,7 +2065,7 @@
             }
         }
 
-        long nextWait = SYNC_MANAGER_HEARTBEAT_TIME;
+        long nextWait = EXCHANGE_SERVICE_HEARTBEAT_TIME;
         long now = System.currentTimeMillis();
 
         // Start up threads that need it; use a query which finds eas mailboxes where the
@@ -2128,10 +2158,7 @@
                         Mailbox m = EmailContent.getContent(c, Mailbox.class);
                         requestSync(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) {
+                        if (hasSendableMessages(c)) {
                             Mailbox m = EmailContent.getContent(c, Mailbox.class);
                             startServiceThread(new EasOutboxService(this, m), m);
                         }
@@ -2200,17 +2227,26 @@
         serviceRequest(mailboxId, 5*SECONDS, reason);
     }
 
-    static public void serviceRequest(long mailboxId, long ms, int reason) {
-        SyncManager syncManager = INSTANCE;
-        if (syncManager == null) return;
-        Mailbox m = Mailbox.restoreMailboxWithId(syncManager, mailboxId);
-        // Never allow manual start of Drafts or Outbox via serviceRequest
-        if (m == null || m.mType == Mailbox.TYPE_DRAFTS || m.mType == Mailbox.TYPE_OUTBOX) {
-            log("Ignoring serviceRequest for drafts/outbox/null mailbox");
-            return;
+    /**
+     * Return a boolean indicating whether the mailbox can be synced
+     * @param m the mailbox
+     * @return whether or not the mailbox can be synced
+     */
+    static /*package*/ boolean isSyncable(Mailbox m) {
+        if (m == null || m.mType == Mailbox.TYPE_DRAFTS || m.mType == Mailbox.TYPE_OUTBOX ||
+                m.mType >= Mailbox.TYPE_NOT_SYNCABLE) {
+            return false;
         }
+        return true;
+    }
+
+    static public void serviceRequest(long mailboxId, long ms, int reason) {
+        ExchangeService exchangeService = INSTANCE;
+        if (exchangeService == null) return;
+        Mailbox m = Mailbox.restoreMailboxWithId(exchangeService, mailboxId);
+        if (!isSyncable(m)) return;
         try {
-            AbstractSyncService service = syncManager.mServiceMap.get(mailboxId);
+            AbstractSyncService service = exchangeService.mServiceMap.get(mailboxId);
             if (service != null) {
                 service.mRequestTime = System.currentTimeMillis() + ms;
                 kick("service request");
@@ -2223,14 +2259,14 @@
     }
 
     static public void serviceRequestImmediate(long mailboxId) {
-        SyncManager syncManager = INSTANCE;
-        if (syncManager == null) return;
-        AbstractSyncService service = syncManager.mServiceMap.get(mailboxId);
+        ExchangeService exchangeService = INSTANCE;
+        if (exchangeService == null) return;
+        AbstractSyncService service = exchangeService.mServiceMap.get(mailboxId);
         if (service != null) {
             service.mRequestTime = System.currentTimeMillis();
-            Mailbox m = Mailbox.restoreMailboxWithId(syncManager, mailboxId);
+            Mailbox m = Mailbox.restoreMailboxWithId(exchangeService, mailboxId);
             if (m != null) {
-                service.mAccount = Account.restoreAccountWithId(syncManager, m.mAccountKey);
+                service.mAccount = Account.restoreAccountWithId(exchangeService, m.mAccountKey);
                 service.mMailbox = m;
                 kick("service request immediate");
             }
@@ -2238,17 +2274,17 @@
     }
 
     static public void sendMessageRequest(Request req) {
-        SyncManager syncManager = INSTANCE;
-        if (syncManager == null) return;
-        Message msg = Message.restoreMessageWithId(syncManager, req.mMessageId);
+        ExchangeService exchangeService = INSTANCE;
+        if (exchangeService == null) return;
+        Message msg = Message.restoreMessageWithId(exchangeService, req.mMessageId);
         if (msg == null) {
             return;
         }
         long mailboxId = msg.mMailboxKey;
-        AbstractSyncService service = syncManager.mServiceMap.get(mailboxId);
+        AbstractSyncService service = exchangeService.mServiceMap.get(mailboxId);
 
         if (service == null) {
-            service = startManualSync(mailboxId, SYNC_SERVICE_PART_REQUEST, req);
+            startManualSync(mailboxId, SYNC_SERVICE_PART_REQUEST, req);
             kick("part request");
         } else {
             service.addRequest(req);
@@ -2263,14 +2299,14 @@
      * @return whether or not the Mailbox is available for syncing (i.e. is a valid push target)
      */
     static public int pingStatus(long mailboxId) {
-        SyncManager syncManager = INSTANCE;
-        if (syncManager == null) return PING_STATUS_OK;
+        ExchangeService exchangeService = INSTANCE;
+        if (exchangeService == null) return PING_STATUS_OK;
         // Already syncing...
-        if (syncManager.mServiceMap.get(mailboxId) != null) {
+        if (exchangeService.mServiceMap.get(mailboxId) != null) {
             return PING_STATUS_RUNNING;
         }
         // No errors or a transient error, don't ping...
-        SyncError error = syncManager.mSyncErrorMap.get(mailboxId);
+        SyncError error = exchangeService.mSyncErrorMap.get(mailboxId);
         if (error != null) {
             if (error.fatal) {
                 return PING_STATUS_UNABLE;
@@ -2281,47 +2317,52 @@
         return PING_STATUS_OK;
     }
 
-    static public AbstractSyncService startManualSync(long mailboxId, int reason, Request req) {
-        SyncManager syncManager = INSTANCE;
-        if (syncManager == null) return null;
+    static public void startManualSync(long mailboxId, int reason, Request req) {
+        ExchangeService exchangeService = INSTANCE;
+        if (exchangeService == null) return;
         synchronized (sSyncLock) {
-            if (syncManager.mServiceMap.get(mailboxId) == null) {
-                syncManager.mSyncErrorMap.remove(mailboxId);
-                Mailbox m = Mailbox.restoreMailboxWithId(syncManager, mailboxId);
+            AbstractSyncService svc = exchangeService.mServiceMap.get(mailboxId);
+            if (svc == null) {
+                exchangeService.mSyncErrorMap.remove(mailboxId);
+                Mailbox m = Mailbox.restoreMailboxWithId(exchangeService, mailboxId);
                 if (m != null) {
                     log("Starting sync for " + m.mDisplayName);
-                    syncManager.requestSync(m, reason, req);
+                    exchangeService.requestSync(m, reason, req);
+                }
+            } else {
+                // If this is a ui request, set the sync reason for the service
+                if (reason >= SYNC_CALLBACK_START) {
+                    svc.mSyncReason = reason;
                 }
             }
         }
-        return syncManager.mServiceMap.get(mailboxId);
     }
 
     // DO NOT CALL THIS IN A LOOP ON THE SERVICEMAP
     static private void stopManualSync(long mailboxId) {
-        SyncManager syncManager = INSTANCE;
-        if (syncManager == null) return;
+        ExchangeService exchangeService = INSTANCE;
+        if (exchangeService == null) return;
         synchronized (sSyncLock) {
-            AbstractSyncService svc = syncManager.mServiceMap.get(mailboxId);
+            AbstractSyncService svc = exchangeService.mServiceMap.get(mailboxId);
             if (svc != null) {
                 log("Stopping sync for " + svc.mMailboxName);
                 svc.stop();
                 svc.mThread.interrupt();
-                syncManager.releaseWakeLock(mailboxId);
+                exchangeService.releaseWakeLock(mailboxId);
             }
         }
     }
 
     /**
-     * Wake up SyncManager to check for mailboxes needing service
+     * Wake up ExchangeService to check for mailboxes needing service
      */
     static public void kick(String reason) {
-       SyncManager syncManager = INSTANCE;
-       if (syncManager != null) {
-            synchronized (syncManager) {
+       ExchangeService exchangeService = INSTANCE;
+       if (exchangeService != null) {
+            synchronized (exchangeService) {
                 //INSTANCE.log("Kick: " + reason);
-                syncManager.mKicked = true;
-                syncManager.notify();
+                exchangeService.mKicked = true;
+                exchangeService.notify();
             }
         }
         if (sConnectivityLock != null) {
@@ -2332,26 +2373,26 @@
     }
 
     static public void accountUpdated(long acctId) {
-        SyncManager syncManager = INSTANCE;
-        if (syncManager == null) return;
+        ExchangeService exchangeService = INSTANCE;
+        if (exchangeService == null) return;
         synchronized (sSyncLock) {
-            for (AbstractSyncService svc : syncManager.mServiceMap.values()) {
+            for (AbstractSyncService svc : exchangeService.mServiceMap.values()) {
                 if (svc.mAccount.mId == acctId) {
-                    svc.mAccount = Account.restoreAccountWithId(syncManager, acctId);
+                    svc.mAccount = Account.restoreAccountWithId(exchangeService, acctId);
                 }
             }
         }
     }
 
     /**
-     * Tell SyncManager to remove the mailbox from the map of mailboxes with sync errors
+     * Tell ExchangeService to remove the mailbox from the map of mailboxes with sync errors
      * @param mailboxId the id of the mailbox
      */
     static public void removeFromSyncErrorMap(long mailboxId) {
-        SyncManager syncManager = INSTANCE;
-        if (syncManager == null) return;
+        ExchangeService exchangeService = INSTANCE;
+        if (exchangeService == null) return;
         synchronized(sSyncLock) {
-            syncManager.mSyncErrorMap.remove(mailboxId);
+            exchangeService.mSyncErrorMap.remove(mailboxId);
         }
     }
 
@@ -2362,42 +2403,57 @@
      * @param svc the service that is finished
      */
     static public void done(AbstractSyncService svc) {
-        SyncManager syncManager = INSTANCE;
-        if (syncManager == null) return;
+        ExchangeService exchangeService = INSTANCE;
+        if (exchangeService == null) return;
         synchronized(sSyncLock) {
             long mailboxId = svc.mMailboxId;
-            HashMap<Long, SyncError> errorMap = syncManager.mSyncErrorMap;
+            HashMap<Long, SyncError> errorMap = exchangeService.mSyncErrorMap;
             SyncError syncError = errorMap.get(mailboxId);
-            syncManager.releaseMailbox(mailboxId);
+            exchangeService.releaseMailbox(mailboxId);
             int exitStatus = svc.mExitStatus;
+            Mailbox m = Mailbox.restoreMailboxWithId(exchangeService, mailboxId);
+            if (m == null) return;
+
+            if (exitStatus != AbstractSyncService.EXIT_LOGIN_FAILURE) {
+                long accountId = m.mAccountKey;
+                Account account = Account.restoreAccountWithId(exchangeService, accountId);
+                if (account == null) return;
+                if (exchangeService.releaseSyncHolds(exchangeService,
+                        AbstractSyncService.EXIT_LOGIN_FAILURE, account)) {
+                    NotificationController.getInstance(exchangeService)
+                        .cancelLoginFailedNotification(accountId);
+                }
+            }
+
             switch (exitStatus) {
                 case AbstractSyncService.EXIT_DONE:
-                    if (!svc.mRequests.isEmpty()) {
+                    if (svc.hasPendingRequests()) {
                         // TODO Handle this case
                     }
                     errorMap.remove(mailboxId);
                     // If we've had a successful sync, clear the shutdown count
-                    synchronized (SyncManager.class) {
+                    synchronized (ExchangeService.class) {
                         sClientConnectionManagerShutdownCount = 0;
                     }
                     break;
                 // I/O errors get retried at increasing intervals
                 case AbstractSyncService.EXIT_IO_ERROR:
-                    Mailbox m = Mailbox.restoreMailboxWithId(syncManager, mailboxId);
-                    if (m == null) return;
                     if (syncError != null) {
                         syncError.escalate();
                         log(m.mDisplayName + " held for " + syncError.holdDelay + "ms");
                     } else {
-                        errorMap.put(mailboxId, syncManager.new SyncError(exitStatus, false));
+                        errorMap.put(mailboxId, exchangeService.new SyncError(exitStatus, false));
                         log(m.mDisplayName + " added to syncErrorMap, hold for 15s");
                     }
                     break;
                 // These errors are not retried automatically
-                case AbstractSyncService.EXIT_SECURITY_FAILURE:
                 case AbstractSyncService.EXIT_LOGIN_FAILURE:
+                    NotificationController.getInstance(exchangeService)
+                        .showLoginFailedNotification(m.mAccountKey);
+                    // Fall through
+                case AbstractSyncService.EXIT_SECURITY_FAILURE:
                 case AbstractSyncService.EXIT_EXCEPTION:
-                    errorMap.put(mailboxId, syncManager.new SyncError(exitStatus, true));
+                    errorMap.put(mailboxId, exchangeService.new SyncError(exitStatus, true));
                     break;
             }
             kick("sync completed");
diff --git a/src/com/android/exchange/MailboxAlarmReceiver.java b/src/com/android/exchange/MailboxAlarmReceiver.java
index 565b158..7958c03 100644
--- a/src/com/android/exchange/MailboxAlarmReceiver.java
+++ b/src/com/android/exchange/MailboxAlarmReceiver.java
@@ -22,20 +22,19 @@
 import android.content.Intent;
 
 /**
- * MailboxAlarmReceiver is used to "wake up" the SyncManager at the appropriate time(s).  It may
+ * MailboxAlarmReceiver is used to "wake up" the ExchangeService 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 mailboxId = intent.getLongExtra("mailbox", SyncManager.SYNC_MANAGER_ID);
-        // SYNC_MANAGER_SERVICE_ID tells us that the service is asking to be started
-        if (mailboxId == SyncManager.SYNC_MANAGER_SERVICE_ID) {
-            context.startService(new Intent(context, SyncManager.class));
+        long mailboxId = intent.getLongExtra("mailbox", ExchangeService.EXTRA_MAILBOX_ID);
+        // EXCHANGE_SERVICE_MAILBOX_ID tells us that the service is asking to be started
+        if (mailboxId == ExchangeService.EXCHANGE_SERVICE_MAILBOX_ID) {
+            context.startService(new Intent(context, ExchangeService.class));
         } else {
-            SyncManager.log("Alarm received for: " + SyncManager.alarmOwner(mailboxId));
-            SyncManager.alert(context, mailboxId);
+            ExchangeService.alert(context, mailboxId);
         }
     }
 }
diff --git a/src/com/android/exchange/MessageMoveRequest.java b/src/com/android/exchange/MessageMoveRequest.java
new file mode 100644
index 0000000..4f6a5d7
--- /dev/null
+++ b/src/com/android/exchange/MessageMoveRequest.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.exchange;
+
+/**
+ * MessageMoveRequest is the EAS wrapper for requesting a "move to folder"
+ */
+public class MessageMoveRequest extends Request {
+    public final long mMailboxId;
+
+    public MessageMoveRequest(long messageId, long mailboxId) {
+        mMessageId = messageId;
+        mMailboxId = mailboxId;
+    }
+}
diff --git a/src/com/android/exchange/adapter/AbstractSyncAdapter.java b/src/com/android/exchange/adapter/AbstractSyncAdapter.java
index 6473000..14cca36 100644
--- a/src/com/android/exchange/adapter/AbstractSyncAdapter.java
+++ b/src/com/android/exchange/adapter/AbstractSyncAdapter.java
@@ -20,8 +20,10 @@
 import com.android.email.Email;
 import com.android.email.provider.EmailContent.Account;
 import com.android.email.provider.EmailContent.Mailbox;
+import com.android.exchange.Eas;
 import com.android.exchange.EasSyncService;
 
+import android.content.ContentResolver;
 import android.content.Context;
 
 import java.io.IOException;
@@ -39,10 +41,13 @@
     public static final int DAYS = HOURS*24;
     public static final int WEEKS = DAYS*7;
 
+    protected static final String PIM_WINDOW_SIZE = "4";
+
     public Mailbox mMailbox;
     public EasSyncService mService;
     public Context mContext;
     public Account mAccount;
+    public final ContentResolver mContentResolver;
     public final android.accounts.Account mAccountManagerAccount;
 
     // Create the data for local changes that need to be sent up to the server
@@ -56,18 +61,26 @@
     public abstract String getCollectionName();
     public abstract void cleanup();
     public abstract boolean isSyncable();
+    // Add sync options (filter, body type - html vs plain, and truncation)
+    public abstract void sendSyncOptions(Double protocolVersion, Serializer s)
+        throws IOException;
+    /**
+     * Delete all records of this class in this account
+     */
+    public abstract void wipe();
 
     public boolean isLooping() {
         return false;
     }
 
-    public AbstractSyncAdapter(Mailbox mailbox, EasSyncService service) {
-        mMailbox = mailbox;
+    public AbstractSyncAdapter(EasSyncService service) {
         mService = service;
+        mMailbox = service.mMailbox;
         mContext = service.mContext;
         mAccount = service.mAccount;
         mAccountManagerAccount = new android.accounts.Account(mAccount.mEmailAddress,
                 Email.EXCHANGE_ACCOUNT_MANAGER_TYPE);
+        mContentResolver = mContext.getContentResolver();
     }
 
     public void userLog(String ...strings) {
@@ -79,6 +92,36 @@
     }
 
     /**
+     * Set sync options common to PIM's (contacts and calendar)
+     * @param protocolVersion the protocol version under which we're syncing
+     * @param the filter to use (or null)
+     * @param s the Serializer
+     * @throws IOException
+     */
+    protected void setPimSyncOptions(Double protocolVersion, String filter, Serializer s)
+            throws IOException {
+        s.tag(Tags.SYNC_DELETES_AS_MOVES);
+        s.tag(Tags.SYNC_GET_CHANGES);
+        s.data(Tags.SYNC_WINDOW_SIZE, PIM_WINDOW_SIZE);
+        s.start(Tags.SYNC_OPTIONS);
+        // Set the filter (lookback), if provided
+        if (filter != null) {
+            s.data(Tags.SYNC_FILTER_TYPE, filter);
+        }
+        // Set the truncation amount and body type
+        if (protocolVersion >= Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE) {
+            s.start(Tags.BASE_BODY_PREFERENCE);
+            // Plain text
+            s.data(Tags.BASE_TYPE, Eas.BODY_PREFERENCE_TEXT);
+            s.data(Tags.BASE_TRUNCATION_SIZE, Eas.EAS12_TRUNCATION_SIZE);
+            s.end();
+        } else {
+            s.data(Tags.SYNC_TRUNCATION, Eas.EAS2_5_TRUNCATION_SIZE);
+        }
+        s.end();
+    }
+
+    /**
      * Returns the current SyncKey; override if the SyncKey is stored elsewhere (as for Contacts)
      * @return the current SyncKey for the Mailbox
      * @throws IOException
diff --git a/src/com/android/exchange/adapter/AbstractSyncParser.java b/src/com/android/exchange/adapter/AbstractSyncParser.java
index af17b79..250767b 100644
--- a/src/com/android/exchange/adapter/AbstractSyncParser.java
+++ b/src/com/android/exchange/adapter/AbstractSyncParser.java
@@ -21,7 +21,7 @@
 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 com.android.exchange.ExchangeService;
 
 import android.content.ContentResolver;
 import android.content.ContentValues;
@@ -75,11 +75,6 @@
      */
     public abstract void commit() throws IOException;
 
-    /**
-     * Delete all records of this class in this account
-     */
-    public abstract void wipe();
-
     public boolean isLooping() {
         return mLooping;
     }
@@ -121,13 +116,13 @@
                         // TODO Make frequency conditional on user settings!
                         mMailbox.mSyncInterval = Mailbox.CHECK_INTERVAL_PUSH;
                         mService.errorLog("Bad sync key; RESET and delete data");
-                        wipe();
+                        mAdapter.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);
+                        ExchangeService.reloadFolderList(mContext, mAccount.mId, true);
                     }
                     // TODO Look at other error codes and consider what's to be done
                 }
@@ -192,11 +187,13 @@
 
         if (abortSyncs) {
             userLog("Aborting account syncs due to mailbox change to ping...");
-            SyncManager.stopAccountSyncs(mAccount.mId);
+            ExchangeService.stopAccountSyncs(mAccount.mId);
         }
 
         // Let the caller know that there's more to do
-        userLog("Returning moreAvailable = " + moreAvailable);
+        if (moreAvailable) {
+            userLog("MoreAvailable");
+        }
         return moreAvailable;
     }
 
diff --git a/src/com/android/exchange/adapter/AccountSyncAdapter.java b/src/com/android/exchange/adapter/AccountSyncAdapter.java
index ed85dd7..cf54ed5 100644
--- a/src/com/android/exchange/adapter/AccountSyncAdapter.java
+++ b/src/com/android/exchange/adapter/AccountSyncAdapter.java
@@ -1,6 +1,5 @@
 package com.android.exchange.adapter;
 
-import com.android.email.provider.EmailContent.Mailbox;
 import com.android.exchange.EasSyncService;
 
 import java.io.IOException;
@@ -8,8 +7,8 @@
 
 public class AccountSyncAdapter extends AbstractSyncAdapter {
 
-    public AccountSyncAdapter(Mailbox mailbox, EasSyncService service) {
-        super(mailbox, service);
+    public AccountSyncAdapter(EasSyncService service) {
+        super(service);
      }
 
     @Override
@@ -17,6 +16,10 @@
     }
 
     @Override
+    public void wipe() {
+    }
+
+    @Override
     public String getCollectionName() {
         return null;
     }
@@ -35,4 +38,8 @@
     public boolean isSyncable() {
         return true;
     }
+
+    @Override
+    public void sendSyncOptions(Double protocolVersion, Serializer s) throws IOException {
+    }
 }
diff --git a/src/com/android/exchange/adapter/CalendarSyncAdapter.java b/src/com/android/exchange/adapter/CalendarSyncAdapter.java
index 317f225..197cdcf 100644
--- a/src/com/android/exchange/adapter/CalendarSyncAdapter.java
+++ b/src/com/android/exchange/adapter/CalendarSyncAdapter.java
@@ -20,7 +20,6 @@
 import com.android.email.Email;
 import com.android.email.Utility;
 import com.android.email.provider.EmailContent;
-import com.android.email.provider.EmailContent.Mailbox;
 import com.android.email.provider.EmailContent.Message;
 import com.android.exchange.Eas;
 import com.android.exchange.EasOutboxService;
@@ -35,14 +34,13 @@
 import android.content.ContentUris;
 import android.content.ContentValues;
 import android.content.Entity;
+import android.content.Entity.NamedContentValues;
 import android.content.EntityIterator;
 import android.content.OperationApplicationException;
-import android.content.Entity.NamedContentValues;
 import android.database.Cursor;
 import android.net.Uri;
 import android.os.RemoteException;
 import android.provider.Calendar;
-import android.provider.SyncStateContract;
 import android.provider.Calendar.Attendees;
 import android.provider.Calendar.Calendars;
 import android.provider.Calendar.Events;
@@ -51,6 +49,7 @@
 import android.provider.Calendar.Reminders;
 import android.provider.Calendar.SyncState;
 import android.provider.ContactsContract.RawContacts;
+import android.provider.SyncStateContract;
 import android.text.TextUtils;
 import android.util.Log;
 
@@ -59,10 +58,10 @@
 import java.text.ParseException;
 import java.util.ArrayList;
 import java.util.GregorianCalendar;
+import java.util.Map.Entry;
 import java.util.StringTokenizer;
 import java.util.TimeZone;
 import java.util.UUID;
-import java.util.Map.Entry;
 
 /**
  * Sync adapter class for EAS calendars
@@ -154,15 +153,15 @@
     private long mCalendarId = -1;
     private String mCalendarIdString;
     private String[] mCalendarIdArgument;
-    private String mEmailAddress;
+    /*package*/ String mEmailAddress;
 
     private ArrayList<Long> mDeletedIdList = new ArrayList<Long>();
     private ArrayList<Long> mUploadedIdList = new ArrayList<Long>();
     private ArrayList<Long> mSendCancelIdList = new ArrayList<Long>();
     private ArrayList<Message> mOutgoingMailList = new ArrayList<Message>();
 
-    public CalendarSyncAdapter(Mailbox mailbox, EasSyncService service) {
-        super(mailbox, service);
+    public CalendarSyncAdapter(EasSyncService service) {
+        super(service);
         mEmailAddress = mAccount.mEmailAddress;
         Cursor c = mService.mContentResolver.query(Calendars.CONTENT_URI,
                 new String[] {Calendars._ID}, CALENDAR_SELECTION,
@@ -191,6 +190,18 @@
     }
 
     @Override
+    public void wipe() {
+        // Delete the calendar associated with this account
+        mContentResolver.delete(Calendars.CONTENT_URI, CALENDAR_SELECTION,
+                new String[] {mEmailAddress, Email.EXCHANGE_ACCOUNT_MANAGER_TYPE});
+    }
+
+    @Override
+    public void sendSyncOptions(Double protocolVersion, Serializer s) throws IOException  {
+        setPimSyncOptions(protocolVersion, Eas.FILTER_2_WEEKS, s);
+    }
+
+    @Override
     public boolean isSyncable() {
         return ContentResolver.getSyncAutomatically(mAccountManagerAccount, Calendar.AUTHORITY);
     }
@@ -279,14 +290,6 @@
             mAccountUri = Events.CONTENT_URI;
         }
 
-        @Override
-        public void wipe() {
-            // Delete the calendar associated with this account
-            // TODO Make sure the Events, etc. are also deleted
-            mContentResolver.delete(Calendars.CONTENT_URI, CALENDAR_SELECTION,
-                    new String[] {mEmailAddress, Email.EXCHANGE_ACCOUNT_MANAGER_TYPE});
-        }
-
         private void addOrganizerToAttendees(CalendarOperations ops, long eventId,
                 String organizerName, String organizerEmail) {
             // Handle the organizer (who IS an attendee on device, but NOT in EAS)
@@ -410,11 +413,11 @@
                     Cursor c = getServerIdCursor(serverId);
                     long id = -1;
                     try {
-                        if (c.moveToFirst()) {
+                        if (c != null && c.moveToFirst()) {
                             id = c.getLong(0);
                         }
                     } finally {
-                        c.close();
+                        if (c != null) c.close();
                     }
                     if (id > 0) {
                         // DTSTAMP can come first, and we simply need to track it
@@ -499,7 +502,7 @@
                         cv.put(Events.EVENT_LOCATION, getValue());
                         break;
                     case Tags.CALENDAR_RECURRENCE:
-                        String rrule = recurrenceParser(ops);
+                        String rrule = recurrenceParser();
                         if (rrule != null) {
                             cv.put(Events.RRULE, rrule);
                         }
@@ -723,7 +726,7 @@
             return true;
         }
 
-        private String recurrenceParser(CalendarOperations ops) throws IOException {
+        public String recurrenceParser() throws IOException {
             // Turn this information into an RRULE
             int type = -1;
             int occurrences = -1;
@@ -830,7 +833,7 @@
                         cv.put(Events.EVENT_LOCATION, getValue());
                         break;
                     case Tags.CALENDAR_RECURRENCE:
-                        String rrule = recurrenceParser(ops);
+                        String rrule = recurrenceParser();
                         if (rrule != null) {
                             cv.put(Events.RRULE, rrule);
                         }
@@ -1264,7 +1267,7 @@
         }
     }
 
-    private class CalendarOperations extends ArrayList<ContentProviderOperation> {
+    protected class CalendarOperations extends ArrayList<ContentProviderOperation> {
         private static final long serialVersionUID = 1L;
         public int mCount = 0;
         private ContentProviderResult[] mResults = null;
@@ -1813,7 +1816,7 @@
                         cr.update(ContentUris.withAppendedId(EVENTS_URI, eventId), cidValues,
                                 null, null);
                     } else {
-                        if (entityValues.getAsInteger(Events.DELETED) == 1) {
+                        if (entityValues.getAsInteger(Calendar.EventsColumns.DELETED) == 1) {
                             userLog("Deleting event with serverId: ", serverId);
                             s.start(Tags.SYNC_DELETE).data(Tags.SYNC_SERVER_ID, serverId).end();
                             mDeletedIdList.add(eventId);
@@ -1879,7 +1882,7 @@
                                     exEntity.addSubValue(ncv.uri, ncv.values);
                                 }
 
-                                if ((getInt(exValues, Events.DELETED) == 1) ||
+                                if ((getInt(exValues, Calendar.EventsColumns.DELETED) == 1) ||
                                         (getInt(exValues, Events.STATUS) ==
                                             Events.STATUS_CANCELED)) {
                                     flag = Message.FLAG_OUTGOING_MEETING_CANCEL;
diff --git a/src/com/android/exchange/adapter/ContactsSyncAdapter.java b/src/com/android/exchange/adapter/ContactsSyncAdapter.java
index 8b4adba..cc65a26 100644
--- a/src/com/android/exchange/adapter/ContactsSyncAdapter.java
+++ b/src/com/android/exchange/adapter/ContactsSyncAdapter.java
@@ -17,32 +17,24 @@
 
 package com.android.exchange.adapter;
 
-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.ContentProviderOperation.Builder;
 import android.content.ContentProviderResult;
 import android.content.ContentResolver;
 import android.content.ContentUris;
 import android.content.ContentValues;
 import android.content.Entity;
+import android.content.Entity.NamedContentValues;
 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.RawContactsEntity;
-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;
@@ -56,6 +48,14 @@
 import android.provider.ContactsContract.CommonDataKinds.StructuredName;
 import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
 import android.provider.ContactsContract.CommonDataKinds.Website;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.Groups;
+import android.provider.ContactsContract.RawContacts;
+import android.provider.ContactsContract.RawContactsEntity;
+import android.provider.ContactsContract.Settings;
+import android.provider.ContactsContract.SyncState;
+import android.provider.SyncStateContract;
+import android.text.TextUtils;
 import android.text.util.Rfc822Token;
 import android.text.util.Rfc822Tokenizer;
 import android.util.Base64;
@@ -75,7 +75,10 @@
     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 String[] GROUP_TITLE_PROJECTION = new String[] {Groups.TITLE};
+    private static final String MIMETYPE_GROUP_MEMBERSHIP_AND_ID_EQUALS = Data.MIMETYPE + "='" +
+        GroupMembership.CONTENT_ITEM_TYPE + "' AND " + GroupMembership.GROUP_ROW_ID + "=?";
+    private static final String[] GROUPS_ID_PROJECTION = new String[] {Groups._ID};
 
     private static final ArrayList<NamedContentValues> EMPTY_ARRAY_NAMEDCONTENTVALUES
         = new ArrayList<NamedContentValues>();
@@ -123,10 +126,14 @@
     ArrayList<Long> mDeletedIdList = new ArrayList<Long>();
     ArrayList<Long> mUpdatedIdList = new ArrayList<Long>();
 
+    private final Uri mAccountUri;
+    private final ContentResolver mContentResolver;
     private boolean mGroupsUsed = false;
 
-    public ContactsSyncAdapter(Mailbox mailbox, EasSyncService service) {
-        super(mailbox, service);
+    public ContactsSyncAdapter(EasSyncService service) {
+        super(service);
+        mAccountUri = uriWithAccountAndIsSyncAdapter(RawContacts.CONTENT_URI);
+        mContentResolver = mContext.getContentResolver();
     }
 
     static Uri addCallerIsSyncAdapterParameter(Uri uri) {
@@ -136,6 +143,11 @@
     }
 
     @Override
+    public void sendSyncOptions(Double protocolVersion, Serializer s) throws IOException  {
+        setPimSyncOptions(protocolVersion, null, s);
+    }
+
+    @Override
     public boolean isSyncable() {
         return ContentResolver.getSyncAutomatically(
                 mAccountManagerAccount, ContactsContract.AUTHORITY);
@@ -147,6 +159,12 @@
         return p.parse();
     }
 
+
+    @Override
+    public void wipe() {
+        mContentResolver.delete(mAccountUri, null, null);
+    }
+
     interface UntypedRow {
         public void addValues(RowBuilder builder);
         public boolean isSameAs(int type, String value);
@@ -325,18 +343,11 @@
 
         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)
@@ -943,7 +954,7 @@
         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)];
+            new int[Integer.parseInt(AbstractSyncAdapter.PIM_WINDOW_SIZE)];
         private int mContactIndexCount = 0;
         private ContentProviderResult[] mResults = null;
 
@@ -1071,8 +1082,8 @@
 
             // 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);
+            for (NamedContentValues values : result) {
+                list.remove(values);
             }
 
             // Return the row found (or null)
@@ -1542,21 +1553,11 @@
 
     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));
-        }
+        sendStringData(s, cv, StructuredPostal.CITY, fieldNames[0]);
+        sendStringData(s, cv, StructuredPostal.COUNTRY, fieldNames[1]);
+        sendStringData(s, cv, StructuredPostal.POSTCODE, fieldNames[2]);
+        sendStringData(s, cv, StructuredPostal.REGION, fieldNames[3]);
+        sendStringData(s, cv, StructuredPostal.STREET, fieldNames[4]);
     }
 
     private void sendStructuredPostal(Serializer s, ContentValues cv) throws IOException {
@@ -1575,63 +1576,47 @@
         }
     }
 
+    private void sendStringData(Serializer s, ContentValues cv, String column, int tag)
+            throws IOException {
+        if (cv.containsKey(column)) {
+            String value = cv.getAsString(column);
+            if (!TextUtils.isEmpty(value)) {
+                s.data(tag, value);
+            }
+        }
+    }
+
     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));
-        }
+        sendStringData(s, cv, StructuredName.FAMILY_NAME, Tags.CONTACTS_LAST_NAME);
+        sendStringData(s, cv, StructuredName.GIVEN_NAME, Tags.CONTACTS_FIRST_NAME);
+        sendStringData(s, cv, StructuredName.MIDDLE_NAME, Tags.CONTACTS_MIDDLE_NAME);
+        sendStringData(s, cv, StructuredName.SUFFIX, Tags.CONTACTS_SUFFIX);
+        sendStringData(s, cv, StructuredName.PHONETIC_GIVEN_NAME, Tags.CONTACTS_YOMI_FIRST_NAME);
+        sendStringData(s, cv, StructuredName.PHONETIC_FAMILY_NAME, Tags.CONTACTS_YOMI_LAST_NAME);
+        sendStringData(s, cv, StructuredName.PREFIX, Tags.CONTACTS_TITLE);
         if (cv.containsKey(StructuredName.DISPLAY_NAME)) {
             displayName = cv.getAsString(StructuredName.DISPLAY_NAME);
-            s.data(Tags.CONTACTS_FILE_AS, displayName);
+            if (!TextUtils.isEmpty(displayName)) {
+                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));
-        }
+        sendStringData(s, cv, EasBusiness.ACCOUNT_NAME, Tags.CONTACTS2_ACCOUNT_NAME);
+        sendStringData(s, cv, EasBusiness.CUSTOMER_ID, Tags.CONTACTS2_CUSTOMER_ID);
+        sendStringData(s, cv, EasBusiness.GOVERNMENT_ID, Tags.CONTACTS2_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));
-        }
+        sendStringData(s, cv, EasPersonal.ANNIVERSARY, Tags.CONTACTS_ANNIVERSARY);
+        sendStringData(s, cv, EasPersonal.FILE_AS, Tags.CONTACTS_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));
-        }
+        sendStringData(s, cv, Event.START_DATE, Tags.CONTACTS_BIRTHDAY);
     }
 
     private void sendPhoto(Serializer s, ContentValues cv) throws IOException {
@@ -1646,30 +1631,18 @@
     }
 
     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));
-        }
+        sendStringData(s, cv, Organization.TITLE, Tags.CONTACTS_JOB_TITLE);
+        sendStringData(s, cv, Organization.COMPANY, Tags.CONTACTS_COMPANY_NAME);
+        sendStringData(s, cv, Organization.DEPARTMENT, Tags.CONTACTS_DEPARTMENT);
+        sendStringData(s, cv, Organization.OFFICE_LOCATION, Tags.CONTACTS_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));
-        }
+        sendStringData(s, cv, Nickname.NAME, Tags.CONTACTS2_NICKNAME);
     }
 
     private void sendWebpage(Serializer s, ContentValues cv) throws IOException {
-        if (cv.containsKey(Website.URL)) {
-            s.data(Tags.CONTACTS_WEBPAGE, cv.getAsString(Website.URL));
-        }
+        sendStringData(s, cv, Website.URL, Tags.CONTACTS_WEBPAGE);
     }
 
     private void sendNote(Serializer s, ContentValues cv) throws IOException {
@@ -1772,17 +1745,47 @@
         }
     }
 
+    private void dirtyContactsWithinDirtyGroups() {
+        ContentResolver cr = mService.mContentResolver;
+        Cursor c = cr.query(uriWithAccountAndIsSyncAdapter(Groups.CONTENT_URI),
+                GROUPS_ID_PROJECTION, Groups.DIRTY + "=1", null, null);
+        try {
+            if (c.getCount() > 0) {
+                String[] updateArgs = new String[1];
+                ContentValues updateValues = new ContentValues();
+                while (c.moveToNext()) {
+                    // For each, "touch" all data rows with this group id; this will mark contacts
+                    // in this group as dirty (per ContactsContract).  We will then know to upload
+                    // them to the server with the modified group information
+                    long id = c.getLong(0);
+                    updateValues.put(GroupMembership.GROUP_ROW_ID, id);
+                    updateArgs[0] = Long.toString(id);
+                    cr.update(Data.CONTENT_URI, updateValues,
+                            MIMETYPE_GROUP_MEMBERSHIP_AND_ID_EQUALS, updateArgs);
+                }
+                // Really delete groups that are marked deleted
+                cr.delete(uriWithAccountAndIsSyncAdapter(Groups.CONTENT_URI), Groups.DELETED + "=1",
+                        null);
+                // Clear the dirty flag for all of our groups
+                updateValues.clear();
+                updateValues.put(Groups.DIRTY, 0);
+                cr.update(uriWithAccountAndIsSyncAdapter(Groups.CONTENT_URI), updateValues, null,
+                        null);
+            }
+        } finally {
+            c.close();
+        }
+    }
+
     @Override
     public boolean sendLocalChanges(Serializer s) throws IOException {
-        // First, let's find Contacts that have changed.
         ContentResolver cr = mService.mContentResolver;
-        Uri uri = RawContactsEntity.CONTENT_URI.buildUpon()
-                .appendQueryParameter(RawContacts.ACCOUNT_NAME, mAccount.mEmailAddress)
-                .appendQueryParameter(RawContacts.ACCOUNT_TYPE,
-                        com.android.email.Email.EXCHANGE_ACCOUNT_MANAGER_TYPE)
-                .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
-                .build();
 
+        // Find any groups of ours that are dirty and dirty those groups' members
+        dirtyContactsWithinDirtyGroups();
+
+        // First, let's find Contacts that have changed.
+        Uri uri = uriWithAccountAndIsSyncAdapter(RawContactsEntity.CONTENT_URI);
         if (getSyncKey().equals("0")) {
             return false;
         }
@@ -1893,7 +1896,7 @@
                     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);
+                                GROUP_TITLE_PROJECTION, null, null, null);
                         try {
                             // Presumably, this should always succeed, but ...
                             if (c.moveToFirst()) {
diff --git a/src/com/android/exchange/adapter/EmailSyncAdapter.java b/src/com/android/exchange/adapter/EmailSyncAdapter.java
index 4e478b5..d4af3ce 100644
--- a/src/com/android/exchange/adapter/EmailSyncAdapter.java
+++ b/src/com/android/exchange/adapter/EmailSyncAdapter.java
@@ -17,13 +17,17 @@
 
 package com.android.exchange.adapter;
 
+import com.android.email.LegacyConversions;
 import com.android.email.Utility;
 import com.android.email.mail.Address;
 import com.android.email.mail.MeetingInfo;
+import com.android.email.mail.MessagingException;
 import com.android.email.mail.PackedString;
+import com.android.email.mail.Part;
+import com.android.email.mail.internet.MimeMessage;
+import com.android.email.mail.internet.MimeUtility;
 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;
@@ -32,9 +36,11 @@
 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.provider.EmailProvider;
 import com.android.email.service.MailService;
 import com.android.exchange.Eas;
 import com.android.exchange.EasSyncService;
+import com.android.exchange.MessageMoveRequest;
 import com.android.exchange.utility.CalendarUtilities;
 
 import android.content.ContentProviderOperation;
@@ -47,6 +53,7 @@
 import android.os.RemoteException;
 import android.webkit.MimeTypeMap;
 
+import java.io.ByteArrayInputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.util.ArrayList;
@@ -74,26 +81,143 @@
         new String[] { Message.RECORD_ID, MessageColumns.SUBJECT };
 
     private static final String WHERE_BODY_SOURCE_MESSAGE_KEY = Body.SOURCE_MESSAGE_KEY + "=?";
+    private static final String WHERE_MAILBOX_KEY_AND_MOVED =
+        MessageColumns.MAILBOX_KEY + "=? AND (" + MessageColumns.FLAGS + "&" +
+        EasSyncService.MESSAGE_FLAG_MOVED_MESSAGE + ")!=0";
+    private static final String[] FETCH_REQUEST_PROJECTION =
+        new String[] {EmailContent.RECORD_ID, SyncColumns.SERVER_ID};
+    private static final int FETCH_REQUEST_RECORD_ID = 0;
+    private static final int FETCH_REQUEST_SERVER_ID = 1;
+
+    private static final String EMAIL_WINDOW_SIZE = "5";
 
     String[] mBindArguments = new String[2];
     String[] mBindArgument = new String[1];
 
-    ArrayList<Long> mDeletedIdList = new ArrayList<Long>();
-    ArrayList<Long> mUpdatedIdList = new ArrayList<Long>();
+    /*package*/ ArrayList<Long> mDeletedIdList = new ArrayList<Long>();
+    /*package*/ ArrayList<Long> mUpdatedIdList = new ArrayList<Long>();
+    /*package*/ ArrayList<FetchRequest> mFetchRequestList = new ArrayList<FetchRequest>();
+    private boolean mFetchNeeded = false;
 
     // Holds the parser's value for isLooping()
     boolean mIsLooping = false;
 
-    public EmailSyncAdapter(Mailbox mailbox, EasSyncService service) {
-        super(mailbox, service);
+    public EmailSyncAdapter(EasSyncService service) {
+        super(service);
+    }
+
+    @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);
+        mService.clearRequests();
+        // Delete attachments...
+        AttachmentProvider.deleteAllMailboxAttachmentFiles(mContext, mAccount.mId, mMailbox.mId);
+    }
+
+    private String getEmailFilter() {
+        switch (mAccount.mSyncLookback) {
+            case com.android.email.Account.SYNC_WINDOW_1_DAY:
+                return Eas.FILTER_1_DAY;
+            case com.android.email.Account.SYNC_WINDOW_3_DAYS:
+                return Eas.FILTER_3_DAYS;
+            case com.android.email.Account.SYNC_WINDOW_1_WEEK:
+                return Eas.FILTER_1_WEEK;
+            case com.android.email.Account.SYNC_WINDOW_2_WEEKS:
+                return Eas.FILTER_2_WEEKS;
+            case com.android.email.Account.SYNC_WINDOW_1_MONTH:
+                return Eas.FILTER_1_MONTH;
+            case com.android.email.Account.SYNC_WINDOW_ALL:
+                return Eas.FILTER_ALL;
+            default:
+                return Eas.FILTER_1_WEEK;
+        }
+    }
+
+    /**
+     * Holder for fetch request information (record id and server id)
+     */
+    static class FetchRequest {
+        final long messageId;
+        final String serverId;
+
+        FetchRequest(long _messageId, String _serverId) {
+            messageId = _messageId;
+            serverId = _serverId;
+        }
+    }
+
+    @Override
+    public void sendSyncOptions(Double protocolVersion, Serializer s)
+            throws IOException  {
+        mFetchRequestList.clear();
+        // Find partially loaded messages; this should typically be a rare occurrence
+        Cursor c = mContext.getContentResolver().query(Message.CONTENT_URI,
+                FETCH_REQUEST_PROJECTION,
+                MessageColumns.FLAG_LOADED + "=" + Message.FLAG_LOADED_PARTIAL + " AND " +
+                MessageColumns.MAILBOX_KEY + "=?",
+                new String[] {Long.toString(mMailbox.mId)}, null);
+        try {
+            // Put all of these messages into a list; we'll need both id and server id
+            while (c.moveToNext()) {
+                mFetchRequestList.add(new FetchRequest(c.getLong(FETCH_REQUEST_RECORD_ID),
+                        c.getString(FETCH_REQUEST_SERVER_ID)));
+            }
+        } finally {
+            c.close();
+        }
+
+        // The "empty" case is typical; we send a request for changes, and also specify a sync
+        // window, body preference type (HTML for EAS 12.0 and later; MIME for EAS 2.5), and
+        // truncation
+        // If there are fetch requests, we only want the fetches (i.e. no changes from the server)
+        // so we turn MIME support off.  Note that we are always using EAS 2.5 if there are fetch
+        // requests
+        if (mFetchRequestList.isEmpty()) {
+            s.tag(Tags.SYNC_DELETES_AS_MOVES);
+            s.tag(Tags.SYNC_GET_CHANGES);
+            s.data(Tags.SYNC_WINDOW_SIZE, EMAIL_WINDOW_SIZE);
+            s.start(Tags.SYNC_OPTIONS);
+            // Set the lookback appropriately (EAS calls this a "filter")
+            s.data(Tags.SYNC_FILTER_TYPE, getEmailFilter());
+            // Set the truncation amount for all classes
+            if (protocolVersion >= Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE) {
+                s.start(Tags.BASE_BODY_PREFERENCE);
+                // HTML for email
+                s.data(Tags.BASE_TYPE, Eas.BODY_PREFERENCE_HTML);
+                s.data(Tags.BASE_TRUNCATION_SIZE, Eas.EAS12_TRUNCATION_SIZE);
+                s.end();
+            } else {
+                // Use MIME data for EAS 2.5
+                s.data(Tags.SYNC_MIME_SUPPORT, Eas.MIME_BODY_PREFERENCE_MIME);
+                s.data(Tags.SYNC_MIME_TRUNCATION, Eas.EAS2_5_TRUNCATION_SIZE);
+            }
+            s.end();
+        } else {
+            s.start(Tags.SYNC_OPTIONS);
+            // Ask for plain text, rather than MIME data.  This guarantees that we'll get a usable
+            // text body
+            s.data(Tags.SYNC_MIME_SUPPORT, Eas.MIME_BODY_PREFERENCE_TEXT);
+            s.data(Tags.SYNC_TRUNCATION, Eas.EAS2_5_TRUNCATION_SIZE);
+            s.end();
+        }
     }
 
     @Override
     public boolean parse(InputStream is) throws IOException {
         EasEmailSyncParser p = new EasEmailSyncParser(is, this);
+        mFetchNeeded = false;
         boolean res = p.parse();
         // Hold on to the parser's value for isLooping() to pass back to the service
         mIsLooping = p.isLooping();
+        // If we've need a body fetch, or we've just finished one, return true in order to continue
+        if (mFetchNeeded || !mFetchRequestList.isEmpty()) {
+            return true;
+        }
         return res;
     }
 
@@ -118,6 +242,7 @@
         private String mMailboxIdAsString;
 
         ArrayList<Message> newEmails = new ArrayList<Message>();
+        ArrayList<Message> fetchedEmails = new ArrayList<Message>();
         ArrayList<Long> deletedEmails = new ArrayList<Long>();
         ArrayList<ServerChange> changedEmails = new ArrayList<ServerChange>();
 
@@ -126,18 +251,9 @@
             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>();
+            boolean truncated = false;
 
             while (nextTag(Tags.SYNC_APPLICATION_DATA) != END) {
                 switch (tag) {
@@ -151,7 +267,7 @@
                     case Tags.EMAIL_FROM:
                         Address[] froms = Address.parse(getValue());
                         if (froms != null && froms.length > 0) {
-                          msg.mDisplayName = froms[0].toFriendly();
+                            msg.mDisplayName = froms[0].toFriendly();
                         }
                         msg.mFrom = Address.pack(froms);
                         break;
@@ -176,6 +292,24 @@
                     case Tags.EMAIL_FLAG:
                         msg.mFlagFavorite = flagParser();
                         break;
+                    case Tags.EMAIL_MIME_TRUNCATED:
+                        truncated = getValueInt() == 1;
+                        break;
+                    case Tags.EMAIL_MIME_DATA:
+                        // We get MIME data for EAS 2.5.  First we parse it, then we take the
+                        // html and/or plain text data and store it in the message
+                        if (truncated) {
+                            // If the MIME data is truncated, don't bother parsing it, because
+                            // it will take time and throw an exception anyway when EOF is reached
+                            // In this case, we will load the body separately by tagging the message
+                            // "partially loaded".
+                            userLog("Partially loaded: ", msg.mServerId);
+                            msg.mFlagLoaded = Message.FLAG_LOADED_PARTIAL;
+                            mFetchNeeded = true;
+                        } else {
+                            mimeBodyParser(msg, getValue());
+                        }
+                        break;
                     case Tags.EMAIL_BODY:
                         String text = getValue();
                         msg.mText = text;
@@ -237,6 +371,9 @@
                     case Tags.EMAIL_RECURRENCES:
                         recurrencesParser();
                         break;
+                    case Tags.EMAIL_RESPONSE_REQUESTED:
+                        packedString.put(MeetingInfo.MEETING_RESPONSE_REQUESTED, getValue());
+                        break;
                     default:
                         skipTag();
                 }
@@ -324,6 +461,34 @@
             }
         }
 
+        /**
+         * Parses untruncated MIME data, saving away the text parts
+         * @param msg the message we're building
+         * @param mimeData the MIME data we've received from the server
+         * @throws IOException
+         */
+        private void mimeBodyParser(Message msg, String mimeData) throws IOException {
+            try {
+                ByteArrayInputStream in = new ByteArrayInputStream(mimeData.getBytes());
+                // The constructor parses the message
+                MimeMessage mimeMessage = new MimeMessage(in);
+                // Now process body parts & attachments
+                ArrayList<Part> viewables = new ArrayList<Part>();
+                // We'll ignore the attachments, as we'll get them directly from EAS
+                ArrayList<Part> attachments = new ArrayList<Part>();
+                MimeUtility.collectParts(mimeMessage, viewables, attachments);
+                Body tempBody = new Body();
+                // updateBodyFields fills in the content fields of the Body
+                LegacyConversions.updateBodyFields(tempBody, msg, viewables);
+                // But we need them in the message itself for handling during commit()
+                msg.mHtml = tempBody.mHtmlContent;
+                msg.mText = tempBody.mTextContent;
+            } catch (MessagingException e) {
+                // This would most likely indicate a broken stream
+                throw new IOException(e);
+            }
+        }
+
         private void attachmentsParser(ArrayList<Attachment> atts, Message msg) throws IOException {
             while (nextTag(Tags.EMAIL_ATTACHMENTS) != END) {
                 switch (tag) {
@@ -369,6 +534,7 @@
                 att.mFileName = fileName;
                 att.mLocation = location;
                 att.mMimeType = getMimeTypeFromFileName(fileName);
+                att.mAccountKey = mService.mAccount.mId;
                 atts.add(att);
                 msg.mFlagAttachment = true;
             }
@@ -519,7 +685,14 @@
         }
 
         @Override
-        public void responsesParser() {
+        public void responsesParser() throws IOException {
+            while (nextTag(Tags.SYNC_RESPONSES) != END) {
+                if (tag == Tags.SYNC_ADD || tag == Tags.SYNC_CHANGE || tag == Tags.SYNC_DELETE) {
+                    // We can ignore all of these
+                } else if (tag == Tags.SYNC_FETCH) {
+                    addParser(fetchedEmails);
+                }
+            }
         }
 
         @Override
@@ -529,17 +702,50 @@
             // 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: fetchedEmails) {
+                // If there's really no text, we're done here
+                if (msg.mText == null) continue;
+                // Find the original message's id (by serverId and mailbox)
+                Cursor c = getServerIdCursor(msg.mServerId, EmailContent.ID_PROJECTION);
+                String id = null;
+                try {
+                    if (c.moveToFirst()) {
+                        id = c.getString(EmailContent.ID_PROJECTION_COLUMN);
+                    }
+                } finally {
+                    c.close();
+                }
+
+                // If we find one, we do two things atomically: 1) set the body text for the
+                // message, and 2) mark the message loaded (i.e. completely loaded)
+                if (id != null) {
+                    userLog("Fetched body successfully for ", id);
+                    mBindArgument[0] = id;
+                    ops.add(ContentProviderOperation.newUpdate(Body.CONTENT_URI)
+                            .withSelection(Body.MESSAGE_KEY + "=?", mBindArgument)
+                            .withValue(Body.TEXT_CONTENT, msg.mText)
+                            .build());
+                    ops.add(ContentProviderOperation.newUpdate(Message.CONTENT_URI)
+                            .withSelection(EmailContent.RECORD_ID + "=?", mBindArgument)
+                            .withValue(Message.FLAG_LOADED, Message.FLAG_LOADED_COMPLETE)
+                            .build());
+                }
+            }
+
             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) {
@@ -610,6 +816,11 @@
             ops.add(ContentProviderOperation.newDelete(
                     ContentUris.withAppendedId(Message.UPDATED_CONTENT_URI, id)).build());
         }
+        // Delete any moved messages (since we've just synced the mailbox, and no longer need the
+        // placeholder message); this prevents duplicates from appearing in the mailbox.
+        mBindArgument[0] = Long.toString(mMailbox.mId);
+        ops.add(ContentProviderOperation.newDelete(Message.CONTENT_URI)
+                .withSelection(WHERE_MAILBOX_KEY_AND_MOVED, mBindArgument).build());
     }
 
     @Override
@@ -732,6 +943,19 @@
         // This code is split out for unit testing purposes
         boolean firstCommand = sendDeletedItems(s, mDeletedIdList, true);
 
+        if (!mFetchRequestList.isEmpty()) {
+            // Add FETCH commands for messages that need a body (i.e. we didn't find it during
+            // our earlier sync; this happens only in EAS 2.5 where the body couldn't be found
+            // after parsing the message's MIME data)
+            if (firstCommand) {
+                s.start(Tags.SYNC_COMMANDS);
+                firstCommand = false;
+            }
+            for (FetchRequest req: mFetchRequestList) {
+                s.start(Tags.SYNC_FETCH).data(Tags.SYNC_SERVER_ID, req.serverId).end();
+            }
+        }
+
         // Find our trash mailbox, since deletions will have been moved there...
         long trashMailboxId =
             Mailbox.findMailboxOfType(mContext, mMailbox.mAccountKey, Mailbox.TYPE_TRASH);
@@ -775,9 +999,22 @@
                     boolean flagChange = false;
                     boolean readChange = false;
 
-                    int flag = 0;
+                    long mailbox = currentCursor.getLong(UPDATES_MAILBOX_KEY_COLUMN);
+                    if (mailbox != c.getLong(Message.LIST_MAILBOX_KEY_COLUMN)) {
+                        // The message has moved to another mailbox; add a request for this
+                        // Note: The Sync command doesn't handle moving messages, so we need
+                        // to handle this as a "request" (similar to meeting response and
+                        // attachment load)
+                        mService.addRequest(new MessageMoveRequest(id, mailbox));
+                        // Regardless of other changes that might be made, we don't want to indicate
+                        // that this message has been updated until the move request has been
+                        // handled (without this, a crash between the flag upsync and the move
+                        // would cause the move to be lost)
+                        mUpdatedIdList.remove(id);
+                    }
 
                     // We can only send flag changes to the server in 12.0 or later
+                    int flag = 0;
                     if (mService.mProtocolVersionDouble >= Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE) {
                         flag = currentCursor.getInt(UPDATES_FLAG_COLUMN);
                         if (flag != c.getInt(Message.LIST_FAVORITE_COLUMN)) {
diff --git a/src/com/android/exchange/adapter/FolderSyncParser.java b/src/com/android/exchange/adapter/FolderSyncParser.java
index 3706b18..7a675d5 100644
--- a/src/com/android/exchange/adapter/FolderSyncParser.java
+++ b/src/com/android/exchange/adapter/FolderSyncParser.java
@@ -17,15 +17,16 @@
 
 package com.android.exchange.adapter;
 
+import com.android.email.Utility;
 import com.android.email.provider.AttachmentProvider;
 import com.android.email.provider.EmailContent;
-import com.android.email.provider.EmailProvider;
 import com.android.email.provider.EmailContent.AccountColumns;
 import com.android.email.provider.EmailContent.Mailbox;
 import com.android.email.provider.EmailContent.MailboxColumns;
+import com.android.email.provider.EmailProvider;
 import com.android.exchange.Eas;
+import com.android.exchange.ExchangeService;
 import com.android.exchange.MockParserStream;
-import com.android.exchange.SyncManager;
 
 import android.content.ContentProviderOperation;
 import android.content.ContentUris;
@@ -38,6 +39,7 @@
 import java.io.InputStream;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.HashMap;
 import java.util.List;
 
 /**
@@ -51,7 +53,7 @@
     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 USER_GENERIC_TYPE = 1;
     public static final int INBOX_TYPE = 2;
     public static final int DRAFTS_TYPE = 3;
     public static final int DELETED_TYPE = 4;
@@ -64,13 +66,15 @@
     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);
+    // EAS types that we are willing to consider valid folders for EAS sync
+    public static final List<Integer> VALID_EAS_FOLDER_TYPES = Arrays.asList(INBOX_TYPE,
+            DRAFTS_TYPE, DELETED_TYPE, SENT_TYPE, OUTBOX_TYPE, USER_MAILBOX_TYPE, CALENDAR_TYPE,
+            CONTACTS_TYPE, USER_GENERIC_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 " +
+    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 +
@@ -114,7 +118,7 @@
                         mContentResolver.delete(Mailbox.CONTENT_URI, ALL_BUT_ACCOUNT_MAILBOX,
                                 new String[] {Long.toString(mAccountId)});
                         // Stop existing syncs and reconstruct _main
-                        SyncManager.stopNonAccountMailboxSyncsForAccount(mAccountId);
+                        ExchangeService.stopNonAccountMailboxSyncsForAccount(mAccountId);
                         res = true;
                         resetFolders = true;
                     } else {
@@ -176,7 +180,7 @@
         }
     }
 
-    public void addParser(ArrayList<ContentProviderOperation> ops) throws IOException {
+    public Mailbox addParser() throws IOException {
         String name = null;
         String serverId = null;
         String parentId = null;
@@ -204,57 +208,100 @@
                     skipTag();
             }
         }
-        if (mValidFolderTypes.contains(type)) {
-            Mailbox m = new Mailbox();
-            m.mDisplayName = name;
-            m.mServerId = serverId;
-            m.mAccountKey = mAccountId;
-            m.mType = Mailbox.TYPE_MAIL;
+
+        if (VALID_EAS_FOLDER_TYPES.contains(type)) {
+            Mailbox mailbox = new Mailbox();
+            mailbox.mDisplayName = name;
+            mailbox.mServerId = serverId;
+            mailbox.mAccountKey = mAccountId;
+            mailbox.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;
+            mailbox.mSyncInterval = Mailbox.CHECK_INTERVAL_NEVER;
             switch (type) {
                 case INBOX_TYPE:
-                    m.mType = Mailbox.TYPE_INBOX;
-                    m.mSyncInterval = mAccount.mSyncInterval;
+                    mailbox.mType = Mailbox.TYPE_INBOX;
+                    mailbox.mSyncInterval = mAccount.mSyncInterval;
                     break;
                 case CONTACTS_TYPE:
-                    m.mType = Mailbox.TYPE_CONTACTS;
-                    m.mSyncInterval = mAccount.mSyncInterval;
+                    mailbox.mType = Mailbox.TYPE_CONTACTS;
+                    mailbox.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;
+                    // TYPE_OUTBOX mailboxes are known by ExchangeService to sync whenever they
+                    // aren't empty.  The value of mSyncFrequency is ignored for this kind of
+                    // mailbox.
+                    mailbox.mType = Mailbox.TYPE_OUTBOX;
                     break;
                 case SENT_TYPE:
-                    m.mType = Mailbox.TYPE_SENT;
+                    mailbox.mType = Mailbox.TYPE_SENT;
                     break;
                 case DRAFTS_TYPE:
-                    m.mType = Mailbox.TYPE_DRAFTS;
+                    mailbox.mType = Mailbox.TYPE_DRAFTS;
                     break;
                 case DELETED_TYPE:
-                    m.mType = Mailbox.TYPE_TRASH;
+                    mailbox.mType = Mailbox.TYPE_TRASH;
                     break;
                 case CALENDAR_TYPE:
-                    m.mType = Mailbox.TYPE_CALENDAR;
-                    m.mSyncInterval = mAccount.mSyncInterval;
+                    mailbox.mType = Mailbox.TYPE_CALENDAR;
+                    mailbox.mSyncInterval = mAccount.mSyncInterval;
+                    break;
+                case USER_GENERIC_TYPE:
+                    mailbox.mType = Mailbox.TYPE_UNKNOWN;
                     break;
             }
 
             // Make boxes like Contacts and Calendar invisible in the folder list
-            m.mFlagVisible = (m.mType < Mailbox.TYPE_NOT_EMAIL);
+            mailbox.mFlagVisible = (mailbox.mType < Mailbox.TYPE_NOT_EMAIL);
 
             if (!parentId.equals("0")) {
-                m.mParentServerId = parentId;
+                mailbox.mParentServerId = parentId;
             }
 
-            userLog("Adding mailbox: ", m.mDisplayName);
-            ops.add(ContentProviderOperation
-                    .newInsert(Mailbox.CONTENT_URI).withValues(m.toContentValues()).build());
+            return mailbox;
         }
+        return null;
+    }
 
-        return;
+    /**
+     * Determine whether a given mailbox holds mail, rather than other data.  We do this by first
+     * checking the type of the mailbox (if it's a known good type, great; if it's a known bad
+     * type, return false).  If it's unknown, we check the parent, first by trying to find it in
+     * the current set of newly synced items, and then by looking it up in EmailProvider.  If
+     * we can find the parent, we use the same rules to determine if it holds mail; if it does,
+     * then its children do as well, so that's a go.
+     *
+     * @param mailbox the mailbox we're checking
+     * @param mailboxMap a HashMap relating server id's of mailboxes in the current sync set to
+     * the corresponding mailbox structures
+     * @return whether or not the mailbox contains email (rather than PIM or unknown data)
+     */
+    /*package*/ boolean isValidMailFolder(Mailbox mailbox, HashMap<String, Mailbox> mailboxMap) {
+        int folderType = mailbox.mType;
+        // Automatically accept our email types
+        if (folderType < Mailbox.TYPE_NOT_EMAIL) return true;
+        // Automatically reject everything else but "unknown"
+        if (folderType != Mailbox.TYPE_UNKNOWN) return false;
+        // If this is TYPE_UNKNOWN, check the parent
+        Mailbox parent = mailboxMap.get(mailbox.mParentServerId);
+        // If the parent is in the map, then check it out; if not, it could be an existing saved
+        // Mailbox, so we'll have to query the database
+        if (parent == null) {
+            mBindArguments[0] = Long.toString(mAccount.mId);
+            mBindArguments[1] = mailbox.mParentServerId;
+            long parentId = Utility.getFirstRowInt(mContext, Mailbox.CONTENT_URI,
+                    EmailContent.ID_PROJECTION,
+                    MailboxColumns.ACCOUNT_KEY + "=? AND " + MailboxColumns.SERVER_ID + "=?",
+                    mBindArguments, null, EmailContent.ID_PROJECTION_COLUMN, -1);
+            if (parentId != -1) {
+                // Get the parent from the database
+                parent = Mailbox.restoreMailboxWithId(mContext, parentId);
+                if (parent == null) return false;
+            } else {
+                return false;
+            }
+        }
+        return isValidMailFolder(parent, mailboxMap);
     }
 
     public void updateParser(ArrayList<ContentProviderOperation> ops) throws IOException {
@@ -303,12 +350,27 @@
     }
 
     public void changesParser() throws IOException {
-        // Keep track of new boxes, deleted boxes, updated boxes
         ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();
+        // Mailboxes that we known contain email
+        ArrayList<Mailbox> validMailboxes = new ArrayList<Mailbox>();
+        // Mailboxes that we're unsure about
+        ArrayList<Mailbox> userMailboxes = new ArrayList<Mailbox>();
+        // Maps folder serverId to mailbox type
+        HashMap<String, Mailbox> mailboxMap = new HashMap<String, Mailbox>();
 
         while (nextTag(Tags.FOLDER_CHANGES) != END) {
             if (tag == Tags.FOLDER_ADD) {
-                addParser(ops);
+                Mailbox mailbox = addParser();
+                if (mailbox != null) {
+                    // Save away the type of this folder
+                    mailboxMap.put(mailbox.mServerId, mailbox);
+                    // And add the mailbox to the proper list
+                    if (type == USER_MAILBOX_TYPE) {
+                        userMailboxes.add(mailbox);
+                    } else {
+                        validMailboxes.add(mailbox);
+                    }
+                }
             } else if (tag == Tags.FOLDER_DELETE) {
                 deleteParser(ops);
             } else if (tag == Tags.FOLDER_UPDATE) {
@@ -327,6 +389,22 @@
             return;
         }
 
+        // Go through the generic user mailboxes; we'll call them valid if any parent is valid
+        for (Mailbox m: userMailboxes) {
+            if (isValidMailFolder(m, mailboxMap)) {
+                m.mType = Mailbox.TYPE_MAIL;
+                validMailboxes.add(m);
+            } else {
+                userLog("Rejecting unknown type mailbox: " + m.mDisplayName);
+            }
+        }
+
+        for (Mailbox m: validMailboxes) {
+            userLog("Adding mailbox: ", m.mDisplayName);
+            ops.add(ContentProviderOperation
+                    .newInsert(Mailbox.CONTENT_URI).withValues(m.toContentValues()).build());
+        }
+
         // Create the new mailboxes in a single batch operation
         // Don't save any data if the service has been stopped
         synchronized (mService.getSynchronizer()) {
@@ -387,10 +465,6 @@
     }
 
     @Override
-    public void wipe() {
-    }
-
-    @Override
     public void responsesParser() throws IOException {
     }
 
diff --git a/src/com/android/exchange/adapter/GalParser.java b/src/com/android/exchange/adapter/GalParser.java
index fffad70..94c8cdf 100644
--- a/src/com/android/exchange/adapter/GalParser.java
+++ b/src/com/android/exchange/adapter/GalParser.java
@@ -17,6 +17,7 @@
 
 import com.android.exchange.EasSyncService;
 import com.android.exchange.provider.GalResult;
+import com.android.exchange.provider.GalResult.GalData;
 
 import java.io.IOException;
 import java.io.InputStream;
@@ -52,22 +53,54 @@
          return mGalResult.total > 0;
      }
 
-     public void parseProperties(GalResult galResult) throws IOException {
-         String displayName = null;
-         String email = null;
-         while (nextTag(Tags.SEARCH_STORE) != END) {
-             if (tag == Tags.GAL_DISPLAY_NAME) {
-                 displayName = getValue();
-             } else if (tag == Tags.GAL_EMAIL_ADDRESS) {
-                 email = getValue();
-             } else {
-                 skipTag();
-             }
-         }
-         if (displayName != null && email != null) {
-             galResult.addGalData(0, displayName, email);
-         }
-     }
+    public void parseProperties(GalResult galResult) throws IOException {
+        GalData galData = new GalData();
+        while (nextTag(Tags.SEARCH_STORE) != END) {
+            switch(tag) {
+                // Display name and email address use both legacy and new code for galData
+                case Tags.GAL_DISPLAY_NAME: 
+                    String displayName = getValue();
+                    galData.put(GalData.DISPLAY_NAME, displayName);
+                    galData.displayName = displayName;
+                    break;
+                case Tags.GAL_EMAIL_ADDRESS:
+                    String emailAddress = getValue();
+                    galData.put(GalData.EMAIL_ADDRESS, emailAddress);
+                    galData.emailAddress = emailAddress;
+                    break;
+                case Tags.GAL_PHONE:
+                    galData.put(GalData.WORK_PHONE, getValue());
+                    break;
+                case Tags.GAL_OFFICE:
+                    galData.put(GalData.OFFICE, getValue());
+                    break;
+                case Tags.GAL_TITLE:
+                    galData.put(GalData.TITLE, getValue());
+                    break;
+                case Tags.GAL_COMPANY:
+                    galData.put(GalData.COMPANY, getValue());
+                    break;
+                case Tags.GAL_ALIAS:
+                    galData.put(GalData.ALIAS, getValue());
+                    break;
+                case Tags.GAL_FIRST_NAME:
+                    galData.put(GalData.FIRST_NAME, getValue());
+                    break;
+                case Tags.GAL_LAST_NAME:
+                    galData.put(GalData.LAST_NAME, getValue());
+                    break;
+                case Tags.GAL_HOME_PHONE:
+                    galData.put(GalData.HOME_PHONE, getValue());
+                    break;
+                case Tags.GAL_MOBILE_PHONE:
+                    galData.put(GalData.MOBILE_PHONE, getValue());
+                    break;
+                default:
+                    skipTag();
+            }
+        }
+        galResult.addGalData(galData);
+    }
 
      public void parseResult(GalResult galResult) throws IOException {
          while (nextTag(Tags.SEARCH_STORE) != END) {
diff --git a/src/com/android/exchange/adapter/MoveItemsParser.java b/src/com/android/exchange/adapter/MoveItemsParser.java
new file mode 100644
index 0000000..542331d
--- /dev/null
+++ b/src/com/android/exchange/adapter/MoveItemsParser.java
@@ -0,0 +1,113 @@
+/* Copyright (C) 2010 The Android Open Source Project.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.exchange.adapter;
+
+import com.android.exchange.EasSyncService;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Parse the result of a MoveItems command.
+ */
+public class MoveItemsParser extends Parser {
+    private final EasSyncService mService;
+    private int mStatusCode = 0;
+    private String mNewServerId;
+
+    // These are the EAS status codes for MoveItems
+    private static final int STATUS_NO_SOURCE_FOLDER = 1;
+    private static final int STATUS_NO_DESTINATION_FOLDER = 2;
+    private static final int STATUS_SUCCESS = 3;
+    private static final int STATUS_SOURCE_DESTINATION_SAME = 4;
+    private static final int STATUS_INTERNAL_ERROR = 5;
+    private static final int STATUS_ALREADY_EXISTS = 6;
+    private static final int STATUS_LOCKED = 7;
+
+    // These are the status values we return to callers
+    public static final int STATUS_CODE_SUCCESS = 1;
+    public static final int STATUS_CODE_REVERT = 2;
+    public static final int STATUS_CODE_RETRY = 3;
+
+    public MoveItemsParser(InputStream in, EasSyncService service) throws IOException {
+        super(in);
+        mService = service;
+    }
+
+    public int getStatusCode() {
+        return mStatusCode;
+    }
+
+    public String getNewServerId() {
+        return mNewServerId;
+    }
+
+    public void parseResponse() throws IOException {
+        while (nextTag(Tags.MOVE_RESPONSE) != END) {
+            if (tag == Tags.MOVE_STATUS) {
+                int status = getValueInt();
+                // Convert the EAS status code with our external codes
+                switch(status) {
+                    case STATUS_SUCCESS:
+                    case STATUS_SOURCE_DESTINATION_SAME:
+                    case STATUS_ALREADY_EXISTS:
+                        // Same destination and already exists are ok with us; we'll continue as
+                        // if the move succeeded
+                        mStatusCode = STATUS_CODE_SUCCESS;
+                        break;
+                    case STATUS_LOCKED:
+                        // This sounds like a transient error, so we can safely retry
+                        mStatusCode = STATUS_CODE_RETRY;
+                        break;
+                    case STATUS_NO_SOURCE_FOLDER:
+                    case STATUS_NO_DESTINATION_FOLDER:
+                    case STATUS_INTERNAL_ERROR:
+                    default:
+                        // These are non-recoverable, so we'll revert the message to its original
+                        // mailbox.  If there's an unknown response, revert
+                        mStatusCode = STATUS_CODE_REVERT;
+                        break;
+                }
+                if (status != STATUS_SUCCESS) {
+                    // There's not much to be done if this fails
+                    mService.userLog("Error in MoveItems: " + status);
+                }
+            } else if (tag == Tags.MOVE_DSTMSGID) {
+                mNewServerId = getValue();
+                mService.userLog("Moved message id is now: " + mNewServerId);
+            } else {
+                skipTag();
+            }
+        }
+    }
+
+    @Override
+    public boolean parse() throws IOException {
+        boolean res = false;
+        if (nextTag(START_DOCUMENT) != Tags.MOVE_MOVE_ITEMS) {
+            throw new IOException();
+        }
+        while (nextTag(START_DOCUMENT) != END_DOCUMENT) {
+            if (tag == Tags.MOVE_RESPONSE) {
+                parseResponse();
+            } else {
+                skipTag();
+            }
+        }
+        return res;
+    }
+}
+
diff --git a/src/com/android/exchange/adapter/Parser.java b/src/com/android/exchange/adapter/Parser.java
index 57e3f50..68d6af5 100644
--- a/src/com/android/exchange/adapter/Parser.java
+++ b/src/com/android/exchange/adapter/Parser.java
@@ -275,7 +275,7 @@
         }
         // 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.
+        // ExchangeService
         throw new EodException();
     }
 
diff --git a/src/com/android/exchange/adapter/ProvisionParser.java b/src/com/android/exchange/adapter/ProvisionParser.java
index 701f621..f9429eb 100644
--- a/src/com/android/exchange/adapter/ProvisionParser.java
+++ b/src/com/android/exchange/adapter/ProvisionParser.java
@@ -65,9 +65,12 @@
         int passwordMode = PolicySet.PASSWORD_MODE_NONE;
         int maxPasswordFails = 0;
         int maxScreenLockTime = 0;
-        boolean supported = true;
+        int passwordExpirationDays = 0;
+        int passwordHistory = 0;
+        int passwordComplexChars = 0;
 
         while (nextTag(Tags.PROVISION_EAS_PROVISION_DOC) != END) {
+            boolean tagIsSupported = true;
             switch (tag) {
                 case Tags.PROVISION_DEVICE_PASSWORD_ENABLED:
                     if (getValueInt() == 1) {
@@ -91,39 +94,130 @@
                 case Tags.PROVISION_MAX_DEVICE_PASSWORD_FAILED_ATTEMPTS:
                     maxPasswordFails = getValueInt();
                     break;
+                case Tags.PROVISION_DEVICE_PASSWORD_EXPIRATION:
+                    passwordExpirationDays = getValueInt();
+                    break;
+                case Tags.PROVISION_DEVICE_PASSWORD_HISTORY:
+                    passwordHistory = getValueInt();
+                    break;
                 case Tags.PROVISION_ALLOW_SIMPLE_DEVICE_PASSWORD:
                     // Ignore this unless there's any MSFT documentation for what this means
                     // Hint: I haven't seen any that's more specific than "simple"
                     getValue();
                     break;
-                // The following policy, if false, can't be supported at the moment
+                // The following policies, if false, can't be supported at the moment
                 case Tags.PROVISION_ATTACHMENTS_ENABLED:
+                case Tags.PROVISION_ALLOW_STORAGE_CARD:
+                case Tags.PROVISION_ALLOW_CAMERA:
+                case Tags.PROVISION_ALLOW_UNSIGNED_APPLICATIONS:
+                case Tags.PROVISION_ALLOW_UNSIGNED_INSTALLATION_PACKAGES:
+                case Tags.PROVISION_ALLOW_WIFI:
+                case Tags.PROVISION_ALLOW_TEXT_MESSAGING:
+                case Tags.PROVISION_ALLOW_POP_IMAP_EMAIL:
+                case Tags.PROVISION_ALLOW_IRDA:
+                case Tags.PROVISION_ALLOW_HTML_EMAIL:
+                case Tags.PROVISION_ALLOW_BROWSER:
+                case Tags.PROVISION_ALLOW_CONSUMER_EMAIL:
+                case Tags.PROVISION_ALLOW_INTERNET_SHARING:
                     if (getValueInt() == 0) {
-                       supported = false;
+                        tagIsSupported = false;
+                    }
+                    break;
+                // Bluetooth: 0 = no bluetooth; 1 = only hands-free; 2 = allowed
+                case Tags.PROVISION_ALLOW_BLUETOOTH:
+                    if (getValueInt() != 2) {
+                        tagIsSupported = false;
                     }
                     break;
                 // The following policies, if true, can't be supported at the moment
                 case Tags.PROVISION_DEVICE_ENCRYPTION_ENABLED:
                 case Tags.PROVISION_PASSWORD_RECOVERY_ENABLED:
-                case Tags.PROVISION_DEVICE_PASSWORD_EXPIRATION:
-                case Tags.PROVISION_DEVICE_PASSWORD_HISTORY:
-                case Tags.PROVISION_MAX_ATTACHMENT_SIZE:
+                case Tags.PROVISION_REQUIRE_DEVICE_ENCRYPTION:
+                case Tags.PROVISION_REQUIRE_SIGNED_SMIME_MESSAGES:
+                case Tags.PROVISION_REQUIRE_ENCRYPTED_SMIME_MESSAGES:
+                case Tags.PROVISION_REQUIRE_SIGNED_SMIME_ALGORITHM:
+                case Tags.PROVISION_REQUIRE_ENCRYPTION_SMIME_ALGORITHM:
+                case Tags.PROVISION_REQUIRE_MANUAL_SYNC_WHEN_ROAMING:
                     if (getValueInt() == 1) {
-                        supported = false;
+                        tagIsSupported = false;
+                    }
+                    break;
+                // The following, if greater than zero, can't be supported at the moment
+                case Tags.PROVISION_MAX_ATTACHMENT_SIZE:
+                    if (getValueInt() > 0) {
+                        tagIsSupported = false;
+                    }
+                    break;
+                // Complex characters are supported
+                case Tags.PROVISION_MIN_DEVICE_PASSWORD_COMPLEX_CHARS:
+                    passwordComplexChars = getValueInt();
+                    break;
+                // The following policies are moot; they allow functionality that we don't support
+                case Tags.PROVISION_ALLOW_DESKTOP_SYNC:
+                case Tags.PROVISION_ALLOW_SMIME_ENCRYPTION_NEGOTIATION:
+                case Tags.PROVISION_ALLOW_SMIME_SOFT_CERTS:
+                case Tags.PROVISION_ALLOW_REMOTE_DESKTOP:
+                    skipTag();
+                    break;
+                // We don't handle approved/unapproved application lists
+                case Tags.PROVISION_UNAPPROVED_IN_ROM_APPLICATION_LIST:
+                case Tags.PROVISION_APPROVED_APPLICATION_LIST:
+                    // Parse and throw away the content
+                    if (specifiesApplications(tag)) {
+                        tagIsSupported = false;
+                    }
+                    break;
+                // NOTE: We can support these entirely within the email application if we choose
+                case Tags.PROVISION_MAX_CALENDAR_AGE_FILTER:
+                case Tags.PROVISION_MAX_EMAIL_AGE_FILTER:
+                    // 0 indicates no specified filter
+                    if (getValueInt() != 0) {
+                        tagIsSupported = false;
+                    }
+                    break;
+                // NOTE: We can support these entirely within the email application if we choose
+                case Tags.PROVISION_MAX_EMAIL_BODY_TRUNCATION_SIZE:
+                case Tags.PROVISION_MAX_EMAIL_HTML_BODY_TRUNCATION_SIZE:
+                    String value = getValue();
+                    // -1 indicates no required truncation
+                    if (!value.equals("-1")) {
+                        tagIsSupported = false;
                     }
                     break;
                 default:
                     skipTag();
             }
 
-            if (!supported) {
+            if (!tagIsSupported) {
                 log("Policy not supported: " + tag);
                 mIsSupportable = false;
             }
         }
 
         mPolicySet = new SecurityPolicy.PolicySet(minPasswordLength, passwordMode,
-                    maxPasswordFails, maxScreenLockTime, true);
+                maxPasswordFails, maxScreenLockTime, true, passwordExpirationDays, passwordHistory,
+                passwordComplexChars);
+    }
+
+    /**
+     * Return whether or not either of the application list tags specifies any applications
+     * @param endTag the tag whose children we're walking through
+     * @return whether any applications were specified (by name or by hash)
+     * @throws IOException
+     */
+    private boolean specifiesApplications(int endTag) throws IOException {
+        boolean specifiesApplications = false;
+        while (nextTag(endTag) != END) {
+            switch (tag) {
+                case Tags.PROVISION_APPLICATION_NAME:
+                case Tags.PROVISION_HASH:
+                    specifiesApplications = true;
+                    break;
+                default:
+                    skipTag();
+            }
+        }
+        return specifiesApplications;
     }
 
     class ShadowPolicySet {
@@ -131,9 +225,12 @@
         int mPasswordMode = PolicySet.PASSWORD_MODE_NONE;
         int mMaxPasswordFails = 0;
         int mMaxScreenLockTime = 0;
+        int mPasswordExpiration = 0;
+        int mPasswordHistory = 0;
+        int mPasswordComplexChars = 0;
     }
 
-    public void parseProvisionDocXml(String doc) throws IOException {
+    /*package*/ void parseProvisionDocXml(String doc) throws IOException {
         ShadowPolicySet sps = new ShadowPolicySet();
 
         try {
@@ -155,13 +252,14 @@
         }
 
         mPolicySet = new PolicySet(sps.mMinPasswordLength, sps.mPasswordMode, sps.mMaxPasswordFails,
-                sps.mMaxScreenLockTime, true);
+                sps.mMaxScreenLockTime, true, sps.mPasswordExpiration, sps.mPasswordHistory,
+                sps.mPasswordComplexChars);
     }
 
     /**
      * Return true if password is required; otherwise false.
      */
-    boolean parseSecurityPolicy(XmlPullParser parser, ShadowPolicySet sps)
+    private boolean parseSecurityPolicy(XmlPullParser parser, ShadowPolicySet sps)
             throws XmlPullParserException, IOException {
         boolean passwordRequired = true;
         while (true) {
@@ -184,7 +282,7 @@
         return passwordRequired;
     }
 
-    void parseCharacteristic(XmlPullParser parser, ShadowPolicySet sps)
+    private void parseCharacteristic(XmlPullParser parser, ShadowPolicySet sps)
             throws XmlPullParserException, IOException {
         boolean enforceInactivityTimer = true;
         while (true) {
@@ -226,7 +324,7 @@
         }
     }
 
-    void parseRegistry(XmlPullParser parser, ShadowPolicySet sps)
+    private void parseRegistry(XmlPullParser parser, ShadowPolicySet sps)
             throws XmlPullParserException, IOException {
       while (true) {
           int type = parser.nextTag();
@@ -241,7 +339,7 @@
       }
     }
 
-    void parseWapProvisioningDoc(XmlPullParser parser, ShadowPolicySet sps)
+    private void parseWapProvisioningDoc(XmlPullParser parser, ShadowPolicySet sps)
             throws XmlPullParserException, IOException {
         while (true) {
             int type = parser.nextTag();
@@ -265,7 +363,7 @@
         }
     }
 
-    public void parseProvisionData() throws IOException {
+    private void parseProvisionData() throws IOException {
         while (nextTag(Tags.PROVISION_DATA) != END) {
             if (tag == Tags.PROVISION_EAS_PROVISION_DOC) {
                 parseProvisionDocWbxml();
@@ -275,7 +373,7 @@
         }
     }
 
-    public void parsePolicy() throws IOException {
+    private void parsePolicy() throws IOException {
         String policyType = null;
         while (nextTag(Tags.PROVISION_POLICY) != END) {
             switch (tag) {
@@ -304,7 +402,7 @@
         }
     }
 
-    public void parsePolicies() throws IOException {
+    private void parsePolicies() throws IOException {
         while (nextTag(Tags.PROVISION_POLICIES) != END) {
             if (tag == Tags.PROVISION_POLICY) {
                 parsePolicy();
diff --git a/src/com/android/exchange/adapter/Serializer.java b/src/com/android/exchange/adapter/Serializer.java
index daedec2..bd16909 100644
--- a/src/com/android/exchange/adapter/Serializer.java
+++ b/src/com/android/exchange/adapter/Serializer.java
@@ -23,6 +23,7 @@
 
 package com.android.exchange.adapter;
 
+import com.android.email.Email;
 import com.android.exchange.Eas;
 import com.android.exchange.utility.FileLogger;
 
@@ -37,7 +38,7 @@
 public class Serializer {
 
     private static final String TAG = "Serializer";
-    private boolean logging = false;    // DO NOT CHECK IN WITH THIS TRUE!
+    private boolean logging = Email.DEBUG && Log.isLoggable(TAG, Log.VERBOSE);
 
     private static final int NOT_PENDING = -1;
 
diff --git a/src/com/android/exchange/adapter/Tags.java b/src/com/android/exchange/adapter/Tags.java
index ef70981..2745e33 100644
--- a/src/com/android/exchange/adapter/Tags.java
+++ b/src/com/android/exchange/adapter/Tags.java
@@ -596,7 +596,7 @@
             "MinDevicePasswordComplexCharacters", "AllowWiFi", "AllowTextMessaging",
             "AllowPOPIMAPEmail", "AllowBluetooth", "AllowIrDA", "RequireManualSyncWhenRoaming",
             "AllowDesktopSync",
-            "MaxCalendarAgeFilder", "AllowHTMLEmail", "MaxEmailAgeFilder",
+            "MaxCalendarAgeFilder", "AllowHTMLEmail", "MaxEmailAgeFilter",
             "MaxEmailBodyTruncationSize", "MaxEmailHTMLBodyTruncationSize",
             "RequireSignedSMIMEMessages", "RequireEncryptedSMIMEMessages",
             "RequireSignedSMIMEAlgorithm", "RequireEncryptionSMIMEAlgorithm",
diff --git a/src/com/android/exchange/provider/ExchangeDirectoryProvider.java b/src/com/android/exchange/provider/ExchangeDirectoryProvider.java
new file mode 100644
index 0000000..b267da9
--- /dev/null
+++ b/src/com/android/exchange/provider/ExchangeDirectoryProvider.java
@@ -0,0 +1,421 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.exchange.provider;
+
+import com.android.email.R;
+import com.android.email.Utility;
+import com.android.email.VendorPolicyLoader;
+import com.android.email.mail.PackedString;
+import com.android.email.provider.EmailContent;
+import com.android.email.provider.EmailContent.Account;
+import com.android.email.provider.EmailContent.AccountColumns;
+import com.android.exchange.EasSyncService;
+import com.android.exchange.provider.GalResult.GalData;
+
+import android.accounts.AccountManager;
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.UriMatcher;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.net.Uri;
+import android.os.Binder;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Directory;
+import android.provider.ContactsContract.RawContacts;
+import android.provider.ContactsContract.CommonDataKinds.Email;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.CommonDataKinds.StructuredName;
+import android.provider.ContactsContract.Contacts.Data;
+import android.text.TextUtils;
+
+import java.util.HashMap;
+import java.util.List;
+
+/**
+ * ExchangeDirectoryProvider provides real-time data from the Exchange server; at the moment, it is
+ * used solely to provide GAL (Global Address Lookup) service to email address adapters
+ */
+public class ExchangeDirectoryProvider extends ContentProvider {
+    public static final String EXCHANGE_GAL_AUTHORITY = "com.android.exchange.directory.provider";
+
+    private static final int DEFAULT_CONTACT_ID = 1;
+    private static final int DEFAULT_LOOKUP_LIMIT = 20;
+
+    private static final int GAL_BASE = 0;
+    private static final int GAL_DIRECTORIES = GAL_BASE;
+    private static final int GAL_FILTER = GAL_BASE + 1;
+    private static final int GAL_CONTACT = GAL_BASE + 2;
+    private static final int GAL_CONTACT_WITH_ID = GAL_BASE + 3;
+    private static final int GAL_EMAIL_FILTER = GAL_BASE + 4;
+
+    private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH);
+    /*package*/ final HashMap<String, Long> mAccountIdMap = new HashMap<String, Long>();
+
+    static {
+        sURIMatcher.addURI(EXCHANGE_GAL_AUTHORITY, "directories", GAL_DIRECTORIES);
+        sURIMatcher.addURI(EXCHANGE_GAL_AUTHORITY, "contacts/filter/*", GAL_FILTER);
+        sURIMatcher.addURI(EXCHANGE_GAL_AUTHORITY, "contacts/lookup/*/entities", GAL_CONTACT);
+        sURIMatcher.addURI(EXCHANGE_GAL_AUTHORITY, "contacts/lookup/*/#/entities",
+                GAL_CONTACT_WITH_ID);
+        sURIMatcher.addURI(EXCHANGE_GAL_AUTHORITY, "data/emails/filter/*", GAL_EMAIL_FILTER);
+    }
+
+    @Override
+    public boolean onCreate() {
+        return true;
+    }
+
+    static class GalProjection {
+        final int size;
+        final HashMap<String, Integer> columnMap = new HashMap<String, Integer>();
+
+        GalProjection(String[] projection) {
+            size = projection.length;
+            for (int i = 0; i < projection.length; i++) {
+                columnMap.put(projection[i], i);
+            }
+        }
+    }
+
+    static class GalContactRow {
+        private final GalProjection mProjection;
+        private Object[] row;
+        static long dataId = 1;
+
+        GalContactRow(GalProjection projection, long contactId, String lookupKey,
+                String accountName, String displayName) {
+            this.mProjection = projection;
+            row = new Object[projection.size];
+
+            put(Contacts.Entity.CONTACT_ID, contactId);
+
+            // We only have one raw contact per aggregate, so they can have the same ID
+            put(Contacts.Entity.RAW_CONTACT_ID, contactId);
+            put(Contacts.Entity.DATA_ID, dataId++);
+
+            put(Contacts.DISPLAY_NAME, displayName);
+
+            // TODO alternative display name
+            put(Contacts.DISPLAY_NAME_ALTERNATIVE, displayName);
+
+            put(RawContacts.ACCOUNT_TYPE, com.android.email.Email.EXCHANGE_ACCOUNT_MANAGER_TYPE);
+            put(RawContacts.ACCOUNT_NAME, accountName);
+            put(RawContacts.RAW_CONTACT_IS_READ_ONLY, 1);
+            put(Data.IS_READ_ONLY, 1);
+        }
+
+        Object[] getRow () {
+            return row;
+        }
+
+        void put(String columnName, Object value) {
+            Integer integer = mProjection.columnMap.get(columnName);
+            if (integer != null) {
+                row[integer] = value;
+            } else {
+                System.out.println("Unsupported column: " + columnName);
+            }
+        }
+
+        static void addEmailAddress(MatrixCursor cursor, GalProjection galProjection,
+                long contactId, String lookupKey, String accountName, String displayName,
+                String address) {
+            if (!TextUtils.isEmpty(address)) {
+                GalContactRow r = new GalContactRow(
+                        galProjection, contactId, lookupKey, accountName, displayName);
+                r.put(Data.MIMETYPE, Email.CONTENT_ITEM_TYPE);
+                r.put(Email.TYPE, Email.TYPE_WORK);
+                r.put(Email.ADDRESS, address);
+                cursor.addRow(r.getRow());
+            }
+        }
+
+        static void addPhoneRow(MatrixCursor cursor, GalProjection projection, long contactId,
+                String lookupKey, String accountName, String displayName, int type, String number) {
+            if (!TextUtils.isEmpty(number)) {
+                GalContactRow r = new GalContactRow(
+                        projection, contactId, lookupKey, accountName, displayName);
+                r.put(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
+                r.put(Phone.TYPE, type);
+                r.put(Phone.NUMBER, number);
+                cursor.addRow(r.getRow());
+            }
+        }
+
+        public static void addNameRow(MatrixCursor cursor, GalProjection galProjection,
+                long contactId, String lookupKey, String accountName, String displayName,
+                String firstName, String lastName) {
+            GalContactRow r = new GalContactRow(
+                    galProjection, contactId, lookupKey, accountName, displayName);
+            r.put(Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE);
+            r.put(StructuredName.GIVEN_NAME, firstName);
+            r.put(StructuredName.FAMILY_NAME, lastName);
+            r.put(StructuredName.DISPLAY_NAME, displayName);
+            cursor.addRow(r.getRow());
+        }
+    }
+
+    /**
+     * Find the record id of an Account, given its name (email address)
+     * @param accountName the name of the account
+     * @return the record id of the Account, or -1 if not found
+     */
+    /*package*/ long getAccountIdByName(Context context, String accountName) {
+        Long accountId = mAccountIdMap.get(accountName);
+        if (accountId == null) {
+            accountId = Utility.getFirstRowLong(context, Account.CONTENT_URI,
+                    EmailContent.ID_PROJECTION, AccountColumns.EMAIL_ADDRESS + "=?",
+                    new String[] {accountName}, null, EmailContent.ID_PROJECTION_COLUMN , -1L);
+            if (accountId != -1) {
+                mAccountIdMap.put(accountName, accountId);
+            }
+        }
+        return accountId;
+    }
+
+    @Override
+    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+            String sortOrder) {
+        int match = sURIMatcher.match(uri);
+        MatrixCursor cursor;
+        Object[] row;
+        PackedString ps;
+        String lookupKey;
+
+        switch (match) {
+            case GAL_DIRECTORIES: {
+                // Assuming that GAL can be used with all exchange accounts
+                android.accounts.Account[] accounts = AccountManager.get(getContext())
+                        .getAccountsByType(com.android.email.Email.EXCHANGE_ACCOUNT_MANAGER_TYPE);
+                cursor = new MatrixCursor(projection);
+                if (accounts != null) {
+                    for (android.accounts.Account account : accounts) {
+                        row = new Object[projection.length];
+
+                        for (int i = 0; i < projection.length; i++) {
+                            String column = projection[i];
+                            if (column.equals(Directory.ACCOUNT_NAME)) {
+                                row[i] = account.name;
+                            } else if (column.equals(Directory.ACCOUNT_TYPE)) {
+                                row[i] = account.type;
+                            } else if (column.equals(Directory.TYPE_RESOURCE_ID)) {
+                                if (VendorPolicyLoader.getInstance(getContext())
+                                        .useAlternateExchangeStrings()) {
+                                    row[i] = R.string.exchange_name_alternate;
+                                } else {
+                                    row[i] = R.string.exchange_name;
+                                }
+                            } else if (column.equals(Directory.DISPLAY_NAME)) {
+                                row[i] = account.name;
+                            } else if (column.equals(Directory.EXPORT_SUPPORT)) {
+                                row[i] = Directory.EXPORT_SUPPORT_SAME_ACCOUNT_ONLY;
+                            } else if (column.equals(Directory.SHORTCUT_SUPPORT)) {
+                                row[i] = Directory.SHORTCUT_SUPPORT_NONE;
+                            }
+                        }
+                        cursor.addRow(row);
+                    }
+                }
+                return cursor;
+            }
+
+            case GAL_FILTER:
+            case GAL_EMAIL_FILTER: {
+                String filter = uri.getLastPathSegment();
+                // We should have at least two characters before doing a GAL search
+                if (filter == null || filter.length() < 2) {
+                    return null;
+                }
+
+                String accountName = uri.getQueryParameter(RawContacts.ACCOUNT_NAME);
+                if (accountName == null) {
+                    return null;
+                }
+
+                // Enforce a limit on the number of lookup responses
+                String limitString = uri.getQueryParameter(ContactsContract.LIMIT_PARAM_KEY);
+                int limit = DEFAULT_LOOKUP_LIMIT;
+                if (limitString != null) {
+                    try {
+                        limit = Integer.parseInt(limitString);
+                    } catch (NumberFormatException e) {
+                        limit = 0;
+                    }
+                    if (limit <= 0) {
+                        throw new IllegalArgumentException("Limit not valid: " + limitString);
+                    }
+                }
+
+                long callingId = Binder.clearCallingIdentity();
+                try {
+                    // Find the account id to pass along to EasSyncService
+                    long accountId = getAccountIdByName(getContext(), accountName);
+                    if (accountId == -1) {
+                        // The account was deleted?
+                        return null;
+                    }
+
+                    // Get results from the Exchange account
+                    GalResult galResult = EasSyncService.searchGal(getContext(), accountId,
+                            filter, limit);
+                    if (galResult != null) {
+                        return buildGalResultCursor(projection, galResult);
+                    }
+                } finally {
+                    Binder.restoreCallingIdentity(callingId);
+                }
+                break;
+            }
+
+            case GAL_CONTACT:
+            case GAL_CONTACT_WITH_ID: {
+                String accountName = uri.getQueryParameter(RawContacts.ACCOUNT_NAME);
+                if (accountName == null) {
+                    return null;
+                }
+
+                GalProjection galProjection = new GalProjection(projection);
+                cursor = new MatrixCursor(projection);
+                // Handle the decomposition of the key into rows suitable for CP2
+                List<String> pathSegments = uri.getPathSegments();
+                lookupKey = pathSegments.get(2);
+                long contactId = (match == GAL_CONTACT_WITH_ID)
+                        ? Long.parseLong(pathSegments.get(3))
+                        : DEFAULT_CONTACT_ID;
+                ps = new PackedString(lookupKey);
+                String displayName = ps.get(GalData.DISPLAY_NAME);
+                GalContactRow.addEmailAddress(cursor, galProjection, contactId, lookupKey,
+                        accountName, displayName, ps.get(GalData.EMAIL_ADDRESS));
+                GalContactRow.addPhoneRow(cursor, galProjection, contactId, accountName,
+                        displayName, displayName, Phone.TYPE_HOME, ps.get(GalData.HOME_PHONE));
+                GalContactRow.addPhoneRow(cursor, galProjection, contactId, accountName,
+                        displayName, displayName, Phone.TYPE_WORK, ps.get(GalData.WORK_PHONE));
+                GalContactRow.addPhoneRow(cursor, galProjection, contactId, accountName,
+                        displayName, displayName, Phone.TYPE_MOBILE, ps.get(GalData.MOBILE_PHONE));
+                GalContactRow.addNameRow(cursor, galProjection, contactId, accountName, displayName,
+                        ps.get(GalData.FIRST_NAME), ps.get(GalData.LAST_NAME), displayName);
+                return cursor;
+            }
+        }
+
+        return null;
+    }
+
+    /*package*/ Cursor buildGalResultCursor(String[] projection, GalResult galResult) {
+        int displayNameIndex = -1;
+        int alternateDisplayNameIndex = -1;;
+        int emailIndex = -1;
+        int idIndex = -1;
+        int lookupIndex = -1;
+
+        for (int i = 0; i < projection.length; i++) {
+            String column = projection[i];
+            if (Contacts.DISPLAY_NAME.equals(column) ||
+                    Contacts.DISPLAY_NAME_PRIMARY.equals(column)) {
+                displayNameIndex = i;
+            } else if (Contacts.DISPLAY_NAME_ALTERNATIVE.equals(column)) {
+                alternateDisplayNameIndex = i;
+            } else if (CommonDataKinds.Email.ADDRESS.equals(column)) {
+                emailIndex = i;
+            } else if (Contacts._ID.equals(column)) {
+                idIndex = i;
+            } else if (Contacts.LOOKUP_KEY.equals(column)) {
+                lookupIndex = i;
+            }
+        }
+
+        Object[] row = new Object[projection.length];
+
+        /*
+         * ContactsProvider will ensure that every request has a non-null projection.
+         */
+        MatrixCursor cursor = new MatrixCursor(projection);
+        int count = galResult.galData.size();
+        for (int i = 0; i < count; i++) {
+            GalData galDataRow = galResult.galData.get(i);
+            String firstName = galDataRow.get(GalData.FIRST_NAME);
+            String lastName = galDataRow.get(GalData.LAST_NAME);
+            String displayName = galDataRow.get(GalData.DISPLAY_NAME);
+            // If we don't have a display name, try to create one using first and last name
+            if (displayName == null) {
+                if (firstName != null && lastName != null) {
+                    displayName = firstName + " " + lastName;
+                } else if (firstName != null) {
+                    displayName = firstName;
+                } else if (lastName != null) {
+                    displayName = lastName;
+                }
+            }
+            galDataRow.put(GalData.DISPLAY_NAME, displayName);
+
+            if (displayNameIndex != -1) {
+                row[displayNameIndex] = displayName;
+            }
+            if (alternateDisplayNameIndex != -1) {
+                // Try to create an alternate display name, using first and last name
+                // TODO: Check with Contacts team to make sure we're using this properly
+                if (firstName != null && lastName != null) {
+                    row[alternateDisplayNameIndex] = lastName + " " + firstName;
+                } else {
+                    row[alternateDisplayNameIndex] = displayName;
+                }
+            }
+            if (emailIndex != -1) {
+                row[emailIndex] = galDataRow.get(GalData.EMAIL_ADDRESS);
+            }
+            if (idIndex != -1) {
+                row[idIndex] = i + 1;  // Let's be 1 based
+            }
+            if (lookupIndex != -1) {
+                // We use the packed string as our lookup key; it contains ALL of the gal data
+                // We do this because we are not able to provide a stable id to ContactsProvider
+                row[lookupIndex] = Uri.encode(galDataRow.toPackedString());
+            }
+            cursor.addRow(row);
+        }
+        return cursor;
+    }
+
+    @Override
+    public String getType(Uri uri) {
+        int match = sURIMatcher.match(uri);
+        switch (match) {
+            case GAL_FILTER:
+                return Contacts.CONTENT_ITEM_TYPE;
+        }
+        return null;
+    }
+
+    @Override
+    public int delete(Uri uri, String selection, String[] selectionArgs) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Uri insert(Uri uri, ContentValues values) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+        throw new UnsupportedOperationException();
+    }
+}
diff --git a/src/com/android/exchange/provider/ExchangeProvider.java b/src/com/android/exchange/provider/ExchangeProvider.java
deleted file mode 100644
index 2a66879..0000000
--- a/src/com/android/exchange/provider/ExchangeProvider.java
+++ /dev/null
@@ -1,143 +0,0 @@
-/*
- * Copyright (C) 2010 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.exchange.provider;
-
-import com.android.exchange.EasSyncService;
-import com.android.exchange.provider.GalResult.GalData;
-
-import android.content.ContentProvider;
-import android.content.ContentValues;
-import android.content.UriMatcher;
-import android.database.Cursor;
-import android.database.MatrixCursor;
-import android.net.Uri;
-import android.os.Bundle;
-
-/**
- * ExchangeProvider provides real-time data from the Exchange server; at the moment, it is used
- * solely to provide GAL (Global Address Lookup) service to email address adapters
- */
-public class ExchangeProvider extends ContentProvider {
-    public static final String EXCHANGE_AUTHORITY = "com.android.exchange.provider";
-    public static final Uri GAL_URI = Uri.parse("content://" + EXCHANGE_AUTHORITY + "/gal/");
-
-    private static final int GAL_BASE = 0;
-    private static final int GAL_FILTER = GAL_BASE;
-
-    public static final String[] GAL_PROJECTION = new String[] {"_id", "displayName", "data"};
-    public static final int GAL_COLUMN_ID = 0;
-    public static final int GAL_COLUMN_DISPLAYNAME = 1;
-    public static final int GAL_COLUMN_DATA = 2;
-
-    public static final String EXTRAS_TOTAL_RESULTS = "com.android.exchange.provider.TOTAL_RESULTS";
-
-    private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH);
-
-    static {
-        // Exchange URI matching table
-        UriMatcher matcher = sURIMatcher;
-        // The URI for GAL lookup contains three user-supplied parameters in the path:
-        // 1) the account id of the Exchange account
-        // 2) the constraint (filter) text
-        matcher.addURI(EXCHANGE_AUTHORITY, "gal/*/*", GAL_FILTER);
-    }
-
-    private static void addGalDataRow(MatrixCursor mc, long id, String name, String address) {
-        mc.newRow().add(id).add(name).add(address);
-    }
-
-    @Override
-    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
-            String sortOrder) {
-        int match = sURIMatcher.match(uri);
-        switch (match) {
-            case GAL_FILTER:
-                long accountId = -1;
-                // Pull out our parameters
-                MatrixCursorExtras c = new MatrixCursorExtras(GAL_PROJECTION);
-                String accountIdString = uri.getPathSegments().get(1);
-                String filter = uri.getPathSegments().get(2);
-                // Make sure we get a valid id; otherwise throw an exception
-                try {
-                    accountId = Long.parseLong(accountIdString);
-                } catch (NumberFormatException e) {
-                    throw new IllegalArgumentException("Illegal value in URI");
-                }
-                // Get results from the Exchange account
-                GalResult galResult = EasSyncService.searchGal(getContext(), accountId, filter);
-                if (galResult != null) {
-                    for (GalData data : galResult.galData) {
-                        addGalDataRow(c, data._id, data.displayName, data.emailAddress);
-                    }
-                    // Use cursor side channel to report metadata
-                    final Bundle bundle = new Bundle();
-                    bundle.putInt(EXTRAS_TOTAL_RESULTS, galResult.total);
-                    c.setExtras(bundle);
-                }
-                return c;
-            default:
-                throw new IllegalArgumentException("Unknown URI " + uri);
-        }
-    }
-
-    @Override
-    public int delete(Uri uri, String selection, String[] selectionArgs) {
-        return -1;
-    }
-
-    @Override
-    public String getType(Uri uri) {
-        return "vnd.android.cursor.dir/gal-entry";
-    }
-
-    @Override
-    public Uri insert(Uri uri, ContentValues values) {
-        return null;
-    }
-
-    @Override
-    public boolean onCreate() {
-        return false;
-    }
-
-    @Override
-    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
-        return -1;
-    }
-
-    /**
-     * A simple extension to MatrixCursor that supports extras
-     */
-    private static class MatrixCursorExtras extends MatrixCursor {
-
-        private Bundle mExtras;
-
-        public MatrixCursorExtras(String[] columnNames) {
-            super(columnNames);
-            mExtras = null;
-        }
-
-        public void setExtras(Bundle extras) {
-            mExtras = extras;
-        }
-
-        @Override
-        public Bundle getExtras() {
-            return mExtras;
-        }
-    }
-}
diff --git a/src/com/android/exchange/provider/GalEmailAddressAdapter.java b/src/com/android/exchange/provider/GalEmailAddressAdapter.java
deleted file mode 100644
index e0e3f4f..0000000
--- a/src/com/android/exchange/provider/GalEmailAddressAdapter.java
+++ /dev/null
@@ -1,366 +0,0 @@
-/* Copyright (C) 2010 The Android Open Source Project.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.exchange.provider;
-
-import com.android.email.Email;
-import com.android.email.EmailAddressAdapter;
-import com.android.email.R;
-import com.android.email.provider.EmailContent.Account;
-import com.android.email.provider.EmailContent.HostAuth;
-
-import android.app.Activity;
-import android.content.Context;
-import android.database.Cursor;
-import android.database.MatrixCursor;
-import android.database.MergeCursor;
-import android.net.Uri;
-import android.util.Log;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.ListView;
-import android.widget.TextView;
-
-/**
- * Email Address adapter that performs asynchronous GAL lookups.
- */
-public class GalEmailAddressAdapter extends EmailAddressAdapter {
-    // DO NOT CHECK IN SET TO TRUE
-    private static final boolean DEBUG_GAL_LOG = false;
-
-    // Don't run GAL query until there are 3 characters typed
-    private static final int MINIMUM_GAL_CONSTRAINT_LENGTH = 3;
-
-    private Activity mActivity;
-    private Account mAccount;
-    private boolean mAccountHasGal;
-    private String mAccountEmailDomain;
-    private LayoutInflater mInflater;
-
-    // Local variables to track status of the search
-    private int mSeparatorDisplayCount;
-    private int mSeparatorTotalCount;
-
-    public GalEmailAddressAdapter(Activity activity) {
-        super(activity);
-        mActivity = activity;
-        mAccount = null;
-        mAccountHasGal = false;
-        mInflater = (LayoutInflater) activity.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
-    }
-
-    /**
-     * Set the account ID when known.  Not used for generic contacts lookup;  Use when
-     * linking lookup to specific account.
-     */
-    @Override
-    public void setAccount(Account account) {
-        mAccount = account;
-        mAccountHasGal = false;
-        int finalSplit = mAccount.mEmailAddress.lastIndexOf('@');
-        mAccountEmailDomain = mAccount.mEmailAddress.substring(finalSplit + 1);
-    }
-
-    /**
-     * Sniff the provided account and if it's EAS, record "mAccounthHasGal".  If not,
-     * clear mAccount so we just ignore it.
-     */
-    private void checkGalAccount(Account account) {
-        HostAuth ha = HostAuth.restoreHostAuthWithId(mActivity, account.mHostAuthKeyRecv);
-        if (ha != null) {
-            if ("eas".equalsIgnoreCase(ha.mProtocol)) {
-                mAccountHasGal = true;
-                return;
-            }
-        }
-        // for any reason, we could not identify a GAL account, so clear mAccount
-        // and we'll never check this again
-        mAccount = null;
-        mAccountHasGal = false;
-    }
-
-    @Override
-    public Cursor runQueryOnBackgroundThread(final CharSequence constraint) {
-        // One time (and not in the UI thread) - check the account and see if it support GAL
-        // If not, clear it so we never bother again
-        if (mAccount != null && mAccountHasGal == false) {
-            checkGalAccount(mAccount);
-        }
-
-        // Get the cursor from ContactsProvider, and set up to exit immediately, returning it
-        Cursor contactsCursor = super.runQueryOnBackgroundThread(constraint);
-        // If we don't have a GAL  account or we don't have a constraint that's long enough,
-        // just return the raw contactsCursor
-        if (!mAccountHasGal || constraint == null) {
-            return contactsCursor;
-        }
-        final String constraintString = constraint.toString().trim();
-        if (constraintString.length() < MINIMUM_GAL_CONSTRAINT_LENGTH) {
-            return contactsCursor;
-        }
-
-        // Strategy for handling dynamic GAL lookup.
-        //  1. Create cursor that we can use now (and update later)
-        //  2. Return it immediately
-        //  3. Spawn a thread that will update the cursor when results arrive or search fails
-
-        final MatrixCursor matrixCursor = new MatrixCursor(ExchangeProvider.GAL_PROJECTION);
-        final MyMergeCursor mergedResultCursor =
-            new MyMergeCursor(new Cursor[] {contactsCursor, matrixCursor});
-        mergedResultCursor.setSeparatorPosition(contactsCursor.getCount());
-        mSeparatorDisplayCount = -1;
-        mSeparatorTotalCount = -1;
-        new Thread(new Runnable() {
-            public void run() {
-                // Uri format is account/constraint
-                Uri galUri =
-                    ExchangeProvider.GAL_URI.buildUpon()
-                        .appendPath(Long.toString(mAccount.mId))
-                        .appendPath(constraintString).build();
-                if (DEBUG_GAL_LOG) {
-                    Log.d(Email.LOG_TAG, "Query: " + galUri);
-                }
-                // Use ExchangeProvider to get the results of the GAL query
-                final Cursor galCursor =
-                    mContentResolver.query(galUri, ExchangeProvider.GAL_PROJECTION,
-                            null, null, null);
-                // There are three result cases to handle here.
-                //  1. matrixCursor is closed - this means the UI no longer cares about us
-                //  2. gal cursor is null or empty - remove separator and exit
-                //  3. gal cursor has results - update separator and add results to matrix cursor
-
-                // Case 1: The merged cursor has already been dropped, (e.g. results superceded)
-                if (mergedResultCursor.isClosed()) {
-                    if (DEBUG_GAL_LOG) {
-                        Log.d(Email.LOG_TAG, "Drop result (cursor closed, bg thread)");
-                    }
-                    return;
-                }
-
-                // Cases 2 & 3 have UI aspects, so do them in the UI thread
-                mActivity.runOnUiThread(new Runnable() {
-                    public void run() {
-                        // Case 1:  (final re-check):  Merged cursor already dropped
-                        if (mergedResultCursor.isClosed()) {
-                            if (DEBUG_GAL_LOG) {
-                                Log.d(Email.LOG_TAG, "Drop result (cursor closed, ui thread)");
-                            }
-                            return;
-                        }
-
-                        // Case 2:  Gal cursor is null or empty
-                        if (galCursor == null || galCursor.getCount() == 0) {
-                            if (DEBUG_GAL_LOG) {
-                                Log.d(Email.LOG_TAG, "Drop empty result");
-                            }
-                            mergedResultCursor.setSeparatorPosition(ListView.INVALID_POSITION);
-                            GalEmailAddressAdapter.this.notifyDataSetChanged();
-                            return;
-                        }
-
-                        // Case 3: Real results
-                        galCursor.moveToPosition(-1);
-                        while (galCursor.moveToNext()) {
-                            MatrixCursor.RowBuilder rb = matrixCursor.newRow();
-                            rb.add(galCursor.getLong(ExchangeProvider.GAL_COLUMN_ID));
-                            rb.add(galCursor.getString(ExchangeProvider.GAL_COLUMN_DISPLAYNAME));
-                            rb.add(galCursor.getString(ExchangeProvider.GAL_COLUMN_DATA));
-                        }
-                        // Replace the separator text with "totals"
-                        mSeparatorDisplayCount = galCursor.getCount();
-                        mSeparatorTotalCount =
-                            galCursor.getExtras().getInt(ExchangeProvider.EXTRAS_TOTAL_RESULTS);
-                        // Notify UI that the cursor changed
-                        if (DEBUG_GAL_LOG) {
-                            Log.d(Email.LOG_TAG, "Notify result, added=" + mSeparatorDisplayCount);
-                        }
-                        GalEmailAddressAdapter.this.notifyDataSetChanged();
-                    }});
-            }}).start();
-        return mergedResultCursor;
-    }
-
-    /*
-     * The following series of overrides insert the separator between contacts & GAL contacts
-     * TODO: extract most of this into a CursorAdapter superclass, and share with AccountFolderList
-     */
-
-    /**
-     * Get the separator position, which is tucked into the cursor to deal with threading.
-     * Result is invalid for any other cursor types (e.g. the raw contacts cursor)
-     */
-    private int getSeparatorPosition() {
-        Cursor c = this.getCursor();
-        if (c instanceof MyMergeCursor) {
-            return ((MyMergeCursor)c).getSeparatorPosition();
-        } else {
-            return ListView.INVALID_POSITION;
-        }
-    }
-
-    /**
-     * Prevents the separator view from recycling into the other views
-     */
-    @Override
-    public int getItemViewType(int position) {
-        if (position == getSeparatorPosition()) {
-            return IGNORE_ITEM_VIEW_TYPE;
-        }
-        return super.getItemViewType(position);
-    }
-
-    /**
-     * Injects the separator view when required
-     */
-    @Override
-    public View getView(int position, View convertView, ViewGroup parent) {
-        // The base class's getView() checks for mDataValid at the beginning, but we don't have
-        // to do that, because if the cursor is invalid getCount() returns 0, in which case this
-        // method wouldn't get called.
-
-        // Handle the separator here - create & bind
-        if (position == getSeparatorPosition()) {
-            View separator;
-            separator = mInflater.inflate(R.layout.recipient_dropdown_separator, parent, false);
-            TextView text1 = (TextView) separator.findViewById(R.id.text1);
-            View progress = separator.findViewById(R.id.progress);
-            String bannerText;
-            if (mSeparatorDisplayCount == -1) {
-                // Display "Searching <account>..."
-                bannerText = mContext.getString(R.string.gal_searching_fmt, mAccountEmailDomain);
-                progress.setVisibility(View.VISIBLE);
-            } else {
-                if (mSeparatorDisplayCount == mSeparatorTotalCount) {
-                    // Display "x results from <account>"
-                    bannerText = mContext.getResources().getQuantityString(
-                            R.plurals.gal_completed_fmt, mSeparatorDisplayCount,
-                            mSeparatorDisplayCount, mAccountEmailDomain);
-                } else {
-                    // Display "First x results from <account>"
-                    bannerText = mContext.getString(R.string.gal_completed_limited_fmt,
-                            mSeparatorDisplayCount, mAccountEmailDomain);
-                }
-                progress.setVisibility(View.GONE);
-            }
-            text1.setText(bannerText);
-            return separator;
-        }
-        return super.getView(getRealPosition(position), convertView, parent);
-    }
-
-    /**
-     * Forces navigation to skip over the separator
-     */
-    @Override
-    public boolean areAllItemsEnabled() {
-        return false;
-    }
-
-    /**
-     * Forces navigation to skip over the separator
-     */
-    @Override
-    public boolean isEnabled(int position) {
-        return position != getSeparatorPosition();
-    }
-
-    /**
-     * Adjusts list count to include separator
-     */
-    @Override
-    public int getCount() {
-        int count = super.getCount();
-        if (getSeparatorPosition() != ListView.INVALID_POSITION) {
-            // Increment for separator, if we have anything to show.
-            count += 1;
-        }
-        return count;
-    }
-
-    /**
-     * Converts list position to cursor position
-     */
-    private int getRealPosition(int pos) {
-        int separatorPosition = getSeparatorPosition();
-        if (separatorPosition == ListView.INVALID_POSITION) {
-            // No separator, identity map
-            return pos;
-        } else if (pos <= separatorPosition) {
-            // Before or at the separator, identity map
-            return pos;
-        } else {
-            // After the separator, remove 1 from the pos to get the real underlying pos
-            return pos - 1;
-        }
-    }
-
-    /**
-     * Returns the item using external position numbering (no separator)
-     */
-    @Override
-    public Object getItem(int pos) {
-        return super.getItem(getRealPosition(pos));
-    }
-
-    /**
-     * Returns the item id using external position numbering (no separator)
-     */
-    @Override
-    public long getItemId(int pos) {
-        if (pos == getSeparatorPosition()) {
-            return View.NO_ID;
-        }
-        return super.getItemId(getRealPosition(pos));
-    }
-
-    /**
-     * Lightweight override of MergeCursor.  Synchronizes "mClosed" / "isClosed()" so we
-     * can safely check if it has been closed, in the threading jumble of our adapter.
-     * Also holds the separator position, so it can be tracked with the cursor itself and avoid
-     * errors when multiple cursors are in flight.
-     */
-    private static class MyMergeCursor extends MergeCursor {
-
-        private int mSeparatorPosition;
-
-        public MyMergeCursor(Cursor[] cursors) {
-            super(cursors);
-            mClosed = false;
-            mSeparatorPosition = ListView.INVALID_POSITION;
-        }
-
-        @Override
-        public synchronized void close() {
-            super.close();
-            if (DEBUG_GAL_LOG) {
-                Log.d(Email.LOG_TAG, "Closing MyMergeCursor");
-            }
-        }
-
-        @Override
-        public synchronized boolean isClosed() {
-            return super.isClosed();
-        }
-
-        void setSeparatorPosition(int newPos) {
-            mSeparatorPosition = newPos;
-        }
-
-        int getSeparatorPosition() {
-            return mSeparatorPosition;
-        }
-    }
-}
diff --git a/src/com/android/exchange/provider/GalResult.java b/src/com/android/exchange/provider/GalResult.java
index 3700622..e7f2477 100644
--- a/src/com/android/exchange/provider/GalResult.java
+++ b/src/com/android/exchange/provider/GalResult.java
@@ -15,6 +15,8 @@
 
 package com.android.exchange.provider;
 
+import com.android.email.mail.PackedString;
+
 import java.util.ArrayList;
 
 /**
@@ -29,19 +31,64 @@
     public GalResult() {
     }
 
+    /**
+     * Legacy method for email address autocomplete
+     */
     public void addGalData(long id, String displayName, String emailAddress) {
         galData.add(new GalData(id, displayName, emailAddress));
     }
 
-    public static class GalData {
-        final long _id;
-        final String displayName;
-        final String emailAddress;
+    public void addGalData(GalData data) {
+        galData.add(data);
+    }
 
+    public static class GalData {
+        // PackedString constants for GalData
+        public static final String ID = "_id";
+        public static final String DISPLAY_NAME = "displayName";
+        public static final String EMAIL_ADDRESS = "emailAddress";
+        public static final String WORK_PHONE = "workPhone";
+        public static final String HOME_PHONE = "homePhone";
+        public static final String MOBILE_PHONE = "mobilePhone";
+        public static final String FIRST_NAME = "firstName";
+        public static final String LAST_NAME = "lastName";
+        public static final String COMPANY = "company";
+        public static final String TITLE = "title";
+        public static final String OFFICE = "office";
+        public static final String ALIAS = "alias";
+        // The Builder we use to construct the PackedString
+        PackedString.Builder builder = new PackedString.Builder();
+
+        // The following three fields are for legacy email autocomplete
+        public long _id = 0;
+        public String displayName;
+        public String emailAddress;
+
+        /**
+         * Legacy constructor for email address autocomplete
+         */
         private GalData(long id, String _displayName, String _emailAddress) {
+            put(ID, Long.toString(id));
             _id = id;
+            put(DISPLAY_NAME, _displayName);
             displayName = _displayName;
+            put(EMAIL_ADDRESS, _emailAddress);
             emailAddress = _emailAddress;
         }
+
+        public GalData() {
+        }
+
+        public String get(String field) {
+            return builder.get(field);
+        }
+
+        public void put(String field, String value) {
+            builder.put(field, value);
+        }
+
+        public String toPackedString() {
+            return builder.toString();
+        }
     }
 }
diff --git a/src/com/android/exchange/utility/CalendarUtilities.java b/src/com/android/exchange/utility/CalendarUtilities.java
index 7dd24e0..e1879f6 100644
--- a/src/com/android/exchange/utility/CalendarUtilities.java
+++ b/src/com/android/exchange/utility/CalendarUtilities.java
@@ -18,6 +18,7 @@
 
 import com.android.email.Email;
 import com.android.email.R;
+import com.android.email.ResourceHelper;
 import com.android.email.Utility;
 import com.android.email.mail.Address;
 import com.android.email.provider.EmailContent;
@@ -27,7 +28,7 @@
 import com.android.email.provider.EmailContent.Message;
 import com.android.exchange.Eas;
 import com.android.exchange.EasSyncService;
-import com.android.exchange.SyncManager;
+import com.android.exchange.ExchangeService;
 import com.android.exchange.adapter.Serializer;
 import com.android.exchange.adapter.Tags;
 
@@ -325,7 +326,8 @@
         String tziString = sTziStringCache.get(tz);
         if (tziString != null) {
             if (Eas.USER_LOG) {
-                SyncManager.log(TAG, "TZI string for " + tz.getDisplayName() + " found in cache.");
+                ExchangeService.log(TAG, "TZI string for " + tz.getDisplayName() +
+                        " found in cache.");
             }
             return tziString;
         }
@@ -698,14 +700,14 @@
         TimeZone timeZone = sTimeZoneCache.get(timeZoneString);
         if (timeZone != null) {
             if (Eas.USER_LOG) {
-                SyncManager.log(TAG, " Using cached TimeZone " + timeZone.getDisplayName());
+                ExchangeService.log(TAG, " Using cached TimeZone " + timeZone.getDisplayName());
             }
         } else {
             timeZone = tziStringToTimeZoneImpl(timeZoneString);
             if (timeZone == null) {
                 // If we don't find a match, we just return the current TimeZone.  In theory, this
                 // shouldn't be happening...
-                SyncManager.alwaysLog("TimeZone not found using default: " + timeZoneString);
+                ExchangeService.alwaysLog("TimeZone not found using default: " + timeZoneString);
                 timeZone = TimeZone.getDefault();
             }
             sTimeZoneCache.put(timeZoneString, timeZone);
@@ -743,7 +745,7 @@
                 // is the offset, and we know that all of the zoneId's match; we'll take the first
                 timeZone = TimeZone.getTimeZone(zoneIds[0]);
                 if (Eas.USER_LOG) {
-                    SyncManager.log(TAG, "TimeZone without DST found by offset: " +
+                    ExchangeService.log(TAG, "TimeZone without DST found by offset: " +
                             timeZone.getDisplayName());
                 }
                 return timeZone;
@@ -790,7 +792,8 @@
                 // is the offset, and we know that all of the zoneId's match; we'll take the first
                 timeZone = TimeZone.getTimeZone(zoneIds[0]);
                 if (Eas.USER_LOG) {
-                    SyncManager.log(TAG, "No TimeZone with correct DST settings; using first: " +
+                    ExchangeService.log(TAG,
+                            "No TimeZone with correct DST settings; using first: " +
                             timeZone.getDisplayName());
                 }
                 return timeZone;
@@ -929,6 +932,7 @@
         toCalendar.set(fromCalendar.get(GregorianCalendar.YEAR),
                 fromCalendar.get(GregorianCalendar.MONTH),
                 fromCalendar.get(GregorianCalendar.DATE), 0, 0, 0);
+        toCalendar.set(GregorianCalendar.MILLISECOND, 0);
         return toCalendar.getTimeInMillis();
     }
 
@@ -1013,15 +1017,40 @@
     }
 
     /**
-     * Convenience method to add "until" to an EAS calendar stream
+     * Convenience method to add "count", "interval", and "until" to an EAS calendar stream
+     * According to EAS docs, OCCURRENCES must always come before INTERVAL
      */
-    static void addUntil(String rrule, Serializer s) throws IOException {
+    static private void addCountIntervalAndUntil(String rrule, Serializer s) throws IOException {
+        String count = tokenFromRrule(rrule, "COUNT=");
+        if (count != null) {
+            s.data(Tags.CALENDAR_RECURRENCE_OCCURRENCES, count);
+        }
+        String interval = tokenFromRrule(rrule, "INTERVAL=");
+        if (interval != null) {
+            s.data(Tags.CALENDAR_RECURRENCE_INTERVAL, interval);
+        }
         String until = tokenFromRrule(rrule, "UNTIL=");
         if (until != null) {
             s.data(Tags.CALENDAR_RECURRENCE_UNTIL, recurrenceUntilToEasUntil(until));
         }
     }
 
+    static private void addByDay(String byDay, Serializer s) throws IOException {
+        // This can be 1WE (1st Wednesday) or -1FR (last Friday)
+        int weekOfMonth = byDay.charAt(0);
+        String bareByDay;
+        if (weekOfMonth == '-') {
+            // -1 is the only legal case (last week) Use "5" for EAS
+            weekOfMonth = 5;
+            bareByDay = byDay.substring(2);
+        } else {
+            weekOfMonth = weekOfMonth - '0';
+            bareByDay = byDay.substring(1);
+        }
+        s.data(Tags.CALENDAR_RECURRENCE_WEEKOFMONTH, Integer.toString(weekOfMonth));
+        s.data(Tags.CALENDAR_RECURRENCE_DAYOFWEEK, generateEasDayOfWeek(bareByDay));
+    }
+
     /**
      * Write recurrence information to EAS based on the RRULE in CalendarProvider
      * @param rrule the RRULE, from CalendarProvider
@@ -1035,7 +1064,7 @@
     static public void recurrenceFromRrule(String rrule, long startTime, Serializer s)
             throws IOException {
         if (Eas.USER_LOG) {
-            SyncManager.log(TAG, "RRULE: " + rrule);
+            ExchangeService.log(TAG, "RRULE: " + rrule);
         }
         String freq = tokenFromRrule(rrule, "FREQ=");
         // If there's no FREQ=X, then we don't write a recurrence
@@ -1045,19 +1074,17 @@
             if (freq.equals("DAILY")) {
                 s.start(Tags.CALENDAR_RECURRENCE);
                 s.data(Tags.CALENDAR_RECURRENCE_TYPE, "0");
-                s.data(Tags.CALENDAR_RECURRENCE_INTERVAL, "1");
-                addUntil(rrule, s);
+                addCountIntervalAndUntil(rrule, s);
                 s.end();
             } else if (freq.equals("WEEKLY")) {
                 s.start(Tags.CALENDAR_RECURRENCE);
                 s.data(Tags.CALENDAR_RECURRENCE_TYPE, "1");
-                s.data(Tags.CALENDAR_RECURRENCE_INTERVAL, "1");
                 // Requires a day of week (whereas RRULE does not)
+                addCountIntervalAndUntil(rrule, s);
                 String byDay = tokenFromRrule(rrule, "BYDAY=");
                 if (byDay != null) {
                     s.data(Tags.CALENDAR_RECURRENCE_DAYOFWEEK, generateEasDayOfWeek(byDay));
                 }
-                addUntil(rrule, s);
                 s.end();
             } else if (freq.equals("MONTHLY")) {
                 String byMonthDay = tokenFromRrule(rrule, "BYMONTHDAY=");
@@ -1065,35 +1092,24 @@
                     // The nth day of the month
                     s.start(Tags.CALENDAR_RECURRENCE);
                     s.data(Tags.CALENDAR_RECURRENCE_TYPE, "2");
+                    addCountIntervalAndUntil(rrule, s);
                     s.data(Tags.CALENDAR_RECURRENCE_DAYOFMONTH, byMonthDay);
-                    addUntil(rrule, s);
                     s.end();
                 } else {
                     String byDay = tokenFromRrule(rrule, "BYDAY=");
-                    String bareByDay;
                     if (byDay != null) {
-                        // This can be 1WE (1st Wednesday) or -1FR (last Friday)
-                        int wom = byDay.charAt(0);
-                        if (wom == '-') {
-                            // -1 is the only legal case (last week) Use "5" for EAS
-                            wom = 5;
-                            bareByDay = byDay.substring(2);
-                        } else {
-                            wom = wom - '0';
-                            bareByDay = byDay.substring(1);
-                        }
                         s.start(Tags.CALENDAR_RECURRENCE);
                         s.data(Tags.CALENDAR_RECURRENCE_TYPE, "3");
-                        s.data(Tags.CALENDAR_RECURRENCE_WEEKOFMONTH, Integer.toString(wom));
-                        s.data(Tags.CALENDAR_RECURRENCE_DAYOFWEEK, generateEasDayOfWeek(bareByDay));
-                        addUntil(rrule, s);
+                        addCountIntervalAndUntil(rrule, s);
+                        addByDay(byDay, s);
                         s.end();
                     }
                 }
             } else if (freq.equals("YEARLY")) {
                 String byMonth = tokenFromRrule(rrule, "BYMONTH=");
                 String byMonthDay = tokenFromRrule(rrule, "BYMONTHDAY=");
-                if (byMonth == null || byMonthDay == null) {
+                String byDay = tokenFromRrule(rrule, "BYDAY=");
+                if (byMonth == null && byMonthDay == null) {
                     // Calculate the month and day from the startDate
                     GregorianCalendar cal = new GregorianCalendar();
                     cal.setTimeInMillis(startTime);
@@ -1101,12 +1117,19 @@
                     byMonth = Integer.toString(cal.get(Calendar.MONTH) + 1);
                     byMonthDay = Integer.toString(cal.get(Calendar.DAY_OF_MONTH));
                 }
-                s.start(Tags.CALENDAR_RECURRENCE);
-                s.data(Tags.CALENDAR_RECURRENCE_TYPE, "5");
-                s.data(Tags.CALENDAR_RECURRENCE_DAYOFMONTH, byMonthDay);
-                s.data(Tags.CALENDAR_RECURRENCE_MONTHOFYEAR, byMonth);
-                addUntil(rrule, s);
-                s.end();
+                if (byMonth != null && (byMonthDay != null || byDay != null)) {
+                    s.start(Tags.CALENDAR_RECURRENCE);
+                    s.data(Tags.CALENDAR_RECURRENCE_TYPE, byDay == null ? "5" : "6");
+                    addCountIntervalAndUntil(rrule, s);
+                    s.data(Tags.CALENDAR_RECURRENCE_MONTHOFYEAR, byMonth);
+                    // Note that both byMonthDay and byDay can't be true in a valid RRULE
+                    if (byMonthDay != null) {
+                        s.data(Tags.CALENDAR_RECURRENCE_DAYOFMONTH, byMonthDay);
+                    } else {
+                        addByDay(byDay, s);
+                    }
+                    s.end();
+                }
             }
         }
     }
@@ -1128,12 +1151,12 @@
         StringBuilder rrule = new StringBuilder("FREQ=" + sTypeToFreq[type]);
 
         // INTERVAL and COUNT
-        if (interval > 0) {
-            rrule.append(";INTERVAL=" + interval);
-        }
         if (occurrences > 0) {
             rrule.append(";COUNT=" + occurrences);
         }
+        if (interval > 0) {
+            rrule.append(";INTERVAL=" + interval);
+        }
 
         // Days, weeks, months, etc.
         switch(type) {
@@ -1188,13 +1211,13 @@
         cv.put(Calendars._SYNC_ACCOUNT_TYPE, Email.EXCHANGE_ACCOUNT_MANAGER_TYPE);
         cv.put(Calendars.SYNC_EVENTS, 1);
         cv.put(Calendars.SELECTED, 1);
-        cv.put(Calendars.HIDDEN, 0);
         // Don't show attendee status if we're the organizer
         cv.put(Calendars.ORGANIZER_CAN_RESPOND, 0);
 
         // TODO Coordinate account colors w/ Calendar, if possible
         // Make Email account color opaque
-        cv.put(Calendars.COLOR, 0xFF000000 | Email.getAccountColor(account.mId));
+        int color = ResourceHelper.getInstance(service.mContext).getAccountColor(account.mId);
+        cv.put(Calendars.COLOR, color);
         cv.put(Calendars.TIMEZONE, Time.getCurrentTimezone());
         cv.put(Calendars.ACCESS_LEVEL, Calendars.OWNER_ACCESS);
         cv.put(Calendars.OWNER_ACCOUNT, account.mEmailAddress);
diff --git a/tests/src/com/android/exchange/CalendarSyncEnablerTest.java b/tests/src/com/android/exchange/CalendarSyncEnablerTest.java
index 16b9b28..b244c93 100644
--- a/tests/src/com/android/exchange/CalendarSyncEnablerTest.java
+++ b/tests/src/com/android/exchange/CalendarSyncEnablerTest.java
@@ -18,7 +18,7 @@
 
 import com.android.email.AccountTestCase;
 import com.android.email.Email;
-import com.android.email.service.MailService;
+import com.android.email.NotificationController;
 
 import android.accounts.Account;
 import android.accounts.AccountManager;
@@ -193,7 +193,9 @@
         enabler.showNotification("a@b.com");
 
         // Remove the notification.  Comment it out when you want to know how it looks like.
+        // TODO If NotificationController supports this notification, we can just mock it out
+        // and remove this code.
         ((NotificationManager) getContext().getSystemService(Context.NOTIFICATION_SERVICE))
-                .cancel(MailService.NOTIFICATION_ID_EXCHANGE_CALENDAR_ADDED);
+                .cancel(NotificationController.NOTIFICATION_ID_EXCHANGE_CALENDAR_ADDED);
     }
 }
diff --git a/tests/src/com/android/exchange/EasOutboxServiceTests.java b/tests/src/com/android/exchange/EasOutboxServiceTests.java
new file mode 100644
index 0000000..c13327a
--- /dev/null
+++ b/tests/src/com/android/exchange/EasOutboxServiceTests.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.exchange;
+
+import com.android.email.provider.EmailContent.Mailbox;
+
+import android.content.Context;
+import android.test.AndroidTestCase;
+
+/**
+ * You can run this entire test case with:
+ *   runtest -c com.android.exchange.EasOutboxServiceTests email
+ */
+
+public class EasOutboxServiceTests extends AndroidTestCase {
+
+    Context mMockContext;
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        mMockContext = getContext();
+    }
+
+    public void testGenerateSmartSendCmd() {
+        EasOutboxService svc = new EasOutboxService(mMockContext, new Mailbox());
+        // Test encoding of collection id; colon should be preserved
+        String cmd = svc.generateSmartSendCmd(true, "1339085683659694034", "Mail:^f");
+        assertEquals("SmartReply&ItemId=1339085683659694034&CollectionId=Mail:%5Ef", cmd);
+        // Test encoding of item id
+        cmd = svc.generateSmartSendCmd(false, "14:&3", "6");
+        assertEquals("SmartForward&ItemId=14:%263&CollectionId=6", cmd);
+    }
+}
diff --git a/tests/src/com/android/exchange/EasSyncServiceTests.java b/tests/src/com/android/exchange/EasSyncServiceTests.java
index 5a6e649..04c41de 100644
--- a/tests/src/com/android/exchange/EasSyncServiceTests.java
+++ b/tests/src/com/android/exchange/EasSyncServiceTests.java
@@ -25,15 +25,22 @@
 
 import android.content.Context;
 import android.test.AndroidTestCase;
+import android.util.Base64;
 
 import java.io.File;
 import java.io.IOException;
 
 /**
  * You can run this entire test case with:
- * runtest -c com.android.exchange.EasSyncServiceTests email
+ *   runtest -c com.android.exchange.EasSyncServiceTests email
  */
- public class EasSyncServiceTests extends AndroidTestCase {
+
+public class EasSyncServiceTests extends AndroidTestCase {
+    static private final String USER = "user";
+    static private final String PASSWORD = "password";
+    static private final String HOST = "xxx.host.zzz";
+    static private final String ID = "id";
+
     Context mMockContext;
 
     @Override
@@ -84,6 +91,86 @@
         }
     }
 
+    public void testAddHeaders() {
+        HttpRequestBase method = new HttpPost();
+        EasSyncService svc = new EasSyncService();
+        svc.mAuthString = "auth";
+        svc.mProtocolVersion = "12.1";
+        svc.mAccount = null;
+        // With second argument false, there should be no header
+        svc.setHeaders(method, false);
+        Header[] headers = method.getHeaders("X-MS-PolicyKey");
+        assertEquals(0, headers.length);
+        // With second argument true, there should always be a header
+        // The value will be "0" without an account
+        method.removeHeaders("X-MS-PolicyKey");
+        svc.setHeaders(method, true);
+        headers = method.getHeaders("X-MS-PolicyKey");
+        assertEquals(1, headers.length);
+        assertEquals("0", headers[0].getValue());
+        // With an account, but null security key, the header's value should be "0"
+        Account account = new Account();
+        account.mSecuritySyncKey = null;
+        svc.mAccount = account;
+        method.removeHeaders("X-MS-PolicyKey");
+        svc.setHeaders(method, true);
+        headers = method.getHeaders("X-MS-PolicyKey");
+        assertEquals(1, headers.length);
+        assertEquals("0", headers[0].getValue());
+        // With an account and security key, the header's value should be the security key
+        account.mSecuritySyncKey = "key";
+        svc.mAccount = account;
+        method.removeHeaders("X-MS-PolicyKey");
+        svc.setHeaders(method, true);
+        headers = method.getHeaders("X-MS-PolicyKey");
+        assertEquals(1, headers.length);
+        assertEquals("key", headers[0].getValue());
+    }
+
+    public void testGetProtocolVersionDouble() {
+        assertEquals(Eas.SUPPORTED_PROTOCOL_EX2003_DOUBLE,
+                Eas.getProtocolVersionDouble(Eas.SUPPORTED_PROTOCOL_EX2003));
+        assertEquals(Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE,
+                Eas.getProtocolVersionDouble(Eas.SUPPORTED_PROTOCOL_EX2007));
+        assertEquals(Eas.SUPPORTED_PROTOCOL_EX2007_SP1_DOUBLE,
+                Eas.getProtocolVersionDouble(Eas.SUPPORTED_PROTOCOL_EX2007_SP1));
+    }
+
+    private EasSyncService setupService(String user) {
+        EasSyncService svc = new EasSyncService();
+        svc.mUserName = user;
+        svc.mPassword = PASSWORD;
+        svc.mDeviceId = ID;
+        svc.mHostAddress = HOST;
+        return svc;
+    }
+
+    public void testMakeUriString() throws IOException {
+        // Simple user name and command
+        EasSyncService svc = setupService(USER);
+        String uriString = svc.makeUriString("OPTIONS", null);
+        // These next two should now be cached
+        assertNotNull(svc.mAuthString);
+        assertNotNull(svc.mCmdString);
+        assertEquals("Basic " + Base64.encodeToString((USER+":"+PASSWORD).getBytes(),
+                Base64.NO_WRAP), svc.mAuthString);
+        assertEquals("&User=" + USER + "&DeviceId=" + ID + "&DeviceType=" +
+                EasSyncService.DEVICE_TYPE, svc.mCmdString);
+        assertEquals("https://" + HOST + "/Microsoft-Server-ActiveSync?Cmd=OPTIONS" +
+                svc.mCmdString, uriString);
+        // User name that requires encoding
+        String user = "name_with_underscore@foo%bar.com";
+        svc = setupService(user);
+        uriString = svc.makeUriString("OPTIONS", null);
+        assertEquals("Basic " + Base64.encodeToString((user+":"+PASSWORD).getBytes(),
+                Base64.NO_WRAP), svc.mAuthString);
+        String safeUserName = "name_with_underscore%40foo%25bar.com";
+        assertEquals("&User=" + safeUserName + "&DeviceId=" + ID + "&DeviceType=" +
+                EasSyncService.DEVICE_TYPE, svc.mCmdString);
+        assertEquals("https://" + HOST + "/Microsoft-Server-ActiveSync?Cmd=OPTIONS" +
+                svc.mCmdString, uriString);
+    }
+
     public void testResetHeartbeats() {
         EasSyncService svc = new EasSyncService();
         // Test case in which the minimum and force heartbeats need to come up
@@ -125,41 +212,4 @@
         assertEquals(100, svc.mPingForceHeartbeat);
         assertFalse(svc.mPingHeartbeatDropped);
     }
-
-    public void testAddHeaders() {
-        HttpRequestBase method = new HttpPost();
-        EasSyncService svc = new EasSyncService();
-        svc.mAuthString = "auth";
-        svc.mProtocolVersion = "12.1";
-        svc.mDeviceType = "android";
-        svc.mAccount = null;
-        // With second argument false, there should be no header
-        svc.setHeaders(method, false);
-        Header[] headers = method.getHeaders("X-MS-PolicyKey");
-        assertEquals(0, headers.length);
-        // With second argument true, there should always be a header
-        // The value will be "0" without an account
-        method.removeHeaders("X-MS-PolicyKey");
-        svc.setHeaders(method, true);
-        headers = method.getHeaders("X-MS-PolicyKey");
-        assertEquals(1, headers.length);
-        assertEquals("0", headers[0].getValue());
-        // With an account, but null security key, the header's value should be "0"
-        Account account = new Account();
-        account.mSecuritySyncKey = null;
-        svc.mAccount = account;
-        method.removeHeaders("X-MS-PolicyKey");
-        svc.setHeaders(method, true);
-        headers = method.getHeaders("X-MS-PolicyKey");
-        assertEquals(1, headers.length);
-        assertEquals("0", headers[0].getValue());
-        // With an account and security key, the header's value should be the security key
-        account.mSecuritySyncKey = "key";
-        svc.mAccount = account;
-        method.removeHeaders("X-MS-PolicyKey");
-        svc.setHeaders(method, true);
-        headers = method.getHeaders("X-MS-PolicyKey");
-        assertEquals(1, headers.length);
-        assertEquals("key", headers[0].getValue());
-    }
 }
diff --git a/tests/src/com/android/exchange/ExchangeServiceAccountTests.java b/tests/src/com/android/exchange/ExchangeServiceAccountTests.java
new file mode 100644
index 0000000..9b92726
--- /dev/null
+++ b/tests/src/com/android/exchange/ExchangeServiceAccountTests.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2009 Marc Blank
+ * Licensed to The Android Open Source Project.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.exchange;
+
+import com.android.email.AccountTestCase;
+import com.android.email.provider.EmailProvider;
+import com.android.email.provider.ProviderTestUtils;
+import com.android.email.provider.EmailContent.Account;
+import com.android.email.provider.EmailContent.Mailbox;
+import com.android.exchange.ExchangeService.SyncError;
+
+import android.content.Context;
+
+import java.util.HashMap;
+
+/**
+ * You can run this entire test case with:
+ *   runtest -c com.android.exchange.ExchangeServiceAccountTests email
+ */
+public class ExchangeServiceAccountTests extends AccountTestCase {
+
+    EmailProvider mProvider;
+    Context mMockContext;
+
+    public ExchangeServiceAccountTests() {
+        super();
+    }
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        mMockContext = getMockContext();
+    }
+
+    public void testReleaseSyncHolds() {
+        Context context = mMockContext;
+        ExchangeService exchangeService = new ExchangeService();
+        SyncError securityErrorAccount1 =
+            exchangeService.new SyncError(AbstractSyncService.EXIT_SECURITY_FAILURE, false);
+        SyncError ioError =
+            exchangeService.new SyncError(AbstractSyncService.EXIT_IO_ERROR, false);
+        SyncError securityErrorAccount2 =
+            exchangeService.new SyncError(AbstractSyncService.EXIT_SECURITY_FAILURE, false);
+        // Create account and two mailboxes
+        Account acct1 = ProviderTestUtils.setupAccount("acct1", true, context);
+        Mailbox box1 = ProviderTestUtils.setupMailbox("box1", acct1.mId, true, context);
+        Mailbox box2 = ProviderTestUtils.setupMailbox("box2", acct1.mId, true, context);
+        Account acct2 = ProviderTestUtils.setupAccount("acct2", true, context);
+        Mailbox box3 = ProviderTestUtils.setupMailbox("box3", acct2.mId, true, context);
+        Mailbox box4 = ProviderTestUtils.setupMailbox("box4", acct2.mId, true, context);
+
+        HashMap<Long, SyncError> errorMap = exchangeService.mSyncErrorMap;
+        // Add errors into the map
+        errorMap.put(box1.mId, securityErrorAccount1);
+        errorMap.put(box2.mId, ioError);
+        errorMap.put(box3.mId, securityErrorAccount2);
+        errorMap.put(box4.mId, securityErrorAccount2);
+        // We should have 4
+        assertEquals(4, errorMap.keySet().size());
+        // Release the holds on acct2 (there are two of them)
+        assertTrue(exchangeService.releaseSyncHolds(context,
+                AbstractSyncService.EXIT_SECURITY_FAILURE, acct2));
+        // There should be two left
+        assertEquals(2, errorMap.keySet().size());
+        // And these are the two...
+        assertNotNull(errorMap.get(box2.mId));
+        assertNotNull(errorMap.get(box1.mId));
+
+        // Put the two back
+        errorMap.put(box3.mId, securityErrorAccount2);
+        errorMap.put(box4.mId, securityErrorAccount2);
+        // We should have 4 again
+        assertEquals(4, errorMap.keySet().size());
+        // Release all of the security holds
+        assertTrue(exchangeService.releaseSyncHolds(context,
+                AbstractSyncService.EXIT_SECURITY_FAILURE, null));
+        // There should be one left
+        assertEquals(1, errorMap.keySet().size());
+        // And this is the one
+        assertNotNull(errorMap.get(box2.mId));
+
+        // Release the i/o holds on account 2 (there aren't any)
+        assertFalse(exchangeService.releaseSyncHolds(context,
+                AbstractSyncService.EXIT_IO_ERROR, acct2));
+        // There should still be one left
+        assertEquals(1, errorMap.keySet().size());
+
+        // Release the i/o holds on account 1 (there's one)
+        assertTrue(exchangeService.releaseSyncHolds(context,
+                AbstractSyncService.EXIT_IO_ERROR, acct1));
+        // There should still be one left
+        assertEquals(0, errorMap.keySet().size());
+    }
+
+    public void testIsSyncable() {
+        Context context = mMockContext;
+        Account acct1 = ProviderTestUtils.setupAccount("acct1", true, context);
+        Mailbox box1 = ProviderTestUtils.setupMailbox("box1", acct1.mId, true, context,
+                Mailbox.TYPE_DRAFTS);
+        Mailbox box2 = ProviderTestUtils.setupMailbox("box2", acct1.mId, true, context,
+                Mailbox.TYPE_OUTBOX);
+        Mailbox box3 = ProviderTestUtils.setupMailbox("box2", acct1.mId, true, context,
+                Mailbox.TYPE_ATTACHMENT);
+        Mailbox box4 = ProviderTestUtils.setupMailbox("box2", acct1.mId, true, context,
+                Mailbox.TYPE_NOT_SYNCABLE + 64);
+        Mailbox box5 = ProviderTestUtils.setupMailbox("box2", acct1.mId, true, context,
+                Mailbox.TYPE_MAIL);
+        assertFalse(ExchangeService.isSyncable(null));
+        assertFalse(ExchangeService.isSyncable(box1));
+        assertFalse(ExchangeService.isSyncable(box2));
+        assertFalse(ExchangeService.isSyncable(box3));
+        assertFalse(ExchangeService.isSyncable(box4));
+        assertTrue(ExchangeService.isSyncable(box5));
+    }
+}
diff --git a/tests/src/com/android/exchange/SyncManagerTest.java b/tests/src/com/android/exchange/ExchangeServiceTest.java
similarity index 89%
rename from tests/src/com/android/exchange/SyncManagerTest.java
rename to tests/src/com/android/exchange/ExchangeServiceTest.java
index 0c4ecf1..98703c2 100644
--- a/tests/src/com/android/exchange/SyncManagerTest.java
+++ b/tests/src/com/android/exchange/ExchangeServiceTest.java
@@ -22,7 +22,7 @@
 
 import java.io.File;
 
-public class SyncManagerTest extends AndroidTestCase {
+public class ExchangeServiceTest extends AndroidTestCase {
     private static class MyContext extends ContextWrapper {
         public boolean isGetFileStreamPathCalled;
 
@@ -40,7 +40,7 @@
     public void testGetDeviceId() throws Exception {
         final MyContext context = new MyContext(getContext());
 
-        final String id = SyncManager.getDeviceId(context);
+        final String id = ExchangeService.getDeviceId(context);
 
         // Consists of alpha-numeric
         assertTrue(id.matches("^[a-zA-Z0-9]+$"));
@@ -49,7 +49,7 @@
         // isGetFileStreamPathCalled here.
 
         context.isGetFileStreamPathCalled = false;
-        final String cachedId = SyncManager.getDeviceId(context);
+        final String cachedId = ExchangeService.getDeviceId(context);
 
         // Should be the same.
         assertEquals(id, cachedId);
diff --git a/tests/src/com/android/exchange/SyncManagerAccountTests.java b/tests/src/com/android/exchange/SyncManagerAccountTests.java
deleted file mode 100644
index 3b67dc1..0000000
--- a/tests/src/com/android/exchange/SyncManagerAccountTests.java
+++ /dev/null
@@ -1,214 +0,0 @@
-/*
- * Copyright (C) 2009 Marc Blank
- * Licensed to The Android Open Source Project.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.exchange;
-
-import com.android.email.AccountTestCase;
-import com.android.email.Email;
-import com.android.email.provider.EmailContent;
-import com.android.email.provider.EmailProvider;
-import com.android.email.provider.ProviderTestUtils;
-import com.android.email.provider.EmailContent.Account;
-import com.android.email.provider.EmailContent.Mailbox;
-import com.android.exchange.SyncManager.SyncError;
-
-import android.accounts.AccountManager;
-import android.content.ContentResolver;
-import android.content.ContentUris;
-import android.content.Context;
-
-import java.util.HashMap;
-
-/**
- * You can run this entire test case with:
- *   runtest -c com.android.exchange.SyncManagerAccountTests email
- */
-public class SyncManagerAccountTests extends AccountTestCase {
-
-    EmailProvider mProvider;
-    Context mMockContext;
-
-    public SyncManagerAccountTests() {
-        super();
-    }
-
-    @Override
-    public void setUp() throws Exception {
-        super.setUp();
-        mMockContext = getMockContext();
-        // Delete any test accounts we might have created earlier
-        deleteTemporaryAccountManagerAccounts();
-    }
-
-    @Override
-    public void tearDown() throws Exception {
-        super.tearDown();
-        // Delete any test accounts we might have created earlier
-        deleteTemporaryAccountManagerAccounts();
-    }
-
-    /**
-     * Confirm that the test below is functional (and non-destructive) when there are
-     * prexisting (non-test) accounts in the account manager.
-     */
-    public void testTestReconcileAccounts() {
-        Account firstAccount = null;
-        final String TEST_USER_ACCOUNT = "__user_account_test_1";
-        Context context = getContext();
-        try {
-            // Note:  Unlike calls to setupProviderAndAccountManagerAccount(), we are creating
-            // *real* accounts here (not in the mock provider)
-            createAccountManagerAccount(TEST_USER_ACCOUNT + TEST_ACCOUNT_SUFFIX);
-            firstAccount = ProviderTestUtils.setupAccount(TEST_USER_ACCOUNT, true, context);
-            // Now run the test with the "user" accounts in place
-            testReconcileAccounts();
-        } finally {
-            if (firstAccount != null) {
-                boolean firstAccountFound = false;
-                // delete the provider account
-                context.getContentResolver().delete(firstAccount.getUri(), null, null);
-                // delete the account manager account
-                android.accounts.Account[] accountManagerAccounts = AccountManager.get(context)
-                        .getAccountsByType(Email.EXCHANGE_ACCOUNT_MANAGER_TYPE);
-                for (android.accounts.Account accountManagerAccount: accountManagerAccounts) {
-                    if ((TEST_USER_ACCOUNT + TEST_ACCOUNT_SUFFIX)
-                            .equals(accountManagerAccount.name)) {
-                        deleteAccountManagerAccount(accountManagerAccount);
-                        firstAccountFound = true;
-                    }
-                }
-                assertTrue(firstAccountFound);
-            }
-        }
-    }
-
-    /**
-     * Note, there is some inherent risk in this test, as it creates *real* accounts in the
-     * system (it cannot use the mock context with the Account Manager).
-     */
-    public void testReconcileAccounts() {
-        // Note that we can't use mMockContext for AccountManager interactions, as it isn't a fully
-        // functional Context.
-        Context context = getContext();
-
-        // Capture the baseline (account manager accounts) so we can measure the changes
-        // we're making, irrespective of the number of actual accounts, and not destroy them
-        android.accounts.Account[] baselineAccounts =
-            AccountManager.get(context).getAccountsByType(Email.EXCHANGE_ACCOUNT_MANAGER_TYPE);
-
-        // Set up three accounts, both in AccountManager and in EmailProvider
-        Account firstAccount = setupProviderAndAccountManagerAccount(getTestAccountName("1"));
-        setupProviderAndAccountManagerAccount(getTestAccountName("2"));
-        setupProviderAndAccountManagerAccount(getTestAccountName("3"));
-
-        // Check that they're set up properly
-        assertEquals(3, EmailContent.count(mMockContext, Account.CONTENT_URI, null, null));
-        android.accounts.Account[] accountManagerAccounts =
-                getAccountManagerAccounts(baselineAccounts);
-        assertEquals(3, accountManagerAccounts.length);
-
-        // Delete account "2" from AccountManager
-        android.accounts.Account removedAccount =
-            makeAccountManagerAccount(getTestAccountEmailAddress("2"));
-        deleteAccountManagerAccount(removedAccount);
-
-        // Confirm it's deleted
-        accountManagerAccounts = getAccountManagerAccounts(baselineAccounts);
-        assertEquals(2, accountManagerAccounts.length);
-
-        // Run the reconciler
-        ContentResolver resolver = mMockContext.getContentResolver();
-        SyncManager.reconcileAccountsWithAccountManager(context,
-                makeSyncManagerAccountList(), accountManagerAccounts, true, resolver);
-
-        // There should now be only two EmailProvider accounts
-        assertEquals(2, EmailContent.count(mMockContext, Account.CONTENT_URI, null, null));
-
-        // Ok, now we've got two of each; let's delete a provider account
-        resolver.delete(ContentUris.withAppendedId(Account.CONTENT_URI, firstAccount.mId),
-                null, null);
-        // ...and then there was one
-        assertEquals(1, EmailContent.count(mMockContext, Account.CONTENT_URI, null, null));
-
-        // Run the reconciler
-        SyncManager.reconcileAccountsWithAccountManager(context,
-                makeSyncManagerAccountList(), accountManagerAccounts, true, resolver);
-
-        // There should now be only one AccountManager account
-        accountManagerAccounts = getAccountManagerAccounts(baselineAccounts);
-        assertEquals(1, accountManagerAccounts.length);
-        // ... and it should be account "3"
-        assertEquals(getTestAccountEmailAddress("3"), accountManagerAccounts[0].name);
-    }
-
-    public void testReleaseSyncHolds() {
-        Context context = mMockContext;
-        SyncManager syncManager = new SyncManager();
-        SyncError securityErrorAccount1 =
-            syncManager.new SyncError(AbstractSyncService.EXIT_SECURITY_FAILURE, false);
-        SyncError ioError =
-            syncManager.new SyncError(AbstractSyncService.EXIT_IO_ERROR, false);
-        SyncError securityErrorAccount2 =
-            syncManager.new SyncError(AbstractSyncService.EXIT_SECURITY_FAILURE, false);
-        // Create account and two mailboxes
-        Account acct1 = ProviderTestUtils.setupAccount("acct1", true, context);
-        Mailbox box1 = ProviderTestUtils.setupMailbox("box1", acct1.mId, true, context);
-        Mailbox box2 = ProviderTestUtils.setupMailbox("box2", acct1.mId, true, context);
-        Account acct2 = ProviderTestUtils.setupAccount("acct2", true, context);
-        Mailbox box3 = ProviderTestUtils.setupMailbox("box3", acct2.mId, true, context);
-        Mailbox box4 = ProviderTestUtils.setupMailbox("box4", acct2.mId, true, context);
-
-        HashMap<Long, SyncError> errorMap = syncManager.mSyncErrorMap;
-        // Add errors into the map
-        errorMap.put(box1.mId, securityErrorAccount1);
-        errorMap.put(box2.mId, ioError);
-        errorMap.put(box3.mId, securityErrorAccount2);
-        errorMap.put(box4.mId, securityErrorAccount2);
-        // We should have 4
-        assertEquals(4, errorMap.keySet().size());
-        // Release the holds on acct2 (there are two of them)
-        syncManager.releaseSyncHolds(context, AbstractSyncService.EXIT_SECURITY_FAILURE, acct2);
-        // There should be two left
-        assertEquals(2, errorMap.keySet().size());
-        // And these are the two...
-        assertNotNull(errorMap.get(box2.mId));
-        assertNotNull(errorMap.get(box1.mId));
-
-        // Put the two back
-        errorMap.put(box3.mId, securityErrorAccount2);
-        errorMap.put(box4.mId, securityErrorAccount2);
-        // We should have 4 again
-        assertEquals(4, errorMap.keySet().size());
-        // Release all of the security holds
-        syncManager.releaseSyncHolds(context, AbstractSyncService.EXIT_SECURITY_FAILURE, null);
-        // There should be one left
-        assertEquals(1, errorMap.keySet().size());
-        // And this is the one
-        assertNotNull(errorMap.get(box2.mId));
-
-        // Release the i/o holds on account 2 (there aren't any)
-        syncManager.releaseSyncHolds(context, AbstractSyncService.EXIT_IO_ERROR, acct2);
-        // There should still be one left
-        assertEquals(1, errorMap.keySet().size());
-
-        // Release the i/o holds on account 1 (there's one)
-        syncManager.releaseSyncHolds(context, AbstractSyncService.EXIT_IO_ERROR, acct1);
-        // There should still be one left
-        assertEquals(0, errorMap.keySet().size());
-    }
-
-}
diff --git a/tests/src/com/android/exchange/adapter/CalendarSyncAdapterTests.java b/tests/src/com/android/exchange/adapter/CalendarSyncAdapterTests.java
index eb87ccd..bffa3f8 100644
--- a/tests/src/com/android/exchange/adapter/CalendarSyncAdapterTests.java
+++ b/tests/src/com/android/exchange/adapter/CalendarSyncAdapterTests.java
@@ -16,13 +16,22 @@
 
 package com.android.exchange.adapter;
 
+import com.android.exchange.adapter.CalendarSyncAdapter.CalendarOperations;
 import com.android.exchange.adapter.CalendarSyncAdapter.EasCalendarSyncParser;
+import com.android.exchange.provider.MockProvider;
 
+import android.content.ContentProviderOperation;
 import android.content.ContentValues;
+import android.content.OperationApplicationException;
+import android.database.Cursor;
+import android.os.RemoteException;
+import android.provider.Calendar.Attendees;
 import android.provider.Calendar.Events;
 
+import java.io.ByteArrayInputStream;
 import java.io.IOException;
 import java.util.GregorianCalendar;
+import java.util.List;
 import java.util.TimeZone;
 
 /**
@@ -31,6 +40,21 @@
  */
 
 public class CalendarSyncAdapterTests extends SyncAdapterTestCase<CalendarSyncAdapter> {
+    private static final String[] ATTENDEE_PROJECTION = new String[] {Attendees.ATTENDEE_EMAIL,
+            Attendees.ATTENDEE_NAME, Attendees.ATTENDEE_STATUS};
+    private static final int ATTENDEE_EMAIL = 0;
+    private static final int ATTENDEE_NAME = 1;
+    private static final int ATTENDEE_STATUS = 2;
+
+    private static final String SINGLE_ATTENDEE_EMAIL = "attendee@host.com";
+    private static final String SINGLE_ATTENDEE_NAME = "Bill Attendee";
+
+    // This is the US/Pacific time zone as a base64-encoded TIME_ZONE_INFORMATION structure, as
+    // it would appear coming from an Exchange server
+    private static final String TEST_TIME_ZONE = "4AEAAFAAYQBjAGkAZgBpAGMAIABTAHQAYQBuAGQAYQByA" +
+        "GQAIABUAGkAbQBlAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAsAAAABAAIAAAAAAAAAAAAAAFAAY" +
+        "QBjAGkAZgBpAGMAIABEAGEAeQBsAGkAZwBoAHQAIABUAGkAbQBlAAAAAAAAAAAAAAAAAAAAAAAAA" +
+        "AAAAAAAAAMAAAACAAIAAAAAAAAAxP///w==";
 
     public CalendarSyncAdapterTests() {
         super();
@@ -173,4 +197,227 @@
         cv.put(Events.DURATION, "P1D");
         assertTrue(p.isValidEventValues(cv));
     }
+
+    private void addAttendeesToSerializer(Serializer s, int num) throws IOException {
+        for (int i = 0; i < num; i++) {
+            s.start(Tags.CALENDAR_ATTENDEE);
+            s.data(Tags.CALENDAR_ATTENDEE_EMAIL, "frederick" + num +
+                    ".flintstone@this.that.verylongservername.com");
+            s.data(Tags.CALENDAR_ATTENDEE_TYPE, "1");
+            s.data(Tags.CALENDAR_ATTENDEE_NAME, "Frederick" + num + " Flintstone, III");
+            s.end();
+        }
+    }
+
+    private void addAttendeeToSerializer(Serializer s, String email, String name)
+            throws IOException {
+        s.start(Tags.CALENDAR_ATTENDEE);
+        s.data(Tags.CALENDAR_ATTENDEE_EMAIL, email);
+        s.data(Tags.CALENDAR_ATTENDEE_TYPE, "1");
+        s.data(Tags.CALENDAR_ATTENDEE_NAME, name);
+        s.end();
+    }
+
+    private int countInsertOperationsForTable(CalendarOperations ops, String tableName) {
+        int cnt = 0;
+        for (ContentProviderOperation op: ops) {
+            List<String> segments = op.getUri().getPathSegments();
+            if (segments.get(0).equalsIgnoreCase(tableName) &&
+                    op.getType() == ContentProviderOperation.TYPE_INSERT) {
+                cnt++;
+            }
+        }
+        return cnt;
+    }
+
+    class TestEvent extends Serializer {
+        CalendarSyncAdapter mAdapter;
+        EasCalendarSyncParser mParser;
+        Serializer mSerializer;
+
+        TestEvent() throws IOException {
+            super(false);
+            mAdapter = getTestSyncAdapter(CalendarSyncAdapter.class);
+            mParser = mAdapter.new EasCalendarSyncParser(getTestInputStream(), mAdapter);
+        }
+
+        void setUserEmailAddress(String addr) {
+            mAdapter.mAccount.mEmailAddress = addr;
+            mAdapter.mEmailAddress = addr;
+        }
+
+        EasCalendarSyncParser getParser() throws IOException {
+            // Set up our parser's input and eat the initial tag
+            mParser.resetInput(new ByteArrayInputStream(toByteArray()));
+            mParser.nextTag(0);
+            return mParser;
+        }
+
+        // setupPreAttendees and setupPostAttendees initialize calendar data in the order in which
+        // they would appear in an actual EAS session.  Between these two calls, we initialize
+        // attendee data, which varies between the following tests
+        TestEvent setupPreAttendees() throws IOException {
+            start(Tags.SYNC_APPLICATION_DATA);
+            data(Tags.CALENDAR_TIME_ZONE, TEST_TIME_ZONE);
+            data(Tags.CALENDAR_DTSTAMP, "20100518T213156Z");
+            data(Tags.CALENDAR_START_TIME, "20100518T220000Z");
+            data(Tags.CALENDAR_SUBJECT, "Documentation");
+            data(Tags.CALENDAR_UID, "4417556B-27DE-4ECE-B679-A63EFE1F9E85");
+            data(Tags.CALENDAR_ORGANIZER_NAME, "Fred Squatibuquitas");
+            data(Tags.CALENDAR_ORGANIZER_EMAIL, "fred.squatibuquitas@prettylongdomainname.com");
+            return this;
+        }
+
+        TestEvent setupPostAttendees()throws IOException {
+            data(Tags.CALENDAR_LOCATION, "CR SF 601T2/North Shore Presentation Self Service (16)");
+            data(Tags.CALENDAR_END_TIME, "20100518T223000Z");
+            start(Tags.BASE_BODY);
+            data(Tags.BASE_BODY_PREFERENCE, "1");
+            data(Tags.BASE_ESTIMATED_DATA_SIZE, "69105"); // The number is ignored by the parser
+            data(Tags.BASE_DATA,
+                    "This is the event description; we should probably make it longer");
+            end(); // BASE_BODY
+            start(Tags.CALENDAR_RECURRENCE);
+            data(Tags.CALENDAR_RECURRENCE_TYPE, "1"); // weekly
+            data(Tags.CALENDAR_RECURRENCE_INTERVAL, "1");
+            data(Tags.CALENDAR_RECURRENCE_OCCURRENCES, "10");
+            data(Tags.CALENDAR_RECURRENCE_DAYOFWEEK, "12"); // tue, wed
+            data(Tags.CALENDAR_RECURRENCE_UNTIL, "2005-04-14T00:00:00.000Z");
+            end();  // CALENDAR_RECURRENCE
+            data(Tags.CALENDAR_SENSITIVITY, "0");
+            data(Tags.CALENDAR_BUSY_STATUS, "2");
+            data(Tags.CALENDAR_ALL_DAY_EVENT, "0");
+            data(Tags.CALENDAR_MEETING_STATUS, "3");
+            data(Tags.BASE_NATIVE_BODY_TYPE, "3");
+            end().done(); // SYNC_APPLICATION_DATA
+            return this;
+        }
+    }
+
+    public void testAddEvent() throws IOException {
+        TestEvent event = new TestEvent();
+        event.setupPreAttendees();
+        event.start(Tags.CALENDAR_ATTENDEES);
+        addAttendeesToSerializer(event, 10);
+        event.end(); // CALENDAR_ATTENDEES
+        event.setupPostAttendees();
+
+        EasCalendarSyncParser p = event.getParser();
+        p.addEvent(p.mOps, "1:1", false);
+        // There should be 1 event
+        assertEquals(1, countInsertOperationsForTable(p.mOps, "events"));
+        // Two attendees (organizer and 10 attendees)
+        assertEquals(11, countInsertOperationsForTable(p.mOps, "attendees"));
+        // dtstamp, meeting status, attendees, attendees redacted, and upsync prohibited
+        assertEquals(5, countInsertOperationsForTable(p.mOps, "extendedproperties"));
+    }
+
+    public void testAddEventIllegal() throws IOException {
+        // We don't send a start time; the event is illegal and nothing should be added
+        TestEvent event = new TestEvent();
+        event.start(Tags.SYNC_APPLICATION_DATA);
+        event.data(Tags.CALENDAR_TIME_ZONE, TEST_TIME_ZONE);
+        event.data(Tags.CALENDAR_DTSTAMP, "20100518T213156Z");
+        event.data(Tags.CALENDAR_SUBJECT, "Documentation");
+        event.data(Tags.CALENDAR_UID, "4417556B-27DE-4ECE-B679-A63EFE1F9E85");
+        event.data(Tags.CALENDAR_ORGANIZER_NAME, "Fred Squatibuquitas");
+        event.data(Tags.CALENDAR_ORGANIZER_EMAIL, "fred.squatibuquitas@prettylongdomainname.com");
+        event.start(Tags.CALENDAR_ATTENDEES);
+        addAttendeesToSerializer(event, 10);
+        event.end(); // CALENDAR_ATTENDEES
+        event.setupPostAttendees();
+
+        EasCalendarSyncParser p = event.getParser();
+        p.addEvent(p.mOps, "1:1", false);
+        assertEquals(0, countInsertOperationsForTable(p.mOps, "events"));
+        assertEquals(0, countInsertOperationsForTable(p.mOps, "attendees"));
+        assertEquals(0, countInsertOperationsForTable(p.mOps, "extendedproperties"));
+    }
+
+    public void testAddEventRedactedAttendees() throws IOException {
+        TestEvent event = new TestEvent();
+        event.setupPreAttendees();
+        event.start(Tags.CALENDAR_ATTENDEES);
+        addAttendeesToSerializer(event, 100);
+        event.end(); // CALENDAR_ATTENDEES
+        event.setupPostAttendees();
+
+        EasCalendarSyncParser p = event.getParser();
+        p.addEvent(p.mOps, "1:1", false);
+        // There should be 1 event
+        assertEquals(1, countInsertOperationsForTable(p.mOps, "events"));
+        // One attendees (organizer; all others are redacted)
+        assertEquals(1, countInsertOperationsForTable(p.mOps, "attendees"));
+        // dtstamp, meeting status, and attendees redacted
+        assertEquals(3, countInsertOperationsForTable(p.mOps, "extendedproperties"));
+    }
+
+    /**
+     * Setup for the following three tests, which check attendee status of an added event
+     * @param userEmail the email address of the user
+     * @param update whether or not the event is an update (rather than new)
+     * @return a Cursor to the Attendee records added to our MockProvider
+     * @throws IOException
+     * @throws RemoteException
+     * @throws OperationApplicationException
+     */
+    private Cursor setupAddEventOneAttendee(String userEmail, boolean update)
+            throws IOException, RemoteException, OperationApplicationException {
+        TestEvent event = new TestEvent();
+        event.setupPreAttendees();
+        event.start(Tags.CALENDAR_ATTENDEES);
+        addAttendeeToSerializer(event, SINGLE_ATTENDEE_EMAIL, SINGLE_ATTENDEE_NAME);
+        event.setUserEmailAddress(userEmail);
+        event.end(); // CALENDAR_ATTENDEES
+        event.setupPostAttendees();
+
+        EasCalendarSyncParser p = event.getParser();
+        p.addEvent(p.mOps, "1:1", update);
+        // Send the CPO's to the mock provider
+        mMockResolver.applyBatch(MockProvider.AUTHORITY, p.mOps);
+        return mMockResolver.query(MockProvider.uri(Attendees.CONTENT_URI), ATTENDEE_PROJECTION,
+                null, null, null);
+    }
+
+    public void testAddEventOneAttendee() throws IOException, RemoteException,
+            OperationApplicationException {
+        Cursor c = setupAddEventOneAttendee("foo@bar.com", false);
+        assertEquals(2, c.getCount());
+        // The organizer should be "accepted", the unknown attendee "none"
+        while (c.moveToNext()) {
+            if (SINGLE_ATTENDEE_EMAIL.equals(c.getString(ATTENDEE_EMAIL))) {
+                assertEquals(Attendees.ATTENDEE_STATUS_NONE, c.getInt(ATTENDEE_STATUS));
+            } else {
+                assertEquals(Attendees.ATTENDEE_STATUS_ACCEPTED, c.getInt(ATTENDEE_STATUS));
+            }
+        }
+    }
+
+    public void testAddEventSelfAttendee() throws IOException, RemoteException,
+            OperationApplicationException {
+        Cursor c = setupAddEventOneAttendee(SINGLE_ATTENDEE_EMAIL, false);
+        // The organizer should be "accepted", and our user/attendee should be "done" even though
+        // the busy status = 2 (because we can't tell from a status of 2 on new events)
+        while (c.moveToNext()) {
+            if (SINGLE_ATTENDEE_EMAIL.equals(c.getString(ATTENDEE_EMAIL))) {
+                assertEquals(Attendees.ATTENDEE_STATUS_NONE, c.getInt(ATTENDEE_STATUS));
+            } else {
+                assertEquals(Attendees.ATTENDEE_STATUS_ACCEPTED, c.getInt(ATTENDEE_STATUS));
+            }
+        }
+    }
+
+    public void testAddEventSelfAttendeeUpdate() throws IOException, RemoteException,
+            OperationApplicationException {
+        Cursor c = setupAddEventOneAttendee(SINGLE_ATTENDEE_EMAIL, true);
+        // The organizer should be "accepted", and our user/attendee should be "accepted" (because
+        // busy status = 2 and this is an update
+        while (c.moveToNext()) {
+            if (SINGLE_ATTENDEE_EMAIL.equals(c.getString(ATTENDEE_EMAIL))) {
+                assertEquals(Attendees.ATTENDEE_STATUS_ACCEPTED, c.getInt(ATTENDEE_STATUS));
+            } else {
+                assertEquals(Attendees.ATTENDEE_STATUS_ACCEPTED, c.getInt(ATTENDEE_STATUS));
+            }
+        }
+    }
 }
diff --git a/tests/src/com/android/exchange/adapter/EmailSyncAdapterTests.java b/tests/src/com/android/exchange/adapter/EmailSyncAdapterTests.java
index 7a4ea1b..e64b951 100644
--- a/tests/src/com/android/exchange/adapter/EmailSyncAdapterTests.java
+++ b/tests/src/com/android/exchange/adapter/EmailSyncAdapterTests.java
@@ -17,12 +17,12 @@
 package com.android.exchange.adapter;
 
 import com.android.email.provider.EmailContent;
-import com.android.email.provider.ProviderTestUtils;
 import com.android.email.provider.EmailContent.Account;
 import com.android.email.provider.EmailContent.Body;
 import com.android.email.provider.EmailContent.Mailbox;
 import com.android.email.provider.EmailContent.Message;
 import com.android.email.provider.EmailContent.SyncColumns;
+import com.android.email.provider.ProviderTestUtils;
 import com.android.exchange.EasSyncService;
 import com.android.exchange.adapter.EmailSyncAdapter.EasEmailSyncParser;
 import com.android.exchange.adapter.EmailSyncAdapter.EasEmailSyncParser.ServerChange;
@@ -48,7 +48,7 @@
      */
     public void testGetMimeTypeFromFileName() throws IOException {
         EasSyncService service = getTestService();
-        EmailSyncAdapter adapter = new EmailSyncAdapter(service.mMailbox, service);
+        EmailSyncAdapter adapter = new EmailSyncAdapter(service);
         EasEmailSyncParser p = adapter.new EasEmailSyncParser(getTestInputStream(), adapter);
         // Test a few known types
         String mimeType = p.getMimeTypeFromFileName("foo.jpg");
@@ -87,7 +87,7 @@
 
     public void testSendDeletedItems() throws IOException {
         EasSyncService service = getTestService();
-        EmailSyncAdapter adapter = new EmailSyncAdapter(service.mMailbox, service);
+        EmailSyncAdapter adapter = new EmailSyncAdapter(service);
         Serializer s = new Serializer();
         ArrayList<Long> ids = new ArrayList<Long>();
         ArrayList<Long> deletedIds = new ArrayList<Long>();
@@ -150,7 +150,7 @@
 
     void setupSyncParserAndAdapter(Account account, Mailbox mailbox) throws IOException {
         EasSyncService service = getTestService(account, mailbox);
-        mSyncAdapter = new EmailSyncAdapter(mailbox, service);
+        mSyncAdapter = new EmailSyncAdapter(service);
         mSyncParser = mSyncAdapter.new EasEmailSyncParser(getTestInputStream(), mSyncAdapter);
     }
 
diff --git a/tests/src/com/android/exchange/adapter/FolderSyncParserTests.java b/tests/src/com/android/exchange/adapter/FolderSyncParserTests.java
new file mode 100644
index 0000000..3122913
--- /dev/null
+++ b/tests/src/com/android/exchange/adapter/FolderSyncParserTests.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.exchange.adapter;
+
+import com.android.email.provider.EmailContent.Account;
+import com.android.email.provider.EmailContent.Mailbox;
+import com.android.email.provider.ProviderTestUtils;
+import com.android.exchange.EasSyncService;
+
+import java.io.IOException;
+import java.util.HashMap;
+
+/**
+ * You can run this entire test case with:
+ *   runtest -c com.android.exchange.adapter.FolderSyncParserTests email
+ */
+
+public class FolderSyncParserTests extends SyncAdapterTestCase<EmailSyncAdapter> {
+
+    public FolderSyncParserTests() {
+        super();
+    }
+
+    public void testIsValidMailFolder() throws IOException {
+        EasSyncService service = getTestService();
+        EmailSyncAdapter adapter = new EmailSyncAdapter(service);
+        FolderSyncParser parser = new FolderSyncParser(getTestInputStream(), adapter);
+        HashMap<String, Mailbox> mailboxMap = new HashMap<String, Mailbox>();
+        Account acct = ProviderTestUtils.setupAccount("account", true, mMockContext);
+        // The parser needs the mAccount set
+        parser.mAccount = acct;
+
+        // Don't save the box; just create it, and give it a server id
+        Mailbox boxMailType = ProviderTestUtils.setupMailbox("box1", acct.mId, false, mMockContext,
+                Mailbox.TYPE_MAIL);
+        boxMailType.mServerId = "1:1";
+        // Automatically valid since TYPE_MAIL
+        assertTrue(parser.isValidMailFolder(boxMailType, mailboxMap));
+
+        Mailbox boxCalendarType = ProviderTestUtils.setupMailbox("box", acct.mId, false,
+                mMockContext, Mailbox.TYPE_CALENDAR);
+        Mailbox boxContactsType = ProviderTestUtils.setupMailbox("box", acct.mId, false,
+                mMockContext, Mailbox.TYPE_CONTACTS);
+        Mailbox boxTasksType = ProviderTestUtils.setupMailbox("box", acct.mId, false,
+                mMockContext, Mailbox.TYPE_TASKS);
+        // Automatically invalid since TYPE_CALENDAR and TYPE_CONTACTS
+        assertFalse(parser.isValidMailFolder(boxCalendarType, mailboxMap));
+        assertFalse(parser.isValidMailFolder(boxContactsType, mailboxMap));
+        assertFalse(parser.isValidMailFolder(boxTasksType, mailboxMap));
+
+        // Unknown boxes are invalid unless they have a parent that's valid
+        Mailbox boxUnknownType = ProviderTestUtils.setupMailbox("box", acct.mId, false,
+                mMockContext, Mailbox.TYPE_UNKNOWN);
+        assertFalse(parser.isValidMailFolder(boxUnknownType, mailboxMap));
+        boxUnknownType.mParentServerId = boxMailType.mServerId;
+        // We shouldn't find the parent yet
+        assertFalse(parser.isValidMailFolder(boxUnknownType, mailboxMap));
+        // Put the mailbox in the map; the unknown box should now be valid
+        mailboxMap.put(boxMailType.mServerId, boxMailType);
+        assertTrue(parser.isValidMailFolder(boxUnknownType, mailboxMap));
+
+        // Clear the map, but save away the parent box
+        mailboxMap.clear();
+        assertFalse(parser.isValidMailFolder(boxUnknownType, mailboxMap));
+        boxMailType.save(mMockContext);
+        // The box should now be valid
+        assertTrue(parser.isValidMailFolder(boxUnknownType, mailboxMap));
+
+        // Somewhat harder case.  The parent will be in the map, but also unknown.  The parent's
+        // parent will be in the database.
+        Mailbox boxParentUnknownType = ProviderTestUtils.setupMailbox("box", acct.mId, false,
+                mMockContext, Mailbox.TYPE_UNKNOWN);
+        assertFalse(parser.isValidMailFolder(boxParentUnknownType, mailboxMap));
+        // Give the unknown type parent a parent (boxMailType)
+        boxParentUnknownType.mServerId = "1:2";
+        boxParentUnknownType.mParentServerId = boxMailType.mServerId;
+        // Give our unknown box an unknown parent
+        boxUnknownType.mParentServerId = boxParentUnknownType.mServerId;
+        // Confirm the box is still invalid
+        assertFalse(parser.isValidMailFolder(boxUnknownType, mailboxMap));
+        // Put the unknown type parent into the mailbox map
+        mailboxMap.put(boxParentUnknownType.mServerId, boxParentUnknownType);
+        // Our unknown box should now be valid, because 1) the parent is unknown, BUT 2) the
+        // parent's parent is a mail type
+        assertTrue(parser.isValidMailFolder(boxUnknownType, mailboxMap));
+    }
+}
diff --git a/tests/src/com/android/exchange/adapter/SyncAdapterTestCase.java b/tests/src/com/android/exchange/adapter/SyncAdapterTestCase.java
index 00b6b23..d6172a9 100644
--- a/tests/src/com/android/exchange/adapter/SyncAdapterTestCase.java
+++ b/tests/src/com/android/exchange/adapter/SyncAdapterTestCase.java
@@ -16,15 +16,16 @@
 
 package com.android.exchange.adapter;
 
-import com.android.email.provider.EmailProvider;
 import com.android.email.provider.EmailContent.Account;
 import com.android.email.provider.EmailContent.Mailbox;
+import com.android.email.provider.EmailProvider;
 import com.android.exchange.EasSyncService;
 import com.android.exchange.adapter.EmailSyncAdapter.EasEmailSyncParser;
+import com.android.exchange.provider.MockProvider;
 
-import android.content.ContentResolver;
 import android.content.Context;
 import android.test.ProviderTestCase2;
+import android.test.mock.MockContentResolver;
 
 import java.io.ByteArrayInputStream;
 import java.io.InputStream;
@@ -35,7 +36,7 @@
         extends ProviderTestCase2<EmailProvider> {
     EmailProvider mProvider;
     Context mMockContext;
-    ContentResolver mMockResolver;
+    MockContentResolver mMockResolver;
     Mailbox mMailbox;
     Account mAccount;
     EmailSyncAdapter mSyncAdapter;
@@ -49,7 +50,8 @@
     public void setUp() throws Exception {
         super.setUp();
         mMockContext = getMockContext();
-        mMockResolver = mMockContext.getContentResolver();
+        mMockResolver = (MockContentResolver)mMockContext.getContentResolver();
+        mMockResolver.addProvider(MockProvider.AUTHORITY, new MockProvider(mMockContext));
     }
 
     @Override
@@ -84,12 +86,12 @@
         return service;
     }
 
-    T getTestSyncAdapter(Class<T> klass) {
+    protected T getTestSyncAdapter(Class<T> klass) {
         EasSyncService service = getTestService();
         Constructor<T> c;
         try {
-            c = klass.getDeclaredConstructor(new Class[] {Mailbox.class, EasSyncService.class});
-            return c.newInstance(service.mMailbox, service);
+            c = klass.getDeclaredConstructor(new Class[] {EasSyncService.class});
+            return c.newInstance(service);
         } catch (SecurityException e) {
         } catch (NoSuchMethodException e) {
         } catch (IllegalArgumentException e) {
diff --git a/tests/src/com/android/exchange/provider/ExchangeDirectoryProviderTests.java b/tests/src/com/android/exchange/provider/ExchangeDirectoryProviderTests.java
new file mode 100644
index 0000000..fff1ec3
--- /dev/null
+++ b/tests/src/com/android/exchange/provider/ExchangeDirectoryProviderTests.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.exchange.provider;
+
+import com.android.email.mail.PackedString;
+import com.android.email.provider.EmailProvider;
+import com.android.email.provider.ProviderTestUtils;
+import com.android.email.provider.EmailContent.Account;
+import com.android.exchange.provider.GalResult.GalData;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.net.Uri;
+import android.provider.ContactsContract.CommonDataKinds;
+import android.provider.ContactsContract.Contacts;
+import android.test.ProviderTestCase2;
+
+/**
+ * You can run this entire test case with:
+ *   runtest -c com.android.exchange.provider.ExchangeDirectoryProviderTests email
+ */
+public class ExchangeDirectoryProviderTests extends ProviderTestCase2<EmailProvider> {
+
+    public ExchangeDirectoryProviderTests() {
+        super(EmailProvider.class, EmailProvider.EMAIL_AUTHORITY);
+    }
+
+    // Create a test projection; we should only get back values for display name and email address
+    private static final String[] GAL_RESULT_PROJECTION =
+        new String[] {Contacts.DISPLAY_NAME, CommonDataKinds.Email.ADDRESS, Contacts.CONTENT_TYPE,
+            Contacts.LOOKUP_KEY};
+    private static final int GAL_RESULT_COLUMN_DISPLAY_NAME = 0;
+    private static final int GAL_RESULT_COLUMN_EMAIL_ADDRESS = 1;
+    private static final int GAL_RESULT_COLUMN_CONTENT_TYPE = 2;
+    private static final int GAL_RESULT_COLUMN_LOOKUP_KEY = 3;
+
+    public void testBuildSimpleGalResultCursor() {
+        GalResult result = new GalResult();
+        result.addGalData(1, "Alice Aardvark", "alice@aardvark.com");
+        result.addGalData(2, "Bob Badger", "bob@badger.com");
+        result.addGalData(3, "Clark Cougar", "clark@cougar.com");
+        result.addGalData(4, "Dan Dolphin", "dan@dolphin.com");
+        // Make sure our returned cursor has the expected contents
+        ExchangeDirectoryProvider provider = new ExchangeDirectoryProvider();
+        Cursor c = provider.buildGalResultCursor(GAL_RESULT_PROJECTION, result);
+        assertNotNull(c);
+        assertEquals(MatrixCursor.class, c.getClass());
+        assertEquals(4, c.getCount());
+        for (int i = 0; i < 4; i++) {
+            GalData data = result.galData.get(i);
+            assertTrue(c.moveToNext());
+            assertEquals(data.displayName, c.getString(GAL_RESULT_COLUMN_DISPLAY_NAME));
+            assertEquals(data.emailAddress, c.getString(GAL_RESULT_COLUMN_EMAIL_ADDRESS));
+            assertNull(c.getString(GAL_RESULT_COLUMN_CONTENT_TYPE));
+        }
+    }
+
+    private static final String[][] DISPLAY_NAME_TEST_FIELDS = {
+        {"Alice", "Aardvark", "Another Name"},
+        {"Alice", "Aardvark", null},
+        {"Alice", null, null},
+        {null, "Aardvark", null},
+        {null, null, null}
+    };
+    private static final int TEST_FIELD_FIRST_NAME = 0;
+    private static final int TEST_FIELD_LAST_NAME = 1;
+    private static final int TEST_FIELD_DISPLAY_NAME = 2;
+    private static final String[] EXPECTED_DISPLAY_NAMES = new String[] {"Another Name",
+        "Alice Aardvark", "Alice", "Aardvark", null};
+
+    private GalResult getTestDisplayNameResult() {
+        GalResult result = new GalResult();
+        for (int i = 0; i < DISPLAY_NAME_TEST_FIELDS.length; i++) {
+            GalData galData = new GalData();
+            String[] names = DISPLAY_NAME_TEST_FIELDS[i];
+            galData.put(GalData.FIRST_NAME, names[TEST_FIELD_FIRST_NAME]);
+            galData.put(GalData.LAST_NAME, names[TEST_FIELD_LAST_NAME]);
+            galData.put(GalData.DISPLAY_NAME, names[TEST_FIELD_DISPLAY_NAME]);
+            result.addGalData(galData);
+        }
+        return result;
+    }
+
+    public void testDisplayNameLogic() {
+        GalResult result = getTestDisplayNameResult();
+        // Make sure our returned cursor has the expected contents
+        ExchangeDirectoryProvider provider = new ExchangeDirectoryProvider();
+        Cursor c = provider.buildGalResultCursor(GAL_RESULT_PROJECTION, result);
+        assertNotNull(c);
+        assertEquals(MatrixCursor.class, c.getClass());
+        assertEquals(DISPLAY_NAME_TEST_FIELDS.length, c.getCount());
+        for (int i = 0; i < EXPECTED_DISPLAY_NAMES.length; i++) {
+            assertTrue(c.moveToNext());
+            assertEquals(EXPECTED_DISPLAY_NAMES[i], c.getString(GAL_RESULT_COLUMN_DISPLAY_NAME));
+        }
+    }
+
+    public void testLookupKeyLogic() {
+        GalResult result = getTestDisplayNameResult();
+        // Make sure our returned cursor has the expected contents
+        ExchangeDirectoryProvider provider = new ExchangeDirectoryProvider();
+        Cursor c = provider.buildGalResultCursor(GAL_RESULT_PROJECTION, result);
+        assertNotNull(c);
+        assertEquals(MatrixCursor.class, c.getClass());
+        assertEquals(DISPLAY_NAME_TEST_FIELDS.length, c.getCount());
+        for (int i = 0; i < EXPECTED_DISPLAY_NAMES.length; i++) {
+            assertTrue(c.moveToNext());
+            PackedString ps =
+                new PackedString(Uri.decode(c.getString(GAL_RESULT_COLUMN_LOOKUP_KEY)));
+            String[] testFields = DISPLAY_NAME_TEST_FIELDS[i];
+            assertEquals(testFields[TEST_FIELD_FIRST_NAME], ps.get(GalData.FIRST_NAME));
+            assertEquals(testFields[TEST_FIELD_LAST_NAME], ps.get(GalData.LAST_NAME));
+            assertEquals(EXPECTED_DISPLAY_NAMES[i], ps.get(GalData.DISPLAY_NAME));
+        }
+    }
+
+    public void testGetAccountIdByName() {
+        Context context = getMockContext();
+        ExchangeDirectoryProvider provider = new ExchangeDirectoryProvider();
+        // Nothing up my sleeve
+        assertNull(provider.mAccountIdMap.get("foo@android.com"));
+        assertNull(provider.mAccountIdMap.get("bar@android.com"));
+        // Create accounts; the email addresses will be the first argument + "@android.com"
+        Account acctFoo = ProviderTestUtils.setupAccount("foo", true, context);
+        Account acctBar = ProviderTestUtils.setupAccount("bar", true, context);
+        // Make sure we can retrieve them, and that the map is populated
+        assertEquals(acctFoo.mId, provider.getAccountIdByName(context, "foo@android.com"));
+        assertEquals(acctBar.mId, provider.getAccountIdByName(context, "bar@android.com"));
+        assertEquals((Long)acctFoo.mId, provider.mAccountIdMap.get("foo@android.com"));
+        assertEquals((Long)acctBar.mId, provider.mAccountIdMap.get("bar@android.com"));
+    }
+}
diff --git a/tests/src/com/android/exchange/provider/MockProvider.java b/tests/src/com/android/exchange/provider/MockProvider.java
new file mode 100644
index 0000000..177ec55
--- /dev/null
+++ b/tests/src/com/android/exchange/provider/MockProvider.java
@@ -0,0 +1,227 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.exchange.provider;
+
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.UriMatcher;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.net.Uri;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Set;
+import java.util.Map.Entry;
+
+/**
+ * MockProvider is a ContentProvider that can be used to simulate the storage and retrieval of
+ * records from any ContentProvider, even if that ContentProvider does not exist in the caller's
+ * package.  It is specifically designed to enable testing of sync adapters that create
+ * ContentProviderOperations (CPOs) that are then executed using ContentResolver.applyBatch()
+ *
+ * Why is this useful?  Because we can't instantiate CalendarProvider or ContactsProvider from our
+ * package, as required by MockContentResolver.addProvider()
+ *
+ * Usage:
+ *     ContentResolver.applyBatch(MockProvider.AUTHORITY, batch) will cause the CPOs to be executed,
+ *     returning an array of ContentProviderResult; in the case of inserts, the result will include
+ *     a Uri that can be used via query().  Note that the CPOs in the batch can contain references
+ *     to any authority.
+ *
+ *     query() does not allow non-null selection, selectionArgs, or sortOrder arguments; the
+ *     presence of these will result in an UnsupportedOperationException
+ *
+ *     insert() acts as expected, returning a Uri that can be directly used in a query
+ *
+ *     delete() and update() do not allow non-null selection or selectionArgs arguments; the
+ *     presence of these will result in an UnsupportedOperationException
+ *
+ *     NOTE: When using any operation other than applyBatch, the Uri to be used must be created
+ *     with MockProvider.uri(yourUri).  This guarantees that the operation is sent to MockProvider
+ *
+ *     NOTE: MockProvider only simulates direct storage/retrieval of rows; it does not (and can not)
+ *     simulate other actions (e.g. creation of ancillary data) that the actual provider might
+ *     perform
+ *
+ *     NOTE: See MockProviderTests for usage examples
+ **/
+public class MockProvider extends ContentProvider {
+    public static final String AUTHORITY = "com.android.exchange.mock.provider";
+    /*package*/ static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH);
+
+    /*package*/ static final int TABLE = 100;
+    /*package*/ static final int RECORD = 101;
+
+    public static final String ID_COLUMN = "_id";
+
+    public MockProvider(Context context) {
+        super(context, null, null, null);
+    }
+
+    public MockProvider() {
+        super();
+    }
+
+    // We'll store our values here
+    private HashMap<String, ContentValues> mMockStore = new HashMap<String, ContentValues>();
+    // And we'll generate new id's from here
+    long mMockId = 1;
+
+    /**
+     * Create a Uri for MockProvider from a given Uri
+     * @param uri the Uri from which the MockProvider Uri will be created
+     * @return a Uri that can be used with MockProvider
+     */
+    public static Uri uri(Uri uri) {
+        return new Uri.Builder().scheme("content").authority(AUTHORITY)
+            .path(uri.getPath().substring(1)).build();
+    }
+
+    @Override
+    public int delete(Uri uri, String selection, String[] selectionArgs) {
+        if (selection != null || selectionArgs != null) {
+            throw new UnsupportedOperationException();
+        }
+        String path = uri.getPath();
+        if (mMockStore.containsKey(path)) {
+            mMockStore.remove(path);
+            return 1;
+        } else {
+            return 0;
+        }
+    }
+
+    @Override
+    public String getType(Uri uri) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Uri insert(Uri uri, ContentValues values) {
+        // Remove the leading slash
+        String table = uri.getPath().substring(1);
+        long id = mMockId++;
+        Uri newUri = new Uri.Builder().scheme("content").authority(AUTHORITY).path(table)
+            .appendPath(Long.toString(id)).build();
+        // Remember to store the _id
+        values.put(ID_COLUMN, id);
+        mMockStore.put(newUri.getPath(), values);
+        int match = sURIMatcher.match(uri);
+        if (match == UriMatcher.NO_MATCH) {
+            sURIMatcher.addURI(AUTHORITY, table, TABLE);
+            sURIMatcher.addURI(AUTHORITY, table + "/#", RECORD);
+        }
+        return newUri;
+    }
+
+    @Override
+    public boolean onCreate() {
+        return false;
+    }
+
+    @Override
+    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+            String sortOrder) {
+        if (selection != null || selectionArgs != null || sortOrder != null || projection == null) {
+            throw new UnsupportedOperationException();
+        }
+        final int match = sURIMatcher.match(uri(uri));
+        ArrayList<ContentValues> valuesList = new ArrayList<ContentValues>();
+        switch(match) {
+            case TABLE:
+                Set<Entry<String, ContentValues>> entrySet = mMockStore.entrySet();
+                String prefix = uri.getPath() + "/";
+                for (Entry<String, ContentValues> entry: entrySet) {
+                    if (entry.getKey().startsWith(prefix)) {
+                        valuesList.add(entry.getValue());
+                    }
+                }
+                break;
+            case RECORD:
+                ContentValues values = mMockStore.get(uri.getPath());
+                if (values != null) {
+                    valuesList.add(values);
+                }
+                break;
+            default:
+                throw new IllegalArgumentException("Unknown URI " + uri);
+        }
+        MatrixCursor cursor = new MatrixCursor(projection, 1);
+        for (ContentValues cv: valuesList) {
+            Object[] rowValues = new Object[projection.length];
+            int i = 0;
+            for (String column: projection) {
+                rowValues[i++] = cv.get(column);
+            }
+            cursor.addRow(rowValues);
+        }
+        return cursor;
+    }
+
+    @Override
+    public int update(Uri uri, ContentValues newValues, String selection, String[] selectionArgs) {
+        if (selection != null || selectionArgs != null) {
+            throw new UnsupportedOperationException();
+        }
+        final int match = sURIMatcher.match(uri(uri));
+        ArrayList<ContentValues> updateValuesList = new ArrayList<ContentValues>();
+        String path = uri.getPath();
+        switch(match) {
+            case TABLE:
+                Set<Entry<String, ContentValues>> entrySet = mMockStore.entrySet();
+                String prefix = path + "/";
+                for (Entry<String, ContentValues> entry: entrySet) {
+                    if (entry.getKey().startsWith(prefix)) {
+                        updateValuesList.add(entry.getValue());
+                    }
+                }
+                break;
+            case RECORD:
+                ContentValues cv = mMockStore.get(path);
+                if (cv != null) {
+                    updateValuesList.add(cv);
+                }
+                break;
+            default:
+                throw new IllegalArgumentException("Unknown URI " + uri);
+        }
+        Set<Entry<String, Object>> newValuesSet = newValues.valueSet();
+        for (Entry<String, Object> entry: newValuesSet) {
+            String key = entry.getKey();
+            Object value = entry.getValue();
+            for (ContentValues targetValues: updateValuesList) {
+                if (value instanceof Integer) {
+                    targetValues.put(key, (Integer)value);
+                } else if (value instanceof Long) {
+                    targetValues.put(key, (Long)value);
+                } else if (value instanceof String) {
+                    targetValues.put(key, (String)value);
+                } else if (value instanceof Boolean) {
+                    targetValues.put(key, (Boolean)value);
+                } else {
+                    throw new IllegalArgumentException();
+                }
+            }
+        }
+        for (ContentValues targetValues: updateValuesList) {
+            mMockStore.put(path + "/" + targetValues.getAsLong(ID_COLUMN), targetValues);
+        }
+        return updateValuesList.size();
+    }
+}
diff --git a/tests/src/com/android/exchange/provider/MockProviderTests.java b/tests/src/com/android/exchange/provider/MockProviderTests.java
new file mode 100644
index 0000000..558faa4
--- /dev/null
+++ b/tests/src/com/android/exchange/provider/MockProviderTests.java
@@ -0,0 +1,190 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.exchange.provider;
+
+import android.content.ContentProviderOperation;
+import android.content.ContentProviderResult;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.OperationApplicationException;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.RemoteException;
+import android.test.ProviderTestCase2;
+import android.test.mock.MockContentResolver;
+
+import java.util.ArrayList;
+
+/**
+ * You can run this entire test case with:
+ *   runtest -c com.android.exchange.provider.MockProviderTests email
+ */
+public class MockProviderTests extends ProviderTestCase2<MockProvider> {
+    Context mMockContext;
+    MockContentResolver mMockResolver;
+
+    private static final String CANHAZ_AUTHORITY = "com.android.canhaz";
+    private static final String PONY_TABLE = "pony";
+    private static final String PONY_COLUMN_NAME = "name";
+    private static final String PONY_COLUMN_TYPE = "type";
+    private static final String PONY_COLUMN_LEGS= "legs";
+    private static final String PONY_COLUMN_CAN_RIDE = "canRide";
+    private static final String[] PONY_PROJECTION = {MockProvider.ID_COLUMN, PONY_COLUMN_NAME,
+        PONY_COLUMN_TYPE, PONY_COLUMN_LEGS, PONY_COLUMN_CAN_RIDE};
+    private static final int PONY_ID = 0;
+    private static final int PONY_NAME = 1;
+    private static final int PONY_TYPE = 2;
+    private static final int PONY_LEGS = 3;
+    private static final int PONY_CAN_RIDE = 4;
+
+    public MockProviderTests() {
+        super(MockProvider.class, MockProvider.AUTHORITY);
+    }
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        mMockContext = getMockContext();
+        mMockResolver = (MockContentResolver)mMockContext.getContentResolver();
+        mMockResolver.addProvider(CANHAZ_AUTHORITY, new MockProvider(mMockContext));
+    }
+
+    @Override
+    public void tearDown() throws Exception {
+        super.tearDown();
+    }
+
+    private ContentValues ponyValues(String name, String type, int legs, boolean canRide) {
+        ContentValues cv = new ContentValues();
+        cv.put(PONY_COLUMN_NAME, name);
+        cv.put(PONY_COLUMN_TYPE, type);
+        cv.put(PONY_COLUMN_LEGS, legs);
+        cv.put(PONY_COLUMN_CAN_RIDE, canRide ? 1 : 0);
+        return cv;
+    }
+
+    private ContentProviderResult[] setupPonies() throws RemoteException,
+            OperationApplicationException {
+        // The Uri is content://com.android.canhaz/pony
+        Uri uri = new Uri.Builder().scheme("content").authority(CANHAZ_AUTHORITY)
+            .path(PONY_TABLE).build();
+        // Our array of CPO's to be used with applyBatch
+        ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();
+
+        // Insert two ponies
+        ContentValues pony1 = ponyValues("Flicka", "wayward", 4, true);
+        ops.add(ContentProviderOperation.newInsert(uri).withValues(pony1).build());
+        ContentValues pony2 = ponyValues("Elise", "dastardly", 3, false);
+        ops.add(ContentProviderOperation.newInsert(uri).withValues(pony2).build());
+        // Apply the batch with one insert operation
+        return mMockResolver.applyBatch(MockProvider.AUTHORITY, ops);
+    }
+
+    private Uri getPonyUri() {
+        return new Uri.Builder().scheme("content").authority(CANHAZ_AUTHORITY)
+            .path(PONY_TABLE).build();
+    }
+
+    public void testInsertQueryandDelete() throws RemoteException, OperationApplicationException {
+        // The Uri is content://com.android.canhaz/pony
+        ContentProviderResult[] results = setupPonies();
+        Uri uri = getPonyUri();
+
+        // Check the results
+        assertNotNull(results);
+        assertEquals(2, results.length);
+        // Make sure that we've created matcher entries for pony and pony/#
+        assertEquals(MockProvider.TABLE, MockProvider.sURIMatcher.match(MockProvider.uri(uri)));
+        assertEquals(MockProvider.RECORD,
+                MockProvider.sURIMatcher.match(MockProvider.uri(results[0].uri)));
+        Cursor c = mMockResolver.query(MockProvider.uri(uri), PONY_PROJECTION, null, null, null);
+        assertNotNull(c);
+        assertEquals(2, c.getCount());
+        long eliseId = -1;
+        long flickaId = -1;
+        while (c.moveToNext()) {
+            String name = c.getString(PONY_NAME);
+            if ("Flicka".equals(name)) {
+                assertEquals("Flicka", c.getString(PONY_NAME));
+                assertEquals("wayward", c.getString(PONY_TYPE));
+                assertEquals(4, c.getInt(PONY_LEGS));
+                assertEquals(1, c.getInt(PONY_CAN_RIDE));
+                flickaId = c.getLong(PONY_ID);
+            } else if ("Elise".equals(name)) {
+                assertEquals("dastardly", c.getString(PONY_TYPE));
+                assertEquals(3, c.getInt(PONY_LEGS));
+                assertEquals(0, c.getInt(PONY_CAN_RIDE));
+                eliseId = c.getLong(PONY_ID);
+            } else {
+                fail("Wrong record: " + name);
+            }
+        }
+
+        // eliseId and flickaId should have been set
+        assertNotSame(-1, eliseId);
+        assertNotSame(-1, flickaId);
+        // Delete the elise record
+        assertEquals(1, mMockResolver.delete(ContentUris.withAppendedId(MockProvider.uri(uri),
+                eliseId), null, null));
+        c = mMockResolver.query(MockProvider.uri(uri), PONY_PROJECTION, null, null, null);
+        assertNotNull(c);
+        // There should be one left (Flicka)
+        assertEquals(1, c.getCount());
+        assertTrue(c.moveToNext());
+        assertEquals("Flicka", c.getString(PONY_NAME));
+    }
+
+    public void testUpdate() throws RemoteException, OperationApplicationException {
+        // The Uri is content://com.android.canhaz/pony
+        Uri uri = getPonyUri();
+        setupPonies();
+        Cursor c = mMockResolver.query(MockProvider.uri(uri), PONY_PROJECTION, null, null, null);
+        assertNotNull(c);
+        assertEquals(2, c.getCount());
+        // Give all the ponies 5 legs
+        ContentValues cv = new ContentValues();
+        cv.put(PONY_COLUMN_LEGS, 5);
+        assertEquals(2, mMockResolver.update(MockProvider.uri(uri), cv, null, null));
+        c = mMockResolver.query(MockProvider.uri(uri), PONY_PROJECTION, null, null, null);
+        assertNotNull(c);
+        // We should still have two records, and each should have 5 legs, but otherwise be the same
+        assertEquals(2, c.getCount());
+        long eliseId = -1;
+        long flickaId = -1;
+        while (c.moveToNext()) {
+            String name = c.getString(PONY_NAME);
+            if ("Flicka".equals(name)) {
+                assertEquals("Flicka", c.getString(PONY_NAME));
+                assertEquals("wayward", c.getString(PONY_TYPE));
+                assertEquals(5, c.getInt(PONY_LEGS));
+                assertEquals(1, c.getInt(PONY_CAN_RIDE));
+                flickaId = c.getLong(PONY_ID);
+            } else if ("Elise".equals(name)) {
+                assertEquals("dastardly", c.getString(PONY_TYPE));
+                assertEquals(5, c.getInt(PONY_LEGS));
+                assertEquals(0, c.getInt(PONY_CAN_RIDE));
+                eliseId = c.getLong(PONY_ID);
+            } else {
+                fail("Wrong record: " + name);
+            }
+        }
+        // eliseId and flickaId should have been set
+        assertNotSame(-1, eliseId);
+        assertNotSame(-1, flickaId);
+    }
+}
diff --git a/tests/src/com/android/exchange/utility/CalendarUtilitiesTests.java b/tests/src/com/android/exchange/utility/CalendarUtilitiesTests.java
index 65443fa..9011a16 100644
--- a/tests/src/com/android/exchange/utility/CalendarUtilitiesTests.java
+++ b/tests/src/com/android/exchange/utility/CalendarUtilitiesTests.java
@@ -22,16 +22,22 @@
 import com.android.email.provider.EmailContent.Account;
 import com.android.email.provider.EmailContent.Attachment;
 import com.android.email.provider.EmailContent.Message;
+import com.android.exchange.adapter.CalendarSyncAdapter;
+import com.android.exchange.adapter.Parser;
+import com.android.exchange.adapter.Serializer;
+import com.android.exchange.adapter.SyncAdapterTestCase;
+import com.android.exchange.adapter.Tags;
+import com.android.exchange.adapter.CalendarSyncAdapter.EasCalendarSyncParser;
 
 import android.content.ContentValues;
 import android.content.Entity;
 import android.content.res.Resources;
 import android.provider.Calendar.Attendees;
 import android.provider.Calendar.Events;
-import android.test.AndroidTestCase;
 import android.util.Log;
 
 import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
 import java.io.IOException;
 import java.io.StringReader;
 import java.text.DateFormat;
@@ -51,7 +57,7 @@
  * http://www.ietf.org/rfc/rfc2445.txt
  */
 
-public class CalendarUtilitiesTests extends AndroidTestCase {
+public class CalendarUtilitiesTests extends SyncAdapterTestCase<CalendarSyncAdapter> {
 
     // Some prebuilt time zones, Base64 encoded (as they arrive from EAS)
     // More time zones to be added over time
@@ -564,6 +570,8 @@
         // on either side of the transition.
         // Use CST for testing (any other will do as well, as long as it has DST)
         TimeZone tz = TimeZone.getTimeZone("US/Central");
+        // Confirm that this time zone uses DST
+        assertTrue(tz.useDaylightTime());
         // Get a calendar at January 1st of the current year
         GregorianCalendar calendar = new GregorianCalendar(tz);
         calendar.set(CalendarUtilities.sCurrentYear, Calendar.JANUARY, 1);
@@ -590,10 +598,10 @@
         assertTrue(tz.inDaylightTime(beforeDate));
         assertFalse(tz.inDaylightTime(afterDate));
 
-        // Captain Renault: What in heaven's name brought you to Casablanca?
-        // Rick: My health. I came to Casablanca for the waters.
-        // Also, they have no daylight savings time
-        tz = TimeZone.getTimeZone("Africa/Casablanca");
+        // Kinshasa has no daylight savings time
+        tz = TimeZone.getTimeZone("Africa/Kinshasa");
+        // Confirm that there's no DST for this time zone
+        assertFalse(tz.useDaylightTime());
         // Get a calendar at January 1st of the current year
         calendar = new GregorianCalendar(tz);
         calendar.set(CalendarUtilities.sCurrentYear, Calendar.JANUARY, 1);
@@ -610,7 +618,7 @@
         // Every Monday for 2 weeks
         String rrule = CalendarUtilities.rruleFromRecurrence(
                 1 /*Weekly*/, 2 /*Occurrences*/, 1 /*Interval*/, 2 /*Monday*/, 0, 0, 0, null);
-        assertEquals("FREQ=WEEKLY;INTERVAL=1;COUNT=2;BYDAY=MO", rrule);
+        assertEquals("FREQ=WEEKLY;COUNT=2;INTERVAL=1;BYDAY=MO", rrule);
         // Every Tuesday and Friday
         rrule = CalendarUtilities.rruleFromRecurrence(
                 1 /*Weekly*/, 0 /*Occurrences*/, 0 /*Interval*/, 36 /*Tue&Fri*/, 0, 0, 0, null);
@@ -638,6 +646,41 @@
     }
 
     /**
+     * Given a CalendarSyncAdapter and an RRULE, serialize the RRULE via recurrentFromRrule and
+     * then parse the result.  Assert that the resulting RRULE is the same as the original.
+     * @param adapter a CalendarSyncAdapter
+     * @param rrule an RRULE string that will be tested
+     * @throws IOException
+     */
+    private void testSingleRecurrenceFromRrule(CalendarSyncAdapter adapter, String rrule)
+            throws IOException {
+        Serializer s = new Serializer();
+        CalendarUtilities.recurrenceFromRrule(rrule, 0, s);
+        s.done();
+        EasCalendarSyncParser parser = adapter.new EasCalendarSyncParser(
+                new ByteArrayInputStream(s.toByteArray()), adapter);
+        // The first element should be the outer CALENDAR_RECURRENCE tag
+        assertEquals(Tags.CALENDAR_RECURRENCE, parser.nextTag(Parser.START_DOCUMENT));
+        assertEquals(rrule, parser.recurrenceParser());
+    }
+
+    /**
+     * Round-trip test of RRULE handling; we serialize an RRULE and then parse the result; the
+     * result should be identical to the original RRULE
+     */
+    public void testRecurrenceFromRrule() throws IOException {
+        // A test sync adapter we can use throughout the test
+        CalendarSyncAdapter adapter = getTestSyncAdapter(CalendarSyncAdapter.class);
+
+        testSingleRecurrenceFromRrule(adapter, "FREQ=WEEKLY;COUNT=2;INTERVAL=1;BYDAY=MO");
+        testSingleRecurrenceFromRrule(adapter, "FREQ=WEEKLY;BYDAY=TU,FR");
+        testSingleRecurrenceFromRrule(adapter, "FREQ=MONTHLY;BYDAY=-1SA");
+        testSingleRecurrenceFromRrule(adapter, "FREQ=MONTHLY;BYDAY=3WE,3TH");
+        testSingleRecurrenceFromRrule(adapter, "FREQ=YEARLY;BYMONTHDAY=31;BYMONTH=10");
+        testSingleRecurrenceFromRrule(adapter, "FREQ=YEARLY;BYDAY=1TU;BYMONTH=6");
+    }
+
+    /**
      * For debugging purposes, to help keep track of parsing errors.
      */
     private class UnterminatedBlockException extends IOException {
@@ -776,12 +819,14 @@
                 nodst++;
             }
         }
-        assertTrue(norule < rule/10);
         Log.d("TimeZoneGeneration",
                 "Rule: " + rule + ", No DST: " + nodst + ", No rule: " + norule);
         for (String nr: norulelist) {
             Log.d("TimeZoneGeneration", "No rule: " + nr);
         }
+        // This is an empirical sanity test; we shouldn't have too many time zones with DST and
+        // without a rule.
+        assertTrue(norule < rule/8);
     }
 
     public void testGetUidFromGlobalObjId() {