diff --git a/src/com/android/exchange/SyncManager.java b/src/com/android/exchange/SyncManager.java index 7513f9e85..0ada2bc09 100644 --- a/src/com/android/exchange/SyncManager.java +++ b/src/com/android/exchange/SyncManager.java @@ -1311,8 +1311,10 @@ public class SyncManager extends Service implements Runnable { INSTANCE.notify(); } } - synchronized (sConnectivityLock) { - sConnectivityLock.notify(); + if (sConnectivityLock != null) { + synchronized (sConnectivityLock) { + sConnectivityLock.notify(); + } } } diff --git a/src/com/android/exchange/adapter/AbstractSyncAdapter.java b/src/com/android/exchange/adapter/AbstractSyncAdapter.java index d28ee3b38..8d9fb583d 100644 --- a/src/com/android/exchange/adapter/AbstractSyncAdapter.java +++ b/src/com/android/exchange/adapter/AbstractSyncAdapter.java @@ -31,6 +31,13 @@ import java.io.InputStream; * */ public abstract class AbstractSyncAdapter { + + public static final int SECONDS = 1000; + public static final int MINUTES = SECONDS*60; + public static final int HOURS = MINUTES*60; + public static final int DAYS = HOURS*24; + public static final int WEEKS = DAYS*7; + public Mailbox mMailbox; public EasSyncService mService; public Context mContext; diff --git a/src/com/android/exchange/adapter/EmailSyncAdapter.java b/src/com/android/exchange/adapter/EmailSyncAdapter.java index 9416b0137..a6e17b60b 100644 --- a/src/com/android/exchange/adapter/EmailSyncAdapter.java +++ b/src/com/android/exchange/adapter/EmailSyncAdapter.java @@ -49,6 +49,7 @@ import android.webkit.MimeTypeMap; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; +import java.util.Calendar; import java.util.GregorianCalendar; import java.util.TimeZone; @@ -61,8 +62,10 @@ public class EmailSyncAdapter extends AbstractSyncAdapter { private static final int UPDATES_READ_COLUMN = 0; private static final int UPDATES_MAILBOX_KEY_COLUMN = 1; private static final int UPDATES_SERVER_ID_COLUMN = 2; + private static final int UPDATES_FLAG_COLUMN = 3; private static final String[] UPDATES_PROJECTION = - {MessageColumns.FLAG_READ, MessageColumns.MAILBOX_KEY, SyncColumns.SERVER_ID}; + {MessageColumns.FLAG_READ, MessageColumns.MAILBOX_KEY, SyncColumns.SERVER_ID, + MessageColumns.FLAG_FAVORITE}; String[] bindArguments = new String[2]; @@ -524,6 +527,36 @@ public class EmailSyncAdapter extends AbstractSyncAdapter { } } + private String formatTwo(int num) { + if (num < 10) { + return "0" + (char)('0' + num); + } else + return Integer.toString(num); + } + + /** + * Create date/time in RFC8601 format. Oddly enough, for calendar date/time, Microsoft uses + * a different format that excludes the punctuation (this is why I'm not putting this in a + * parent class) + */ + public String formatDateTime(Calendar calendar) { + StringBuilder sb = new StringBuilder(); + //YYYY-MM-DDTHH:MM:SS.MSSZ + sb.append(calendar.get(Calendar.YEAR)); + sb.append('-'); + sb.append(formatTwo(calendar.get(Calendar.MONTH) + 1)); + sb.append('-'); + sb.append(formatTwo(calendar.get(Calendar.DAY_OF_MONTH))); + sb.append('T'); + sb.append(formatTwo(calendar.get(Calendar.HOUR_OF_DAY))); + sb.append(':'); + sb.append(formatTwo(calendar.get(Calendar.MINUTE))); + sb.append(':'); + sb.append(formatTwo(calendar.get(Calendar.SECOND))); + sb.append(".000Z"); + return sb.toString(); + } + @Override public boolean sendLocalChanges(Serializer s, EasSyncService service) throws IOException { ContentResolver cr = mContext.getContentResolver(); @@ -589,23 +622,70 @@ public class EmailSyncAdapter extends AbstractSyncAdapter { continue; } + boolean flagChange = false; + boolean readChange = false; + + int flag = 0; + + // We can only send flag changes to the server in 12.0 or later + if (mService.mProtocolVersionDouble >= 12.0) { + flag = currentCursor.getInt(UPDATES_FLAG_COLUMN); + if (flag != c.getInt(Message.LIST_FAVORITE_COLUMN)) { + flagChange = true; + } + } + int read = currentCursor.getInt(UPDATES_READ_COLUMN); if (read == c.getInt(Message.LIST_READ_COLUMN)) { - // The read state hasn't really changed, so move on... + readChange = true; + } + + if (!flagChange && !readChange) { + // In this case, we've got nothing to send to the server continue; } + if (first) { s.start(Tags.SYNC_COMMANDS); first = false; } - // Send the change to "read". We'll do "flagged" here eventually as well - // TODO Add support for flags here (EAS 12.0 and above) - // Or is this not safe?? + // Send the change to "read" and "favorite" (flagged) s.start(Tags.SYNC_CHANGE) .data(Tags.SYNC_SERVER_ID, c.getString(Message.LIST_SERVER_ID_COLUMN)) - .start(Tags.SYNC_APPLICATION_DATA) - .data(Tags.EMAIL_READ, Integer.toString(read)) - .end().end(); // SYNC_APPLICATION_DATA, SYNC_CHANGE + .start(Tags.SYNC_APPLICATION_DATA); + if (readChange) { + s.data(Tags.EMAIL_READ, Integer.toString(read)); + } + // "Flag" is a relatively complex concept in EAS 12.0 and above. It is not only + // the boolean "favorite" that we think of in Gmail, but it also represents a + // follow up action, which can include a subject, start and due dates, and even + // recurrences. We don't support any of this as yet, but EAS 12.0 and higher + // require that a flag contain a status, a type, and four date fields, two each + // for start date and end (due) date. + if (flagChange) { + if (flag != 0) { + // Status 2 = set flag + s.start(Tags.EMAIL_FLAG).data(Tags.EMAIL_FLAG_STATUS, "2"); + // "FollowUp" is the standard type + s.data(Tags.EMAIL_FLAG_TYPE, "FollowUp"); + long now = System.currentTimeMillis(); + Calendar calendar = + GregorianCalendar.getInstance(TimeZone.getTimeZone("GMT")); + calendar.setTimeInMillis(now); + // Flags are required to have a start date and end date (duplicated) + // First, we'll set the current date/time in GMT as the start time + String utc = formatDateTime(calendar); + s.data(Tags.TASK_START_DATE, utc).data(Tags.TASK_UTC_START_DATE, utc); + // And then we'll use one week from today for completion date + calendar.setTimeInMillis(now + 1*WEEKS); + utc = formatDateTime(calendar); + s.data(Tags.TASK_DUE_DATE, utc).data(Tags.TASK_UTC_DUE_DATE, utc); + s.end(); + } else { + s.tag(Tags.EMAIL_FLAG); + } + } + s.end().end(); // SYNC_APPLICATION_DATA, SYNC_CHANGE } finally { currentCursor.close(); } diff --git a/src/com/android/exchange/adapter/Tags.java b/src/com/android/exchange/adapter/Tags.java index a1c878c98..d8221813c 100644 --- a/src/com/android/exchange/adapter/Tags.java +++ b/src/com/android/exchange/adapter/Tags.java @@ -39,6 +39,7 @@ public class Tags { public static final int MOVE = 0x05; public static final int GIE = 0x06; public static final int FOLDER = 0x07; + public static final int TASK = 0x09; public static final int CONTACTS2 = 0x0C; public static final int PING = 0x0D; public static final int GAL = 0x10; @@ -277,6 +278,39 @@ public class Tags { public static final int EMAIL_FLAG_TYPE = EMAIL_PAGE + 0x3D; public static final int EMAIL_COMPLETE_TIME = EMAIL_PAGE + 0x3E; + public static final int TASK_PAGE = TASK << PAGE_SHIFT; + public static final int TASK_BODY = TASK_PAGE + 5; + public static final int TASK_BODY_SIZE = TASK_PAGE + 6; + public static final int TASK_BODY_TRUNCATED = TASK_PAGE + 7; + public static final int TASK_CATEGORIES = TASK_PAGE + 8; + public static final int TASK_CATEGORY = TASK_PAGE + 9; + public static final int TASK_COMPLETE = TASK_PAGE + 0xA; + public static final int TASK_DATE_COMPLETED = TASK_PAGE + 0xB; + public static final int TASK_DUE_DATE = TASK_PAGE + 0xC; + public static final int TASK_UTC_DUE_DATE = TASK_PAGE + 0xD; + public static final int TASK_IMPORTANCE = TASK_PAGE + 0xE; + public static final int TASK_RECURRENCE = TASK_PAGE + 0xF; + public static final int TASK_RECURRENCE_TYPE = TASK_PAGE + 0x10; + public static final int TASK_RECURRENCE_START = TASK_PAGE + 0x11; + public static final int TASK_RECURRENCE_UNTIL = TASK_PAGE + 0x12; + public static final int TASK_RECURRENCE_OCCURRENCES = TASK_PAGE + 0x13; + public static final int TASK_RECURRENCE_INTERVAL = TASK_PAGE + 0x14; + public static final int TASK_RECURRENCE_DAY_OF_MONTH = TASK_PAGE + 0x15; + public static final int TASK_RECURRENCE_DAY_OF_WEEK = TASK_PAGE + 0x16; + public static final int TASK_RECURRENCE_WEEK_OF_MONTH = TASK_PAGE + 0x17; + public static final int TASK_RECURRENCE_MONTH_OF_YEAR = TASK_PAGE + 0x18; + public static final int TASK_RECURRENCE_REGENERATE = TASK_PAGE + 0x19; + public static final int TASK_RECURRENCE_DEAD_OCCUR = TASK_PAGE + 0x1A; + public static final int TASK_REMINDER_SET = TASK_PAGE + 0x1B; + public static final int TASK_REMINDER_TIME = TASK_PAGE + 0x1C; + public static final int TASK_SENSITIVITY = TASK_PAGE + 0x1D; + public static final int TASK_START_DATE = TASK_PAGE + 0x1E; + public static final int TASK_UTC_START_DATE = TASK_PAGE + 0x1F; + public static final int TASK_SUBJECT = TASK_PAGE + 0x20; + public static final int COMPRESSED_RTF = TASK_PAGE + 0x21; + public static final int ORDINAL_DATE = TASK_PAGE + 0x22; + public static final int SUBORDINAL_DATE = TASK_PAGE + 0x23; + public static final int MOVE_PAGE = MOVE << PAGE_SHIFT; public static final int MOVE_MOVE_ITEMS = MOVE_PAGE + 5; public static final int MOVE_MOVE = MOVE_PAGE + 6; @@ -410,6 +444,13 @@ public class Tags { }, { // 0x09 Tasks + "Body", "BodySize", "BodyTruncated", "Categories", "Category", "Complete", + "DateCompleted", "DueDate", "UTCDueDate", "Importance", "Recurrence", "RecurrenceType", + "RecurrenceStart", "RecurrenceUntil", "RecurrenceOccurrences", "RecurrenceInterval", + "RecurrenceDOM", "RecurrenceDOW", "RecurrenceWOM", "RecurrenceMOY", + "RecurrenceRegenerate", "RecurrenceDeadOccur", "ReminderSet", "ReminderTime", + "Sensitivity", "StartDate", "UTCStartDate", "Subject", "CompressedRTF", "OrdinalDate", + "SubordinalDate" }, { // 0x0A ResolveRecipients diff --git a/tests/src/com/android/exchange/EasEmailSyncAdapterTests.java b/tests/src/com/android/exchange/EasEmailSyncAdapterTests.java index da9c67403..6d7ba5ec2 100644 --- a/tests/src/com/android/exchange/EasEmailSyncAdapterTests.java +++ b/tests/src/com/android/exchange/EasEmailSyncAdapterTests.java @@ -26,6 +26,8 @@ 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 { @@ -38,22 +40,32 @@ public class EasEmailSyncAdapterTests extends AndroidTestCase { 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 { - Mailbox mailbox = new Mailbox(); - mailbox.mId = -1; - Account account = new Account(); - account.mId = -1; - EasSyncService service = new EasSyncService(); - service.mContext = getContext(); - service.mMailbox = mailbox; - service.mAccount = account; - EmailSyncAdapter adapter = new EmailSyncAdapter(mailbox, service); - EasEmailSyncParser p; - p = adapter.new EasEmailSyncParser(getTestInputStream(), service); + EasSyncService service = getTestService(); + EmailSyncAdapter adapter = new EmailSyncAdapter(service.mMailbox, service); + EasEmailSyncParser p = adapter.new EasEmailSyncParser(getTestInputStream(), service); // Test a few known types String mimeType = p.getMimeTypeFromFileName("foo.jpg"); assertEquals("image/jpeg", mimeType); @@ -72,4 +84,17 @@ public class EasEmailSyncAdapterTests extends AndroidTestCase { 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); + } }