Fix problem with unencoded data in URI sent to EAS

* We need to encode the itemId and collectionId for SmartForward and
  SmartReply
* Add unit test for the fix
* Small change + test to EasSyncService usage of URI encoding to use
  a non-deprecated method

Bug: 2787725
Change-Id: I428b308b56cc359b8cdd9e42bc3f42c65b6797dc
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 a4464dc..f58604c 100644
--- a/src/com/android/exchange/EasSyncService.java
+++ b/src/com/android/exchange/EasSyncService.java
@@ -77,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;
@@ -94,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;
@@ -185,7 +185,7 @@
     protected String mDeviceId = null;
     /*package*/ String mDeviceType = "Android";
     /*package*/ String mAuthString = null;
-    private String mCmdString = null;
+    /*package*/ String mCmdString = null;
     public String mHostAddress;
     public String mUserName;
     public String mPassword;
@@ -1099,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();
         }
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 2f070d5..320a1d1 100644
--- a/tests/src/com/android/exchange/EasSyncServiceTests.java
+++ b/tests/src/com/android/exchange/EasSyncServiceTests.java
@@ -25,6 +25,7 @@
 
 import android.content.Context;
 import android.test.AndroidTestCase;
+import android.util.Base64;
 
 import java.io.File;
 import java.io.IOException;
@@ -35,6 +36,12 @@
  */
 
 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
@@ -130,4 +137,39 @@
         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);
+    }
 }