Don't delete referenced messages from the Exchange server DO NOT MERGE

* Addresses #2287439 incompletely
* The most likely reason for a reply/forward to get stuck in the Outbox
  is that the referenced message has been deleted from the client, with
  the deletion occuring BEFORE the message gets sent (currently, the two
  are completely independent)
* This change causes deletes NOT to be sent to the server if the message
  to be deleted is referenced by an outgoing message

Change-Id: Iad3777282385bea82276f363d6f95ba8b07cc01c
diff --git a/src/com/android/exchange/adapter/EmailSyncAdapter.java b/src/com/android/exchange/adapter/EmailSyncAdapter.java
index 6a5d2a5..946ae4e 100644
--- a/src/com/android/exchange/adapter/EmailSyncAdapter.java
+++ b/src/com/android/exchange/adapter/EmailSyncAdapter.java
@@ -24,6 +24,7 @@
 import com.android.email.provider.EmailContent.Account;
 import com.android.email.provider.EmailContent.AccountColumns;
 import com.android.email.provider.EmailContent.Attachment;
+import com.android.email.provider.EmailContent.Body;
 import com.android.email.provider.EmailContent.Mailbox;
 import com.android.email.provider.EmailContent.Message;
 import com.android.email.provider.EmailContent.MessageColumns;
@@ -65,8 +66,10 @@
     private static final String[] MESSAGE_ID_SUBJECT_PROJECTION =
         new String[] { Message.RECORD_ID, MessageColumns.SUBJECT };
 
+    private static final String WHERE_BODY_SOURCE_MESSAGE_KEY = Body.SOURCE_MESSAGE_KEY + "=?";
 
-    String[] bindArguments = new String[2];
+    String[] mBindArguments = new String[2];
+    String[] mBindArgument = new String[1];
 
     ArrayList<Long> mDeletedIdList = new ArrayList<Long>();
     ArrayList<Long> mUpdatedIdList = new ArrayList<Long>();
@@ -308,10 +311,10 @@
         }
 
         private Cursor getServerIdCursor(String serverId, String[] projection) {
-            bindArguments[0] = serverId;
-            bindArguments[1] = mMailboxIdAsString;
+            mBindArguments[0] = serverId;
+            mBindArguments[1] = mMailboxIdAsString;
             return mContentResolver.query(Message.CONTENT_URI, projection,
-                    WHERE_SERVER_ID_AND_MAILBOX_KEY, bindArguments, null);
+                    WHERE_SERVER_ID_AND_MAILBOX_KEY, mBindArguments, null);
         }
 
         private void deleteParser(ArrayList<Long> deletes, int entryTag) throws IOException {
@@ -562,6 +565,68 @@
         return sb.toString();
     }
 
+    /**
+     * Note that messages in the deleted database preserve the message's unique id; therefore, we
+     * can utilize this id to find references to the message.  The only reference situation at this
+     * point is in the Body table; it is when sending messages via SmartForward and SmartReply
+     */
+    private boolean messageReferenced(ContentResolver cr, long id) {
+        mBindArgument[0] = Long.toString(id);
+        // See if this id is referenced in a body
+        Cursor c = cr.query(Body.CONTENT_URI, Body.ID_PROJECTION, WHERE_BODY_SOURCE_MESSAGE_KEY,
+                mBindArgument, null);
+        try {
+            return c.moveToFirst();
+        } finally {
+            c.close();
+        }
+    }
+
+    /*private*/ /**
+     * Serialize commands to delete items from the server; as we find items to delete, add their
+     * id's to the deletedId's array
+     *
+     * @param s the Serializer we're using to create post data
+     * @param deletedIds ids whose deletions are being sent to the server
+     * @param first whether or not this is the first command being sent
+     * @return true if SYNC_COMMANDS hasn't been sent (false otherwise)
+     * @throws IOException
+     */
+    boolean sendDeletedItems(Serializer s, ArrayList<Long> deletedIds, boolean first)
+            throws IOException {
+        ContentResolver cr = mContext.getContentResolver();
+
+        // Find any of our deleted items
+        Cursor c = cr.query(Message.DELETED_CONTENT_URI, Message.LIST_PROJECTION,
+                MessageColumns.MAILBOX_KEY + '=' + mMailbox.mId, null, null);
+        // We keep track of the list of deleted item id's so that we can remove them from the
+        // deleted table after the server receives our command
+        deletedIds.clear();
+        try {
+            while (c.moveToNext()) {
+                String serverId = c.getString(Message.LIST_SERVER_ID_COLUMN);
+                // Keep going if there's no serverId
+                if (serverId == null) {
+                    continue;
+                // Also check if this message is referenced elsewhere
+                } else if (messageReferenced(cr, c.getLong(Message.CONTENT_ID_COLUMN))) {
+                    userLog("Postponing deletion of referenced message: ", serverId);
+                    continue;
+                } else if (first) {
+                    s.start(Tags.SYNC_COMMANDS);
+                    first = false;
+                }
+                // Send the command to delete this message
+                s.start(Tags.SYNC_DELETE).data(Tags.SYNC_SERVER_ID, serverId).end();
+                deletedIds.add(c.getLong(Message.LIST_ID_COLUMN));
+            }
+        } finally {
+            c.close();
+        }
+
+       return first;
+    }
+
     @Override
     public boolean sendLocalChanges(Serializer s) throws IOException {
         ContentResolver cr = mContext.getContentResolver();
@@ -571,37 +636,15 @@
             return false;
         }
 
-        // Find any of our deleted items
-        Cursor c = cr.query(Message.DELETED_CONTENT_URI, Message.LIST_PROJECTION,
-                MessageColumns.MAILBOX_KEY + '=' + mMailbox.mId, null, null);
-        boolean first = true;
-        // We keep track of the list of deleted item id's so that we can remove them from the
-        // deleted table after the server receives our command
-        mDeletedIdList.clear();
-        try {
-            while (c.moveToNext()) {
-                String serverId = c.getString(Message.LIST_SERVER_ID_COLUMN);
-                // Keep going if there's no serverId
-                if (serverId == null) {
-                    continue;
-                } else if (first) {
-                    s.start(Tags.SYNC_COMMANDS);
-                    first = false;
-                }
-                // Send the command to delete this message
-                s.start(Tags.SYNC_DELETE).data(Tags.SYNC_SERVER_ID, serverId).end();
-                mDeletedIdList.add(c.getLong(Message.LIST_ID_COLUMN));
-            }
-        } finally {
-            c.close();
-        }
+        // This code is split out for unit testing purposes
+        boolean firstCommand = sendDeletedItems(s, mDeletedIdList, true);
 
         // Find our trash mailbox, since deletions will have been moved there...
         long trashMailboxId =
             Mailbox.findMailboxOfType(mContext, mMailbox.mAccountKey, Mailbox.TYPE_TRASH);
 
         // Do the same now for updated items
-        c = cr.query(Message.UPDATED_CONTENT_URI, Message.LIST_PROJECTION,
+        Cursor c = cr.query(Message.UPDATED_CONTENT_URI, Message.LIST_PROJECTION,
                 MessageColumns.MAILBOX_KEY + '=' + mMailbox.mId, null, null);
 
         // We keep track of the list of updated item id's as we did above with deleted items
@@ -627,9 +670,9 @@
                     }
                     // If the message is now in the trash folder, it has been deleted by the user
                     if (currentCursor.getLong(UPDATES_MAILBOX_KEY_COLUMN) == trashMailboxId) {
-                         if (first) {
+                         if (firstCommand) {
                             s.start(Tags.SYNC_COMMANDS);
-                            first = false;
+                            firstCommand = false;
                         }
                         // Send the command to delete this message
                         s.start(Tags.SYNC_DELETE).data(Tags.SYNC_SERVER_ID, serverId).end();
@@ -659,9 +702,9 @@
                         continue;
                     }
 
-                    if (first) {
+                    if (firstCommand) {
                         s.start(Tags.SYNC_COMMANDS);
-                        first = false;
+                        firstCommand = false;
                     }
                     // Send the change to "read" and "favorite" (flagged)
                     s.start(Tags.SYNC_CHANGE)
@@ -708,7 +751,7 @@
             c.close();
         }
 
-        if (!first) {
+        if (!firstCommand) {
             s.end(); // SYNC_COMMANDS
         }
         return false;
diff --git a/tests/src/com/android/exchange/EasEmailSyncAdapterTests.java b/tests/src/com/android/exchange/EasEmailSyncAdapterTests.java
deleted file mode 100644
index a3e7dae..0000000
--- a/tests/src/com/android/exchange/EasEmailSyncAdapterTests.java
+++ /dev/null
@@ -1,103 +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.provider.EmailContent.Account;
-import com.android.email.provider.EmailContent.Mailbox;
-import com.android.exchange.adapter.EmailSyncAdapter;
-import com.android.exchange.adapter.EmailSyncAdapter.EasEmailSyncParser;
-
-import android.test.AndroidTestCase;
-
-import java.io.ByteArrayInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.GregorianCalendar;
-import java.util.TimeZone;
-
-public class EasEmailSyncAdapterTests extends AndroidTestCase {
-
-    /**
-     * Create and return a short, simple InputStream that has at least four bytes, which is all
-     * that's required to initialize an EasParser (the parent class of EasEmailSyncParser)
-     * @return the InputStream
-     */
-    public InputStream getTestInputStream() {
-        return new ByteArrayInputStream(new byte[] {0, 0, 0, 0, 0});
-    }
-
-    EasSyncService getTestService() {
-        Account account = new Account();
-        account.mId = -1;
-        Mailbox mailbox = new Mailbox();
-        mailbox.mId = -1;
-        EasSyncService service = new EasSyncService();
-        service.mContext = getContext();
-        service.mMailbox = mailbox;
-        service.mAccount = account;
-        return service;
-    }
-
-    EmailSyncAdapter getTestSyncAdapter() {
-        EasSyncService service = getTestService();
-        EmailSyncAdapter adapter = new EmailSyncAdapter(service.mMailbox, service);
-        return adapter;
-    }
-
-    /**
-     * Check functionality for getting mime type from a file name (using its extension)
-     * The default for all unknown files is application/octet-stream
-     */
-    public void testGetMimeTypeFromFileName() throws IOException {
-        EasSyncService service = getTestService();
-        EmailSyncAdapter adapter = new EmailSyncAdapter(service.mMailbox, service);
-        EasEmailSyncParser p = adapter.new EasEmailSyncParser(getTestInputStream(), adapter);
-        // Test a few known types
-        String mimeType = p.getMimeTypeFromFileName("foo.jpg");
-        assertEquals("image/jpeg", mimeType);
-        // Make sure this is case insensitive
-        mimeType = p.getMimeTypeFromFileName("foo.JPG");
-        assertEquals("image/jpeg", mimeType);
-        mimeType = p.getMimeTypeFromFileName("this_is_a_weird_filename.gif");
-        assertEquals("image/gif", mimeType);
-        // Test an illegal file name ending with the extension prefix
-        mimeType = p.getMimeTypeFromFileName("foo.");
-        assertEquals("application/octet-stream", mimeType);
-        // Test a really awful name
-        mimeType = p.getMimeTypeFromFileName(".....");
-        assertEquals("application/octet-stream", mimeType);
-        // Test a bare file name (no extension)
-        mimeType = p.getMimeTypeFromFileName("foo");
-        assertEquals("application/octet-stream", mimeType);
-        // And no name at all (null isn't a valid input)
-        mimeType = p.getMimeTypeFromFileName("");
-        assertEquals("application/octet-stream", mimeType);
-    }
-
-    public void testFormatDateTime() throws IOException {
-        EmailSyncAdapter adapter = getTestSyncAdapter();
-        GregorianCalendar calendar = new GregorianCalendar(TimeZone.getTimeZone("GMT"));
-        // Calendar is odd, months are zero based, so the first 11 below is December...
-        calendar.set(2008, 11, 11, 18, 19, 20);
-        String date = adapter.formatDateTime(calendar);
-        assertEquals("2008-12-11T18:19:20.000Z", date);
-        calendar.clear();
-        calendar.set(2012, 0, 2, 23, 0, 1);
-        date = adapter.formatDateTime(calendar);
-        assertEquals("2012-01-02T23:00:01.000Z", date);
-    }
-}
diff --git a/tests/src/com/android/exchange/adapter/EmailSyncAdapterTests.java b/tests/src/com/android/exchange/adapter/EmailSyncAdapterTests.java
new file mode 100644
index 0000000..a702428
--- /dev/null
+++ b/tests/src/com/android/exchange/adapter/EmailSyncAdapterTests.java
@@ -0,0 +1,195 @@
+/*
+ * 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.adapter;
+
+import com.android.email.provider.EmailContent;
+import com.android.email.provider.EmailProvider;
+import com.android.email.provider.ProviderTestUtils;
+import com.android.email.provider.EmailContent.Account;
+import com.android.email.provider.EmailContent.Body;
+import com.android.email.provider.EmailContent.Mailbox;
+import com.android.email.provider.EmailContent.Message;
+import com.android.exchange.EasSyncService;
+import com.android.exchange.adapter.EmailSyncAdapter.EasEmailSyncParser;
+
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.test.ProviderTestCase2;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.GregorianCalendar;
+import java.util.TimeZone;
+
+public class EmailSyncAdapterTests extends ProviderTestCase2<EmailProvider> {
+
+    EmailProvider mProvider;
+    Context mMockContext;
+
+    public EmailSyncAdapterTests() {
+        super(EmailProvider.class, EmailProvider.EMAIL_AUTHORITY);
+    }
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        mMockContext = getMockContext();
+    }
+
+    @Override
+    public void tearDown() throws Exception {
+        super.tearDown();
+    }
+
+    /**
+     * Create and return a short, simple InputStream that has at least four bytes, which is all
+     * that's required to initialize an EasParser (the parent class of EasEmailSyncParser)
+     * @return the InputStream
+     */
+    public InputStream getTestInputStream() {
+        return new ByteArrayInputStream(new byte[] {0, 0, 0, 0, 0});
+    }
+
+    EasSyncService getTestService() {
+        Account account = new Account();
+        account.mId = -1;
+        Mailbox mailbox = new Mailbox();
+        mailbox.mId = -1;
+        EasSyncService service = new EasSyncService();
+        service.mContext = getContext();
+        service.mMailbox = mailbox;
+        service.mAccount = account;
+        return service;
+    }
+
+    EmailSyncAdapter getTestSyncAdapter() {
+        EasSyncService service = getTestService();
+        EmailSyncAdapter adapter = new EmailSyncAdapter(service.mMailbox, service);
+        return adapter;
+    }
+
+    /**
+     * Check functionality for getting mime type from a file name (using its extension)
+     * The default for all unknown files is application/octet-stream
+     */
+    public void testGetMimeTypeFromFileName() throws IOException {
+        EasSyncService service = getTestService();
+        EmailSyncAdapter adapter = new EmailSyncAdapter(service.mMailbox, service);
+        EasEmailSyncParser p = adapter.new EasEmailSyncParser(getTestInputStream(), adapter);
+        // Test a few known types
+        String mimeType = p.getMimeTypeFromFileName("foo.jpg");
+        assertEquals("image/jpeg", mimeType);
+        // Make sure this is case insensitive
+        mimeType = p.getMimeTypeFromFileName("foo.JPG");
+        assertEquals("image/jpeg", mimeType);
+        mimeType = p.getMimeTypeFromFileName("this_is_a_weird_filename.gif");
+        assertEquals("image/gif", mimeType);
+        // Test an illegal file name ending with the extension prefix
+        mimeType = p.getMimeTypeFromFileName("foo.");
+        assertEquals("application/octet-stream", mimeType);
+        // Test a really awful name
+        mimeType = p.getMimeTypeFromFileName(".....");
+        assertEquals("application/octet-stream", mimeType);
+        // Test a bare file name (no extension)
+        mimeType = p.getMimeTypeFromFileName("foo");
+        assertEquals("application/octet-stream", mimeType);
+        // And no name at all (null isn't a valid input)
+        mimeType = p.getMimeTypeFromFileName("");
+        assertEquals("application/octet-stream", mimeType);
+    }
+
+    public void testFormatDateTime() throws IOException {
+        EmailSyncAdapter adapter = getTestSyncAdapter();
+        GregorianCalendar calendar = new GregorianCalendar(TimeZone.getTimeZone("GMT"));
+        // Calendar is odd, months are zero based, so the first 11 below is December...
+        calendar.set(2008, 11, 11, 18, 19, 20);
+        String date = adapter.formatDateTime(calendar);
+        assertEquals("2008-12-11T18:19:20.000Z", date);
+        calendar.clear();
+        calendar.set(2012, 0, 2, 23, 0, 1);
+        date = adapter.formatDateTime(calendar);
+        assertEquals("2012-01-02T23:00:01.000Z", date);
+    }
+
+    public void testSendDeletedItems() throws IOException {
+        EasSyncService service = getTestService();
+        EmailSyncAdapter adapter = new EmailSyncAdapter(service.mMailbox, service);
+        Serializer s = new Serializer();
+        ArrayList<Long> ids = new ArrayList<Long>();
+        ArrayList<Long> deletedIds = new ArrayList<Long>();
+
+        Context context = mMockContext;
+        adapter.mContext = context;
+        final ContentResolver resolver = context.getContentResolver();
+
+        // Create account and two mailboxes
+        Account acct = ProviderTestUtils.setupAccount("account", true, context);
+        adapter.mAccount = acct;
+        Mailbox box1 = ProviderTestUtils.setupMailbox("box1", acct.mId, true, context);
+        adapter.mMailbox = box1;
+
+        // Create 3 messages
+        Message msg1 =
+            ProviderTestUtils.setupMessage("message1", acct.mId, box1.mId, true, true, context);
+        ids.add(msg1.mId);
+        Message msg2 =
+            ProviderTestUtils.setupMessage("message2", acct.mId, box1.mId, true, true, context);
+        ids.add(msg2.mId);
+        Message msg3 =
+            ProviderTestUtils.setupMessage("message3", acct.mId, box1.mId, true, true, context);
+        ids.add(msg3.mId);
+        assertEquals(3, EmailContent.count(context, Message.CONTENT_URI, null, null));
+
+        // Delete them
+        for (long id: ids) {
+            resolver.delete(ContentUris.withAppendedId(Message.SYNCED_CONTENT_URI, id), null, null);
+        }
+
+        // Confirm that the messages are in the proper table
+        assertEquals(0, EmailContent.count(context, Message.CONTENT_URI, null, null));
+        assertEquals(3, EmailContent.count(context, Message.DELETED_CONTENT_URI, null, null));
+
+        // Call code to send deletions; the id's of the ones actually deleted will be in the
+        // deletedIds list
+        adapter.sendDeletedItems(s, deletedIds, true);
+        assertEquals(3, deletedIds.size());
+
+        // Clear this out for the next test
+        deletedIds.clear();
+
+        // Create a new message
+        Message msg4 =
+            ProviderTestUtils.setupMessage("message3", acct.mId, box1.mId, true, true, context);
+        assertEquals(1, EmailContent.count(context, Message.CONTENT_URI, null, null));
+        // Find the body for this message
+        Body body = Body.restoreBodyWithMessageId(context, msg4.mId);
+        // Set its source message to msg2's id
+        ContentValues values = new ContentValues();
+        values.put(Body.SOURCE_MESSAGE_KEY, msg2.mId);
+        body.update(context, values);
+
+        // Now send deletions again; this time only two should get deleted; msg2 should NOT be
+        // deleted as it's referenced by msg4
+        adapter.sendDeletedItems(s, deletedIds, true);
+        assertEquals(2, deletedIds.size());
+        assertFalse(deletedIds.contains(msg2.mId));
+    }
+}