Merge "Rename USER_QUERY -> LIVE_QUERY" into jb-mr1-dev
diff --git a/exchange2/AndroidManifest.xml b/exchange2/AndroidManifest.xml
index 7537b73..2f2bad2 100644
--- a/exchange2/AndroidManifest.xml
+++ b/exchange2/AndroidManifest.xml
@@ -147,6 +147,7 @@
             android:authorities="com.android.exchange.directory.provider"
             android:readPermission="android.permission.READ_CONTACTS"
             android:multiprocess="false"
+            android:exported="true"
             >
           <meta-data
               android:name="android.content.ContactDirectory"
diff --git a/exchange2/src/com/android/exchange/EasSyncService.java b/exchange2/src/com/android/exchange/EasSyncService.java
index 657bc84..d0360bc 100644
--- a/exchange2/src/com/android/exchange/EasSyncService.java
+++ b/exchange2/src/com/android/exchange/EasSyncService.java
@@ -1747,6 +1747,8 @@
                     }
 
                     if (emptyStream) {
+                        // Make sure we get rid of updates/deletes
+                        target.cleanup();
                         // If this happens, exit cleanly, and change the interval from push to ping
                         // if necessary
                         userLog("Empty sync response; finishing");
diff --git a/exchange2/src/com/android/exchange/adapter/CalendarSyncAdapter.java b/exchange2/src/com/android/exchange/adapter/CalendarSyncAdapter.java
index 6525610..45e8372 100644
--- a/exchange2/src/com/android/exchange/adapter/CalendarSyncAdapter.java
+++ b/exchange2/src/com/android/exchange/adapter/CalendarSyncAdapter.java
@@ -1721,6 +1721,10 @@
             s.data(Tags.CALENDAR_BUSY_STATUS, Integer.toString(busyStatus));
 
             // Meeting status, 0 = appointment, 1 = meeting, 3 = attendee
+            // In JB, organizer won't be an attendee
+            if (organizerEmail == null && entityValues.containsKey(Events.ORGANIZER)) {
+                organizerEmail = entityValues.getAsString(Events.ORGANIZER);
+            }
             if (mEmailAddress.equalsIgnoreCase(organizerEmail)) {
                 s.data(Tags.CALENDAR_MEETING_STATUS, hasAttendees ? "1" : "0");
             } else {
diff --git a/exchange2/src/com/android/exchange/adapter/EmailSyncAdapter.java b/exchange2/src/com/android/exchange/adapter/EmailSyncAdapter.java
index 87c96cb..c15c253 100644
--- a/exchange2/src/com/android/exchange/adapter/EmailSyncAdapter.java
+++ b/exchange2/src/com/android/exchange/adapter/EmailSyncAdapter.java
@@ -25,6 +25,7 @@
 import android.database.Cursor;
 import android.net.Uri;
 import android.os.RemoteException;
+import android.os.TransactionTooLargeException;
 import android.provider.CalendarContract.Events;
 import android.text.Html;
 import android.text.SpannedString;
@@ -1112,9 +1113,23 @@
 
         @Override
         public void commit() {
+            commitImpl(0);
+        }
+
+        public void commitImpl(int tryCount) {
             // Use a batch operation to handle the changes
             ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();
 
+            // Maximum size of message text per fetch
+            int numFetched = fetchedEmails.size();
+            int maxPerFetch = 0;
+            if (numFetched > 0 && tryCount > 0) {
+                // Educated guess that 450000 chars (900k) is ok; 600k is a killer
+                // Remember that when fetching, we're not getting any other data
+                // We'll keep trying, reducing the maximum each time
+                // Realistically, this will rarely exceed 1, and probably never 2
+                maxPerFetch = 450000 / numFetched / tryCount;
+            }
             for (Message msg: fetchedEmails) {
                 // Find the original message's id (by serverId and mailbox)
                 Cursor c = getServerIdCursor(msg.mServerId, EmailContent.ID_PROJECTION);
@@ -1139,6 +1154,10 @@
                 if (id != null) {
                     userLog("Fetched body successfully for ", id);
                     mBindArgument[0] = id;
+                    if ((maxPerFetch > 0) && (msg.mText.length() > maxPerFetch)) {
+                        userLog("Truncating message to " + maxPerFetch);
+                        msg.mText = msg.mText.substring(0, maxPerFetch) + "...";
+                    }
                     ops.add(ContentProviderOperation.newUpdate(Body.CONTENT_URI)
                             .withSelection(Body.MESSAGE_KEY + "=?", mBindArgument)
                             .withValue(Body.TEXT_CONTENT, msg.mText)
@@ -1193,7 +1212,10 @@
                 try {
                     mContentResolver.applyBatch(EmailContent.AUTHORITY, ops);
                     userLog(mMailbox.mDisplayName, " SyncKey saved as: ", mMailbox.mSyncKey);
-                } catch (RemoteException e) {
+                } catch (TransactionTooLargeException e) {
+                    Log.w(TAG, "Transaction failed on fetched message; retrying...");
+                    commitImpl(++tryCount);
+                 } catch (RemoteException e) {
                     // There is nothing to be done here; fail by returning null
                 } catch (OperationApplicationException e) {
                     // There is nothing to be done here; fail by returning null
diff --git a/exchange2/src/com/android/exchange/adapter/FolderSyncParser.java b/exchange2/src/com/android/exchange/adapter/FolderSyncParser.java
index 2c213a4..411c0f9 100644
--- a/exchange2/src/com/android/exchange/adapter/FolderSyncParser.java
+++ b/exchange2/src/com/android/exchange/adapter/FolderSyncParser.java
@@ -188,8 +188,13 @@
                     }
                 }
             } else if (tag == Tags.FOLDER_SYNC_KEY) {
-                mAccount.mSyncKey = getValue();
-                userLog("New Account SyncKey: ", mAccount.mSyncKey);
+                String newKey = getValue();
+                if (!resetFolders) {
+                    mAccount.mSyncKey = newKey;
+                    userLog("New syncKey: ", newKey);
+                } else {
+                    userLog("Ignoring new syncKey: ", newKey);
+                }
             } else if (tag == Tags.FOLDER_CHANGES) {
                 if (mStatusOnly) return res;
                 changesParser(mOperations, mInitialSync);
diff --git a/exchange2/src/com/android/exchange/utility/CalendarUtilities.java b/exchange2/src/com/android/exchange/utility/CalendarUtilities.java
index a0ac47d..2872e80 100644
--- a/exchange2/src/com/android/exchange/utility/CalendarUtilities.java
+++ b/exchange2/src/com/android/exchange/utility/CalendarUtilities.java
@@ -107,17 +107,19 @@
 
     // The following constants relate to Microsoft's TIME_ZONE_INFORMATION structure
     // For documentation, see http://msdn.microsoft.com/en-us/library/ms725481(VS.85).aspx
+    static final int MSFT_TIME_ZONE_STRING_SIZE = 32;
+
     static final int MSFT_TIME_ZONE_BIAS_OFFSET = 0;
     static final int MSFT_TIME_ZONE_STANDARD_NAME_OFFSET =
         MSFT_TIME_ZONE_BIAS_OFFSET + MSFT_LONG_SIZE;
     static final int MSFT_TIME_ZONE_STANDARD_DATE_OFFSET =
-        MSFT_TIME_ZONE_STANDARD_NAME_OFFSET + (MSFT_WCHAR_SIZE*32);
+        MSFT_TIME_ZONE_STANDARD_NAME_OFFSET + (MSFT_WCHAR_SIZE*MSFT_TIME_ZONE_STRING_SIZE);
     static final int MSFT_TIME_ZONE_STANDARD_BIAS_OFFSET =
         MSFT_TIME_ZONE_STANDARD_DATE_OFFSET + MSFT_SYSTEMTIME_SIZE;
     static final int MSFT_TIME_ZONE_DAYLIGHT_NAME_OFFSET =
         MSFT_TIME_ZONE_STANDARD_BIAS_OFFSET + MSFT_LONG_SIZE;
     static final int MSFT_TIME_ZONE_DAYLIGHT_DATE_OFFSET =
-        MSFT_TIME_ZONE_DAYLIGHT_NAME_OFFSET + (MSFT_WCHAR_SIZE*32);
+        MSFT_TIME_ZONE_DAYLIGHT_NAME_OFFSET + (MSFT_WCHAR_SIZE*MSFT_TIME_ZONE_STRING_SIZE);
     static final int MSFT_TIME_ZONE_DAYLIGHT_BIAS_OFFSET =
         MSFT_TIME_ZONE_DAYLIGHT_DATE_OFFSET + MSFT_SYSTEMTIME_SIZE;
     static final int MSFT_TIME_ZONE_SIZE =
@@ -216,6 +218,19 @@
         bytes[offset] = (byte) ((value >> 8) & 0xFF);
     }
 
+    static String getString(byte[] bytes, int offset, int size) {
+        StringBuilder sb = new StringBuilder();
+        for (int i = 0; i < size; i++) {
+            int ch = bytes[offset + i];
+            if (ch == 0) {
+                break;
+            } else {
+                sb.append((char)ch);
+            }
+        }
+        return sb.toString();
+    }
+
     // Internal structure for storing a time zone date from a SYSTEMTIME structure
     // This date represents either the start or the end time for DST
     static class TimeZoneDate {
@@ -861,19 +876,34 @@
                     if (dstSavings != timeZone.getDSTSavings()) continue;
                     return timeZone;
                 }
-                // In this case, there is no daylight savings time, so the only interesting data
-                // is the offset, and we know that all of the zoneId's match; we'll take the first
                 boolean lenient = false;
+                boolean name = false;
                 if ((dstStart.hour != dstEnd.hour) && (precision == STANDARD_DST_PRECISION)) {
                     timeZone = tziStringToTimeZoneImpl(timeZoneString, LENIENT_DST_PRECISION);
                     lenient = true;
                 } else {
-                    timeZone = TimeZone.getTimeZone(zoneIds[0]);
+                    // We can't find a time zone match, so our last attempt is to see if there's
+                    // a valid time zone name in the TZI; if not we'll just take the first TZ with
+                    // a matching offset (which is likely wrong, but ... what else is there to do)
+                    String tzName = getString(timeZoneBytes, MSFT_TIME_ZONE_STANDARD_NAME_OFFSET,
+                            MSFT_TIME_ZONE_STRING_SIZE);
+                    if (!tzName.isEmpty()) {
+                        TimeZone tz = TimeZone.getTimeZone(tzName);
+                        if (tz != null) {
+                            timeZone = tz;
+                            name = true;
+                        } else {
+                            timeZone = TimeZone.getTimeZone(zoneIds[0]);
+                        }
+                    } else {
+                        timeZone = TimeZone.getTimeZone(zoneIds[0]);
+                    }
                 }
                 if (Eas.USER_LOG) {
                     ExchangeService.log(TAG,
                             "No TimeZone with correct DST settings; using " +
-                            (lenient ? "lenient" : "first") + ": " + timeZone.getID());
+                            (name ? "name" : (lenient ? "lenient" : "first")) + ": " +
+                                    timeZone.getID());
                 }
                 return timeZone;
             }
diff --git a/tests/src/com/android/exchange/utility/CalendarUtilitiesTests.java b/tests/src/com/android/exchange/utility/CalendarUtilitiesTests.java
index 0ecdb8f..77820a5 100644
--- a/tests/src/com/android/exchange/utility/CalendarUtilitiesTests.java
+++ b/tests/src/com/android/exchange/utility/CalendarUtilitiesTests.java
@@ -110,6 +110,13 @@
         "AAAAAAAAAAsAAAABAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" +
         "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAACAAAAAAAAAAAAxP///w==";
 
+    // This string specifies "Europe/London" in the name, but otherwise is somewhat bogus
+    // in that it has unknown time zone dates with a 0 bias (GMT). (From a Zimbra server user)
+    private static final String EUROPE_LONDON_TIME_BY_NAME =
+        "AAAAAEV1cm9wZS9Mb25kb24AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" +
+        "AAAAAAAAAAoAAQAFAAIAAAAAAAAAAAAAAEV1cm9wZS9Mb25kb24AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" +
+        "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAQAFAAEAAAAAAAAAxP///w==";
+
     private static final String ORGANIZER = "organizer@server.com";
     private static final String ATTENDEE = "attendee@server.com";
 
@@ -140,6 +147,9 @@
         tz = CalendarUtilities.tziStringToTimeZone(AUSTRALIA_ACT_TIME);
         assertEquals("Australia/ACT", tz.getID());
 
+        tz = CalendarUtilities.tziStringToTimeZone(EUROPE_LONDON_TIME_BY_NAME);
+        assertEquals("Europe/London", tz.getID());
+
         // Test peculiar MS sent EST data with and without lenient precision; send standard
         // precision + 1 (i.e. 1ms) to make sure the code doesn't automatically flip to lenient
         // when the tz isn't found