Handle provisioning of EAS 14.0

* EAS 14.0 requires a different procedure for sending device
  information than does EAS 14.1
* Sadly, this new procedure is fairly incompatible with the way
  we create/provision accounts, as it requires us to finish
  provisioning BEFORE sending device information and yet
  treating the result as part of the provisioning process
* This is particularly clumsy for us, since we do not normally
  finish provisioning during account validation (we do it after
  the account has been created).
* The present CL includes some patches that enable EAS 14.0
  provisioning to work properly, at least in the most common
  cases; additional testing will be required to determine
  whether we're covering all of the bases

Bug: 5494233
Change-Id: I99c290a7d2b49b600446b4a40339efaae1cb1aac
diff --git a/src/com/android/exchange/EasSyncService.java b/src/com/android/exchange/EasSyncService.java
index 48fb6e4..169f398 100644
--- a/src/com/android/exchange/EasSyncService.java
+++ b/src/com/android/exchange/EasSyncService.java
@@ -72,6 +72,7 @@
 import com.android.exchange.adapter.PingParser;
 import com.android.exchange.adapter.ProvisionParser;
 import com.android.exchange.adapter.Serializer;
+import com.android.exchange.adapter.SettingsParser;
 import com.android.exchange.adapter.Tags;
 import com.android.exchange.provider.GalResult;
 import com.android.exchange.provider.MailboxUtilities;
@@ -388,6 +389,12 @@
             Log.w(TAG, "No supported EAS versions: " + supportedVersions);
             throw new MessagingException(MessagingException.PROTOCOL_VERSION_UNSUPPORTED);
         } else {
+            // Debug code for testing EAS 14.0; disables support for EAS 14.1
+            // "adb shell setprop log.tag.Exchange14 VERBOSE"
+            if (ourVersion.equals(Eas.SUPPORTED_PROTOCOL_EX2010_SP1) &&
+                    Log.isLoggable("Exchange14", Log.VERBOSE)) {
+                ourVersion = Eas.SUPPORTED_PROTOCOL_EX2010;
+            }
             service.mProtocolVersion = ourVersion;
             service.mProtocolVersionDouble = Eas.getProtocolVersionDouble(ourVersion);
             Account account = service.mAccount;
@@ -553,12 +560,19 @@
                         resultCode = MessagingException.SECURITY_POLICIES_REQUIRED;
                         bundle.putParcelable(EmailServiceProxy.VALIDATE_BUNDLE_POLICY_SET,
                                 pp.getPolicy());
+                        if (mProtocolVersionDouble == Eas.SUPPORTED_PROTOCOL_EX2010_DOUBLE) {
+                            mAccount.mSecuritySyncKey = pp.getSecuritySyncKey();
+                            if (!sendSettings()) {
+                                userLog("Denied access: ", CommandStatus.toString(status));
+                                resultCode = MessagingException.ACCESS_DENIED;
+                            }
+                        }
                     } else
                         // If not, set the proper code (the account will not be created)
                         resultCode = MessagingException.SECURITY_POLICIES_UNSUPPORTED;
                         bundle.putStringArray(
                                 EmailServiceProxy.VALIDATE_BUNDLE_UNSUPPORTED_POLICIES,
-                                pp.getUnsupportedPolicies());
+                                ((pp == null) ? new String[0] : pp.getUnsupportedPolicies()));
                 } else if (CommandStatus.isDeniedAccess(status)) {
                     userLog("Denied access: ", CommandStatus.toString(status));
                     resultCode = MessagingException.ACCESS_DENIED;
@@ -1414,8 +1428,14 @@
             } else if (SecurityPolicyDelegate.isActive(mContext, policy)) {
                 // See if the required policies are in force; if they are, acknowledge the policies
                 // to the server and get the final policy key
-                String securitySyncKey = acknowledgeProvision(pp.getSecuritySyncKey(),
-                        PROVISION_STATUS_OK);
+                // NOTE: For EAS 14.0, we already have the acknowledgment in the ProvisionParser
+                String securitySyncKey;
+                if (mProtocolVersionDouble == Eas.SUPPORTED_PROTOCOL_EX2010_DOUBLE) {
+                    securitySyncKey = pp.getSecuritySyncKey();
+                } else {
+                    securitySyncKey = acknowledgeProvision(pp.getSecuritySyncKey(),
+                            PROVISION_STATUS_OK);
+                }
                 if (securitySyncKey != null) {
                     // If attachment policies have changed, fix up any affected attachment records
                     if (oldPolicy != null) {
@@ -1453,8 +1473,8 @@
     private ProvisionParser canProvision() throws IOException {
         Serializer s = new Serializer();
         s.start(Tags.PROVISION_PROVISION);
-        if (mProtocolVersionDouble >= Eas.SUPPORTED_PROTOCOL_EX2010_DOUBLE) {
-            // Send settings information in 14.0 and greater
+        if (mProtocolVersionDouble >= Eas.SUPPORTED_PROTOCOL_EX2010_SP1_DOUBLE) {
+            // Send settings information in 14.1 and greater
             s.start(Tags.SETTINGS_DEVICE_INFORMATION).start(Tags.SETTINGS_SET);
             s.data(Tags.SETTINGS_MODEL, Build.MODEL);
             //s.data(Tags.SETTINGS_IMEI, "");
@@ -1479,7 +1499,16 @@
                 if (pp.parse()) {
                     // The PolicySet in the ProvisionParser will have the requirements for all KNOWN
                     // policies.  If others are required, hasSupportablePolicySet will be false
-                    if (!pp.hasSupportablePolicySet())  {
+                    if (pp.hasSupportablePolicySet() &&
+                            mProtocolVersionDouble == Eas.SUPPORTED_PROTOCOL_EX2010_DOUBLE) {
+                        // In EAS 14.0, we need the final security key in order to use the settings
+                        // command
+                        String policyKey = acknowledgeProvision(pp.getSecuritySyncKey(),
+                                PROVISION_STATUS_OK);
+                        if (policyKey != null) {
+                            pp.setSecuritySyncKey(policyKey);
+                        }
+                    } else if (!pp.hasSupportablePolicySet())  {
                         // 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
@@ -1497,6 +1526,7 @@
         } finally {
             resp.close();
         }
+
         // On failures, simply return null
         return null;
     }
@@ -1556,6 +1586,29 @@
         return null;
     }
 
+    private boolean sendSettings() throws IOException {
+        Serializer s = new Serializer();
+        s.start(Tags.SETTINGS_SETTINGS);
+        s.start(Tags.SETTINGS_DEVICE_INFORMATION).start(Tags.SETTINGS_SET);
+        s.data(Tags.SETTINGS_MODEL, Build.MODEL);
+        s.data(Tags.SETTINGS_OS, "Android " + Build.VERSION.RELEASE);
+        s.data(Tags.SETTINGS_USER_AGENT, USER_AGENT);
+        s.end().end().end().done(); // SETTINGS_SET, SETTINGS_DEVICE_INFORMATION, SETTINGS_SETTINGS
+        EasResponse resp = sendHttpClientPost("Settings", s.toByteArray());
+        try {
+            int code = resp.getStatus();
+            if (code == HttpStatus.SC_OK) {
+                InputStream is = resp.getInputStream();
+                SettingsParser sp = new SettingsParser(is, this);
+                return sp.parse();
+            }
+        } finally {
+            resp.close();
+        }
+        // On failures, simply return false
+        return false;
+    }
+
     /**
      * Translate exit status code to service status code (used in callbacks)
      * @param exitStatus the service's exit status
diff --git a/src/com/android/exchange/adapter/ProvisionParser.java b/src/com/android/exchange/adapter/ProvisionParser.java
index 67c23d2..05a864c 100644
--- a/src/com/android/exchange/adapter/ProvisionParser.java
+++ b/src/com/android/exchange/adapter/ProvisionParser.java
@@ -61,6 +61,10 @@
         return mSecuritySyncKey;
     }
 
+    public void setSecuritySyncKey(String securitySyncKey) {
+        mSecuritySyncKey = securitySyncKey;
+    }
+
     public boolean getRemoteWipe() {
         return mRemoteWipe;
     }
diff --git a/src/com/android/exchange/adapter/SettingsParser.java b/src/com/android/exchange/adapter/SettingsParser.java
new file mode 100644
index 0000000..2cbc753
--- /dev/null
+++ b/src/com/android/exchange/adapter/SettingsParser.java
@@ -0,0 +1,82 @@
+/* Copyright (C) 2011 The Android Open Source Project.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.exchange.adapter;
+
+import com.android.exchange.EasSyncService;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Parse the result of a Settings command.
+ *
+ * We only send the Settings command in EAS 14.0 after sending a Provision command for the first
+ * time.  parse() returns true in the normal case; false if access to the account is denied due
+ * to the actual settings (e.g. if a particular device type isn't allowed by the server)
+ */
+public class SettingsParser extends Parser {
+    private final EasSyncService mService;
+
+    public SettingsParser(InputStream in, EasSyncService service) throws IOException {
+        super(in);
+        mService = service;
+    }
+
+    @Override
+    public boolean parse() throws IOException {
+        boolean res = false;
+        if (nextTag(START_DOCUMENT) != Tags.SETTINGS_SETTINGS) {
+            throw new IOException();
+        }
+        while (nextTag(START_DOCUMENT) != END_DOCUMENT) {
+            if (tag == Tags.SETTINGS_STATUS) {
+                int status = getValueInt();
+                mService.userLog("Settings status = ", status);
+                if (status == 1) {
+                    res = true;
+                } else {
+                    // Access denied = 3; others should never be seen
+                    res = false;
+                }
+            } else if (tag == Tags.SETTINGS_DEVICE_INFORMATION) {
+                parseDeviceInformation();
+            } else {
+                skipTag();
+            }
+        }
+        return res;
+    }
+
+    public void parseDeviceInformation() throws IOException {
+        while (nextTag(Tags.SETTINGS_DEVICE_INFORMATION) != END) {
+            if (tag == Tags.SETTINGS_SET) {
+                parseSet();
+            } else {
+                skipTag();
+            }
+        }
+    }
+
+    public void parseSet() throws IOException {
+        while (nextTag(Tags.SETTINGS_SET) != END) {
+            if (tag == Tags.SETTINGS_STATUS) {
+                mService.userLog("Set status = ", getValueInt());
+            } else {
+                skipTag();
+            }
+        }
+    }
+}