Implement "automatic" sync lookback for Exchange

* Target roughly 150-450 messages on device; these numbers are
  starting points, subject to testing, etc.
* UI (name "automatic", etc.) is tentative

Change-Id: Idd36f447190066469e6254e15a7b4cf10a0fc3e8
diff --git a/res/values/arrays.xml b/res/values/arrays.xml
new file mode 100644
index 0000000..d2f24f6
--- /dev/null
+++ b/res/values/arrays.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<resources>
+
+    <!-- Mail sync window sizes for EAS accounts -->
+    <!--  The window length array/strings below MUST remain in sync with com.android.email -->
+    <string-array name="account_settings_mail_window_entries">
+        <item>@string/account_setup_options_mail_window_auto</item>
+        <item>@string/account_setup_options_mail_window_1day</item>
+        <item>@string/account_setup_options_mail_window_3days</item>
+        <item>@string/account_setup_options_mail_window_1week</item>
+        <item>@string/account_setup_options_mail_window_2weeks</item>
+        <item>@string/account_setup_options_mail_window_1month</item>
+        <item>@string/account_setup_options_mail_window_all</item>
+    </string-array>
+
+</resources>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 358c553..ba1fb02 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -167,4 +167,20 @@
     <!-- A policy in which the device requires device or sd card encryption [CHAR LIMIT=40] -->
     <string name="policy_require_encryption">require device or sd card encryption</string>
 
+    <!--  The window length strings below MUST remain in sync with those in com.android.email -->
+    <!-- In account setup options & account settings screens (exchange), sync window length; this
+        implies loading a 'reasonable' number of messages [CHAR LIMIT=25] -->
+    <string name="account_setup_options_mail_window_auto">Automatic</string>
+    <!-- A sync window length setting (i.e. load messages this far back) [CHAR LIMIT=25]  -->
+    <string name="account_setup_options_mail_window_1day">One day</string>
+    <!-- A sync window length setting (i.e. load messages this far back) [CHAR LIMIT=25]  -->
+    <string name="account_setup_options_mail_window_3days">Three days</string>
+    <!-- A sync window length setting (i.e. load messages this far back) [CHAR LIMIT=25]  -->
+    <string name="account_setup_options_mail_window_1week">One week</string>
+    <!-- A sync window length setting (i.e. load messages this far back) [CHAR LIMIT=25]  -->
+    <string name="account_setup_options_mail_window_2weeks">Two weeks</string>
+    <!-- A sync window length setting (i.e. load messages this far back) [CHAR LIMIT=25]  -->
+    <string name="account_setup_options_mail_window_1month">One month</string>
+    <!-- A sync window length setting (i.e. load messages this far back) [CHAR LIMIT=25]  -->
+    <string name="account_setup_options_mail_window_all">All</string>
 </resources>
diff --git a/src/com/android/exchange/Eas.java b/src/com/android/exchange/Eas.java
index 39c7b5c..ba3770b 100644
--- a/src/com/android/exchange/Eas.java
+++ b/src/com/android/exchange/Eas.java
@@ -18,6 +18,7 @@
 package com.android.exchange;
 
 import com.android.emailcommon.service.EmailServiceProxy;
+import com.android.emailcommon.service.SyncWindow;
 
 import android.util.Log;
 
@@ -64,16 +65,20 @@
     // 6 3 months ago No   Yes
     // 7 6 months ago No   Yes
 
+    public static final String FILTER_AUTO =  Integer.toString(SyncWindow.SYNC_WINDOW_AUTO);
+    // TODO Rationalize this with SYNC_WINDOW_ALL
     public static final String FILTER_ALL = "0";
-    public static final String FILTER_1_DAY = "1";
-    public static final String FILTER_3_DAYS = "2";
-    public static final String FILTER_1_WEEK = "3";
-    public static final String FILTER_2_WEEKS = "4";
-    public static final String FILTER_1_MONTH = "5";
+    public static final String FILTER_1_DAY = Integer.toString(SyncWindow.SYNC_WINDOW_1_DAY);
+    public static final String FILTER_3_DAYS =  Integer.toString(SyncWindow.SYNC_WINDOW_3_DAYS);
+    public static final String FILTER_1_WEEK =  Integer.toString(SyncWindow.SYNC_WINDOW_1_WEEK);
+    public static final String FILTER_2_WEEKS =  Integer.toString(SyncWindow.SYNC_WINDOW_2_WEEKS);
+    public static final String FILTER_1_MONTH =  Integer.toString(SyncWindow.SYNC_WINDOW_1_MONTH);
     public static final String FILTER_3_MONTHS = "6";
     public static final String FILTER_6_MONTHS = "7";
+
     public static final String BODY_PREFERENCE_TEXT = "1";
     public static final String BODY_PREFERENCE_HTML = "2";
+
     public static final String MIME_BODY_PREFERENCE_TEXT = "0";
     public static final String MIME_BODY_PREFERENCE_MIME = "2";
 
diff --git a/src/com/android/exchange/EasSyncService.java b/src/com/android/exchange/EasSyncService.java
index 5106b64..d887b24 100644
--- a/src/com/android/exchange/EasSyncService.java
+++ b/src/com/android/exchange/EasSyncService.java
@@ -130,7 +130,7 @@
     // Command timeout is the the time allowed for reading data from an open connection before an
     // IOException is thrown.  After a small added allowance, our watchdog alarm goes off (allowing
     // us to detect a silently dropped connection).  The allowance is defined below.
-    static private final int COMMAND_TIMEOUT = 30*SECONDS;
+    static public final int COMMAND_TIMEOUT = 30*SECONDS;
     // Connection timeout is the time given to connect to the server before reporting an IOException
     static private final int CONNECTION_TIMEOUT = 20*SECONDS;
     // The extra time allowed beyond the COMMAND_TIMEOUT before which our watchdog alarm triggers
@@ -1516,7 +1516,7 @@
         }
     }
 
-    protected EasResponse sendHttpClientPost(String cmd, HttpEntity entity, int timeout)
+    public EasResponse sendHttpClientPost(String cmd, HttpEntity entity, int timeout)
             throws IOException {
         HttpClient client = getHttpClient(timeout);
         boolean isPingCommand = cmd.equals(PING_COMMAND);
diff --git a/src/com/android/exchange/adapter/EmailSyncAdapter.java b/src/com/android/exchange/adapter/EmailSyncAdapter.java
index e59a25a..ab96551 100644
--- a/src/com/android/exchange/adapter/EmailSyncAdapter.java
+++ b/src/com/android/exchange/adapter/EmailSyncAdapter.java
@@ -30,6 +30,7 @@
 import com.android.emailcommon.provider.EmailContent.Attachment;
 import com.android.emailcommon.provider.EmailContent.Body;
 import com.android.emailcommon.provider.EmailContent.Mailbox;
+import com.android.emailcommon.provider.EmailContent.MailboxColumns;
 import com.android.emailcommon.provider.EmailContent.Message;
 import com.android.emailcommon.provider.EmailContent.MessageColumns;
 import com.android.emailcommon.provider.EmailContent.SyncColumns;
@@ -41,9 +42,14 @@
 import com.android.exchange.CommandStatusException;
 import com.android.exchange.Eas;
 import com.android.exchange.EasSyncService;
+import com.android.exchange.EasSyncService.EasResponse;
 import com.android.exchange.MessageMoveRequest;
+import com.android.exchange.R;
 import com.android.exchange.utility.CalendarUtilities;
 
+import org.apache.http.HttpStatus;
+import org.apache.http.entity.ByteArrayEntity;
+
 import android.content.ContentProviderOperation;
 import android.content.ContentResolver;
 import android.content.ContentUris;
@@ -52,6 +58,7 @@
 import android.database.Cursor;
 import android.net.Uri;
 import android.os.RemoteException;
+import android.util.Log;
 import android.webkit.MimeTypeMap;
 
 import java.io.ByteArrayInputStream;
@@ -123,10 +130,12 @@
 
     private String getEmailFilter() {
         int syncLookback = mMailbox.mSyncLookback;
-        if (syncLookback == 0 /* Unknown; use SYNC_WINDOW_UNKNOWN after MR1 */) {
+        if (syncLookback == SyncWindow.SYNC_WINDOW_UNKNOWN) {
             syncLookback = mAccount.mSyncLookback;
         }
         switch (syncLookback) {
+            case SyncWindow.SYNC_WINDOW_AUTO:
+                return Eas.FILTER_AUTO;
             case SyncWindow.SYNC_WINDOW_1_DAY:
                 return Eas.FILTER_1_DAY;
             case SyncWindow.SYNC_WINDOW_3_DAYS:
@@ -224,9 +233,157 @@
         if (mFetchNeeded || !mFetchRequestList.isEmpty()) {
             return true;
         }
+
+        // Don't check for "auto" on the initial sync
+        if (!("0".equals(mMailbox.mSyncKey))) {
+            // We've completed the first successful sync
+            if (getEmailFilter().equals(Eas.FILTER_AUTO)) {
+                getAutomaticLookback();
+             }
+        }
+
         return res;
     }
 
+    private void getAutomaticLookback() throws IOException {
+        // If we're using an auto lookback, check how many items in the past week
+        // TODO Make the literal ints below constants once we twiddle them a bit
+        int items = getEstimate(Eas.FILTER_1_WEEK);
+        int lookback;
+        if (items > 1050) {
+            // Over 150/day, just use one day (smallest)
+            lookback = SyncWindow.SYNC_WINDOW_1_DAY;
+        } else if (items > 350 || (items == -1)) {
+            // 50-150/day, use 3 days (150 to 450 messages synced)
+            lookback = SyncWindow.SYNC_WINDOW_3_DAYS;
+        } else if (items > 150) {
+            // 20-50/day, use 1 week (140 to 350 messages synced)
+            lookback = SyncWindow.SYNC_WINDOW_1_WEEK;
+        } else if (items > 75) {
+            // 10-25/day, use 1 week (140 to 350 messages synced)
+            lookback = SyncWindow.SYNC_WINDOW_2_WEEKS;
+        } else if (items < 5) {
+            // If there are only a couple, see if it makes sense to get everything
+            items = getEstimate(Eas.FILTER_ALL);
+            if (items >= 0 && items < 100) {
+                lookback = SyncWindow.SYNC_WINDOW_ALL;
+            } else {
+                lookback = SyncWindow.SYNC_WINDOW_1_MONTH;
+            }
+        } else {
+            lookback = SyncWindow.SYNC_WINDOW_1_MONTH;
+        }
+        // Store the new lookback and persist it
+        mMailbox.mSyncLookback = lookback;
+        ContentValues cv = new ContentValues();
+        cv.put(MailboxColumns.SYNC_LOOKBACK, lookback);
+        mContentResolver.update(ContentUris.withAppendedId(
+                Mailbox.CONTENT_URI, mMailbox.mId), cv, null, null);
+        // STOPSHIP Temporary UI - Let the user know
+        CharSequence[] windowEntries = mContext.getResources().getTextArray(
+                R.array.account_settings_mail_window_entries);
+        Utility.showToast(mContext, "Auto lookback: " + windowEntries[lookback]);
+    }
+
+    static class GetItemEstimateParser extends Parser {
+        private static final String TAG = "GetItemEstimateParser";
+        private int mEstimate = -1;
+
+        public GetItemEstimateParser(InputStream in) throws IOException {
+            super(in);
+        }
+
+        public boolean parse() throws IOException {
+            // Loop here through the remaining xml
+            while (nextTag(START_DOCUMENT) != END_DOCUMENT) {
+                if (tag == Tags.GIE_GET_ITEM_ESTIMATE) {
+                    parseGetItemEstimate();
+                } else {
+                    skipTag();
+                }
+            }
+            return true;
+        }
+
+        public void parseGetItemEstimate() throws IOException {
+            while (nextTag(Tags.GIE_GET_ITEM_ESTIMATE) != END) {
+                if (tag == Tags.GIE_RESPONSE) {
+                    parseResponse();
+                } else {
+                    skipTag();
+                }
+            }
+        }
+
+        public void parseResponse() throws IOException {
+            while (nextTag(Tags.GIE_RESPONSE) != END) {
+                if (tag == Tags.GIE_STATUS) {
+                    Log.d(TAG, "GIE status: " + getValue());
+                } else if (tag == Tags.GIE_COLLECTION) {
+                    parseCollection();
+                } else {
+                    skipTag();
+                }
+            }
+        }
+
+        public void parseCollection() throws IOException {
+            while (nextTag(Tags.GIE_COLLECTION) != END) {
+                if (tag == Tags.GIE_CLASS) {
+                    Log.d(TAG, "GIE class: " + getValue());
+                } else if (tag == Tags.GIE_COLLECTION_ID) {
+                    Log.d(TAG, "GIE collectionId: " + getValue());
+                } else if (tag == Tags.GIE_ESTIMATE) {
+                    mEstimate = getValueInt();
+                    Log.d(TAG, "GIE estimate: " + mEstimate);
+                } else {
+                    skipTag();
+                }
+            }
+        }
+    }
+
+    /**
+     * Return the estimated number of items to be synced in the current mailbox, based on the
+     * passed in filter argument
+     * @param filter an EAS "window" filter
+     * @return the estimated number of items to be synced, or -1 if unknown
+     * @throws IOException
+     */
+    private int getEstimate(String filter) throws IOException {
+        Serializer s = new Serializer();
+
+        String className = getCollectionName();
+        String syncKey = getSyncKey();
+        userLog("gie, sending ", className, " syncKey: ", syncKey);
+        s.start(Tags.GIE_GET_ITEM_ESTIMATE).start(Tags.GIE_COLLECTIONS);
+        s.start(Tags.GIE_COLLECTION);
+        // The "Class" element is removed in EAS 12.1 and later versions
+        if (mService.mProtocolVersionDouble < Eas.SUPPORTED_PROTOCOL_EX2007_SP1_DOUBLE) {
+            s.data(Tags.GIE_CLASS, className);
+        }
+        s.data(Tags.GIE_COLLECTION_ID, mMailbox.mServerId);
+        s.data(Tags.SYNC_FILTER_TYPE, filter);
+        s.data(Tags.SYNC_SYNC_KEY, syncKey);
+        s.end(); // GIE_COLLECTION
+        s.end(); // GIE_COLLECTIONS
+        s.end().done(); // GIE_GET_ITEM_ESTIMATE
+        EasResponse resp = mService.sendHttpClientPost("GetItemEstimate",
+                new ByteArrayEntity(s.toByteArray()), EasSyncService.COMMAND_TIMEOUT);
+        int code = resp.getStatus();
+        if (code == HttpStatus.SC_OK) {
+            if (!resp.isEmpty()) {
+                InputStream is = resp.getInputStream();
+                GetItemEstimateParser gieParser = new GetItemEstimateParser(is);
+                gieParser.parse();
+                // Return the estimated number of items
+                return gieParser.mEstimate;
+            }
+        }
+        // If we can't get an estimate, indicate this...
+        return -1;
+    }
+
     /**
      * Return the value of isLooping() as returned from the parser
      */