am a3e021d1: am 3b811ae4: resolved conflicts for merge of fb060de6 to gingerbread

Merge commit 'a3e021d12c96d10f758fb6af3b7a05e85d0d8eeb'

* commit 'a3e021d12c96d10f758fb6af3b7a05e85d0d8eeb':
  Explicitly verify certificate hostname on SSL connections
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index a21a23d..ecbefed 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -143,7 +143,8 @@
         </activity>
         <activity
             android:name=".activity.AccountFolderList"
-            android:launchMode="singleTop" >
+            android:launchMode="singleTop"
+            android:theme="@android:style/Theme.WithActionBar" >
         </activity>
         
         <activity 
@@ -159,7 +160,7 @@
         
         <activity
             android:name=".activity.MailboxList"
-            android:theme="@style/ThemeNoTitleBar">
+            android:theme="@android:style/Theme.WithActionBar" >
         </activity>
         
         <activity
@@ -191,6 +192,12 @@
         <activity
             android:name=".activity.MessageView"
             android:theme="@android:style/Theme.NoTitleBar" >
+            <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"
@@ -216,26 +223,18 @@
             </intent-filter>
         </activity>
         <!--EXCHANGE-REMOVE-SECTION-START-->
-       <receiver android:name="com.android.exchange.EmailSyncAlarmReceiver"/>
-       <receiver android:name="com.android.exchange.MailboxAlarmReceiver"/>
-       <receiver android:name="com.android.exchange.BootReceiver" android:enabled="true">
-             <intent-filter>
-                <action android:name="android.intent.action.BOOT_COMPLETED" />
-            </intent-filter>
-       </receiver>
+        <receiver android:name="com.android.exchange.EmailSyncAlarmReceiver"/>
+        <receiver android:name="com.android.exchange.MailboxAlarmReceiver"/>
         <!--EXCHANGE-REMOVE-SECTION-END-->
 
-       <receiver android:name=".service.BootReceiver" android:enabled="true">
+        <receiver android:name=".service.EmailBroadcastReceiver" android:enabled="true">
             <intent-filter>
                 <action android:name="android.intent.action.BOOT_COMPLETED" />
-            </intent-filter>
-            <intent-filter>
                 <action android:name="android.intent.action.DEVICE_STORAGE_LOW" />
-            </intent-filter>
-            <intent-filter>
                 <action android:name="android.intent.action.DEVICE_STORAGE_OK" />
             </intent-filter>
         </receiver>
+        <service android:name=".service.EmailBroadcastProcessorService" />
 
         <!-- Support for DeviceAdmin / DevicePolicyManager.  See SecurityPolicy class for impl. -->
         <receiver
@@ -250,15 +249,6 @@
                 <action android:name="android.app.action.DEVICE_ADMIN_ENABLED" />
             </intent-filter>
         </receiver>
-        
-        <receiver
-            android:name=".OneTimeInitializer"
-            android:enabled="true"
-            >
-            <intent-filter>
-                <action android:name="android.intent.action.BOOT_COMPLETED" />
-            </intent-filter>
-        </receiver>
 
         <service
             android:name=".service.MailService"
@@ -349,12 +339,20 @@
         <!--EXCHANGE-REMOVE-SECTION-START-->
         <!-- In this release, GAL information is used locally only, so we used the same
              strict permissions. -->
+        <!-- NOTE: ExchangeDirectoryProvider will replace ExchangeProvider after integration with
+             the new GAL/contacts implementation -->
         <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"
             />
+        <provider
+            android:name="com.android.exchange.provider.ExchangeDirectoryProvider"
+            android:authorities="com.android.exchange.directory.provider"
+            android:readPermission="android.permission.READ_CONTACTS"
+            android:multiprocess="false"
+            />
         <!--EXCHANGE-REMOVE-SECTION-END-->
 
     </application>
diff --git a/proguard.flags b/proguard.flags
index bbffc8a..f3b7e57 100644
--- a/proguard.flags
+++ b/proguard.flags
@@ -23,9 +23,9 @@
 
 -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 class ** {
   *** *ForTest(...);
 }
@@ -172,3 +172,7 @@
 -keep class org.apache.james.mime4j.message.Message {
   *;
 }
+
+-keepclasseswithmembers class org.apache.commons.io.IOUtils {
+  *** toByteArray(...);
+}
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 95b4e9e..c7167c3 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -278,9 +278,8 @@
     <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 -->
+    <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. -->
@@ -309,6 +308,8 @@
     <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 that is displayed when a long message is being parsed. -->
+    <string name="message_view_parse_message_toast">Opening message\u2026</string>
 
     <!-- Title of screen when setting up new email account -->
     <string name="account_setup_basics_title">Set up email</string>
@@ -445,6 +446,8 @@
     <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 -->
+    <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>
diff --git a/src/com/android/exchange/AbstractSyncService.java b/src/com/android/exchange/AbstractSyncService.java
index f1fe834..1a2d958 100644
--- a/src/com/android/exchange/AbstractSyncService.java
+++ b/src/com/android/exchange/AbstractSyncService.java
@@ -30,6 +30,7 @@
 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;
@@ -109,10 +110,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 +140,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 {
diff --git a/src/com/android/exchange/BootReceiver.java b/src/com/android/exchange/BootReceiver.java
deleted file mode 100644
index 1ebfa7b..0000000
--- a/src/com/android/exchange/BootReceiver.java
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * Copyright (C) 2009 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.exchange;
-
-import com.android.email.ExchangeUtils;
-
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.util.Log;
-
-public class BootReceiver extends BroadcastReceiver {
-    @Override
-    public void onReceive(Context context, Intent intent) {
-        Log.d("Exchange", "BootReceiver onReceive");
-        ExchangeUtils.startExchangeService(context);
-    }
-}
diff --git a/src/com/android/exchange/Eas.java b/src/com/android/exchange/Eas.java
index 8e561e7..14dc537 100644
--- a/src/com/android/exchange/Eas.java
+++ b/src/com/android/exchange/Eas.java
@@ -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
@@ -91,5 +93,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..b0b39b7 100644
--- a/src/com/android/exchange/EasOutboxService.java
+++ b/src/com/android/exchange/EasOutboxService.java
@@ -36,6 +36,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;
@@ -71,6 +72,11 @@
         }
     }
 
+    /*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
@@ -130,11 +136,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);
 
diff --git a/src/com/android/exchange/EasSyncService.java b/src/com/android/exchange/EasSyncService.java
index 3eb48c6..aeeb90b 100644
--- a/src/com/android/exchange/EasSyncService.java
+++ b/src/com/android/exchange/EasSyncService.java
@@ -21,7 +21,6 @@
 import com.android.email.Utility;
 import com.android.email.SecurityPolicy.PolicySet;
 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;
@@ -78,6 +77,7 @@
 import android.content.Context;
 import android.content.Entity;
 import android.database.Cursor;
+import android.net.Uri;
 import android.os.Bundle;
 import android.os.RemoteException;
 import android.os.SystemClock;
@@ -95,7 +95,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;
@@ -175,13 +174,18 @@
     // MSFT's custom HTTP result code indicating the need to provision
     static private final int HTTP_NEED_PROVISIONING = 449;
 
+    // The EAS protocol Provision status for "we implement all of the policies"
+    static private final String PROVISION_STATUS_OK = "1";
+    // The EAS protocol Provision status meaning "we partially implement the policies"
+    static private final String PROVISION_STATUS_PARTIAL = "2";
+
     // Reasonable default
     public String mProtocolVersion = Eas.DEFAULT_PROTOCOL_VERSION;
     public Double mProtocolVersionDouble;
     protected String mDeviceId = null;
-    private String mDeviceType = "Android";
-    private String mAuthString = null;
-    private String mCmdString = null;
+    /*package*/ String mDeviceType = "Android";
+    /*package*/ String mAuthString = null;
+    /*package*/ String mCmdString = null;
     public String mHostAddress;
     public String mUserName;
     public String mPassword;
@@ -352,7 +356,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;
             }
         }
@@ -363,7 +368,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;
             }
@@ -371,8 +376,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%");
@@ -392,60 +399,75 @@
                 // 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);
+                // 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();
+                // Run second test here for provisioning failures using FolderSync
                 userLog("Try folder sync");
-                s.start(Tags.FOLDER_FOLDER_SYNC).start(Tags.FOLDER_SYNC_KEY).text("0")
+                // Send "0" as the sync key for new accounts; otherwise we'll 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 by the server
                 if (code == HttpStatus.SC_FORBIDDEN || code == HTTP_NEED_PROVISIONING) {
                     // Get the policies and see if we are able to support them
-                    if (svc.canProvision() != null) {
-                        // If so, send the advisory Exception (the account may be created later)
-                        throw new MessagingException(MessagingException.SECURITY_POLICIES_REQUIRED);
+                    ProvisionParser pp = svc.canProvision();
+                    if (pp != null) {
+                        // If so, 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, send the unsupported Exception (the account won't be created)
-                        throw new MessagingException(
-                                MessagingException.SECURITY_POLICIES_UNSUPPORTED);
+                        // 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)
-                    throw new MessagingException(MessagingException.PROTOCOL_VERSION_UNSUPPORTED);
+                    resultCode = 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);
+                    resultCode = MessagingException.UNSPECIFIED_EXCEPTION;
+                } else {
+                    userLog("Validation successful");
                 }
-                userLog("Validation successful");
-                return;
-            }
-            if (isAuthError(code)) {
+            } else if (isAuthError(code)) {
                 userLog("Authentication failed");
-                throw new AuthenticationFailedException("Validation failed");
+                resultCode = MessagingException.AUTHENTICATION_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);
+                resultCode = MessagingException.IOERROR;
             }
         } catch (IOException e) {
             Throwable cause = e.getCause();
             if (cause != null && cause instanceof CertificateException) {
                 userLog("CertificateException caught: ", e.getMessage());
-                throw new MessagingException(MessagingException.GENERAL_SECURITY);
+                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;
     }
 
     /**
@@ -766,19 +788,27 @@
      * @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)
-    {
+    static public GalResult searchGal(Context context, long accountId, String filter) {
         Account acct = SyncManager.getAccountById(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;
@@ -1069,17 +1099,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;
     }
 
-    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();
         }
@@ -1099,18 +1128,21 @@
      * @param method the method we are going to send
      * @param usePolicyKey whether or not a policy key should be sent in the headers
      */
-    private void setHeaders(HttpRequestBase method, boolean usePolicyKey) {
+    /*package*/ void setHeaders(HttpRequestBase method, boolean usePolicyKey) {
         method.setHeader("Authorization", mAuthString);
         method.setHeader("MS-ASProtocolVersion", mProtocolVersion);
         method.setHeader("Connection", "keep-alive");
         method.setHeader("User-Agent", mDeviceType + '/' + Eas.VERSION);
-        if (usePolicyKey && (mAccount != null)) {
-            String key = mAccount.mSecuritySyncKey;
-            if (key == null || key.length() == 0) {
-                return;
-            }
-             if (Eas.PARSER_LOG) {
-                userLog("Policy key: " , key);
+        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
+            // to be enforced
+            String key = "0";
+            if (mAccount != null) {
+                String accountKey = mAccount.mSecuritySyncKey;
+                if (!TextUtils.isEmpty(accountKey)) {
+                    key = accountKey;
+                }
             }
             method.setHeader("X-MS-PolicyKey", key);
         }
@@ -1277,7 +1309,7 @@
             } else if (sp.isActive(ps)) {
                 // See if the required policies are in force; if they are, acknowledge the policies
                 // to the server and get the final policy key
-                String policyKey = acknowledgeProvision(pp.getPolicyKey());
+                String policyKey = acknowledgeProvision(pp.getPolicyKey(), PROVISION_STATUS_OK);
                 if (policyKey != null) {
                     // Write the final policy key to the Account and say we've been successful
                     ps.writeAccount(mAccount, policyKey, true, mContext);
@@ -1314,15 +1346,20 @@
             InputStream is = resp.getEntity().getContent();
             ProvisionParser pp = new ProvisionParser(is, this);
             if (pp.parse()) {
-                // If true, we received policies from the server; see if they are supported by
-                // the framework; if so, return the ProvisionParser containing the policy set and
-                // temporary key
-                PolicySet ps = pp.getPolicySet();
-                // The PolicySet can be null if there are policies we don't know about (e.g. ones
-                // from Exchange 12.1)  If we have a PolicySet, then we ask whether the device can
-                // support the actual parameters of those policies.
-                if ((ps != null) && SecurityPolicy.getInstance(mContext).isSupported(ps)) {
+                // 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;
                 }
             }
         }
@@ -1338,14 +1375,15 @@
      * @throws IOException
      */
     private void acknowledgeRemoteWipe(String tempKey) throws IOException {
-        acknowledgeProvisionImpl(tempKey, true);
+        acknowledgeProvisionImpl(tempKey, PROVISION_STATUS_OK, true);
     }
 
-    private String acknowledgeProvision(String tempKey) throws IOException {
-        return acknowledgeProvisionImpl(tempKey, false);
+    private String acknowledgeProvision(String tempKey, String result) throws IOException {
+        return acknowledgeProvisionImpl(tempKey, result, false);
     }
 
-    private String acknowledgeProvisionImpl(String tempKey, boolean remoteWipe) throws IOException {
+    private String acknowledgeProvisionImpl(String tempKey, String status,
+            boolean remoteWipe) throws IOException {
         Serializer s = new Serializer();
         s.start(Tags.PROVISION_PROVISION).start(Tags.PROVISION_POLICIES);
         s.start(Tags.PROVISION_POLICY);
@@ -1354,11 +1392,11 @@
         s.data(Tags.PROVISION_POLICY_TYPE, getPolicyType());
 
         s.data(Tags.PROVISION_POLICY_KEY, tempKey);
-        s.data(Tags.PROVISION_STATUS, "1");
+        s.data(Tags.PROVISION_STATUS, status);
         s.end().end(); // PROVISION_POLICY, PROVISION_POLICIES
         if (remoteWipe) {
             s.start(Tags.PROVISION_REMOTE_WIPE);
-            s.data(Tags.PROVISION_STATUS, "1");
+            s.data(Tags.PROVISION_STATUS, PROVISION_STATUS_OK);
             s.end();
         }
         s.end().done(); // PROVISION_PROVISION
@@ -1368,7 +1406,7 @@
             InputStream is = resp.getEntity().getContent();
             ProvisionParser pp = new ProvisionParser(is, this);
             if (pp.parse()) {
-                // Return the final polic key from the ProvisionParser
+                // Return the final policy key from the ProvisionParser
                 return pp.getPolicyKey();
             }
         }
@@ -1384,7 +1422,7 @@
      */
     public void runAccountMailbox() throws IOException, EasParserException {
         // Initialize exit status to success
-        mExitStatus = EmailServiceStatus.SUCCESS;
+        mExitStatus = EXIT_DONE;
         try {
             try {
                 SyncManager.callback()
@@ -1393,6 +1431,12 @@
                 // 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");
@@ -1979,9 +2023,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
@@ -2028,6 +2075,24 @@
                     timeout);
             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
+                    mExitStatus = EXIT_DONE;
+                    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);
+                    }
+                    return;
+                }
                 InputStream is = resp.getEntity().getContent();
                 if (is != null) {
                     moreAvailable = target.parse(is);
@@ -2084,7 +2149,7 @@
         if (mProtocolVersion == null) {
             mProtocolVersion = Eas.DEFAULT_PROTOCOL_VERSION;
         }
-        mProtocolVersionDouble = Double.parseDouble(mProtocolVersion);
+        mProtocolVersionDouble = Eas.getProtocolVersionDouble(mProtocolVersion);
         return true;
     }
 
diff --git a/src/com/android/exchange/MailboxAlarmReceiver.java b/src/com/android/exchange/MailboxAlarmReceiver.java
index f8551e4..565b158 100644
--- a/src/com/android/exchange/MailboxAlarmReceiver.java
+++ b/src/com/android/exchange/MailboxAlarmReceiver.java
@@ -29,9 +29,14 @@
 public class MailboxAlarmReceiver extends BroadcastReceiver {
     @Override
     public void onReceive(Context context, Intent intent) {
-        long mid = intent.getLongExtra("mailbox", -1);
-        SyncManager.log("Alarm received for: " + SyncManager.alarmOwner(mid));
-        SyncManager.alert(context, mid);
+        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));
+        } else {
+            SyncManager.log("Alarm received for: " + SyncManager.alarmOwner(mailboxId));
+            SyncManager.alert(context, mailboxId);
+        }
     }
 }
 
diff --git a/src/com/android/exchange/SyncManager.java b/src/com/android/exchange/SyncManager.java
index 7a56cbb..890a99a 100644
--- a/src/com/android/exchange/SyncManager.java
+++ b/src/com/android/exchange/SyncManager.java
@@ -21,7 +21,6 @@
 import com.android.email.Email;
 import com.android.email.SecurityPolicy;
 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;
@@ -114,7 +113,8 @@
     private static final String TAG = "EAS SyncManager";
 
     // The SyncManager's mailbox "id"
-    private static final int SYNC_MANAGER_ID = -1;
+    protected static final int SYNC_MANAGER_ID = -1;
+    protected static final int SYNC_MANAGER_SERVICE_ID = 0;
 
     private static final int SECONDS = 1000;
     private static final int MINUTES = 60*SECONDS;
@@ -182,8 +182,6 @@
     // All threads can use this lock to wait for connectivity
     public static final Object sConnectivityLock = new Object();
     public static boolean sConnectivityHold = false;
-    // Keep our cached list of active Accounts here
-    public static final AccountList sAccountList = new AccountList();
 
     // Keeps track of running services (by mailbox id)
     private HashMap<Long, AbstractSyncService> mServiceMap =
@@ -196,6 +194,8 @@
     private HashMap<Long, PendingIntent> mPendingIntents = new HashMap<Long, PendingIntent>();
     // The actual WakeLock obtained by SyncManager
     private WakeLock mWakeLock = null;
+    // Keep our cached list of active Accounts here
+    public final AccountList mAccountList = new AccountList();
 
     // Observers that we use to look for changed mail-related data
     private Handler mHandler = new Handler();
@@ -204,6 +204,7 @@
     private SyncedMessageObserver mSyncedMessageObserver;
     private MessageObserver mMessageObserver;
     private EasSyncStatusObserver mSyncStatusObserver;
+    private Object mStatusChangeListener;
     private EasAccountsUpdatedListener mAccountsUpdatedListener;
 
     private HashMap<Long, CalendarObserver> mCalendarObservers =
@@ -221,7 +222,7 @@
     // Count of ClientConnectionManager shutdowns
     private static volatile int sClientConnectionManagerShutdownCount = 0;
 
-    private boolean mStop = false;
+    private static volatile boolean sStop = false;
 
     // The reason for SyncManager's next wakeup call
     private String mNextWaitReason;
@@ -285,15 +286,10 @@
      */
     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, SyncManager.this);
         }
 
         public Bundle autoDiscover(String userName, String password) throws RemoteException {
@@ -424,6 +420,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 {
@@ -434,18 +439,18 @@
             super(handler);
             // At startup, we want to see what EAS accounts exist and cache them
             Context context = getContext();
-            synchronized (sAccountList) {
+            synchronized (mAccountList) {
                 Cursor c = getContentResolver().query(Account.CONTENT_URI,
                         Account.CONTENT_PROJECTION, null, null, null);
                 // Build the account list from the cursor
                 try {
-                    collectEasAccounts(c, sAccountList);
+                    collectEasAccounts(c, mAccountList);
                 } finally {
                     c.close();
                 }
 
                 // Create an account mailbox for any account without one
-                for (Account account : sAccountList) {
+                for (Account account : mAccountList) {
                     int cnt = Mailbox.count(context, Mailbox.CONTENT_URI, "accountKey="
                             + account.mId, null);
                     if (cnt == 0) {
@@ -464,8 +469,8 @@
             if (mSyncableEasMailboxSelector == null) {
                 StringBuilder sb = new StringBuilder(WHERE_NOT_INTERVAL_NEVER_AND_ACCOUNT_KEY_IN);
                 boolean first = true;
-                synchronized (sAccountList) {
-                    for (Account account : sAccountList) {
+                synchronized (mAccountList) {
+                    for (Account account : mAccountList) {
                         if (!first) {
                             sb.append(',');
                         } else {
@@ -489,8 +494,8 @@
             if (mEasAccountSelector == null) {
                 StringBuilder sb = new StringBuilder(ACCOUNT_KEY_IN);
                 boolean first = true;
-                synchronized (sAccountList) {
-                    for (Account account : sAccountList) {
+                synchronized (mAccountList) {
+                    for (Account account : mAccountList) {
                         if (!first) {
                             sb.append(',');
                         } else {
@@ -520,14 +525,14 @@
                     Account.CONTENT_PROJECTION, null, null, null);
             try {
                 collectEasAccounts(c, currentAccounts);
-                synchronized (sAccountList) {
-                    for (Account account : sAccountList) {
-                        // 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
+                synchronized (mAccountList) {
+                    for (Account account : mAccountList) {
+                        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(
@@ -536,9 +541,9 @@
                             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) {
@@ -568,7 +573,7 @@
                     }
                     // Look for new accounts
                     for (Account account : currentAccounts) {
-                        if (!sAccountList.contains(account.mId)) {
+                        if (!mAccountList.contains(account.mId)) {
                             // Don't forget to cache the HostAuth
                             HostAuth ha = HostAuth.restoreHostAuthWithId(getContext(),
                                     account.mHostAuthKeyRecv);
@@ -577,14 +582,14 @@
                             // This is an addition; create our magic hidden mailbox...
                             log("Account observer found new account: " + account.mDisplayName);
                             addAccountMailbox(account.mId);
-                            sAccountList.add(account);
+                            mAccountList.add(account);
                             mSyncableEasMailboxSelector = null;
                             mEasAccountSelector = null;
                         }
                     }
                     // Finally, make sure our account list is up to date
-                    sAccountList.clear();
-                    sAccountList.addAll(currentAccounts);
+                    mAccountList.clear();
+                    mAccountList.addAll(currentAccounts);
                 }
             } finally {
                 c.close();
@@ -599,7 +604,7 @@
             new Thread(new Runnable() {
                public void run() {
                    onAccountChanged();
-                }}).start();
+                }}, "Account Observer").start();
         }
 
         private void collectEasAccounts(Cursor c, ArrayList<Account> accounts) {
@@ -773,7 +778,7 @@
                         } finally {
                             c.close();
                         }
-                    }}).start();
+                    }}, "Calendar Observer").start();
             }
         }
     }
@@ -831,9 +836,25 @@
     }
 
     static public Account getAccountById(long accountId) {
-        synchronized (sAccountList) {
-            return sAccountList.getById(accountId);
+        SyncManager syncManager = INSTANCE;
+        if (syncManager != null) {
+            AccountList accountList = syncManager.mAccountList;
+            synchronized (accountList) {
+                return accountList.getById(accountId);
+            }
         }
+        return null;
+    }
+
+    static public Account getAccountByName(String accountName) {
+        SyncManager syncManager = INSTANCE;
+        if (syncManager != null) {
+            AccountList accountList = syncManager.mAccountList;
+            synchronized (accountList) {
+                return accountList.getByName(accountName);
+            }
+        }
+        return null;
     }
 
     static public String getEasAccountSelector() {
@@ -963,13 +984,13 @@
             public void run() {
                 android.accounts.Account[] accountMgrList = AccountManager.get(syncManager)
                         .getAccountsByType(Email.EXCHANGE_ACCOUNT_MANAGER_TYPE);
-                synchronized (sAccountList) {
+                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, sAccountList,
+                        reconcileAccountsWithAccountManager(syncManager, mAccountList,
                                 accountMgrList, false, mResolver);
                     }
                 }
@@ -1055,113 +1076,6 @@
         return mBinder;
     }
 
-    /**
-     * Note that there are two ways the EAS SyncManager service can be created:
-     *
-     * 1) as a background service instantiated via startService (which happens on boot, when the
-     * first EAS account is created, etc), in which case the service thread is spun up, mailboxes
-     * sync, etc. and
-     * 2) to execute an RPC call from the UI, in which case the background service will already be
-     * running most of the time (unless we're creating a first EAS account)
-     *
-     * If the running background service detects that there are no EAS accounts (on boot, if none
-     * were created, or afterward if the last remaining EAS account is deleted), it will call
-     * stopSelf() to terminate operation.
-     *
-     * The goal is to ensure that the background service is running at all times when there is at
-     * least one EAS account in existence
-     *
-     * Because there are edge cases in which our process can crash (typically, this has been seen
-     * in UI crashes, ANR's, etc.), it's possible for the UI to start up again without the
-     * background service having been started.  We explicitly try to start the service in Welcome
-     * (to handle the case of the app having been reloaded).  We also start the service on any
-     * startSync call (if it isn't already running)
-     */
-    @Override
-    public void onCreate() {
-        alwaysLog("!!! EAS SyncManager, onCreate");
-        if (INSTANCE == null) {
-            INSTANCE = this;
-            mResolver = getContentResolver();
-            mAccountObserver = new AccountObserver(mHandler);
-            mResolver.registerContentObserver(Account.CONTENT_URI, true, mAccountObserver);
-            mMailboxObserver = new MailboxObserver(mHandler);
-            mSyncedMessageObserver = new SyncedMessageObserver(mHandler);
-            mMessageObserver = new MessageObserver(mHandler);
-            mSyncStatusObserver = new EasSyncStatusObserver();
-        } else {
-            alwaysLog("!!! EAS SyncManager onCreated, but INSTANCE not null??");
-        }
-        if (sDeviceId == null) {
-            try {
-                getDeviceId(this);
-            } catch (IOException e) {
-                // We can't run in this situation
-                throw new RuntimeException();
-            }
-        }
-        // 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();
-    }
-
-    @Override
-    public int onStartCommand(Intent intent, int flags, int startId) {
-        alwaysLog("!!! EAS SyncManager, onStartCommand");
-
-        // Restore accounts, if it has not happened already
-        AccountBackupRestore.restoreAccountsIfNeeded(this);
-
-        maybeStartSyncManagerThread();
-        if (sServiceThread == null) {
-            alwaysLog("!!! EAS SyncManager, stopping self");
-            stopSelf();
-        }
-        return Service.START_STICKY;
-    }
-
-    @Override
-    public void onDestroy() {
-        alwaysLog("!!! EAS SyncManager, onDestroy");
-        if (INSTANCE != null) {
-            INSTANCE = null;
-            mResolver.unregisterContentObserver(mAccountObserver);
-            unregisterCalendarObservers();
-            mResolver = null;
-            mAccountObserver = null;
-            mMailboxObserver = null;
-            mSyncedMessageObserver = null;
-            mMessageObserver = null;
-            mSyncStatusObserver = null;
-            mAccountsUpdatedListener = null;
-        }
-    }
-
-    void maybeStartSyncManagerThread() {
-        // Start our thread...
-        // See if there are any EAS accounts; otherwise, just go away
-        if (EmailContent.count(this, HostAuth.CONTENT_URI, WHERE_PROTOCOL_EAS, null) > 0) {
-            if (sServiceThread == null || !sServiceThread.isAlive()) {
-                log(sServiceThread == null ? "Starting thread..." : "Restarting thread...");
-                sServiceThread = new Thread(this, "SyncManager");
-                sServiceThread.start();
-            }
-        }
-    }
-
-    static void checkSyncManagerServiceRunning() {
-        // Get the service thread running if it isn't
-        // This is a stopgap for cases in which SyncManager died (due to a crash somewhere in
-        // com.android.email) and hasn't been restarted
-        // See the comment for onCreate for details
-        SyncManager syncManager = INSTANCE;
-        if (syncManager == null) return;
-        if (sServiceThread == null) {
-            alwaysLog("!!! checkSyncManagerServiceRunning; starting service...");
-            syncManager.startService(new Intent(syncManager, SyncManager.class));
-        }
-    }
-
     static public ConnPerRoute sConnPerRoute = new ConnPerRoute() {
         public int getMaxForRoute(HttpRoute route) {
             return 8;
@@ -1453,6 +1367,10 @@
             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: ";
+                if (service.mMailbox != null) {
+                    threadName += service.mMailbox.mDisplayName;
+                }
                 new Thread(new Runnable() {
                    public void run() {
                        Mailbox m = Mailbox.restoreMailboxWithId(syncManager, id);
@@ -1484,7 +1402,7 @@
                                SyncManager.shutdownConnectionManager();
                            }
                        }
-                    }}).start();
+                    }}, threadName).start();
             }
         }
     }
@@ -1541,8 +1459,8 @@
      * Make our sync settings match those of AccountManager
      */
     private void checkPIMSyncSettings() {
-        synchronized (sAccountList) {
-            for (Account account : sAccountList) {
+        synchronized (mAccountList) {
+            for (Account account : mAccountList) {
                 updatePIMSyncSettings(account, Mailbox.TYPE_CONTACTS, ContactsContract.AUTHORITY);
                 updatePIMSyncSettings(account, Mailbox.TYPE_CALENDAR, Calendar.AUTHORITY);
             }
@@ -1668,8 +1586,8 @@
                 // Otherwise, stop all syncs
                 } else {
                     log("Background data off: stop all syncs");
-                    synchronized (sAccountList) {
-                        for (Account account : sAccountList)
+                    synchronized (mAccountList) {
+                        for (Account account : mAccountList)
                             SyncManager.stopAccountSyncs(account.mId);
                     }
                 }
@@ -1723,7 +1641,7 @@
 
     private void requestSync(Mailbox m, int reason, Request req) {
         // Don't sync if there's no connectivity
-        if (sConnectivityHold || (m == null)) return;
+        if (sConnectivityHold || (m == null) || sStop) return;
         synchronized (sSyncLock) {
             Account acct = Account.restoreAccountWithId(this, m.mAccountKey);
             if (acct != null) {
@@ -1770,7 +1688,7 @@
         boolean waiting = false;
         ConnectivityManager cm =
             (ConnectivityManager)this.getSystemService(Context.CONNECTIVITY_SERVICE);
-        while (!mStop) {
+        while (!sStop) {
             NetworkInfo info = cm.getActiveNetworkInfo();
             if (info != null) {
                 // We're done if there's an active network
@@ -1807,9 +1725,111 @@
         }
     }
 
-    public void run() {
-        mStop = false;
+    /**
+     * Note that there are two ways the EAS SyncManager service can be created:
+     *
+     * 1) as a background service instantiated via startService (which happens on boot, when the
+     * first EAS account is created, etc), in which case the service thread is spun up, mailboxes
+     * sync, etc. and
+     * 2) to execute an RPC call from the UI, in which case the background service will already be
+     * running most of the time (unless we're creating a first EAS account)
+     *
+     * If the running background service detects that there are no EAS accounts (on boot, if none
+     * were created, or afterward if the last remaining EAS account is deleted), it will call
+     * stopSelf() to terminate operation.
+     *
+     * The goal is to ensure that the background service is running at all times when there is at
+     * least one EAS account in existence
+     *
+     * Because there are edge cases in which our process can crash (typically, this has been seen
+     * in UI crashes, ANR's, etc.), it's possible for the UI to start up again without the
+     * background service having been started.  We explicitly try to start the service in Welcome
+     * (to handle the case of the app having been reloaded).  We also start the service on any
+     * startSync call (if it isn't already running)
+     */
+    @Override
+    public void onCreate() {
+        synchronized (sSyncLock) {
+            alwaysLog("!!! EAS SyncManager, onCreate");
+            if (sStop) {
+                return;
+            }
+            if (sDeviceId == null) {
+                try {
+                    getDeviceId(this);
+                } catch (IOException e) {
+                    // We can't run in this situation
+                    throw new RuntimeException();
+                }
+            }
+            // 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();
+        }
+    }
 
+    @Override
+    public int onStartCommand(Intent intent, int flags, int startId) {
+        synchronized (sSyncLock) {
+            alwaysLog("!!! EAS SyncManager, onStartCommand");
+            // Restore accounts, if it has not happened already
+            AccountBackupRestore.restoreAccountsIfNeeded(this);
+            maybeStartSyncManagerThread();
+            if (sServiceThread == null) {
+                alwaysLog("!!! EAS SyncManager, stopping self");
+                stopSelf();
+            } else if (sStop) {
+                // If we were in the middle of trying to stop, attempt a restart in 5 seconds
+                setAlarm(SYNC_MANAGER_SERVICE_ID, 5*SECONDS);
+            }
+            return Service.START_STICKY;
+        }
+    }
+
+    @Override
+    public void onDestroy() {
+        synchronized(sSyncLock) {
+            alwaysLog("!!! EAS SyncManager, onDestroy");
+            // Stop the sync manager thread and return
+            synchronized (sSyncLock) {
+                sStop = true;
+                if (sServiceThread != null) {
+                    sServiceThread.interrupt();
+                }
+            }
+        }
+    }
+
+    void maybeStartSyncManagerThread() {
+        // Start our thread...
+        // See if there are any EAS accounts; otherwise, just go away
+        if (EmailContent.count(this, HostAuth.CONTENT_URI, WHERE_PROTOCOL_EAS, null) > 0) {
+            if (sServiceThread == null || !sServiceThread.isAlive()) {
+                log(sServiceThread == null ? "Starting thread..." : "Restarting thread...");
+                sServiceThread = new Thread(this, "SyncManager");
+                INSTANCE = this;
+                sServiceThread.start();
+            }
+        }
+    }
+
+    /**
+     * 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
+     * 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;
+        if (sServiceThread == null) {
+            alwaysLog("!!! checkSyncManagerServiceRunning; starting service...");
+            syncManager.startService(new Intent(syncManager, SyncManager.class));
+        }
+    }
+
+    public void run() {
+        sStop = false;
+        alwaysLog("!!! SyncManager thread running");
         // If we're really debugging, turn on all logging
         if (Eas.DEBUG) {
             Eas.USER_LOG = true;
@@ -1822,41 +1842,56 @@
             Debug.waitForDebugger();
         }
 
-        // Set up our observers; we need them to know when to start/stop various syncs based
-        // on the insert/delete/update of mailboxes and accounts
-        // We also observe synced messages to trigger upsyncs at the appropriate time
-        mResolver.registerContentObserver(Mailbox.CONTENT_URI, false, mMailboxObserver);
-        mResolver.registerContentObserver(Message.SYNCED_CONTENT_URI, true, mSyncedMessageObserver);
-        mResolver.registerContentObserver(Message.CONTENT_URI, true, mMessageObserver);
-        ContentResolver.addStatusChangeListener(ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS,
-                mSyncStatusObserver);
-        mAccountsUpdatedListener = new EasAccountsUpdatedListener();
-        // TODO Find and fix root cause of duplication
-        try {
-            AccountManager.get(getApplication())
-                .addOnAccountsUpdatedListener(mAccountsUpdatedListener, mHandler, true);
-        } catch (IllegalStateException e1) {
-            // This exception is more of a warning; we shouldn't be in the state in which we
-            // already have a listener.
+        // Synchronize here to prevent a shutdown from happening while we initialize our observers
+        // and receivers
+        synchronized (sSyncLock) {
+            if (INSTANCE != null) {
+                mResolver = getContentResolver();
+
+                // Set up our observers; we need them to know when to start/stop various syncs based
+                // on the insert/delete/update of mailboxes and accounts
+                // We also observe synced messages to trigger upsyncs at the appropriate time
+                mAccountObserver = new AccountObserver(mHandler);
+                mResolver.registerContentObserver(Account.CONTENT_URI, true, mAccountObserver);
+                mMailboxObserver = new MailboxObserver(mHandler);
+                mResolver.registerContentObserver(Mailbox.CONTENT_URI, false, mMailboxObserver);
+                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(
+                            ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS, mSyncStatusObserver);
+
+                // Set up our observer for AccountManager
+                mAccountsUpdatedListener = new EasAccountsUpdatedListener();
+                AccountManager.get(getApplication()).addOnAccountsUpdatedListener(
+                        mAccountsUpdatedListener, mHandler, true);
+
+                // Set up receivers for connectivity and background data setting
+                mConnectivityReceiver = new ConnectivityReceiver();
+                registerReceiver(mConnectivityReceiver, new IntentFilter(
+                        ConnectivityManager.CONNECTIVITY_ACTION));
+
+                mBackgroundDataSettingReceiver = new ConnectivityReceiver();
+                registerReceiver(mBackgroundDataSettingReceiver, new IntentFilter(
+                        ConnectivityManager.ACTION_BACKGROUND_DATA_SETTING_CHANGED));
+                // Save away the current background data setting; we'll keep track of it with the
+                // receiver we just registered
+                ConnectivityManager cm = (ConnectivityManager)getSystemService(
+                        Context.CONNECTIVITY_SERVICE);
+                mBackgroundData = cm.getBackgroundDataSetting();
+
+                // See if any settings have changed while we weren't running...
+                checkPIMSyncSettings();
+            }
         }
 
-        // Set up receivers for ConnectivityManager
-        mConnectivityReceiver = new ConnectivityReceiver();
-        registerReceiver(mConnectivityReceiver,
-                new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));
-        mBackgroundDataSettingReceiver = new ConnectivityReceiver();
-        registerReceiver(mBackgroundDataSettingReceiver,
-                 new IntentFilter(ConnectivityManager.ACTION_BACKGROUND_DATA_SETTING_CHANGED));
-        // Save away background data setting; we'll keep track of it with the receiver
-        ConnectivityManager cm =
-            (ConnectivityManager)getSystemService(Context.CONNECTIVITY_SERVICE);
-        mBackgroundData = cm.getBackgroundDataSetting();
-
-        // See if any settings have changed while we weren't running...
-        checkPIMSyncSettings();
-
         try {
-            while (!mStop) {
+            // Loop indefinitely until we're shut down
+            while (!sStop) {
                 runAwake(SYNC_MANAGER_ID);
                 waitForConnectivity();
                 mNextWaitReason = "Heartbeat";
@@ -1877,6 +1912,7 @@
                     }
                 } catch (InterruptedException e) {
                     // Needs to be caught, but causes no problem
+                    log("SyncManager interrupted");
                 } finally {
                     synchronized (this) {
                         if (mKicked) {
@@ -1891,51 +1927,78 @@
             Log.e(TAG, "RuntimeException in SyncManager", e);
             throw e;
         } finally {
-            log("Finishing SyncManager");
-            // Lots of cleanup here
-            // Stop our running syncs
-            stopServiceThreads();
-
-            // Stop receivers and content observers
-            if (mConnectivityReceiver != null) {
-                unregisterReceiver(mConnectivityReceiver);
-            }
-            if (mBackgroundDataSettingReceiver != null) {
-                unregisterReceiver(mBackgroundDataSettingReceiver);
-            }
-
-            if (INSTANCE != null) {
-                ContentResolver resolver = getContentResolver();
-                resolver.unregisterContentObserver(mAccountObserver);
-                resolver.unregisterContentObserver(mMailboxObserver);
-                resolver.unregisterContentObserver(mSyncedMessageObserver);
-                resolver.unregisterContentObserver(mMessageObserver);
-                unregisterCalendarObservers();
-            }
-            // Don't leak the Intent associated with this listener
-            if (mAccountsUpdatedListener != null) {
-                AccountManager.get(this).removeOnAccountsUpdatedListener(mAccountsUpdatedListener);
-                mAccountsUpdatedListener = null;
-            }
-
-            // Clear pending alarms and associated Intents
-            clearAlarms();
-
-            // Release our wake lock, if we have one
-            synchronized (mWakeLocks) {
-                if (mWakeLock != null) {
-                    mWakeLock.release();
-                    mWakeLock = null;
-                }
-            }
-
-            log("Goodbye");
+            shutdown();
         }
+    }
 
-        if (!mStop) {
-            // If this wasn't intentional, try to restart the service
-            throw new RuntimeException("EAS SyncManager crash; please restart me...");
-       }
+    private void shutdown() {
+        synchronized (sSyncLock) {
+            // If INSTANCE is null, we've already been shut down
+            if (INSTANCE != null) {
+                log("SyncManager shutting down...");
+
+                // Stop our running syncs
+                stopServiceThreads();
+
+                // Stop receivers
+                if (mConnectivityReceiver != null) {
+                    unregisterReceiver(mConnectivityReceiver);
+                }
+                if (mBackgroundDataSettingReceiver != null) {
+                    unregisterReceiver(mBackgroundDataSettingReceiver);
+                }
+
+                // Unregister observers
+                ContentResolver resolver = getContentResolver();
+                if (mSyncedMessageObserver != null) {
+                    resolver.unregisterContentObserver(mSyncedMessageObserver);
+                    mSyncedMessageObserver = null;
+                }
+                if (mMessageObserver != null) {
+                    resolver.unregisterContentObserver(mMessageObserver);
+                    mMessageObserver = null;
+                }
+                if (mAccountObserver != null) {
+                    resolver.unregisterContentObserver(mAccountObserver);
+                    mAccountObserver = null;
+                }
+                if (mMailboxObserver != null) {
+                    resolver.unregisterContentObserver(mMailboxObserver);
+                    mMailboxObserver = null;
+                }
+                unregisterCalendarObservers();
+
+                // Remove account listener (registered with AccountManager)
+                if (mAccountsUpdatedListener != null) {
+                    AccountManager.get(this).removeOnAccountsUpdatedListener(
+                            mAccountsUpdatedListener);
+                    mAccountsUpdatedListener = null;
+                }
+
+                // Remove the sync status change listener (and null out the observer)
+                if (mStatusChangeListener != null) {
+                    ContentResolver.removeStatusChangeListener(mStatusChangeListener);
+                    mStatusChangeListener = null;
+                    mSyncStatusObserver = null;
+                }
+
+                // Clear pending alarms and associated Intents
+                clearAlarms();
+
+                // Release our wake lock, if we have one
+                synchronized (mWakeLocks) {
+                    if (mWakeLock != null) {
+                        mWakeLock.release();
+                        mWakeLock = null;
+                    }
+                }
+
+                INSTANCE = null;
+                sServiceThread = null;
+                sStop = false;
+                log("Goodbye");
+            }
+        }
     }
 
     private void releaseMailbox(long mailboxId) {
@@ -2112,7 +2175,7 @@
                         long requestTime = service.mRequestTime;
                         if (requestTime > 0) {
                             long timeToRequest = requestTime - now;
-                            if (service instanceof AbstractSyncService && timeToRequest <= 0) {
+                            if (timeToRequest <= 0) {
                                 service.mRequestTime = 0;
                                 service.alarm();
                             } else if (requestTime > 0 && timeToRequest < nextWait) {
@@ -2137,15 +2200,24 @@
         serviceRequest(mailboxId, 5*SECONDS, reason);
     }
 
+    /**
+     * 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) {
         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;
-        }
+        if (!isSyncable(m)) return;
         try {
             AbstractSyncService service = syncManager.mServiceMap.get(mailboxId);
             if (service != null) {
diff --git a/src/com/android/exchange/adapter/CalendarSyncAdapter.java b/src/com/android/exchange/adapter/CalendarSyncAdapter.java
index 25b6913..59a9823 100644
--- a/src/com/android/exchange/adapter/CalendarSyncAdapter.java
+++ b/src/com/android/exchange/adapter/CalendarSyncAdapter.java
@@ -1251,7 +1251,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;
@@ -1649,7 +1649,7 @@
             }
 
             // Send exception deleted flag if necessary
-            Integer deleted = entityValues.getAsInteger(Events.DELETED);
+            Integer deleted = entityValues.getAsInteger(Calendar.EventsColumns.DELETED);
             boolean isDeleted = deleted != null && deleted == 1;
             Integer eventStatus = entityValues.getAsInteger(Events.STATUS);
             boolean isCanceled = eventStatus != null && eventStatus.equals(Events.STATUS_CANCELED);
@@ -1789,7 +1789,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);
@@ -1855,7 +1855,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..c05a820 100644
--- a/src/com/android/exchange/adapter/ContactsSyncAdapter.java
+++ b/src/com/android/exchange/adapter/ContactsSyncAdapter.java
@@ -1071,8 +1071,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)
diff --git a/src/com/android/exchange/adapter/ProvisionParser.java b/src/com/android/exchange/adapter/ProvisionParser.java
index af1ecdd..bbb3464 100644
--- a/src/com/android/exchange/adapter/ProvisionParser.java
+++ b/src/com/android/exchange/adapter/ProvisionParser.java
@@ -37,11 +37,11 @@
     PolicySet mPolicySet = null;
     String mPolicyKey = null;
     boolean mRemoteWipe = false;
+    boolean mIsSupportable = true;
 
     public ProvisionParser(InputStream in, EasSyncService service) throws IOException {
         super(in);
         mService = service;
-        setDebug(true);
     }
 
     public PolicySet getPolicySet() {
@@ -56,14 +56,22 @@
         return mRemoteWipe;
     }
 
-    public void parseProvisionDocWbxml() throws IOException {
+    public boolean hasSupportablePolicySet() {
+        return (mPolicySet != null) && mIsSupportable;
+    }
+
+    private void parseProvisionDocWbxml() throws IOException {
         int minPasswordLength = 0;
         int passwordMode = PolicySet.PASSWORD_MODE_NONE;
         int maxPasswordFails = 0;
         int maxScreenLockTime = 0;
+        int passwordExpiration = 0;
+        int passwordHistory = 0;
+        int passwordComplexChars = 0;
         boolean canSupport = true;
 
         while (nextTag(Tags.PROVISION_EAS_PROVISION_DOC) != END) {
+            boolean supported = true;
             switch (tag) {
                 case Tags.PROVISION_DEVICE_PASSWORD_ENABLED:
                     if (getValueInt() == 1) {
@@ -87,36 +95,138 @@
                 case Tags.PROVISION_MAX_DEVICE_PASSWORD_FAILED_ATTEMPTS:
                     maxPasswordFails = getValueInt();
                     break;
+                case Tags.PROVISION_DEVICE_PASSWORD_EXPIRATION:
+                    passwordExpiration = getValueInt();
+                    // We don't yet support this
+                    if (passwordExpiration > 0) {
+                        supported = false;
+                    }
+                    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) {
-                        canSupport = false;
+                        supported = false;
+                    }
+                    break;
+                // Bluetooth: 0 = no bluetooth; 1 = only hands-free; 2 = allowed
+                case Tags.PROVISION_ALLOW_BLUETOOTH:
+                    if (getValueInt() != 2) {
+                        supported = 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) {
-                        canSupport = false;
+                        supported = false;
+                    }
+                    break;
+                // The following, if greater than zero, can't be supported at the moment
+                case Tags.PROVISION_MAX_ATTACHMENT_SIZE:
+                    if (getValueInt() > 0) {
+                        supported = false;
+                    }
+                    break;
+                // Complex character setting is only used if we're in "strong" (alphanumeric) mode
+                case Tags.PROVISION_MIN_DEVICE_PASSWORD_COMPLEX_CHARS:
+                    passwordComplexChars = getValueInt();
+                    if ((passwordMode == PolicySet.PASSWORD_MODE_STRONG) &&
+                            (passwordComplexChars > 0)) {
+                        supported = false;
+                    }
+                    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)) {
+                        supported = 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) {
+                        supported = 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")) {
+                        supported = false;
                     }
                     break;
                 default:
                     skipTag();
             }
+
+            if (!supported) {
+                log("Policy not supported: " + tag);
+                mIsSupportable = false;
+            }
         }
 
-        if (canSupport) {
-            mPolicySet = new SecurityPolicy.PolicySet(minPasswordLength, passwordMode,
-                    maxPasswordFails, maxScreenLockTime, true);
+        mPolicySet = new SecurityPolicy.PolicySet(minPasswordLength, passwordMode,
+                maxPasswordFails, maxScreenLockTime, true, passwordExpiration, 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 {
@@ -124,9 +234,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 {
@@ -148,13 +261,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) {
@@ -177,7 +291,7 @@
         return passwordRequired;
     }
 
-    void parseCharacteristic(XmlPullParser parser, ShadowPolicySet sps)
+    private void parseCharacteristic(XmlPullParser parser, ShadowPolicySet sps)
             throws XmlPullParserException, IOException {
         boolean enforceInactivityTimer = true;
         while (true) {
@@ -219,7 +333,7 @@
         }
     }
 
-    void parseRegistry(XmlPullParser parser, ShadowPolicySet sps)
+    private void parseRegistry(XmlPullParser parser, ShadowPolicySet sps)
             throws XmlPullParserException, IOException {
       while (true) {
           int type = parser.nextTag();
@@ -234,7 +348,7 @@
       }
     }
 
-    void parseWapProvisioningDoc(XmlPullParser parser, ShadowPolicySet sps)
+    private void parseWapProvisioningDoc(XmlPullParser parser, ShadowPolicySet sps)
             throws XmlPullParserException, IOException {
         while (true) {
             int type = parser.nextTag();
@@ -258,7 +372,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();
@@ -268,7 +382,7 @@
         }
     }
 
-    public void parsePolicy() throws IOException {
+    private void parsePolicy() throws IOException {
         String policyType = null;
         while (nextTag(Tags.PROVISION_POLICY) != END) {
             switch (tag) {
@@ -297,7 +411,7 @@
         }
     }
 
-    public void parsePolicies() throws IOException {
+    private void parsePolicies() throws IOException {
         while (nextTag(Tags.PROVISION_POLICIES) != END) {
             if (tag == Tags.PROVISION_POLICY) {
                 parsePolicy();
@@ -318,10 +432,10 @@
                 case Tags.PROVISION_STATUS:
                     int status = getValueInt();
                     mService.userLog("Provision status: ", status);
+                    res = (status == 1);
                     break;
                 case Tags.PROVISION_POLICIES:
                     parsePolicies();
-                    res = (mPolicySet != null) || (mPolicyKey != null);
                     break;
                 case Tags.PROVISION_REMOTE_WIPE:
                     // Indicate remote wipe command received
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..5d6121f
--- /dev/null
+++ b/src/com/android/exchange/provider/ExchangeDirectoryProvider.java
@@ -0,0 +1,159 @@
+/*
+ * 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.provider.EmailContent.Account;
+import com.android.exchange.EasSyncService;
+import com.android.exchange.SyncManager;
+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.Binder;
+import android.provider.ContactsContract.CommonDataKinds;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.RawContacts;
+
+/**
+ * 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 GAL_BASE = 0;
+    private static final int GAL_FILTER = GAL_BASE;
+
+    private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH);
+
+    static {
+        sURIMatcher.addURI(EXCHANGE_GAL_AUTHORITY, "contacts/filter/*", GAL_FILTER);
+    }
+
+    @Override
+    public boolean onCreate() {
+        return true;
+    }
+
+    @Override
+    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+            String sortOrder) {
+        String accountName = uri.getQueryParameter(RawContacts.ACCOUNT_NAME);
+        if (accountName == null) {
+            return null;
+        }
+
+        Account account = SyncManager.getAccountByName(accountName);
+        if (account == null) {
+            return null;
+        }
+
+        int match = sURIMatcher.match(uri);
+        switch (match) {
+            case GAL_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;
+                }
+                long callingId = Binder.clearCallingIdentity();
+                try {
+                    // Get results from the Exchange account
+                    GalResult galResult = EasSyncService.searchGal(getContext(), account.mId,
+                            filter);
+                    if (galResult != null) {
+                        return buildGalResultCursor(projection, galResult);
+                    }
+                } finally {
+                    Binder.restoreCallingIdentity(callingId);
+                }
+                break;
+        }
+
+        return null;
+    }
+
+    /*package*/ Cursor buildGalResultCursor(String[] projection, GalResult galResult) {
+        int displayNameIndex = -1;
+        int emailIndex = -1;
+        boolean alternateDisplayName = false;
+
+        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)) {
+                displayNameIndex = i;
+                alternateDisplayName = true;
+
+            } else if (CommonDataKinds.Email.ADDRESS.equals(column)) {
+                emailIndex = i;
+            }
+            // TODO other fields
+        }
+
+        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);
+            if (displayNameIndex != -1) {
+                row[displayNameIndex] = galDataRow.displayName;
+                // TODO Handle alternate display name here
+            }
+            if (emailIndex != -1) {
+                row[emailIndex] = galDataRow.emailAddress;
+            }
+            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/tests/src/com/android/exchange/EasOutboxServiceTests.java b/tests/src/com/android/exchange/EasOutboxServiceTests.java
new file mode 100644
index 0000000..df6bd6c
--- /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
+        String cmd = svc.generateSmartSendCmd(true, "1339085683659694034", "Mail:^f");
+        assertEquals("SmartReply&ItemId=1339085683659694034&CollectionId=Mail%3A%5Ef", cmd);
+        // Test encoding of item id
+        cmd = svc.generateSmartSendCmd(false, "14:3", "6");
+        assertEquals("SmartForward&ItemId=14%3A3&CollectionId=6", cmd);
+    }
+}
diff --git a/tests/src/com/android/exchange/EasSyncServiceTests.java b/tests/src/com/android/exchange/EasSyncServiceTests.java
index d4372a5..320a1d1 100644
--- a/tests/src/com/android/exchange/EasSyncServiceTests.java
+++ b/tests/src/com/android/exchange/EasSyncServiceTests.java
@@ -17,13 +17,31 @@
 
 package com.android.exchange;
 
+import com.android.email.provider.EmailContent.Account;
+
+import org.apache.http.Header;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.client.methods.HttpRequestBase;
+
 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
+ */
+
 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";
+    static private final String TYPE = "type";
+
     Context mMockContext;
 
     @Override
@@ -73,4 +91,85 @@
             }
         }
     }
+
+    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());
+    }
+
+    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.mDeviceType = TYPE;
+        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=" + 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=" + TYPE,
+                svc.mCmdString);
+        assertEquals("https://" + HOST + "/Microsoft-Server-ActiveSync?Cmd=OPTIONS" +
+                svc.mCmdString, uriString);
+    }
 }
diff --git a/tests/src/com/android/exchange/SyncManagerAccountTests.java b/tests/src/com/android/exchange/SyncManagerAccountTests.java
index 3b67dc1..6a325b5 100644
--- a/tests/src/com/android/exchange/SyncManagerAccountTests.java
+++ b/tests/src/com/android/exchange/SyncManagerAccountTests.java
@@ -211,4 +211,24 @@
         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(SyncManager.isSyncable(null));
+        assertFalse(SyncManager.isSyncable(box1));
+        assertFalse(SyncManager.isSyncable(box2));
+        assertFalse(SyncManager.isSyncable(box3));
+        assertFalse(SyncManager.isSyncable(box4));
+        assertTrue(SyncManager.isSyncable(box5));
+    }
 }
diff --git a/tests/src/com/android/exchange/adapter/CalendarSyncAdapterTests.java b/tests/src/com/android/exchange/adapter/CalendarSyncAdapterTests.java
index eb87ccd..bef9938 100644
--- a/tests/src/com/android/exchange/adapter/CalendarSyncAdapterTests.java
+++ b/tests/src/com/android/exchange/adapter/CalendarSyncAdapterTests.java
@@ -16,13 +16,17 @@
 
 package com.android.exchange.adapter;
 
+import com.android.exchange.adapter.CalendarSyncAdapter.CalendarOperations;
 import com.android.exchange.adapter.CalendarSyncAdapter.EasCalendarSyncParser;
 
+import android.content.ContentProviderOperation;
 import android.content.ContentValues;
 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;
 
 /**
@@ -173,4 +177,129 @@
         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 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;
+    }
+
+    /**
+     * Note that there is no way to access the ContentValues inside of a ContentProviderOperation,
+     * which limits the extent to which we can test the result of parsing events.  We can count
+     * the number of objects to be created, but we can't examine them.
+     */
+    // TODO Try to convince fredq to allow access to the ContentValues of a CPO
+    public void testAddEvent() throws IOException {
+        CalendarSyncAdapter adapter = getTestSyncAdapter(CalendarSyncAdapter.class);
+        EasCalendarSyncParser p = adapter.new EasCalendarSyncParser(getTestInputStream(), adapter);
+
+        // Set up an input stream with new event data
+        Serializer s = new Serializer(false);
+        s.start(Tags.SYNC_APPLICATION_DATA);
+        s.data(Tags.CALENDAR_TIME_ZONE, "4AEAAFAAYQBjAGkAZgBpAGMAIABTAHQAYQBuAGQAYQByA" +
+                "GQAIABUAGkAbQBlAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAsAAAABAAIAAAAAAAAAAAAAAFAAY" +
+                "QBjAGkAZgBpAGMAIABEAGEAeQBsAGkAZwBoAHQAIABUAGkAbQBlAAAAAAAAAAAAAAAAAAAAAAAAA" +
+                "AAAAAAAAAMAAAACAAIAAAAAAAAAxP///w==");
+        s.data(Tags.CALENDAR_DTSTAMP, "20100518T213156Z");
+        s.data(Tags.CALENDAR_START_TIME, "20100518T220000Z");
+        s.data(Tags.CALENDAR_SUBJECT, "Documentation");
+        s.data(Tags.CALENDAR_UID, "4417556B-27DE-4ECE-B679-A63EFE1F9E85");
+        s.data(Tags.CALENDAR_ORGANIZER_NAME, "Fred Squatibuquitas");
+        s.data(Tags.CALENDAR_ORGANIZER_EMAIL, "fred.squatibuquitas@prettylongdomainname.com");
+        s.start(Tags.CALENDAR_ATTENDEES);
+        addAttendeesToSerializer(s, 10);
+        s.end(); // CALENDAR_ATTENDEES
+        s.data(Tags.CALENDAR_LOCATION, "CR SF 601T2/North Shore Presentation Self Service (16)");
+        s.data(Tags.CALENDAR_END_TIME, "20100518T223000Z");
+        s.start(Tags.BASE_BODY);
+        s.data(Tags.BASE_BODY_PREFERENCE, "1");
+        s.data(Tags.BASE_ESTIMATED_DATA_SIZE, "69105"); // The number is ignored by the parser
+        s.data(Tags.BASE_DATA, "This is the event description; we should probably make it longer");
+        s.end(); // BASE_BODY
+        s.data(Tags.CALENDAR_SENSITIVITY, "0");
+        s.data(Tags.CALENDAR_BUSY_STATUS, "2");
+        s.data(Tags.CALENDAR_ALL_DAY_EVENT, "0");
+        s.data(Tags.CALENDAR_MEETING_STATUS, "3");
+        s.data(Tags.BASE_NATIVE_BODY_TYPE, "3");
+        s.end().done(); // SYNC_APPLICATION_DATA
+
+        // Set up our parser's input and eat the initial tag
+        byte[] bytes = s.toByteArray();
+        p.resetInput(new ByteArrayInputStream(bytes));
+        p.nextTag(0);
+
+        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 testAddEventRedactedAttendees() throws IOException {
+        CalendarSyncAdapter adapter = getTestSyncAdapter(CalendarSyncAdapter.class);
+        EasCalendarSyncParser p = adapter.new EasCalendarSyncParser(getTestInputStream(), adapter);
+
+        // Set up an input stream with new event data
+        Serializer s = new Serializer(false);
+        s.start(Tags.SYNC_APPLICATION_DATA);
+        s.data(Tags.CALENDAR_TIME_ZONE, "4AEAAFAAYQBjAGkAZgBpAGMAIABTAHQAYQBuAGQAYQByA" +
+                "GQAIABUAGkAbQBlAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAsAAAABAAIAAAAAAAAAAAAAAFAAY" +
+                "QBjAGkAZgBpAGMAIABEAGEAeQBsAGkAZwBoAHQAIABUAGkAbQBlAAAAAAAAAAAAAAAAAAAAAAAAA" +
+                "AAAAAAAAAMAAAACAAIAAAAAAAAAxP///w==");
+        s.data(Tags.CALENDAR_DTSTAMP, "20100518T213156Z");
+        s.data(Tags.CALENDAR_START_TIME, "20100518T220000Z");
+        s.data(Tags.CALENDAR_SUBJECT, "Documentation");
+        s.data(Tags.CALENDAR_UID, "4417556B-27DE-4ECE-B679-A63EFE1F9E85");
+        s.data(Tags.CALENDAR_ORGANIZER_NAME, "Fred Squatibuquitas");
+        s.data(Tags.CALENDAR_ORGANIZER_EMAIL, "fred.squatibuquitas@prettylongdomainname.com");
+        s.start(Tags.CALENDAR_ATTENDEES);
+        addAttendeesToSerializer(s, 100);
+        s.end(); // CALENDAR_ATTENDEES
+        s.data(Tags.CALENDAR_LOCATION, "CR SF 601T2/North Shore Presentation Self Service (16)");
+        s.data(Tags.CALENDAR_END_TIME, "20100518T223000Z");
+        s.start(Tags.BASE_BODY);
+        s.data(Tags.BASE_BODY_PREFERENCE, "1");
+        s.data(Tags.BASE_ESTIMATED_DATA_SIZE, "69105"); // The number is ignored by the parser
+        s.data(Tags.BASE_DATA, "This is the event description; we should probably make it longer");
+        s.end(); // BASE_BODY
+        s.data(Tags.CALENDAR_SENSITIVITY, "0");
+        s.data(Tags.CALENDAR_BUSY_STATUS, "2");
+        s.data(Tags.CALENDAR_ALL_DAY_EVENT, "0");
+        s.data(Tags.CALENDAR_MEETING_STATUS, "3");
+        s.data(Tags.BASE_NATIVE_BODY_TYPE, "3");
+        s.end().done(); // SYNC_APPLICATION_DATA
+
+        // Set up our parser's input and eat the initial tag
+        byte[] bytes = s.toByteArray();
+        p.resetInput(new ByteArrayInputStream(bytes));
+        p.nextTag(0);
+
+        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"));
+    }
 }
diff --git a/tests/src/com/android/exchange/adapter/ProvisionParserTests.java b/tests/src/com/android/exchange/adapter/ProvisionParserTests.java
index 09a9d96..842e4b6 100644
--- a/tests/src/com/android/exchange/adapter/ProvisionParserTests.java
+++ b/tests/src/com/android/exchange/adapter/ProvisionParserTests.java
@@ -113,11 +113,11 @@
         PolicySet ps = parser.getPolicySet();
         assertNotNull(ps);
         // Check the settings to make sure they were parsed correctly
-        assertEquals(5*60, ps.mMaxScreenLockTime);  // Screen lock time is in seconds
-        assertEquals(8, ps.mMinPasswordLength);
-        assertEquals(PolicySet.PASSWORD_MODE_STRONG, ps.mPasswordMode);
-        assertEquals(20, ps.mMaxPasswordFails);
-        assertTrue(ps.mRequireRemoteWipe);
+        assertEquals(5*60, ps.getMaxScreenLockTime());  // Screen lock time is in seconds
+        assertEquals(8, ps.getMinPasswordLength());
+        assertEquals(PolicySet.PASSWORD_MODE_STRONG, ps.getPasswordMode());
+        assertEquals(20, ps.getMaxPasswordFails());
+        assertTrue(ps.isRequireRemoteWipe());
     }
 
     public void testWapProvisionParser2() throws IOException {
@@ -126,7 +126,7 @@
         PolicySet ps = parser.getPolicySet();
         assertNotNull(ps);
         // Password should be set to none; others are ignored in this case.
-        assertEquals(PolicySet.PASSWORD_MODE_NONE, ps.mPasswordMode);
+        assertEquals(PolicySet.PASSWORD_MODE_NONE, ps.getPasswordMode());
     }
 
     public void testWapProvisionParser3() throws IOException {
@@ -135,10 +135,10 @@
         PolicySet ps = parser.getPolicySet();
         assertNotNull(ps);
         // Password should be set to simple
-        assertEquals(2*60, ps.mMaxScreenLockTime);  // Screen lock time is in seconds
-        assertEquals(4, ps.mMinPasswordLength);
-        assertEquals(PolicySet.PASSWORD_MODE_SIMPLE, ps.mPasswordMode);
-        assertEquals(5, ps.mMaxPasswordFails);
-        assertTrue(ps.mRequireRemoteWipe);
+        assertEquals(2*60, ps.getMaxScreenLockTime());  // Screen lock time is in seconds
+        assertEquals(4, ps.getMinPasswordLength());
+        assertEquals(PolicySet.PASSWORD_MODE_SIMPLE, ps.getPasswordMode());
+        assertEquals(5, ps.getMaxPasswordFails());
+        assertTrue(ps.isRequireRemoteWipe());
     }
 }
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..a623aed
--- /dev/null
+++ b/tests/src/com/android/exchange/provider/ExchangeDirectoryProviderTests.java
@@ -0,0 +1,61 @@
+/*
+ * 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.provider.GalResult.GalData;
+
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.provider.ContactsContract.CommonDataKinds;
+import android.provider.ContactsContract.Contacts;
+import android.test.AndroidTestCase;
+
+/**
+ * You can run this entire test case with:
+ *   runtest -c com.android.exchange.provider.ExchangeDirectoryProviderTests email
+ */
+public class ExchangeDirectoryProviderTests extends AndroidTestCase {
+
+    // 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};
+    private static final int GAL_RESULT_DISPLAY_NAME_COLUMN = 0;
+    private static final int GAL_RESULT_EMAIL_ADDRESS_COLUMN = 1;
+    private static final int GAL_RESULT_CONTENT_TYPE_COLUMN = 2;
+
+    public void testBuildGalResultCursor() {
+        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_DISPLAY_NAME_COLUMN));
+            assertEquals(data.emailAddress, c.getString(GAL_RESULT_EMAIL_ADDRESS_COLUMN));
+            assertNull(c.getString(GAL_RESULT_CONTENT_TYPE_COLUMN));
+        }
+    }
+}