From b6493a07ef625c0e290890c2e60256b47a066e5e Mon Sep 17 00:00:00 2001 From: Marc Blank Date: Sun, 5 Jul 2009 12:54:49 -0700 Subject: [PATCH] Major refactor and cleanup of EAS code * Rewrote push logic to encompass multiple folders (i.e. calendar/contacts) * Change inbox from push frequency to ping frequency after initial sync * Implement upsync logic for email (i.e. sending changes to the server) * Did cleanup of some files (there's still some to do) re: format, style * Initial one-way sync of Contacts data - add and delete are implemented * Created adapter package for all parts of the EAS adapter * Created utility package for utility code that will eventually be merged with code in the Email application (Base64, QuotedPrintable, etc.) * SyncManager/AbstractSyncService can be used in the future for other protocols, especially IMAP push --- AndroidManifest.xml | 7 +- .../android/email/activity/MessageList.java | 1 - .../email/mail/MessagingException.java | 2 + .../android/email/provider/EmailContent.java | 9 +- .../android/email/provider/EmailProvider.java | 91 +- .../android/exchange/AbstractSyncService.java | 330 ++++++++ src/com/android/exchange/Eas.java | 62 ++ .../android/exchange/EasEmailSyncParser.java | 356 -------- .../{EodException.java => EasException.java} | 8 +- .../android/exchange/EasFolderSyncParser.java | 216 ----- src/com/android/exchange/EasParser.java | 297 ------- src/com/android/exchange/EasPingService.java | 103 --- src/com/android/exchange/EasService.java | 800 ------------------ src/com/android/exchange/EasSyncService.java | 769 +++++++++++++++++ src/com/android/exchange/EasTags.java | 345 -------- src/com/android/exchange/EmailContent.java | 42 +- src/com/android/exchange/EofException.java | 24 - .../exchange/InteractiveSyncService.java | 50 ++ ...eceiver.java => MailboxAlarmReceiver.java} | 19 +- src/com/android/exchange/ProtocolService.java | 243 ------ ...ion.java => StaleFolderListException.java} | 4 +- src/com/android/exchange/SyncManager.java | 749 ++++++++-------- .../exchange/UserSyncAlarmReceiver.java | 98 +++ .../adapter/EasCalendarSyncAdapter.java | 52 ++ .../adapter/EasContactsSyncAdapter.java | 385 +++++++++ .../exchange/adapter/EasContentParser.java | 141 +++ .../exchange/adapter/EasEmailSyncAdapter.java | 436 ++++++++++ .../exchange/adapter/EasFolderSyncParser.java | 333 ++++++++ .../exchange/{ => adapter}/EasMoveParser.java | 24 +- .../{ => adapter}/EasOutboxService.java | 39 +- .../android/exchange/adapter/EasParser.java | 470 ++++++++++ .../exchange/adapter/EasPingParser.java | 85 ++ .../exchange/adapter/EasSerializer.java | 103 +++ .../exchange/adapter/EasSyncAdapter.java | 47 + src/com/android/exchange/adapter/EasTags.java | 459 ++++++++++ .../android/exchange/{ => adapter}/Wbxml.java | 2 +- .../{ => adapter}/WbxmlSerializer.java | 4 +- .../exchange/{ => utility}/Base64.java | 2 +- .../{ => utility}/QuotedPrintable.java | 11 +- .../{ => utility}/Rfc822Formatter.java | 12 +- 40 files changed, 4383 insertions(+), 2847 deletions(-) create mode 100644 src/com/android/exchange/AbstractSyncService.java create mode 100644 src/com/android/exchange/Eas.java delete mode 100644 src/com/android/exchange/EasEmailSyncParser.java rename src/com/android/exchange/{EodException.java => EasException.java} (79%) delete mode 100644 src/com/android/exchange/EasFolderSyncParser.java delete mode 100644 src/com/android/exchange/EasParser.java delete mode 100644 src/com/android/exchange/EasPingService.java delete mode 100644 src/com/android/exchange/EasService.java create mode 100644 src/com/android/exchange/EasSyncService.java delete mode 100644 src/com/android/exchange/EasTags.java delete mode 100644 src/com/android/exchange/EofException.java create mode 100644 src/com/android/exchange/InteractiveSyncService.java rename src/com/android/exchange/{KeepAliveReceiver.java => MailboxAlarmReceiver.java} (66%) delete mode 100644 src/com/android/exchange/ProtocolService.java rename src/com/android/exchange/{EasParserException.java => StaleFolderListException.java} (87%) create mode 100644 src/com/android/exchange/UserSyncAlarmReceiver.java create mode 100644 src/com/android/exchange/adapter/EasCalendarSyncAdapter.java create mode 100644 src/com/android/exchange/adapter/EasContactsSyncAdapter.java create mode 100644 src/com/android/exchange/adapter/EasContentParser.java create mode 100644 src/com/android/exchange/adapter/EasEmailSyncAdapter.java create mode 100644 src/com/android/exchange/adapter/EasFolderSyncParser.java rename src/com/android/exchange/{ => adapter}/EasMoveParser.java (72%) rename src/com/android/exchange/{ => adapter}/EasOutboxService.java (71%) create mode 100644 src/com/android/exchange/adapter/EasParser.java create mode 100644 src/com/android/exchange/adapter/EasPingParser.java create mode 100644 src/com/android/exchange/adapter/EasSerializer.java create mode 100644 src/com/android/exchange/adapter/EasSyncAdapter.java create mode 100644 src/com/android/exchange/adapter/EasTags.java rename src/com/android/exchange/{ => adapter}/Wbxml.java (98%) rename src/com/android/exchange/{ => adapter}/WbxmlSerializer.java (99%) rename src/com/android/exchange/{ => utility}/Base64.java (99%) rename src/com/android/exchange/{ => utility}/QuotedPrintable.java (90%) rename src/com/android/exchange/{ => utility}/Rfc822Formatter.java (94%) diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 250c78a1e..9f3e5104f 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -24,6 +24,9 @@ + + + @@ -150,7 +153,9 @@ - + + diff --git a/src/com/android/email/activity/MessageList.java b/src/com/android/email/activity/MessageList.java index 78cd2b51e..1a97cbd7d 100644 --- a/src/com/android/email/activity/MessageList.java +++ b/src/com/android/email/activity/MessageList.java @@ -106,7 +106,6 @@ public class MessageList extends ListActivity implements OnItemClickListener, On mListAdapter = new MessageListAdapter(this); setListAdapter(mListAdapter); - mListView.setAdapter(mAdapter); // TODO set title to "account > mailbox (#unread)" diff --git a/src/com/android/email/mail/MessagingException.java b/src/com/android/email/mail/MessagingException.java index 8896c6e9f..25d108805 100644 --- a/src/com/android/email/mail/MessagingException.java +++ b/src/com/android/email/mail/MessagingException.java @@ -41,6 +41,8 @@ public class MessagingException extends Exception { public static final int GENERAL_SECURITY = 4; /** Authentication failed */ public static final int AUTHENTICATION_FAILED = 5; + /** Attempt to create duplicate account */ + public static final int DUPLICATE_ACCOUNT = 6; protected int mExceptionType; diff --git a/src/com/android/email/provider/EmailContent.java b/src/com/android/email/provider/EmailContent.java index b69809cfb..d3634d2a2 100644 --- a/src/com/android/email/provider/EmailContent.java +++ b/src/com/android/email/provider/EmailContent.java @@ -748,6 +748,7 @@ public abstract class EmailContent { public static final int CHECK_INTERVAL_NEVER = -1; public static final int CHECK_INTERVAL_PUSH = -2; + public static final int CHECK_INTERVAL_PING = -3; public static final int SYNC_WINDOW_USER = -1; @@ -1658,7 +1659,7 @@ public abstract class EmailContent { public static final int TYPE_JUNK = 7; // Types after this are used for non-mail mailboxes (as in EAS) - public static final int TYPE_INVISIBLE = 0x40; + public static final int TYPE_NOT_EMAIL = 0x40; public static final int TYPE_CALENDAR = 0x41; public static final int TYPE_CONTACTS = 0x42; public static final int TYPE_TASKS = 0x43; @@ -1756,9 +1757,9 @@ public abstract class EmailContent { public static final String TABLE_NAME = "HostAuth"; public static final Uri CONTENT_URI = Uri.parse(EmailContent.CONTENT_URI + "/hostauth"); - private static final int FLAG_SSL = 1; - private static final int FLAG_TLS = 2; - private static final int FLAG_AUTHENTICATE = 4; + public static final int FLAG_SSL = 1; + public static final int FLAG_TLS = 2; + public static final int FLAG_AUTHENTICATE = 4; public String mProtocol; public String mAddress; diff --git a/src/com/android/email/provider/EmailProvider.java b/src/com/android/email/provider/EmailProvider.java index 5b7c9dd97..3e7106ee4 100644 --- a/src/com/android/email/provider/EmailProvider.java +++ b/src/com/android/email/provider/EmailProvider.java @@ -226,41 +226,56 @@ public class EmailProvider extends ContentProvider { } static void createMessageTable(SQLiteDatabase db) { - String s = " (" + EmailContent.RECORD_ID + " integer primary key autoincrement, " - + SyncColumns.ACCOUNT_KEY + " integer, " - + SyncColumns.SERVER_ID + " integer, " - + SyncColumns.SERVER_VERSION + " integer, " - + SyncColumns.DATA + " text, " - + SyncColumns.DIRTY_COUNT + " integer, " - + MessageColumns.DISPLAY_NAME + " text, " - + MessageColumns.TIMESTAMP + " integer, " - + MessageColumns.SUBJECT + " text, " - + MessageColumns.PREVIEW + " text, " - + MessageColumns.FLAG_READ + " integer, " - + MessageColumns.FLAG_LOADED + " integer, " - + MessageColumns.FLAG_FAVORITE + " integer, " - + MessageColumns.FLAG_ATTACHMENT + " integer, " - + MessageColumns.FLAGS + " integer, " - + MessageColumns.TEXT_INFO + " text, " - + MessageColumns.HTML_INFO + " text, " - + MessageColumns.CLIENT_ID + " integer, " - + MessageColumns.MESSAGE_ID + " text, " - + MessageColumns.THREAD_ID + " text, " - + MessageColumns.MAILBOX_KEY + " integer, " - + MessageColumns.ACCOUNT_KEY + " integer, " - + MessageColumns.REFERENCE_KEY + " integer, " - + MessageColumns.SENDER_LIST + " text, " - + MessageColumns.FROM_LIST + " text, " - + MessageColumns.TO_LIST + " text, " - + MessageColumns.CC_LIST + " text, " - + MessageColumns.BCC_LIST + " text, " - + MessageColumns.REPLY_TO_LIST + " text" - + ");"; + String messageColumns = MessageColumns.DISPLAY_NAME + " text, " + + MessageColumns.TIMESTAMP + " integer, " + + MessageColumns.SUBJECT + " text, " + + MessageColumns.PREVIEW + " text, " + + MessageColumns.FLAG_READ + " integer, " + + MessageColumns.FLAG_LOADED + " integer, " + + MessageColumns.FLAG_FAVORITE + " integer, " + + MessageColumns.FLAG_ATTACHMENT + " integer, " + + MessageColumns.FLAGS + " integer, " + + MessageColumns.TEXT_INFO + " text, " + + MessageColumns.HTML_INFO + " text, " + + MessageColumns.CLIENT_ID + " integer, " + + MessageColumns.MESSAGE_ID + " text, " + + MessageColumns.THREAD_ID + " text, " + + MessageColumns.MAILBOX_KEY + " integer, " + + MessageColumns.ACCOUNT_KEY + " integer, " + + MessageColumns.REFERENCE_KEY + " integer, " + + MessageColumns.SENDER_LIST + " text, " + + MessageColumns.FROM_LIST + " text, " + + MessageColumns.TO_LIST + " text, " + + MessageColumns.CC_LIST + " text, " + + MessageColumns.BCC_LIST + " text, " + + MessageColumns.REPLY_TO_LIST + " text" + + ");"; + + // This String and the following String MUST have the same columns, except for the type + // of those columns! + String createString = " (" + EmailContent.RECORD_ID + " integer primary key autoincrement, " + + SyncColumns.ACCOUNT_KEY + " integer, " + + SyncColumns.SERVER_ID + " integer, " + + SyncColumns.SERVER_VERSION + " integer, " + + SyncColumns.DATA + " text, " + + SyncColumns.DIRTY_COUNT + " integer, " + + messageColumns; + + // For the updated and deleted tables, the id is assigned, but we do want to keep track + // of the ORDER of updates using an autoincrement primary key. We use the DATA column + // at this point; it has no other function + String altCreateString = " (" + EmailContent.RECORD_ID + " integer, " + + SyncColumns.ACCOUNT_KEY + " integer, " + + SyncColumns.SERVER_ID + " integer, " + + SyncColumns.SERVER_VERSION + " integer, " + + SyncColumns.DATA + " integer primary key autoincrement, " + + SyncColumns.DIRTY_COUNT + " integer, " + + messageColumns; // The three tables have the same schema - db.execSQL("create table " + Message.TABLE_NAME + s); - db.execSQL("create table " + Message.UPDATED_TABLE_NAME + s); - db.execSQL("create table " + Message.DELETED_TABLE_NAME + s); + db.execSQL("create table " + Message.TABLE_NAME + createString); + db.execSQL("create table " + Message.UPDATED_TABLE_NAME + altCreateString); + db.execSQL("create table " + Message.DELETED_TABLE_NAME + altCreateString); // For now, indices only on the Message table db.execSQL("create index message_" + MessageColumns.TIMESTAMP @@ -364,7 +379,7 @@ public class EmailProvider extends ContentProvider { + MailboxColumns.FLAG_VISIBLE + " integer, " + MailboxColumns.FLAGS + " integer, " + MailboxColumns.VISIBLE_LIMIT + " integer" - + ");"; + + ");"; db.execSQL("create table " + Mailbox.TABLE_NAME + s); db.execSQL("create index mailbox_" + MailboxColumns.SERVER_ID + " on " + Mailbox.TABLE_NAME + " (" + MailboxColumns.SERVER_ID + ")"); @@ -374,7 +389,7 @@ public class EmailProvider extends ContentProvider { db.execSQL("create trigger mailbox_delete before delete on " + Mailbox.TABLE_NAME + " begin delete from " + Message.TABLE_NAME + " where " + MessageColumns.MAILBOX_KEY + "=old." + EmailContent.RECORD_ID + - "; end"); + "; end"); } static void upgradeMailboxTable(SQLiteDatabase db, int oldVersion, int newVersion) { @@ -518,7 +533,7 @@ public class EmailProvider extends ContentProvider { Log.v(TAG, "EmailProvider.delete: uri=" + uri + ", match is " + match); } - int result; + int result = -1; try { switch (match) { @@ -728,8 +743,8 @@ public class EmailProvider extends ContentProvider { switch (match) { case BODY: case MESSAGE: - case DELETED_MESSAGE: case UPDATED_MESSAGE: + case DELETED_MESSAGE: case ATTACHMENT: case MAILBOX: case ACCOUNT: @@ -844,7 +859,7 @@ public class EmailProvider extends ContentProvider { * update/insert/delete calls? */ public ContentProviderResult[] applyBatch(ArrayList operations) - throws OperationApplicationException { + throws OperationApplicationException { SQLiteDatabase db = getDatabase(getContext()); db.beginTransaction(); try { diff --git a/src/com/android/exchange/AbstractSyncService.java b/src/com/android/exchange/AbstractSyncService.java new file mode 100644 index 000000000..11c2d0fdd --- /dev/null +++ b/src/com/android/exchange/AbstractSyncService.java @@ -0,0 +1,330 @@ +/* + * Copyright (C) 2008-2009 Marc Blank + * Licensed to 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 java.util.ArrayList; + +import com.android.email.Email; +import com.android.email.mail.MessagingException; +import com.android.exchange.EmailContent.Account; +import com.android.exchange.EmailContent.Mailbox; + +import android.content.Context; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.net.NetworkInfo.DetailedState; +import android.util.Log; + +/** + * Base class for all protocol services SyncManager (extends Service, implements + * Runnable) instantiates subclasses to run a sync (either timed, or push, or + * mail placed in outbox, etc.) EasSyncService is currently implemented; my goal + * would be to move IMAP to this structure when it comes time to introduce push + * functionality. + */ +public abstract class AbstractSyncService implements Runnable { + + public String TAG = "ProtocolService"; + + public static final String SUMMARY_PROTOCOL = "_SUMMARY_"; + public static final String SYNCED_PROTOCOL = "_SYNCING_"; + public static final String MOVE_FAVORITES_PROTOCOL = "_MOVE_FAVORITES_"; + public static final int CONNECT_TIMEOUT = 30000; + public static final int NETWORK_WAIT = 15000; + public static final int SECS = 1000; + public static final int MINS = 60 * SECS; + public static final int HRS = 60 * MINS; + public static final int DAYS = 24 * HRS; + public static final String IMAP_PROTOCOL = "imap"; + public static final String EAS_PROTOCOL = "eas"; + public static final int EXIT_DONE = 0; + public static final int EXIT_IO_ERROR = 1; + public static final int EXIT_LOGIN_FAILURE = 2; + public static final int EXIT_EXCEPTION = 3; + + // Making SSL connections is so slow that I'd prefer that only one be + // executed at a time + // Kindly subclasses will synchronize on this before making an SSL + // connection + public static Object sslGovernorToken = new Object(); + public Mailbox mMailbox; + protected long mMailboxId; + protected Thread mThread; + protected int mExitStatus = EXIT_EXCEPTION; + protected String mMailboxName; + public Account mAccount; + protected Context mContext; + protected long mRequestTime = 0; + + protected ArrayList mPartRequests = new ArrayList(); + protected PartRequest mPendingPartRequest = null; + + /** + * Sent by SyncManager to request that the service stop itself cleanly + */ + public abstract void stop(); + + /** + * Sent by SyncManager to indicate a user request requiring service has been + * added to the service's pending request queue + */ + public abstract void ping(); + + /** + * Called to validate an account; abstract to allow each protocol to do what + * is necessary. For consistency with the Email app's original + * functionality, success is indicated by a failure to throw an Exception + * (ugh). Parameters are self-explanatory + * + * @param host + * @param userName + * @param password + * @param port + * @param ssl + * @param context + * @throws MessagingException + */ + public abstract void validateAccount(String host, String userName, String password, int port, + boolean ssl, Context context) throws MessagingException; + + /** + * Sent by SyncManager to determine the state of a running sync This is + * currently unused + * + * @return status code + */ + public int getSyncStatus() { + return 0; + } + + public AbstractSyncService(Context _context, Mailbox _mailbox) { + mContext = _context; + mMailbox = _mailbox; + mMailboxId = _mailbox.mId; + mMailboxName = _mailbox.mServerId; + mAccount = Account.restoreAccountWithId(_context, _mailbox.mAccountKey); + } + + // Will be required when subclasses are instantiated by name + public AbstractSyncService(String prefix) { + } + + /** + * The UI can call this static method to perform account validation. This method wraps each + * protocol's validateAccount method. Arguments are self-explanatory, except where noted. + * + * @param klass the protocol class (EasSyncService.class for example) + * @param host + * @param userName + * @param password + * @param port + * @param ssl + * @param context + * @throws MessagingException + */ + static public void validate(Class klass, String host, + String userName, String password, int port, boolean ssl, Context context) + throws MessagingException { + AbstractSyncService svc; + try { + svc = klass.newInstance(); + svc.validateAccount(host, userName, password, port, ssl, context); + } catch (IllegalAccessException e) { + throw new MessagingException("internal error", e); + } catch (InstantiationException e) { + throw new MessagingException("internal error", e); + } + } + + public static class ValidationResult { + static final int NO_FAILURE = 0; + static final int CONNECTION_FAILURE = 1; + static final int VALIDATION_FAILURE = 2; + static final int EXCEPTION = 3; + + static final ValidationResult succeeded = new ValidationResult(true, NO_FAILURE, null); + boolean success; + int failure = NO_FAILURE; + String reason = null; + Exception exception = null; + + ValidationResult(boolean _success, int _failure, String _reason) { + success = _success; + failure = _failure; + reason = _reason; + } + + ValidationResult(boolean _success) { + success = _success; + } + + ValidationResult(Exception e) { + success = false; + failure = EXCEPTION; + exception = e; + } + + public boolean isSuccess() { + return success; + } + + public String getReason() { + return reason; + } + } + + /** + * Asks SyncManager for a WaitLock for this sync + */ + public final void runAwake() { + //SyncManager.runAwake(mMailboxId); + } + + /** + * Asks SyncManager to release any WaitLock and schedule an alarm at a specified number + * of milliseconds in the future + * + * @param millis + */ + public final void runAsleep(long millis) { + //SyncManager.runAsleep(mMailboxId, millis); + } + + /** + * Convenience method to do user logging (i.e. connection activity). Saves a bunch of + * repetitive code. + * + * @param str the String to log + */ + public void userLog(String str) { + if (Eas.USER_DEBUG) { + Log.i(TAG, str); + } + } + + public void errorLog(String str) { + if (Eas.USER_DEBUG) { + Log.e(TAG, str); + } + } + + /** + * Convenience method to do test logging. Saves a bunch of repetitive code. + * Unlike user logging, TEST_DEBUG is declared final, so that testLog calls should get + * "compiled out" for non-debug builds. + * + * @param str the String to log + */ + protected void testLog(String str) { + if (Eas.TEST_DEBUG) { + Log.v(Email.LOG_TAG, str); + } + } + + /** + * Implements a delay until there is some kind of network connectivity available. This method + * may be supplanted by functionality in SyncManager. + * + * @return the type of network connected to + */ + public int waitForConnectivity() { + ConnectivityManager cm = (ConnectivityManager)mContext + .getSystemService(Context.CONNECTIVITY_SERVICE); + while (true) { + NetworkInfo info = cm.getActiveNetworkInfo(); + if (info != null && info.isConnected()) { + DetailedState state = info.getDetailedState(); + if (state == DetailedState.CONNECTED) { + return info.getType(); + } else { + // TODO Happens sometimes; find out why... + userLog("Not quite connected? Pause 1 second"); + } + pause(1000); + } else { + userLog("Not connected; waiting 15 seconds"); + pause(NETWORK_WAIT); + } + } + } + + /** + * Convenience method to generate a small wait + * + * @param ms time to wait in milliseconds + */ + private void pause(int ms) { + try { + Thread.sleep(ms); + } catch (InterruptedException e) { + } + } + + // What's below here is temporary + + /** + * PartRequest handling (common functionality) + * Can be overridden if desired, but IMAP/EAS both use the next three methods as-is + */ + + public void addPartRequest(PartRequest req) { + synchronized (mPartRequests) { + mPartRequests.add(req); + } + } + + public void removePartRequest(PartRequest req) { + synchronized (mPartRequests) { + mPartRequests.remove(req); + } + } + + public PartRequest hasPartRequest(long emailId, String part) { + synchronized (mPartRequests) { + for (PartRequest pr : mPartRequests) { + if (pr.emailId == emailId && pr.loc.equals(part)) + return pr; + } + } + return null; + } + + // CancelPartRequest is sent in response to user input to stop a request + // (attachment load at this point) + // that is in progress. This will almost certainly require code overriding + // the base functionality, as + // sockets may need to be closed, etc. and this functionality will be + // service dependent. This returns + // the canceled PartRequest or null + public PartRequest cancelPartRequest(long emailId, String part) { + synchronized (mPartRequests) { + PartRequest p = null; + for (PartRequest pr : mPartRequests) { + if (pr.emailId == emailId && pr.loc.equals(part)) { + p = pr; + break; + } + } + if (p != null) { + mPartRequests.remove(p); + return p; + } + } + return null; + } +} diff --git a/src/com/android/exchange/Eas.java b/src/com/android/exchange/Eas.java new file mode 100644 index 000000000..86b5419d5 --- /dev/null +++ b/src/com/android/exchange/Eas.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2008-2009 Marc Blank + * Licensed to 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; + +/** + * Constants used throughout the EAS implementation are stored here. + * + */ +public class Eas { + // For use in collecting user logs + public static boolean USER_DEBUG = false; // DO NOT CHECK IN WITH THIS SET TO TRUE + // For temporary use while debugging + public static boolean TEST_DEBUG = false; // DO NOT CHECK IN WITH THIS SET TO TRUE + + public static String VERSION = "0.1"; + + // From EAS spec + // Mail Cal + // 0 No filter Yes Yes + // 1 1 day ago Yes No + // 2 3 days ago Yes No + // 3 1 week ago Yes No + // 4 2 weeks ago Yes Yes + // 5 1 month ago Yes Yes + // 6 3 months ago No Yes + // 7 6 months ago No Yes + + static final String FILTER_ALL = "0"; + static final String FILTER_1_DAY = "1"; + static final String FILTER_3_DAYS = "2"; + static final String FILTER_1_WEEK = "3"; + static final String FILTER_2_WEEKS = "4"; + static final String FILTER_1_MONTH = "5"; + static final String FILTER_3_MONTHS = "6"; + static final String FILTER_6_MONTHS = "7"; + static final String BODY_PREFERENCE_TEXT = "1"; + static final String BODY_PREFERENCE_HTML = "2"; + + static final String DEFAULT_BODY_TRUNCATION_SIZE = "50000"; + + public static final int FOLDER_STATUS_OK = 1; + public static final int FOLDER_STATUS_INVALID_KEY = 9; + + public void setUserDebug(boolean state) { + USER_DEBUG = state; + } +} diff --git a/src/com/android/exchange/EasEmailSyncParser.java b/src/com/android/exchange/EasEmailSyncParser.java deleted file mode 100644 index e5fd15480..000000000 --- a/src/com/android/exchange/EasEmailSyncParser.java +++ /dev/null @@ -1,356 +0,0 @@ -/* - * Copyright (C) 2008-2009 Marc Blank - * Licensed to 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 java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.GregorianCalendar; -import java.util.TimeZone; - -import com.android.email.provider.EmailProvider; -import com.android.exchange.EmailContent.Account; -import com.android.exchange.EmailContent.Attachment; -import com.android.exchange.EmailContent.Mailbox; -import com.android.exchange.EmailContent.Message; -import com.android.exchange.EmailContent.MessageColumns; -import com.android.exchange.EmailContent.SyncColumns; - -import android.content.ContentProviderOperation; -import android.content.ContentProviderResult; -import android.content.ContentResolver; -import android.content.ContentUris; -import android.content.ContentValues; -import android.content.Context; -import android.content.OperationApplicationException; -import android.database.Cursor; -import android.os.RemoteException; -import android.util.Log; - -public class EasEmailSyncParser extends EasParser { - - private static final String TAG = "EmailSyncParser"; - - private Account mAccount; - private EasService mService; - private ContentResolver mContentResolver; - private Context mContext; - private Mailbox mMailbox; - protected boolean mMoreAvailable = false; - String[] bindArgument = new String[1]; - - public EasEmailSyncParser(InputStream in, EasService service) throws IOException { - super(in); - mService = service; - mContext = service.mContext; - mMailbox = service.mMailbox; - mAccount = service.mAccount; - //setDebug(true); - mContentResolver = mContext.getContentResolver(); - } - - public void parse() throws IOException { - int status; - if (nextTag(START_DOCUMENT) != EasTags.SYNC_SYNC) - throw new IOException(); - while (nextTag(START_DOCUMENT) != END_DOCUMENT) { - if (tag == EasTags.SYNC_COLLECTION || tag == EasTags.SYNC_COLLECTIONS) { - // Ignore - } else if (tag == EasTags.SYNC_STATUS) { - status = getValueInt(); - if (status != 1) { - System.err.println("Sync failed: " + status); - if (status == 3) { - // TODO Bad sync key. Must delete everything and start over...? - mMailbox.mSyncKey = "0"; - Log.w(TAG, "Bad sync key; RESET and delete mailbox contents"); - mContext.getContentResolver() - .delete(Message.CONTENT_URI, - Message.MAILBOX_KEY + "=" + mMailbox.mId, null); - mMoreAvailable = true; - } - } - } else if (tag == EasTags.SYNC_COMMANDS) { - commandsParser(); - } else if (tag == EasTags.SYNC_RESPONSES) { - skipTag(); - } else if (tag == EasTags.SYNC_MORE_AVAILABLE) { - mMoreAvailable = true; - } else if (tag == EasTags.SYNC_SYNC_KEY) { - if (mMailbox.mSyncKey.equals("0")) - mMoreAvailable = true; - mMailbox.mSyncKey = getValue(); - } else - skipTag(); - } - mMailbox.saveOrUpdate(mContext); - } - - public void addParser(ArrayList emails) throws IOException { - Message msg = new Message(); - String to = ""; - String from = ""; - String cc = ""; - String replyTo = ""; - int size = 0; - msg.mAccountKey = mAccount.mId; - msg.mMailboxKey = mMailbox.mId; - msg.mFlagLoaded = Message.LOADED; - - ArrayList atts = new ArrayList(); - boolean inData = false; - - while (nextTag(EasTags.SYNC_ADD) != END) { - switch (tag) { - case EasTags.SYNC_SERVER_ID: // same as EasTags.EMAIL_BODY_SIZE - if (!inData) { - msg.mServerId = getValue(); - } else { - size = Integer.parseInt(getValue()); - } - break; - case EasTags.SYNC_APPLICATION_DATA: - inData = true; - break; - case EasTags.EMAIL_ATTACHMENTS: - break; - case EasTags.EMAIL_ATTACHMENT: - attachmentParser(atts, msg); - break; - case EasTags.EMAIL_TO: - to = getValue(); - break; - case EasTags.EMAIL_FROM: - from = getValue(); - String sender = from; - int q = from.indexOf('\"'); - if (q >= 0) { - int qq = from.indexOf('\"', q + 1); - if (qq > 0) { - sender = from.substring(q + 1, qq); - } - } - msg.mDisplayName = sender; - break; - case EasTags.EMAIL_CC: - cc = getValue(); - break; - case EasTags.EMAIL_REPLY_TO: - replyTo = getValue(); - break; - case EasTags.EMAIL_DATE_RECEIVED: - String date = getValue(); - // 2009-02-11T18:03:03.627Z - GregorianCalendar cal = new GregorianCalendar(); - cal.set(Integer.parseInt(date.substring(0, 4)), - Integer.parseInt(date.substring(5, 7)) - 1, - Integer.parseInt(date.substring(8, 10)), - Integer.parseInt(date.substring(11, 13)), - Integer.parseInt(date.substring(14, 16)), - Integer.parseInt(date.substring(17, 19))); - cal.setTimeZone(TimeZone.getTimeZone("GMT")); - msg.mTimeStamp = cal.getTimeInMillis(); - break; - case EasTags.EMAIL_DISPLAY_TO: - break; - case EasTags.EMAIL_SUBJECT: - msg.mSubject = getValue(); - break; - case EasTags.EMAIL_IMPORTANCE: - break; - case EasTags.EMAIL_READ: - msg.mFlagRead = getValueInt() == 1; - break; - case EasTags.EMAIL_BODY: - msg.mTextInfo = "X;X;8;" + size; // location;encoding;charset;size - msg.mText = getValue(); - // For now... - msg.mPreview = "Fake preview"; //Messages.previewFromText(body); - break; - case EasTags.EMAIL_MESSAGE_CLASS: - break; - default: - skipTag(); - } - } - - // Tell the provider that this is synced back - msg.mServerVersion = mMailbox.mSyncKey; - - msg.mTo = to; - msg.mFrom = from; - msg.mCc = cc; - msg.mReplyTo = replyTo; - if (atts.size() > 0) { - msg.mAttachments = atts; - } - emails.add(msg); - } - - public void attachmentParser(ArrayList atts, Message msg) - throws IOException { - String fileName = null; - String length = null; - String lvl = null; - - while (nextTag(EasTags.EMAIL_ATTACHMENT) != END) { - switch (tag) { - case EasTags.EMAIL_DISPLAY_NAME: - fileName = getValue(); - break; - case EasTags.EMAIL_ATT_NAME: - lvl = getValue(); - break; - case EasTags.EMAIL_ATT_SIZE: - length = getValue(); - break; - default: - skipTag(); - } - } - - if (fileName != null && length != null && lvl != null) { - Attachment att = new Attachment(); - att.mEncoding = "base64"; - att.mSize = Long.parseLong(length); - att.mFileName = fileName; - atts.add(att); - msg.mFlagAttachment = true; - } - } - - public void deleteParser(ArrayList deletes) throws IOException { - while (nextTag(EasTags.SYNC_DELETE) != END) { - switch (tag) { - case EasTags.SYNC_SERVER_ID: - String serverId = getValue(); - Cursor c = mContentResolver.query(Message.CONTENT_URI, - Message.ID_COLUMN_PROJECTION, - SyncColumns.SERVER_ID + "=" + serverId, null, null); - try { - if (c.moveToFirst()) { - mService.log("Deleting " + serverId); - deletes.add(c.getLong(Message.ID_COLUMNS_ID_COLUMN)); - } - } finally { - c.close(); - } - break; - default: - skipTag(); - } - } - } - - public void changeParser(ArrayList changes) throws IOException { - String serverId = null; - boolean oldRead = false; - boolean read = true; - long id = 0; - while (nextTag(EasTags.SYNC_CHANGE) != END) { - switch (tag) { - case EasTags.SYNC_SERVER_ID: - serverId = getValue(); - bindArgument[0] = serverId; - Cursor c = mContentResolver.query(Message.CONTENT_URI, - Message.LIST_PROJECTION, - SyncColumns.SERVER_ID + "=?", bindArgument, null); - try { - if (c.moveToFirst()) { - mService.log("Changing " + serverId); - oldRead = c.getInt(Message.LIST_READ_COLUMN) == Message.READ; - id = c.getLong(Message.LIST_ID_COLUMN); - } - } finally { - c.close(); - } - break; - case EasTags.EMAIL_READ: - read = getValueInt() == 1; - break; - case EasTags.SYNC_APPLICATION_DATA: - break; - default: - skipTag(); - } - } - if (oldRead != read) { - changes.add(id); - } - } - - public void commandsParser() throws IOException { - ArrayList newEmails = new ArrayList(); - ArrayList deletedEmails = new ArrayList(); - ArrayList changedEmails = new ArrayList(); - - while (nextTag(EasTags.SYNC_COMMANDS) != END) { - if (tag == EasTags.SYNC_ADD) { - addParser(newEmails); - } else if (tag == EasTags.SYNC_DELETE) { - deleteParser(deletedEmails); - } else if (tag == EasTags.SYNC_CHANGE) { - changeParser(changedEmails); - } else - skipTag(); - } - - // Use a batch operation to handle the changes - // TODO Notifications - // TODO Store message bodies - ArrayList ops = new ArrayList(); - for (Message content: newEmails) { - content.addSaveOps(ops); - } - for (Long id: deletedEmails) { - ops.add(ContentProviderOperation - .newDelete(ContentUris.withAppendedId(Message.CONTENT_URI, id)).build()); - } - if (!changedEmails.isEmpty()) { - ContentValues cv = new ContentValues(); - // TODO Handle proper priority - // Set this as the correct state (assuming server wins) - cv.put(SyncColumns.DIRTY_COUNT, 0); - cv.put(MessageColumns.FLAG_READ, true); - for (Long id: changedEmails) { - // For now, don't handle read->unread - ops.add(ContentProviderOperation.newUpdate(ContentUris - .withAppendedId(Message.CONTENT_URI, id)).withValues(cv).build()); - } - } - ops.add(ContentProviderOperation.newUpdate(ContentUris - .withAppendedId(Mailbox.CONTENT_URI, mMailbox.mId)) - .withValues(mMailbox.toContentValues()).build()); - - try { - ContentProviderResult[] results = mService.mContext.getContentResolver() - .applyBatch(EmailProvider.EMAIL_AUTHORITY, ops); - for (ContentProviderResult result: results) { - if (result.uri == null) { - Log.v(TAG, "Null result in ContentProviderResult!"); - } - } - } 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 - } - - Log.v(TAG, "Mailbox EOS syncKey now: " + mMailbox.mSyncKey); - } -} diff --git a/src/com/android/exchange/EodException.java b/src/com/android/exchange/EasException.java similarity index 79% rename from src/com/android/exchange/EodException.java rename to src/com/android/exchange/EasException.java index e1b490570..e7613d7b6 100644 --- a/src/com/android/exchange/EodException.java +++ b/src/com/android/exchange/EasException.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2008-2009 Marc Blank + * Copyright (C) 2008-2009 Marc Blank * Licensed to The Android Open Source Project. * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,8 +17,6 @@ package com.android.exchange; -import java.io.IOException; - -public class EodException extends IOException { - private static final long serialVersionUID = 1L; +public class EasException extends Exception { + private static final long serialVersionUID = 5894556952470989968L; } diff --git a/src/com/android/exchange/EasFolderSyncParser.java b/src/com/android/exchange/EasFolderSyncParser.java deleted file mode 100644 index 9a3c14adf..000000000 --- a/src/com/android/exchange/EasFolderSyncParser.java +++ /dev/null @@ -1,216 +0,0 @@ -/* - * Copyright (C) 2008-2009 Marc Blank - * Licensed to 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 java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -import com.android.email.provider.EmailProvider; -import com.android.exchange.EmailContent.Account; -import com.android.exchange.EmailContent.Mailbox; - -import android.content.ContentProviderOperation; -import android.content.ContentProviderResult; -import android.content.ContentUris; -//import android.content.Context; -import android.content.OperationApplicationException; -import android.os.RemoteException; -import android.util.Log; - -public class EasFolderSyncParser extends EasParser { - - public static final String TAG = "FolderSyncParser"; - - public static final int USER_FOLDER_TYPE = 1; - public static final int INBOX_TYPE = 2; - public static final int DRAFTS_TYPE = 3; - public static final int DELETED_TYPE = 4; - public static final int SENT_TYPE = 5; - public static final int OUTBOX_TYPE = 6; - public static final int TASKS_TYPE = 7; - public static final int CALENDAR_TYPE = 8; - public static final int CONTACTS_TYPE = 9; - public static final int NOTES_TYPE = 10; - public static final int JOURNAL_TYPE = 11; - public static final int USER_MAILBOX_TYPE = 12; - - public static final List mMailFolderTypes = - Arrays.asList(INBOX_TYPE,DRAFTS_TYPE,DELETED_TYPE,SENT_TYPE,OUTBOX_TYPE,USER_MAILBOX_TYPE); - - private Account mAccount; - private EasService mService; - //private Context mContext; - private MockParserStream mMock = null; - - public EasFolderSyncParser(InputStream in, EasService service) throws IOException { - super(in); - mService = service; - mAccount = service.mAccount; - //mContext = service.mContext; - if (in instanceof MockParserStream) { - mMock = (MockParserStream)in; - } - } - - public void parse() throws IOException { - //captureOn(); - int status; - if (nextTag(START_DOCUMENT) != EasTags.FOLDER_FOLDER_SYNC) - throw new IOException(); - while (nextTag(START_DOCUMENT) != END_DOCUMENT) { - if (tag == EasTags.FOLDER_STATUS) { - status = getValueInt(); - if (status != 1) { - System.err.println("FolderSync failed: " + status); - } - } else if (tag == EasTags.FOLDER_SYNC_KEY) { - mAccount.mSyncKey = getValue(); - } else if (tag == EasTags.FOLDER_CHANGES) { - changesParser(); - } else - skipTag(); - } - //captureOff(mContext, "FolderSyncParser.txt"); - } - - public void addParser(ArrayList boxes) throws IOException { - String name = null; - String serverId = null; - String parentId = null; - int type = 0; - - while (nextTag(EasTags.FOLDER_ADD) != END) { - switch (tag) { - case EasTags.FOLDER_DISPLAY_NAME: { - name = getValue(); - break; - } - case EasTags.FOLDER_TYPE: { - type = getValueInt(); - break; - } - case EasTags.FOLDER_PARENT_ID: { - parentId = getValue(); - break; - } - case EasTags.FOLDER_SERVER_ID: { - serverId = getValue(); - break; - } - default: - skipTag(); - } - } - if (mMailFolderTypes.contains(type)) { - Mailbox m = new Mailbox(); - m.mDisplayName = name; - m.mServerId = serverId; - m.mAccountKey = mAccount.mId; - if (type == INBOX_TYPE) { - m.mSyncFrequency = Account.CHECK_INTERVAL_PUSH; - m.mType = Mailbox.TYPE_INBOX; - } else if (type == OUTBOX_TYPE) { - //m.mSyncFrequency = MailService.OUTBOX_FREQUENCY; - m.mSyncFrequency = Account.CHECK_INTERVAL_NEVER; - m.mType = Mailbox.TYPE_OUTBOX; - } else { - if (type == SENT_TYPE) { - m.mType = Mailbox.TYPE_SENT; - } else if (type == DRAFTS_TYPE) { - m.mType = Mailbox.TYPE_DRAFTS; - } else if (type == DELETED_TYPE) { - m.mType = Mailbox.TYPE_TRASH; - } - m.mSyncFrequency = Account.CHECK_INTERVAL_NEVER; - } - - if (!parentId.equals("0")) { - m.mParentServerId = parentId; - } - Log.v(TAG, "Adding mailbox: " + m.mDisplayName); - boxes.add(m); - } - - return; - } - - public void changesParser() throws IOException { - // Keep track of new boxes, deleted boxes, updated boxes - ArrayList newBoxes = new ArrayList(); - - while (nextTag(EasTags.FOLDER_CHANGES) != END) { - if (tag == EasTags.FOLDER_ADD) { - addParser(newBoxes); - } else if (tag == EasTags.FOLDER_COUNT) { - getValueInt(); - } else - skipTag(); - } - - for (Mailbox m: newBoxes) { - String parent = m.mParentServerId; - if (parent != null) { - // Wrong except first time! Need to check existing boxes! - //**PROVIDER - m.mFlagVisible = true; //false; - for (Mailbox mm: newBoxes) { - if (mm.mServerId.equals(parent)) { - //mm.parent = true; - } - } - } - } - - if (mMock != null) { - mMock.setResult(newBoxes); - return; - } - - if (!newBoxes.isEmpty()) { - ArrayList ops = new ArrayList(); - for (Mailbox content: newBoxes) { - ContentProviderOperation.Builder b = ContentProviderOperation - .newInsert(Mailbox.CONTENT_URI); - b.withValues(content.toContentValues()); - ops.add(b.build()); - } - ops.add(ContentProviderOperation.newUpdate(ContentUris - .withAppendedId(Account.CONTENT_URI, mAccount.mId)) - .withValues(mAccount.toContentValues()).build()); - - try { - ContentProviderResult[] results = mService.mContext.getContentResolver() - .applyBatch(EmailProvider.EMAIL_AUTHORITY, ops); - for (ContentProviderResult result: results) { - if (result.uri == null) { - return; - } - } - Log.v(TAG, "New syncKey: " + mAccount.mSyncKey); - } 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/src/com/android/exchange/EasParser.java b/src/com/android/exchange/EasParser.java deleted file mode 100644 index d09672979..000000000 --- a/src/com/android/exchange/EasParser.java +++ /dev/null @@ -1,297 +0,0 @@ -/* - * Copyright (C) 2008-2009 Marc Blank - * Licensed to 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 java.io.*; -import java.util.ArrayList; - -import android.content.Context; -import android.util.Log; - - -public abstract class EasParser { - - private static final String TAG = "EasParser"; - - public static final int START_DOCUMENT = 0; - public static final int DONE = 1; - public static final int START = 2; - public static final int END = 3; - public static final int TEXT = 4; - public static final int END_DOCUMENT = 3; - - private static final int NOT_FETCHED = Integer.MIN_VALUE; - private static final int NOT_ENDED = Integer.MIN_VALUE; - - private static final int EOF_BYTE = -1; - - private boolean debug = false; - private boolean capture = false; - private ArrayList captureArray; - private InputStream in; - private int depth; - private int nextId = NOT_FETCHED; - private String[] tagTable; - private String[][] tagTables = new String[24][]; - private String[] nameArray = new String[32]; - private int[] tagArray = new int[32]; - private boolean noContent; - - // Available to all to avoid method calls - public int endTag = NOT_ENDED; - public int type; - public int tag; - public String name; - public String text; - public int num; - - public void parse () throws IOException { - } - - public EasParser (InputStream in) throws IOException { - String[][] pages = EasTags.pages; - for (int i = 0; i < pages.length; i++) { - String[] page = pages[i]; - if (page.length > 0) { - setTagTable(i, page); - } - } - setInput(in); - } - - public void setDebug (boolean val) { - debug = val; - } - - public void captureOn () { - capture = true; - captureArray = new ArrayList(); - } - - public void captureOff (Context context, String file) { - try { - FileOutputStream out = context.openFileOutput(file, Context.MODE_WORLD_WRITEABLE); - out.write(captureArray.toString().getBytes()); - out.close(); - } catch (FileNotFoundException e) { - } catch (IOException e) { - } - } - - public String getValue () throws IOException { - getNext(false); - String val = text; - getNext(false); - if (type != END) { - throw new IOException("No END found!"); - } - endTag = tag; - return val; - } - - public int getValueInt () throws IOException { - getNext(true); - int val = num; - getNext(false); - if (type != END) { - throw new IOException("No END found!"); - } - endTag = tag; - return val; - } - - public int nextTag (int endTag) throws IOException { - while (getNext(false) != DONE) { - if (type == START) { - return tag; - } else if (type == END && tag == endTag) { - return END; - } - } - if (endTag == START_DOCUMENT) { - return END_DOCUMENT; - } - throw new EodException(); - } - - public void skipTag () throws IOException { - int thisTag = tag; - while (getNext(false) != DONE) { - if (type == END && tag == thisTag) { - return; - } - } - throw new EofException(); - } - - public int nextToken() throws IOException { - getNext(false); - return type; - } - - public void setInput(InputStream in) throws IOException { - this.in = in; - readByte(); // version - readInt(); // ? - readInt(); // 106 (UTF-8) - readInt(); // string table length - tagTable = tagTables[0]; - } - - public int next () throws IOException { - getNext(false); - return type; - } - - private final int getNext(boolean asInt) throws IOException { - if (type == END) { - depth--; - } else { - endTag = NOT_ENDED; - } - - if (noContent) { - type = END; - noContent = false; - return type; - } - - text = null; - name = null; - - int id = nextId (); - while (id == Wbxml.SWITCH_PAGE) { - nextId = NOT_FETCHED; - tagTable = tagTables[(readByte())]; - id = nextId(); - } - nextId = NOT_FETCHED; - - switch (id) { - case -1 : - type = DONE; - break; - - case Wbxml.END : - type = END; - if (debug) { - name = nameArray[depth]; - Log.v(TAG, "'); - } - tag = endTag = tagArray[depth]; - break; - - case Wbxml.STR_I : - type = TEXT; - if (asInt) { - num = readInlineInt(); - } else { - text = readInlineString(); - } - if (debug) { - Log.v(TAG, asInt ? Integer.toString(num) : text); - } - break; - - default : - type = START; - tag = id & 0x3F; - noContent = (id & 0x40) == 0; - depth++; - if (debug) { - name = tagTable[tag - 5]; - Log.v(TAG, '<' + name + '>'); - nameArray[depth] = name; - } - tagArray[depth] = tag; - } - - return type; - } - - private int read () throws IOException { - int i = in.read(); - if (capture) { - captureArray.add(i); - } - return i; - } - - private int nextId () throws IOException { - if (nextId == NOT_FETCHED) { - nextId = read(); - } - return nextId; - } - - private int readByte() throws IOException { - int i = read(); - if (i == EOF_BYTE) { - throw new EofException(); - } - return i; - } - - private int readInlineInt() throws IOException { - int result = 0; - - while (true) { - int i = readByte(); - if (i == 0) { - return result; - } - if (i >= '0' && i <= '9') { - result = (result * 10) + (i - '0'); - } else { - throw new IOException("Non integer"); - } - } - } - - private int readInt() throws IOException { - int result = 0; - int i; - - do { - i = readByte(); - result = (result << 7) | (i & 0x7f); - } while ((i & 0x80) != 0); - - return result; - } - - private String readInlineString() throws IOException { - StringBuilder sb = new StringBuilder(4096); - - while (true){ - int i = read(); - if (i == 0) { - break; - } else if (i == EOF_BYTE) { - throw new EofException(); - } - sb.append((char)i); - } - String res = sb.toString(); - return res; - } - - public void setTagTable(int page, String[] table) { - tagTables[page] = table; - } -} \ No newline at end of file diff --git a/src/com/android/exchange/EasPingService.java b/src/com/android/exchange/EasPingService.java deleted file mode 100644 index b8ad048d8..000000000 --- a/src/com/android/exchange/EasPingService.java +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright (C) 2008-2009 Marc Blank - * Licensed to 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 java.io.IOException; -import java.io.InputStream; -import java.net.HttpURLConnection; - -import com.android.exchange.EmailContent.Mailbox; - -import android.content.Context; - -public class EasPingService extends EasService { - - EasService mCaller; - HttpURLConnection mConnection = null; - - public EasPingService(Context _context, Mailbox _mailbox, EasService _caller) { - super(_context, _mailbox); - mCaller = _caller; - mHostAddress = _caller.mHostAddress; - mUserName = _caller.mUserName; - mPassword = _caller.mPassword; - } - - class EASPingParser extends EasParser { - protected boolean mMoreAvailable = false; - - public EASPingParser(InputStream in, EasService service) throws IOException { - super(in); - mMailbox = service.mMailbox; - setDebug(true); - } - - public void parse() throws IOException { - int status; - if (nextTag(START_DOCUMENT) != EasTags.PING_PING) { - throw new IOException(); - } - while (nextTag(START_DOCUMENT) != END_DOCUMENT) { - if (tag == EasTags.PING_STATUS) { - status = getValueInt(); - log("Ping completed, status = " + status); - if (status == 1 || status == 2) { - } - mCaller.ping(); - } else { - skipTag(); - } - } - } - } - - public void stop () { - mConnection.disconnect(); - } - - public void run () { - try { - EASSerializer s = new EASSerializer(); - s.start("Ping").data("HeartbeatInterval", "900").start("PingFolders") - .start("PingFolder").data("PingId", mMailbox.mServerId).data("PingClass", "Email") - .end("PingFolder").end("PingFolders").end("Ping").end(); - String data = s.toString(); - HttpURLConnection uc = sendEASPostCommand("Ping", data); - mConnection = uc; - log("Sending ping, read timeout: " + uc.getReadTimeout() / 1000 + "s"); - int code = uc.getResponseCode(); - log("Response code: " + code); - if (code == HttpURLConnection.HTTP_OK) { - String encoding = uc.getHeaderField("Transfer-Encoding"); - if (encoding == null) { - int len = uc.getHeaderFieldInt("Content-Length", 0); - if (len > 0) { - new EASPingParser(uc.getInputStream(), this).parse(); - } - } - } - } catch (IOException e1) { - e1.printStackTrace(); - } catch (RuntimeException e1) { - e1.printStackTrace(); - } - - mCaller.ping(); - log(Thread.currentThread().getName() + " thread completed..."); - } -} diff --git a/src/com/android/exchange/EasService.java b/src/com/android/exchange/EasService.java deleted file mode 100644 index 1204f1167..000000000 --- a/src/com/android/exchange/EasService.java +++ /dev/null @@ -1,800 +0,0 @@ -/* - * Copyright (C) 2008-2009 Marc Blank - * Licensed to 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 java.io.BufferedReader; -import java.io.BufferedWriter; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.FileReader; -import java.io.FileWriter; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStreamWriter; -import java.net.HttpURLConnection; -import java.net.MalformedURLException; -import java.net.ProtocolException; -import java.net.URI; -import java.net.URL; - -import java.util.Hashtable; - -import javax.net.ssl.HttpsURLConnection; - -import org.apache.http.HttpEntity; -import org.apache.http.HttpResponse; -import org.apache.http.client.methods.HttpPost; -import org.apache.http.conn.ssl.AllowAllHostnameVerifier; -import org.apache.http.impl.client.DefaultHttpClient; - -import com.android.email.Account; -import com.android.email.mail.AuthenticationFailedException; -import com.android.email.mail.MessagingException; -import com.android.exchange.EmailContent.AttachmentColumns; -import com.android.exchange.EmailContent.HostAuth; -import com.android.exchange.EmailContent.Mailbox; -import com.android.exchange.EmailContent.Message; - -import android.content.ContentValues; -import android.content.Context; -import android.database.Cursor; -import android.util.Log; - -public class EasService extends ProtocolService { - - public static final String TAG = "EasService"; - - private static final String WINDOW_SIZE = "10"; - - // From EAS spec - // Mail Cal - // 0 No filter Yes Yes - // 1 1 day ago Yes No - // 2 3 days ago Yes No - // 3 1 week ago Yes No - // 4 2 weeks ago Yes Yes - // 5 1 month ago Yes Yes - // 6 3 months ago No Yes - // 7 6 months ago No Yes - - private static final String FILTER_ALL = "0"; - private static final String FILTER_1_DAY = "1"; - private static final String FILTER_3_DAYS = "2"; - private static final String FILTER_1_WEEK = "3"; - private static final String FILTER_2_WEEKS = "4"; - private static final String FILTER_1_MONTH = "5"; - //private static final String FILTER_3_MONTHS = "6"; - //private static final String FILTER_6_MONTHS = "7"; - - private static final String BODY_PREFERENCE_TEXT = "1"; - //private static final String BODY_PREFERENCE_HTML = "2"; - - // Reasonable to be static for now - static String mProtocolVersion = "12.0"; //"2.5"; - static String mDeviceId = null; - static String mDeviceType = "Android"; - - String mAuthString = null; - String mCmdString = null; - String mVersions; - String mHostAddress; - String mUserName; - String mPassword; - boolean mSentCommands; - boolean mIsIdle = false; - Context mContext; - InputStream mPendingPartInputStream = null; - - private boolean mStop = false; - private Object mWaitTarget = new Object(); - - public EasService (Context _context, Mailbox _mailbox) { - // A comment - super(_context, _mailbox); - mContext = _context; - } - - private EasService (String prefix) { - super(prefix); - } - - public EasService () { - this("EAS Validation"); - } - - @Override - public void ping() { - // TODO Auto-generated method stub - log("We've been pinged!"); - synchronized (mWaitTarget) { - mWaitTarget.notify(); - } - } - - @Override - public void stop() { - // TODO Auto-generated method stub - mStop = true; - } - - public int getSyncStatus () { - return 0; - } - - public void validateAccount (String hostAddress, String userName, String password, - int port, boolean ssl, Context context) throws MessagingException { - try { - log("Testing EAS: " + hostAddress + ", " + userName + ", ssl = " + ssl); - EASSerializer s = new EASSerializer(); - s.start("FolderSync").start("FolderSyncKey").text("0").end("FolderSyncKey") - .end("FolderSync").end(); - String data = s.toString(); - EasService svc = new EasService("%TestAccount%"); - svc.mHostAddress = hostAddress; - svc.mUserName = userName; - svc.mPassword = password; - HttpURLConnection uc = svc.sendEASPostCommand("FolderSync", data); - int code = uc.getResponseCode(); - Log.v(TAG, "Validation response code: " + code); - if (code == HttpURLConnection.HTTP_OK) { - return; - } - if (code == 401 || code == 403) { - Log.v(TAG, "Authentication failed"); - throw new AuthenticationFailedException("Validation failed"); - } - else { - //TODO Need to catch other kinds of errors (e.g. policy related) - Log.v(TAG, "Validation failed, reporting I/O error"); - throw new MessagingException(MessagingException.IOERROR); - } - } catch (IOException e) { - Log.v(TAG, "IOException caught, reporting I/O error: " + e.getMessage()); - throw new MessagingException(MessagingException.IOERROR); - } - - } - - protected HttpURLConnection sendEASPostCommand (String cmd, String data) throws IOException { - HttpURLConnection uc = setupEASCommand("POST", cmd); - if (uc != null) { - uc.setRequestProperty("Content-Length", Integer.toString(data.length() + 2)); - OutputStreamWriter w = new OutputStreamWriter(uc.getOutputStream(), "UTF-8"); - w.write(data); - w.write("\r\n"); - w.flush(); - w.close(); - } - return uc; - } - - static private final int CHUNK_SIZE = 16*1024; - - protected void getAttachment (PartRequest req) throws IOException { - DefaultHttpClient client = new DefaultHttpClient(); - String us = makeUriString("GetAttachment", "&AttachmentName=" + req.att.mLocation); - HttpPost method = new HttpPost(URI.create(us)); - method.setHeader("Authorization", mAuthString); - - HttpResponse res = client.execute(method); - int status = res.getStatusLine().getStatusCode(); - if (status == HttpURLConnection.HTTP_OK) { - HttpEntity e = res.getEntity(); - int len = (int)e.getContentLength(); - String type = e.getContentType().getValue(); - Log.v(TAG, "Attachment code: " + status + ", Length: " + len + ", Type: " + type); - InputStream is = res.getEntity().getContent(); - File f = null; //Attachment.openAttachmentFile(req); - if (f != null) { - FileOutputStream os = new FileOutputStream(f); - if (len > 0) { - try { - mPendingPartRequest = req; - mPendingPartInputStream = is; - byte[] bytes = new byte[CHUNK_SIZE]; - int length = len; - while (len > 0) { - int n = (len > CHUNK_SIZE ? CHUNK_SIZE : len); - int read = is.read(bytes, 0, n); - os.write(bytes, 0, read); - len -= read; - if (req.handler != null) { - long pct = ((length - len) * 100 / length); - req.handler.sendEmptyMessage((int)pct); - } - } - } finally { - mPendingPartRequest = null; - mPendingPartInputStream = null; - } - } - os.flush(); - os.close(); - - ContentValues cv = new ContentValues(); - cv.put(AttachmentColumns.CONTENT_URI, f.getAbsolutePath()); - cv.put(AttachmentColumns.MIME_TYPE, type); - req.att.update(mContext, cv); - // TODO Inform UI that we're done - } - } - } - - private HttpURLConnection setupEASCommand (String method, String cmd) { - return setupEASCommand(method, cmd, null); - } - - private String makeUriString (String cmd, String extra) { - if (mAuthString == null) { - String cs = mUserName + ':' + mPassword; - mAuthString = "Basic " + Base64.encodeBytes(cs.getBytes()); - mCmdString = "&User=" + mUserName + "&DeviceId=" + mDeviceId - + "&DeviceType=" + mDeviceType; - } - - boolean ssl = true; - - // TODO Remove after testing - if (mHostAddress.equalsIgnoreCase("owa.electricmail.com")) { - ssl = false; - } - - String scheme = ssl ? "https" : "http"; - String us = scheme + "://" + mHostAddress + "/Microsoft-Server-ActiveSync"; - if (cmd != null) { - us += "?Cmd=" + cmd + mCmdString; - } - if (extra != null) { - us += extra; - } - return us; - } - - private HttpURLConnection setupEASCommand (String method, String cmd, String extra) { - - // Hack for now - boolean ssl = true; - - // TODO Remove this when no longer needed - if (mHostAddress.equalsIgnoreCase("owa.electricmail.com")) { - ssl = false; - } - - try { - String us = makeUriString(cmd, extra); - URL u = new URL(us); - HttpURLConnection uc = (HttpURLConnection)u.openConnection(); - try { - HttpURLConnection.setFollowRedirects(true); - } catch (Exception e) { - } - - if (ssl) { - ((HttpsURLConnection)uc).setHostnameVerifier(new AllowAllHostnameVerifier()); - } - - uc.setConnectTimeout(10*SECS); - uc.setReadTimeout(20*MINS); - if (method.equals("POST")) { - uc.setDoOutput(true); - } - uc.setRequestMethod(method); - uc.setRequestProperty("Authorization", mAuthString); - - if (extra == null) { - if (cmd != null && cmd.startsWith("SendMail&")) { - uc.setRequestProperty("Content-Type", "message/rfc822"); - } else { - uc.setRequestProperty("Content-Type", "application/vnd.ms-sync.wbxml"); - } - uc.setRequestProperty("MS-ASProtocolVersion", mProtocolVersion); - uc.setRequestProperty("Connection", "keep-alive"); - uc.setRequestProperty("User-Agent", mDeviceType + "/0.3"); - } else { - uc.setRequestProperty("Content-Length", "0"); - } - - return uc; - } catch (MalformedURLException e) { - e.printStackTrace(); - } catch (ProtocolException e) { - e.printStackTrace(); - } catch (IOException e) { - e.printStackTrace(); - } - return null; - } - - static class EASSerializer extends WbxmlSerializer { - ByteArrayOutputStream byteStream = new ByteArrayOutputStream(); - static Hashtable tagTable = null; - - EASSerializer () { - super(); - try { - setOutput(byteStream, null); - if (tagTable == null) { - String[][] pages = EasTags.pages; - for (int i = 0; i < pages.length; i++) { - String[] page = pages[i]; - if (page.length > 0) - setTagTable(i, page); - } - tagTable = getTagTable(); - } else { - setTagTable(tagTable); - } - startDocument("UTF-8", false); - } catch (IOException e) { - e.printStackTrace(); - } - } - - EASSerializer start (String tag) throws IOException { - startTag(null, tag); - return this; - } - - EASSerializer end (String tag) throws IOException { - endTag(null, tag); - return this; - } - - EASSerializer end () throws IOException { - endDocument(); - return this; - } - - EASSerializer data (String tag, String value) throws IOException { - startTag(null, tag); - text(value); - endTag(null, tag); - return this; - } - - EASSerializer tag (String tag) throws IOException { - startTag(null, tag); - endTag(null, tag); - return this; - } - - public EASSerializer text (String str) throws IOException { - super.text(str); - return this; - } - - ByteArrayOutputStream getByteStream () { - return byteStream; - } - - public String toString () { - return byteStream.toString(); - } - } - - public void runMain () { - try { - if (mAccount.mSyncKey == null) { - mAccount.mSyncKey = "0"; - Log.w(TAG, "Account syncKey RESET"); - mAccount.saveOrUpdate(mContext); - } - Log.v(TAG, "Account syncKey: " + mAccount.mSyncKey); - HttpURLConnection uc = setupEASCommand("OPTIONS", null); - if (uc != null) { - int code = uc.getResponseCode(); - Log.v(TAG, "OPTIONS response: " + code); - if (code == HttpURLConnection.HTTP_OK) { - mVersions = uc.getHeaderField("ms-asprotocolversions"); - if (mVersions != null) { - // Determine which version we want to use.. - //List versions = new Chain(mVersions, ',').toList(); - //if (versions.contains("12.0")) { - // mProtocolVersion = "12.0"; - //} else if (versions.contains("2.5")) - mProtocolVersion = "2.5"; - Log.v(TAG, mVersions); - } - else { - String s = readResponseString(uc); - Log.e(TAG, "No EAS versions: " + s); - } - - while (!mStop) { - EASSerializer s = new EASSerializer(); - s.start("FolderSync").start("FolderSyncKey").text(mAccount.mSyncKey) - .end("FolderSyncKey").end("FolderSync").end(); - String data = s.toString(); - uc = sendEASPostCommand("FolderSync", data); - code = uc.getResponseCode(); - Log.v(TAG, "FolderSync response code: " + code); - if (code == HttpURLConnection.HTTP_OK) { - String encoding = uc.getHeaderField("Transfer-Encoding"); - if (encoding == null) { - int len = uc.getHeaderFieldInt("Content-Length", 0); - if (len > 0) { - try { - new EasFolderSyncParser(uc.getInputStream(), this).parse(); - } catch (IOException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - } - } else if (encoding.equalsIgnoreCase("chunked")) { - // TODO We don't handle this yet - } - } - - // For now, we'll just loop - try { - Thread.sleep(15*MINS); - } catch (InterruptedException e) { - } - } - } - } - - } catch (MalformedURLException e) { - e.printStackTrace(); - } catch (IOException e) { - e.printStackTrace(); - } - } - - long handleLocalDeletes (EASSerializer s) throws IOException { - long maxDeleteId = -1; -// //**PROVIDER -// Cursor c = Email.getLocalDeletedCursor(mDatabase, mMailboxId); -// try { -// if (c.moveToFirst()) { -// s.start("Commands"); -// mSentCommands = true; -// do { -// String serverId = c.getString(Email.MPN_UID_COLUMN); -// s.start("Delete").data("ServerId", serverId).end("Delete"); -// mLogger.log("Sending delete of " + serverId); -// long id = c.getLong(Email.MPN_ID_COLUMN); -// if (id > maxDeleteId) -// maxDeleteId = id; -// } while (c.moveToNext()); -// } -// } finally { -// c.close(); -// } - return maxDeleteId; - } - - void handleLocalMoves () throws IOException { -// long maxMoveId = -1; -// -// Cursor c = LocalChange.getCursorWhere(mDatabase, "mailbox=\"" + mMailbox.mServerId + "\" and type=" + LocalChange.MOVE_TYPE); -// try { -// if (c.moveToFirst()) { -// EASSerializer s = new EASSerializer(); -// s.start("MoveItems"); -// -// do { -// s.start("Move").data("SrcMsgId", c.getString(LocalChange.EMAIL_ID_COLUMN)).data("SrcFldId", c.getString(LocalChange.MAILBOX_COLUMN)).data("DstFldId", c.getString(LocalChange.VALUE_COLUMN)).end("Move"); -// } while (c.moveToNext()); -// -// s.end("MoveItems").end(); -// HttpURLConnection uc = sendEASPostCommand("MoveItems", s.toString()); -// int code = uc.getResponseCode(); -// System.err.println("Response code: " + code); -// if (code == HttpURLConnection.HTTP_OK) { -// ByteArrayInputStream is = readResponse(uc); -// if (is != null) { -// EASMoveParser p = new EASMoveParser(is, this); -// p.parse(); -// if (maxMoveId > -1) -// LocalChange.deleteWhere(mDatabase, "_id<=" + maxMoveId + " AND mailbox=" + mMailboxId + " AND type=" + LocalChange.MOVE_TYPE); -// } -// } else { -// // TODO What? -// } -// } -// } finally { -// c.close(); -// } - } - - long handleLocalReads (EASSerializer s) throws IOException { - Cursor c = mContext.getContentResolver().query(Message.CONTENT_URI, Message.LIST_PROJECTION, "mailboxKey=" + mMailboxId, null, null); - long maxReadId = -1; - try { - // if (c.moveToFirst()) { - // if (!mSentCommands) { - // s.start("Commands"); - // mSentCommands = true; - // } - // do { - // String serverId = c.getString(LocalChange.STRING_ARGS_COLUMN); - // if (serverId == null) { - // long id = c.getInt(LocalChange.EMAIL_ID_COLUMN); - // Email.Message msg = Messages.restoreFromId(mContext, id); - // serverId = msg.serverId; - // if (serverId == null) - // serverId = "0:0"; - // } - // - // String value = c.getString(LocalChange.VALUE_COLUMN); - // s.start("Change").data("ServerId", serverId).start("ApplicationData").data("Read", value).end("ApplicationData").end("Change"); - // mLogger.log("Sending read of " + serverId + " = " + value); - // long id = c.getLong(LocalChange.ID_COLUMN); - // if (id > maxReadId) - // maxReadId = id; - // } while (c.moveToNext()); - // } - } finally { - c.close(); - } - return maxReadId; - //return -1; - } - - ByteArrayInputStream readResponse (HttpURLConnection uc) throws IOException { - String encoding = uc.getHeaderField("Transfer-Encoding"); - if (encoding == null) { - int len = uc.getHeaderFieldInt("Content-Length", 0); - if (len > 0) { - InputStream in = uc.getInputStream(); - byte[] bytes = new byte[len]; - int remain = len; - int offs = 0; - while (remain > 0) { - int read = in.read(bytes, offs, remain); - remain -= read; - offs += read; - } - return new ByteArrayInputStream(bytes); - } - } else if (encoding.equalsIgnoreCase("chunked")) { - // TODO We don't handle this yet - return null; - } - return null; - } - - String readResponseString (HttpURLConnection uc) throws IOException { - String encoding = uc.getHeaderField("Transfer-Encoding"); - if (encoding == null) { - int len = uc.getHeaderFieldInt("Content-Length", 0); - if (len > 0) { - InputStream in = uc.getInputStream(); - byte[] bytes = new byte[len]; - int remain = len; - int offs = 0; - while (remain > 0) { - int read = in.read(bytes, offs, remain); - remain -= read; - offs += read; - } - return new String(bytes); - } - } else if (encoding.equalsIgnoreCase("chunked")) { - // TODO We don't handle this yet - return null; - } - return null; - } - - private String getSimulatedDeviceId () { - try { - File f = mContext.getFileStreamPath("deviceName"); - BufferedReader rdr = null; - String id; - if (f.exists()) { - rdr = new BufferedReader(new FileReader(f)); - id = rdr.readLine(); - rdr.close(); - return id; - } else if (f.createNewFile()) { - BufferedWriter w = new BufferedWriter(new FileWriter(f)); - id = "emu" + System.currentTimeMillis(); - w.write(id); - w.close(); - } - } catch (FileNotFoundException e) { - } catch (IOException e) { - } - return null; - } - - public void run() { - mThread = Thread.currentThread(); - mDeviceId = android.provider.Settings.System - .getString(mContext.getContentResolver(), android.provider.Settings.System.ANDROID_ID); - - HostAuth ha = HostAuth.restoreHostAuthWithId(mContext, mAccount.mHostAuthKeyRecv); - mHostAddress = ha.mAddress; - mUserName = ha.mLogin; - mPassword = ha.mPassword; - - if (mDeviceId == null) - mDeviceId = getSimulatedDeviceId(); - Log.v(TAG, "Device id: " + mDeviceId); - - if (mMailbox.mServerId.equals("_main")) { - runMain(); - return; - } - try { - while (!mStop) { - runAwake(); - waitForConnectivity(); - try { -// while (true) { -// PartRequest req = null; -// synchronized(mPartRequests) { -// if (mPartRequests.isEmpty()) { -// break; -// } -// req = mPartRequests.get(0); -// getAttachment(req); -// } -// -// synchronized(mPartRequests) { -// mPartRequests.remove(req); -// } -// } - - boolean moreAvailable = true; - while (!mStop && moreAvailable) { - EASSerializer s = new EASSerializer(); - if (mMailbox.mSyncKey == null) { - Log.w(TAG, "Mailbox syncKey RESET"); - mMailbox.mSyncKey = "0"; - } - Log.v(TAG, "Mailbox syncKey: " + mMailbox.mSyncKey); - s.start("Sync").start("Collections").start("Collection") - .data("Class", "Email") - .data("SyncKey", mMailbox.mSyncKey) - .data("CollectionId", mMailbox.mServerId); - - // Set the lookback appropriately (EAS calls it a "filter") - String filter = FILTER_1_WEEK; - switch (mAccount.mSyncLookback) { - case Account.SYNC_WINDOW_1_DAY: { - filter = FILTER_1_DAY; - break; - } - case Account.SYNC_WINDOW_3_DAYS: { - filter = FILTER_3_DAYS; - break; - } - case Account.SYNC_WINDOW_1_WEEK: { - filter = FILTER_1_WEEK; - break; - } - case Account.SYNC_WINDOW_2_WEEKS: { - filter = FILTER_2_WEEKS; - break; - } - case Account.SYNC_WINDOW_1_MONTH: { - filter = FILTER_1_MONTH; - break; - } - case Account.SYNC_WINDOW_ALL: { - filter = FILTER_ALL; - break; - } - } - - // For some crazy reason, GetChanges can't be used with a SyncKey of 0 - if (!mMailbox.mSyncKey.equals("0")) { - if (mProtocolVersion.equals("12.0")) - s.tag("DeletesAsMoves") - .tag("GetChanges") - .data("WindowSize", WINDOW_SIZE) - .start("Options") - .data("FilterType", filter) - .start("BodyPreference") - .data("BodyPreferenceType", BODY_PREFERENCE_TEXT) // Plain text to start - .data("BodyPreferenceTruncationSize", "50000") - .end("BodyPreference") - .end("Options"); - else - s.tag("DeletesAsMoves") - .tag("GetChanges") - .data("WindowSize", WINDOW_SIZE) - .start("Options") - .data("FilterType", filter) - .end("Options"); - } - - // Send our changes up to the server - mSentCommands = false; -// // Send local deletes to server -// long maxDeleteId = handleLocalDeletes(s); -// // Send local read changes -// long maxReadId = handleLocalReads(s); - if (mSentCommands) { - s.end("Commands"); - } - - s.end("Collection").end("Collections").end("Sync").end(); - HttpURLConnection uc = sendEASPostCommand("Sync", s.toString()); - int code = uc.getResponseCode(); - Log.v(TAG, "Sync response code: " + code); - if (code == HttpURLConnection.HTTP_OK) { - ByteArrayInputStream is = readResponse(uc); - if (is != null) { - EasEmailSyncParser p = new EasEmailSyncParser(is, this); - p.parse(); -// if (maxDeleteId > -1) -// Messages.deleteFromLocalDeletedWhere(mContext, "_id<=" + maxDeleteId); -// if (maxReadId > -1) -// LocalChange.deleteWhere(mDatabase, "_id<=" + maxReadId + " AND mailbox=" + mMailboxId + " AND type=" + LocalChange.READ_TYPE); - moreAvailable = p.mMoreAvailable; - } - } else { - // TODO What? - } - } - - // Handle local moves - handleLocalMoves(); - - if (mMailbox.mSyncFrequency != Account.CHECK_INTERVAL_PUSH) { - return; - } - - // Handle push here... - Thread pingThread = null; - EasPingService pingService = new EasPingService(mContext, mMailbox, this); - runAsleep(10*MINS); - synchronized (mWaitTarget) { - mIsIdle = true; - try { - log("Wait..."); - pingThread = new Thread(pingService); - pingThread.setName("Ping " + pingThread.getId()); - log("Starting thread " + pingThread.getName()); - pingThread.start(); - mWaitTarget.wait(14*MINS); - } catch (InterruptedException e) { - } finally { - runAwake(); - } - log("Wait terminated."); - if (pingThread != null && pingThread.isAlive()) { - // Make the ping service stop, one way or another - log("Stopping " + pingThread.getName()); - pingService.stop(); - pingThread.interrupt(); - } - mIsIdle = false; - } - } catch (IOException e) { - log("IOException: " + e.getMessage()); - //logException(e); - } - } - } catch (Exception e) { - log("Exception: " + e.getMessage()); - //logException(e); - } finally { - log("EAS sync finished."); - //MailService.done(this); - } - } - -} diff --git a/src/com/android/exchange/EasSyncService.java b/src/com/android/exchange/EasSyncService.java new file mode 100644 index 000000000..22a0d375d --- /dev/null +++ b/src/com/android/exchange/EasSyncService.java @@ -0,0 +1,769 @@ +/* + * Copyright (C) 2008-2009 Marc Blank + * Licensed to 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 java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStreamWriter; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.ProtocolException; +import java.net.URI; +import java.net.URL; +import java.net.URLEncoder; + +import java.util.ArrayList; + +import javax.net.ssl.HttpsURLConnection; + +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.conn.ssl.AllowAllHostnameVerifier; +import org.apache.http.impl.client.DefaultHttpClient; + +import com.android.email.mail.AuthenticationFailedException; +import com.android.email.mail.MessagingException; +import com.android.exchange.EmailContent.Account; +import com.android.exchange.EmailContent.Attachment; +import com.android.exchange.EmailContent.AttachmentColumns; +import com.android.exchange.EmailContent.HostAuth; +import com.android.exchange.EmailContent.Mailbox; +import com.android.exchange.EmailContent.MailboxColumns; +import com.android.exchange.adapter.EasContactsSyncAdapter; +import com.android.exchange.adapter.EasEmailSyncAdapter; +import com.android.exchange.adapter.EasFolderSyncParser; +import com.android.exchange.adapter.EasPingParser; +import com.android.exchange.adapter.EasSerializer; +import com.android.exchange.adapter.EasSyncAdapter; +import com.android.exchange.adapter.EasParser.EasParserException; +import com.android.exchange.utility.Base64; + +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.util.Log; + +public class EasSyncService extends InteractiveSyncService { + + private static final String WINDOW_SIZE = "10"; + private static final String WHERE_ACCOUNT_KEY_AND_SERVER_ID = + MailboxColumns.ACCOUNT_KEY + "=? and " + MailboxColumns.SERVER_ID + "=?"; + private static final String WHERE_SYNC_FREQUENCY_PING = + Mailbox.SYNC_FREQUENCY + '=' + Account.CHECK_INTERVAL_PING; + private static final String SYNC_FREQUENCY_PING = + MailboxColumns.SYNC_FREQUENCY + '=' + Account.CHECK_INTERVAL_PING; + + // Reasonable default + String mProtocolVersion = "2.5"; + static String mDeviceId = null; + static String mDeviceType = "Android"; + EasSyncAdapter mTarget; + String mAuthString = null; + String mCmdString = null; + String mVersions; + public String mHostAddress; + public String mUserName; + public String mPassword; + String mDomain = null; + boolean mSentCommands; + boolean mIsIdle = false; + boolean mSsl = true; + public Context mContext; + public ContentResolver mContentResolver; + String[] mBindArguments = new String[2]; + InputStream mPendingPartInputStream = null; + private boolean mStop = false; + private Object mWaitTarget = new Object(); + + public EasSyncService(Context _context, Mailbox _mailbox) { + super(_context, _mailbox); + mContext = _context; + mContentResolver = _context.getContentResolver(); + HostAuth ha = HostAuth.restoreHostAuthWithId(_context, mAccount.mHostAuthKeyRecv); + mSsl = (ha.mFlags & HostAuth.FLAG_SSL) != 0; + } + + private EasSyncService(String prefix) { + super(prefix); + } + + public EasSyncService() { + this("EAS Validation"); + } + + @Override + public void ping() { + userLog("We've been pinged!"); + synchronized (mWaitTarget) { + mWaitTarget.notify(); + } + } + + @Override + public void stop() { + mStop = true; + } + + public int getSyncStatus() { + return 0; + } + + /* (non-Javadoc) + * @see com.android.exchange.SyncService#validateAccount(java.lang.String, java.lang.String, java.lang.String, int, boolean, android.content.Context) + */ + public void validateAccount(String hostAddress, String userName, String password, int port, + boolean ssl, Context context) throws MessagingException { + try { + if (Eas.USER_DEBUG) { + userLog("Testing EAS: " + hostAddress + ", " + userName + ", ssl = " + ssl); + } + EasSerializer s = new EasSerializer(); + s.start("FolderSync").start("FolderSyncKey").text("0").end("FolderSyncKey") + .end("FolderSync").end(); + EasSyncService svc = new EasSyncService("%TestAccount%"); + svc.mHostAddress = hostAddress; + svc.mUserName = userName; + svc.mPassword = password; + svc.mSsl = ssl; + HttpURLConnection uc = svc.sendEASPostCommand("FolderSync", s.toString()); + int code = uc.getResponseCode(); + userLog("Validation response code: " + code); + if (code == HttpURLConnection.HTTP_OK) { + // No exception means successful validation + userLog("Validation successful"); + return; + } + if (code == HttpURLConnection.HTTP_UNAUTHORIZED || + code == HttpURLConnection.HTTP_FORBIDDEN) { + userLog("Authentication failed"); + throw new AuthenticationFailedException("Validation failed"); + } else { + // TODO Need to catch other kinds of errors (e.g. policy) For now, report the code. + userLog("Validation failed, reporting I/O error: " + code); + throw new MessagingException(MessagingException.IOERROR); + } + } catch (IOException e) { + userLog("IOException caught, reporting I/O error: " + e.getMessage()); + throw new MessagingException(MessagingException.IOERROR); + } + + } + + + @Override + public void loadAttachment(Attachment att, ISyncManagerCallback cb) { + // TODO Auto-generated method stub + } + + @Override + public void reloadFolderList() { + // TODO Auto-generated method stub + } + + @Override + public void startSync() { + // TODO Auto-generated method stub + } + + @Override + public void stopSync() { + // TODO Auto-generated method stub + } + + protected HttpURLConnection sendEASPostCommand(String cmd, String data) throws IOException { + HttpURLConnection uc = setupEASCommand("POST", cmd); + if (uc != null) { + uc.setRequestProperty("Content-Length", Integer.toString(data.length() + 2)); + OutputStreamWriter w = new OutputStreamWriter(uc.getOutputStream(), "UTF-8"); + w.write(data); + w.write("\r\n"); + w.flush(); + w.close(); + } + return uc; + } + + static private final int CHUNK_SIZE = 16 * 1024; + + protected void getAttachment(PartRequest req) throws IOException { + DefaultHttpClient client = new DefaultHttpClient(); + String us = makeUriString("GetAttachment", "&AttachmentName=" + req.att.mLocation); + HttpPost method = new HttpPost(URI.create(us)); + method.setHeader("Authorization", mAuthString); + + HttpResponse res = client.execute(method); + int status = res.getStatusLine().getStatusCode(); + if (status == HttpURLConnection.HTTP_OK) { + HttpEntity e = res.getEntity(); + int len = (int)e.getContentLength(); + String type = e.getContentType().getValue(); + if (Eas.TEST_DEBUG) { + Log.v(TAG, "Attachment code: " + status + ", Length: " + len + ", Type: " + type); + } + InputStream is = res.getEntity().getContent(); + // TODO Use the request data, when it's defined. For now, stubbed out + File f = null; // Attachment.openAttachmentFile(req); + if (f != null) { + FileOutputStream os = new FileOutputStream(f); + if (len > 0) { + try { + mPendingPartRequest = req; + mPendingPartInputStream = is; + byte[] bytes = new byte[CHUNK_SIZE]; + int length = len; + while (len > 0) { + int n = (len > CHUNK_SIZE ? CHUNK_SIZE : len); + int read = is.read(bytes, 0, n); + os.write(bytes, 0, read); + len -= read; + if (req.handler != null) { + long pct = ((length - len) * 100 / length); + req.handler.sendEmptyMessage((int)pct); + } + } + } finally { + mPendingPartRequest = null; + mPendingPartInputStream = null; + } + } + os.flush(); + os.close(); + + ContentValues cv = new ContentValues(); + cv.put(AttachmentColumns.CONTENT_URI, f.getAbsolutePath()); + cv.put(AttachmentColumns.MIME_TYPE, type); + req.att.update(mContext, cv); + // TODO Inform UI that we're done + } + } + } + + private HttpURLConnection setupEASCommand(String method, String cmd) throws IOException { + return setupEASCommand(method, cmd, null); + } + + private String makeUriString(String cmd, String extra) { + // Cache the authentication string and the command string + if (mDeviceId == null) + mDeviceId = "droidfu"; + String safeUserName = URLEncoder.encode(mUserName); + if (mAuthString == null) { + String cs = mUserName + ':' + mPassword; + mAuthString = "Basic " + Base64.encodeBytes(cs.getBytes()); + mCmdString = "&User=" + safeUserName + "&DeviceId=" + mDeviceId + "&DeviceType=" + + mDeviceType; + } + + String us = (mSsl ? "https" : "http") + "://" + mHostAddress + + "/Microsoft-Server-ActiveSync"; + if (cmd != null) { + us += "?Cmd=" + cmd + mCmdString; + } + if (extra != null) { + us += extra; + } + return us; + } + + private HttpURLConnection setupEASCommand(String method, String cmd, String extra) + throws IOException { + try { + String us = makeUriString(cmd, extra); + URL u = new URL(us); + HttpURLConnection uc = (HttpURLConnection)u.openConnection(); + HttpURLConnection.setFollowRedirects(true); + + if (mSsl) { + ((HttpsURLConnection)uc).setHostnameVerifier(new AllowAllHostnameVerifier()); + } + + uc.setConnectTimeout(10 * SECS); + uc.setReadTimeout(20 * MINS); + if (method.equals("POST")) { + uc.setDoOutput(true); + } + uc.setRequestMethod(method); + uc.setRequestProperty("Authorization", mAuthString); + + if (extra == null) { + if (cmd != null && cmd.startsWith("SendMail&")) { + uc.setRequestProperty("Content-Type", "message/rfc822"); + } else { + uc.setRequestProperty("Content-Type", "application/vnd.ms-sync.wbxml"); + } + uc.setRequestProperty("MS-ASProtocolVersion", mProtocolVersion); + uc.setRequestProperty("Connection", "keep-alive"); + uc.setRequestProperty("User-Agent", mDeviceType + '/' + Eas.VERSION); + } else { + uc.setRequestProperty("Content-Length", "0"); + } + + return uc; + } catch (MalformedURLException e) { + // TODO See if there is a better exception to throw here and below + throw new IOException(); + } catch (ProtocolException e) { + throw new IOException(); + } + } + + String getTargetCollectionClassFromCursor(Cursor c) { + int type = c.getInt(Mailbox.CONTENT_TYPE_COLUMN); + if (type == Mailbox.TYPE_CONTACTS) { + return "Contacts"; + } else if (type == Mailbox.TYPE_CALENDAR) { + return "Calendar"; + } else { + return "Email"; + } + } + + /** + * Performs FolderSync + * + * @throws IOException + * @throws EasParserException + */ + public void runMain() throws IOException, EasParserException { + try { + if (mAccount.mSyncKey == null) { + mAccount.mSyncKey = "0"; + userLog("Account syncKey RESET"); + mAccount.saveOrUpdate(mContext); + } + + // When we first start up, change all ping mailboxes to push. + ContentValues cv = new ContentValues(); + cv.put(Mailbox.SYNC_FREQUENCY, Account.CHECK_INTERVAL_PUSH); + if (mContentResolver.update(Mailbox.CONTENT_URI, cv, + WHERE_SYNC_FREQUENCY_PING, null) > 0) { + SyncManager.kick(); + } + + userLog("Account syncKey: " + mAccount.mSyncKey); + HttpURLConnection uc = setupEASCommand("OPTIONS", null); + if (uc != null) { + int code = uc.getResponseCode(); + userLog("OPTIONS response: " + code); + if (code == HttpURLConnection.HTTP_OK) { + mVersions = uc.getHeaderField("ms-asprotocolversions"); + if (mVersions != null) { + if (mVersions.contains("12.0")) { + mProtocolVersion = "12.0"; + } + // TODO We only do 2.5 at the moment; add 'else' above when fixed + mProtocolVersion = "2.5"; + userLog(mVersions); + } else { + throw new IOException(); + } + + while (!mStop) { + EasSerializer s = new EasSerializer(); + s.start("FolderSync").start("FolderSyncKey").text(mAccount.mSyncKey).end( + "FolderSyncKey").end("FolderSync").end(); + uc = sendEASPostCommand("FolderSync", s.toString()); + code = uc.getResponseCode(); + if (code == HttpURLConnection.HTTP_OK) { + String encoding = uc.getHeaderField("Transfer-Encoding"); + if (encoding == null) { + int len = uc.getHeaderFieldInt("Content-Length", 0); + if (len > 0) { + InputStream is = uc.getInputStream(); + // Returns true if we need to sync again + if (new EasFolderSyncParser(is, this).parse()) { + continue; + } + } + } else if (encoding.equalsIgnoreCase("chunked")) { + // TODO We don't handle this yet + } + } else { + userLog("FolderSync response error: " + code); + } + + // Wait for push notifications. + try { + runPingLoop(); + } catch (StaleFolderListException e) { + // We break out if we get told about a stale folder list + userLog("Ping interrupted; folder list requires sync..."); + } + } + } + } + } catch (MalformedURLException e) { + throw new IOException(); + } + } + + void runPingLoop() throws IOException, StaleFolderListException { + // Do push for all sync services here + long endTime = System.currentTimeMillis() + (30*MINS); + + while (System.currentTimeMillis() < endTime) { + // Count of pushable mailboxes + int pushCount = 0; + // Count of mailboxes that can be pushed right now + int canPushCount = 0; + EasSerializer s = new EasSerializer(); + HttpURLConnection uc; + int code; + Cursor c = mContentResolver.query(Mailbox.CONTENT_URI, Mailbox.CONTENT_PROJECTION, + MailboxColumns.ACCOUNT_KEY + '=' + mAccount.mId + " and " + SYNC_FREQUENCY_PING, + null, null); + + try { + // Loop through our pushed boxes seeing what is available to push + while (c.moveToNext()) { + pushCount++; + // Two requirements for push: + // 1) SyncManager tells us the mailbox is syncable (not running, not stopped) + // 2) The syncKey isn't "0" (i.e. it's synced at least once) + if (SyncManager.canSync(c.getLong(Mailbox.CONTENT_ID_COLUMN))) { + String syncKey = c.getString(Mailbox.CONTENT_SYNC_KEY_COLUMN); + if (syncKey == null || syncKey.equals("0")) { + continue; + } + if (canPushCount++ == 0) { + // Initialize the Ping command + s.start("Ping").data("HeartbeatInterval", "900").start("PingFolders"); + } + // When we're ready for Calendar/Contacts, we will check folder type + // TODO Save Calendar and Contacts!! Mark as not visible! + String folderClass = getTargetCollectionClassFromCursor(c); + s.start("PingFolder") + .data("PingId", c.getString(Mailbox.CONTENT_SERVER_ID_COLUMN)) + .data("PingClass", folderClass) + .end("PingFolder"); + } + } + } finally { + c.close(); + } + + if (canPushCount > 0) { + // If we have some number that are ready for push, send Ping to the server + s.end("PingFolders").end("Ping").end(); + uc = sendEASPostCommand("Ping", s.toString()); + userLog("Sending ping, timeout: " + uc.getReadTimeout() / 1000 + "s"); + code = uc.getResponseCode(); + userLog("Ping response: " + code); + if (code == HttpURLConnection.HTTP_OK) { + String encoding = uc.getHeaderField("Transfer-Encoding"); + if (encoding == null) { + int len = uc.getHeaderFieldInt("Content-Length", 0); + if (len > 0) { + parsePingResult(uc, mContentResolver); + } else { + // This implies a connection issue that we can't handle + throw new IOException(); + } + } else { + // It shouldn't be possible for EAS server to send chunked data here + throw new IOException(); + } + } else if (code == HttpURLConnection.HTTP_UNAUTHORIZED || + code == HttpURLConnection.HTTP_FORBIDDEN) { + mExitStatus = AbstractSyncService.EXIT_LOGIN_FAILURE; + userLog("Authorization error during Ping: " + code); + throw new IOException(); + } + } else if (pushCount > 0) { + // If we want to Ping, but can't just yet, wait 10 seconds and try again + sleep(10*SECS); + } else { + // We've got nothing to do, so let's hang out for a while + sleep(10*MINS); + } + } + } + + void sleep(long ms) { + try { + Thread.sleep(ms); + } catch (InterruptedException e) { + // Doesn't matter whether we stop early; it's the thought that counts + } + } + + void parsePingResult(HttpURLConnection uc, ContentResolver cr) + throws IOException, StaleFolderListException { + EasPingParser pp = new EasPingParser(uc.getInputStream(), this); + if (pp.parse()) { + // True indicates some mailboxes need syncing... + // syncList has the serverId's of the mailboxes... + mBindArguments[0] = Long.toString(mAccount.mId); + ArrayList syncList = pp.getSyncList(); + for (String serverId: syncList) { + mBindArguments[1] = serverId; + Cursor c = cr.query(Mailbox.CONTENT_URI, Mailbox.CONTENT_PROJECTION, + WHERE_ACCOUNT_KEY_AND_SERVER_ID, mBindArguments, null); + try { + if (c.moveToFirst()) { + SyncManager.startManualSync(c.getLong(Mailbox.CONTENT_ID_COLUMN)); + } + } finally { + c.close(); + } + } + } + } + + ByteArrayInputStream readResponse(HttpURLConnection uc) throws IOException { + String encoding = uc.getHeaderField("Transfer-Encoding"); + if (encoding == null) { + int len = uc.getHeaderFieldInt("Content-Length", 0); + if (len > 0) { + InputStream in = uc.getInputStream(); + byte[] bytes = new byte[len]; + int remain = len; + int offs = 0; + while (remain > 0) { + int read = in.read(bytes, offs, remain); + remain -= read; + offs += read; + } + return new ByteArrayInputStream(bytes); + } + } else if (encoding.equalsIgnoreCase("chunked")) { + // TODO We don't handle this yet + return null; + } + return null; + } + + String readResponseString(HttpURLConnection uc) throws IOException { + String encoding = uc.getHeaderField("Transfer-Encoding"); + if (encoding == null) { + int len = uc.getHeaderFieldInt("Content-Length", 0); + if (len > 0) { + InputStream in = uc.getInputStream(); + byte[] bytes = new byte[len]; + int remain = len; + int offs = 0; + while (remain > 0) { + int read = in.read(bytes, offs, remain); + remain -= read; + offs += read; + } + return new String(bytes); + } + } else if (encoding.equalsIgnoreCase("chunked")) { + // TODO We don't handle this yet + return null; + } + return null; + } + + /** + * EAS requires a unique device id, so that sync is possible from a variety of different + * devices (e.g. the syncKey is specific to a device) If we're on an emulator or some other + * device that doesn't provide one, we can create it as droid where is system time. + * This would work on a real device as well, but it would be better to use the "real" id if + * it's available + */ + private String getSimulatedDeviceId() { + try { + File f = mContext.getFileStreamPath("deviceName"); + BufferedReader rdr = null; + String id; + if (f.exists() && f.canRead()) { + rdr = new BufferedReader(new FileReader(f)); + id = rdr.readLine(); + rdr.close(); + return id; + } else if (f.createNewFile()) { + BufferedWriter w = new BufferedWriter(new FileWriter(f)); + id = "droid" + System.currentTimeMillis(); + w.write(id); + w.close(); + } + } catch (FileNotFoundException e) { + // We'll just use the default below + } catch (IOException e) { + // We'll just use the default below + } + return "droid0"; + } + + /** + * Common code to sync E+PIM data + * + * @param target, an EasMailbox, EasContacts, or EasCalendar object + */ + public void sync(EasSyncAdapter target) throws IOException { + mTarget = target; + Mailbox mailbox = target.mMailbox; + + boolean moreAvailable = true; + while (!mStop && moreAvailable) { + runAwake(); + waitForConnectivity(); + + EasSerializer s = new EasSerializer(); + if (mailbox.mSyncKey == null) { + userLog("Mailbox syncKey RESET"); + mailbox.mSyncKey = "0"; + mailbox.mSyncFrequency = Account.CHECK_INTERVAL_PUSH; + } + String className = target.getCollectionName(); + userLog("Sending " + className + " syncKey: " + mailbox.mSyncKey); + s.start("Sync") + .start("Collections") + .start("Collection") + .data("Class", className) + .data("SyncKey", mailbox.mSyncKey) + .data("CollectionId", mailbox.mServerId) + .tag("DeletesAsMoves"); + + // EAS doesn't like GetChanges if the syncKey is "0"; not documented + if (!mailbox.mSyncKey.equals("0")) { + s.tag("GetChanges"); + } + s.data("WindowSize", WINDOW_SIZE); + boolean options = false; + if (!className.equals("Contacts")) { + options = true; + // Set the lookback appropriately (EAS calls this a "filter") + String filter = Eas.FILTER_1_WEEK; + switch (mAccount.mSyncLookback) { + case com.android.email.Account.SYNC_WINDOW_1_DAY: { + filter = Eas.FILTER_1_DAY; + break; + } + case com.android.email.Account.SYNC_WINDOW_3_DAYS: { + filter = Eas.FILTER_3_DAYS; + break; + } + case com.android.email.Account.SYNC_WINDOW_1_WEEK: { + filter = Eas.FILTER_1_WEEK; + break; + } + case com.android.email.Account.SYNC_WINDOW_2_WEEKS: { + filter = Eas.FILTER_2_WEEKS; + break; + } + case com.android.email.Account.SYNC_WINDOW_1_MONTH: { + filter = Eas.FILTER_1_MONTH; + break; + } + case com.android.email.Account.SYNC_WINDOW_ALL: { + filter = Eas.FILTER_ALL; + break; + } + } + s.start("Options") + .data("FilterType", filter); + } + if (mProtocolVersion.equals("12.0")) { + if (!options) { + options = true; + s.start("Options"); + s.start("BodyPreference") + // Plain text to start + .data("BodyPreferenceType", Eas.BODY_PREFERENCE_TEXT) + .data("BodyPreferenceTruncationSize", Eas.DEFAULT_BODY_TRUNCATION_SIZE) + .end("BodyPreference"); + } + } + if (options) { + s.end("Options"); + } + + // Send our changes up to the server + target.sendLocalChanges(s, this); + + s.end("Collection").end("Collections").end("Sync").end(); + HttpURLConnection uc = sendEASPostCommand("Sync", s.toString()); + int code = uc.getResponseCode(); + if (code == HttpURLConnection.HTTP_OK) { + ByteArrayInputStream is = readResponse(uc); + if (is != null) { + moreAvailable = target.parse(is, this); + } + } else { + userLog("Sync response error: " + code); + if (code == HttpURLConnection.HTTP_UNAUTHORIZED || + code == HttpURLConnection.HTTP_FORBIDDEN) { + mExitStatus = AbstractSyncService.EXIT_LOGIN_FAILURE; + } + return; + } + } + } + + /* (non-Javadoc) + * @see java.lang.Runnable#run() + */ + public void run() { + mThread = Thread.currentThread(); + TAG = mThread.getName(); + mDeviceId = android.provider.Settings.System.getString(mContext.getContentResolver(), + android.provider.Settings.System.ANDROID_ID); + // Generate a device id if we don't have one + if (mDeviceId == null) { + mDeviceId = getSimulatedDeviceId(); + } + HostAuth ha = HostAuth.restoreHostAuthWithId(mContext, mAccount.mHostAuthKeyRecv); + mHostAddress = ha.mAddress; + mUserName = ha.mLogin; + mPassword = ha.mPassword; + + try { + if (mMailbox.mServerId.equals("_main")) { + runMain(); + } else { + EasSyncAdapter target; + if (mMailbox.mType == Mailbox.TYPE_CONTACTS) + target = new EasContactsSyncAdapter(mMailbox); + else { + target = new EasEmailSyncAdapter(mMailbox); + } + // We loop here because someone might have put a request in while we were syncing + // and we've missed that opportunity... + do { + if (mRequestTime != 0) { + userLog("Looping for user request..."); + mRequestTime = 0; + } + sync(target); + } while (mRequestTime != 0); + } + mExitStatus = EXIT_DONE; + } catch (IOException e) { + userLog("Caught IOException"); + mExitStatus = EXIT_IO_ERROR; + } catch (Exception e) { + e.printStackTrace(); + } finally { + userLog(mMailbox.mDisplayName + ": sync finished"); + SyncManager.done(this); + } + } +} diff --git a/src/com/android/exchange/EasTags.java b/src/com/android/exchange/EasTags.java deleted file mode 100644 index f68da26b5..000000000 --- a/src/com/android/exchange/EasTags.java +++ /dev/null @@ -1,345 +0,0 @@ -/* - * Copyright (C) 2008-2009 Marc Blank - * Licensed to 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; - -public class EasTags { - - static final int AIRSYNC = 0x00; - static final int CONTACTS = 0x01; - static final int EMAIL = 0x02; - static final int FOLDER = 0x07; - static final int PING = 0x0D; - static final int GAL = 0x10; - - static final int SYNC_SYNC = 5; - static final int SYNC_RESPONSES = 6; - static final int SYNC_ADD = 7; - static final int SYNC_CHANGE = 8; - static final int SYNC_DELETE = 9; - static final int SYNC_FETCH = 0xA; - static final int SYNC_SYNC_KEY = 0xB; - static final int SYNC_CLIENT_ID = 0xC; - static final int SYNC_SERVER_ID = 0xD; - static final int SYNC_STATUS = 0xE; - static final int SYNC_COLLECTION = 0xF; - static final int SYNC_CLASS = 0x10; - static final int SYNC_VERSION = 0x11; - static final int SYNC_COLLECTION_ID = 0x12; - static final int SYNC_GET_CHANGES = 0x13; - static final int SYNC_MORE_AVAILABLE = 0x14; - static final int SYNC_WINDOW_SIZE = 0x15; - static final int SYNC_COMMANDS = 0x16; - static final int SYNC_OPTIONS = 0x17; - static final int SYNC_FILTER_TYPE = 0x18; - static final int SYNC_TRUNCATION = 0x19; - static final int SYNC_RTF_TRUNCATION = 0x1A; - static final int SYNC_CONFLICT = 0x1B; - static final int SYNC_COLLECTIONS = 0x1C; - static final int SYNC_APPLICATION_DATA = 0x1D; - static final int SYNC_DELETES_AS_MOVES = 0x1E; - static final int SYNC_NOTIFY_GUID = 0x1F; - static final int SYNC_SUPPORTED = 0x20; - static final int SYNC_SOFT_DELETE = 0x21; - static final int SYNC_MIME_SUPPORT = 0x22; - static final int SYNC_MIME_TRUNCATION = 0x23; - static final int SYNC_WAIT = 0x24; - static final int SYNC_LIMIT = 0x25; - static final int SYNC_PARTIAL = 0x26; - - static final int CALENDAR_TIME_ZONE = 5; - static final int CALENDAR_ALL_DAY_EVENT = 6; - static final int CALENDAR_ATTENDEES = 7; - static final int CALENDAR_ATTENDEE = 8; - static final int CALENDAR_ATTENDEE_EMAIL = 9; - static final int CALENDAR_ATTENDEE_NAME = 0xA; - static final int CALENDAR_BODY = 0xB; - static final int CALENDAR_BODY_TRUNCATED = 0xC; - static final int CALENDAR_BUSY_STATUS = 0xD; - static final int CALENDAR_CATEGORIES = 0xE; - static final int CALENDAR_CATEGORY = 0xF; - static final int CALENDAR_COMPRESSED_RTF = 0x10; - static final int CALENDAR_DTSTAMP = 0x11; - static final int CALENDAR_END_TIME = 0x12; - static final int CALENDAR_EXCEPTION = 0x13; - static final int CALENDAR_EXCEPTIONS = 0x14; - static final int CALENDAR_EXCEPTION_IS_DELETED = 0x15; - static final int CALENDAR_EXCEPTION_START_TIME = 0x16; - static final int CALENDAR_LOCATION = 0x17; - static final int CALENDAR_MEETING_STATUS = 0x18; - static final int CALENDAR_ORGANIZER_EMAIL = 0x19; - static final int CALENDAR_ORGANIZER_NAME = 0x1A; - static final int CALENDAR_RECURRENCE = 0x1B; - static final int CALENDAR_RECURRENCE_TYPE = 0x1C; - static final int CALENDAR_RECURRENCE_UNTIL = 0x1D; - static final int CALENDAR_RECURRENCE_OCCURRENCES = 0x1E; - static final int CALENDAR_RECURRENCE_INTERVAL = 0x1F; - static final int CALENDAR_RECURRENCE_DAYOFWEEK = 0x20; - static final int CALENDAR_RECURRENCE_DAYOFMONTH = 0x21; - static final int CALENDAR_RECURRENCE_WEEKOFMONTH = 0x22; - static final int CALENDAR_RECURRENCE_MONTHOFYEAR = 0x23; - static final int CALENDAR_REMINDER_MINS_BEFORE = 0x24; - static final int CALENDAR_SENSITIVITY = 0x25; - static final int CALENDAR_SUBJECT = 0x26; - static final int CALENDAR_START_TIME = 0x27; - static final int CALENDAR_UID = 0x28; - static final int CALENDAR_ATTENDEE_STATUS = 0x29; - static final int CALENDAR_ATTENDEE_TYPE = 0x2A; - - static final int FOLDER_FOLDERS = 5; - static final int FOLDER_FOLDER = 6; - static final int FOLDER_DISPLAY_NAME = 7; - static final int FOLDER_SERVER_ID = 8; - static final int FOLDER_PARENT_ID = 9; - static final int FOLDER_TYPE = 0xA; - static final int FOLDER_RESPONSE = 0xB; - static final int FOLDER_STATUS = 0xC; - static final int FOLDER_CONTENT_CLASS = 0xD; - static final int FOLDER_CHANGES = 0xE; - static final int FOLDER_ADD = 0xF; - static final int FOLDER_DELETE = 0x10; - static final int FOLDER_UPDATE = 0x11; - static final int FOLDER_SYNC_KEY = 0x12; - static final int FOLDER_FOLDER_CREATE = 0x13; - static final int FOLDER_FOLDER_DELETE= 0x14; - static final int FOLDER_FOLDER_UPDATE = 0x15; - static final int FOLDER_FOLDER_SYNC = 0x16; - static final int FOLDER_COUNT = 0x17; - static final int FOLDER_VERSION = 0x18; - - static final int EMAIL_ATTACHMENT = 5; - static final int EMAIL_ATTACHMENTS = 6; - static final int EMAIL_ATT_NAME = 7; - static final int EMAIL_ATT_SIZE = 8; - static final int EMAIL_ATT0ID = 9; - static final int EMAIL_ATT_METHOD = 0xA; - static final int EMAIL_ATT_REMOVED = 0xB; - static final int EMAIL_BODY = 0xC; - static final int EMAIL_BODY_SIZE = 0xD; - static final int EMAIL_BODY_TRUNCATED = 0xE; - static final int EMAIL_DATE_RECEIVED = 0xF; - static final int EMAIL_DISPLAY_NAME = 0x10; - static final int EMAIL_DISPLAY_TO = 0x11; - static final int EMAIL_IMPORTANCE = 0x12; - static final int EMAIL_MESSAGE_CLASS = 0x13; - static final int EMAIL_SUBJECT = 0x14; - static final int EMAIL_READ = 0x15; - static final int EMAIL_TO = 0x16; - static final int EMAIL_CC = 0x17; - static final int EMAIL_FROM = 0x18; - static final int EMAIL_REPLY_TO = 0x19; - static final int EMAIL_ALL_DAY_EVENT = 0x1A; - static final int EMAIL_CATEGORIES = 0x1B; - static final int EMAIL_CATEGORY = 0x1C; - static final int EMAIL_DTSTAMP = 0x1D; - static final int EMAIL_END_TIME = 0x1E; - static final int EMAIL_INSTANCE_TYPE = 0x1F; - static final int EMAIL_INTD_BUSY_STATUS = 0x20; - static final int EMAIL_LOCATION = 0x21; - static final int EMAIL_MEETING_REQUEST = 0x22; - static final int EMAIL_ORGANIZER = 0x23; - static final int EMAIL_RECURRENCE_ID = 0x24; - static final int EMAIL_REMINDER = 0x25; - static final int EMAIL_RESPONSE_REQUESTED = 0x26; - static final int EMAIL_RECURRENCES = 0x27; - static final int EMAIL_RECURRENCE = 0x28; - static final int EMAIL_RECURRENCE_TYPE = 0x29; - static final int EMAIL_RECURRENCE_UNTIL = 0x2A; - static final int EMAIL_RECURRENCE_OCCURRENCES = 0x2B; - static final int EMAIL_RECURRENCE_INTERVAL = 0x2C; - static final int EMAIL_RECURRENCE_DAYOFWEEK = 0x2D; - static final int EMAIL_RECURRENCE_DAYOFMONTH = 0x2E; - static final int EMAIL_RECURRENCE_WEEKOFMONTH = 0x2F; - static final int EMAIL_RECURRENCE_MONTHOFYEAR = 0x30; - static final int EMAIL_START_TIME = 0x31; - static final int EMAIL_SENSITIVITY = 0x32; - static final int EMAIL_TIME_ZONE = 0x33; - static final int EMAIL_GLOBAL_OBJID = 0x34; - static final int EMAIL_THREAD_TOPIC = 0x35; - static final int EMAIL_MIME_DATA = 0x36; - static final int EMAIL_MIME_TRUNCATED = 0x37; - static final int EMAIL_MIME_SIZE = 0x38; - static final int EMAIL_INTERNET_CPID = 0x39; - static final int EMAIL_FLAG = 0x3A; - static final int EMAIL_FLAG_STATUS = 0x3B; - static final int EMAIL_CONTENT_CLASS = 0x3C; - static final int EMAIL_FLAG_TYPE = 0x3D; - static final int EMAIL_COMPLETE_TIME = 0x3E; - - static final int MOVE_MOVE_ITEMS = 5; - static final int MOVE_MOVE = 6; - static final int MOVE_SRCMSGID = 7; - static final int MOVE_SRCFLDID = 8; - static final int MOVE_DSTFLDID = 9; - static final int MOVE_RESPONSE = 0xA; - static final int MOVE_STATUS = 0xB; - static final int MOVE_DSTMSGID = 0xC; - - static final int PING_PING = 5; - static final int PING_AUTD_STATE = 6; - static final int PING_STATUS = 7; - static final int PING_HEARTBEAT_INTERVAL = 8; - static final int PING_FOLDERS = 9; - static final int PING_FOLDER = 0xA; - static final int PING_ID = 0xB; - static final int PING_CLASS = 0xC; - static final int PING_MAX_FOLDERS = 0xD; - - static final int BASE_BODY_PREFERENCE = 5; - static final int BASE_TYPE = 6; - static final int BASE_TRUNCATION_SIZE = 7; - static final int BASE_ALL_OR_NONE = 8; - static final int BASE_RESERVED = 9; - static final int BASE_BODY = 0xA; - static final int BASE_DATA = 0xB; - static final int BASE_ESTIMATED_DATA_SIZE = 0xC; - static final int BASE_TRUNCATED = 0xD; - static final int BASE_ATTACHMENTS = 0xE; - static final int BASE_ATTACHMENT = 0xF; - static final int BASE_DISPLAY_NAME = 0x10; - static final int BASE_FILE_REFERENCE = 0x11; - static final int BASE_METHOD = 0x12; - static final int BASE_CONTENT_ID = 0x13; - static final int BASE_CONTENT_LOCATION = 0x14; - static final int BASE_IS_INLINE = 0x15; - static final int BASE_NATIVE_BODY_TYPE = 0x16; - static final int BASE_CONTENT_TYPE = 0x17; - - static public String[][] pages = { - { // 0x00 AirSync - "Sync", "Responses", "Add", "Change", "Delete", "Fetch", "SyncKey", "ClientId", - "ServerId", "Status", "Collection", "Class", "Version", "CollectionId", "GetChanges", - "MoreAvailable", "WindowSize", "Commands", "Options", "FilterType", "Truncation", - "RTFTruncation", "Conflict", "Collections", "ApplicationData", "DeletesAsMoves", - "NotifyGUID", "Supported", "SoftDelete", "MIMESupport", "MIMETruncation", "Wait", - "Limit", "Partial" - }, - { - // 0x01 Contacts - }, - { - // 0x02 Email - "Attachment", "Attachments", "AttName", "AttSize", "Add0Id", "AttMethod", "AttRemoved", - "Body", "BodySize", "BodyTruncated", "DateReceived", "DisplayName", "DisplayTo", - "Importance", "MessageClass", "Subject", "Read", "To", "CC", "From", "ReplyTo", - "AllDayEvent", "Categories", "Category", "DTStamp", "EndTime", "InstanceType", - "IntDBusyStatus", "Location", "MeetingRequest", "Organizer", "RecurrenceId", "Reminder", - "ResponseRequested", "Recurrences", "Recurence", "Recurrence_Type", "Recurrence_Until", - "Recurrence_Occurrences", "Recurrence_Interval", "Recurrence_DayOfWeek", - "Recurrence_DayOfMonth", "Recurrence_WeekOfMonth", "Recurrence_MonthOfYear", - "StartTime", "Sensitivity", "TimeZone", "GlobalObjId", "ThreadTopic", "MIMEData", - "MIMETruncated", "MIMESize", "InternetCPID", "Flag", "FlagStatus", "ContentClass", - "FlagType", "CompleteTime" - }, - { - // 0x03 AirNotify - }, - { - // 0x04 Calendar - "CalTimeZone", "CalAllDayEvent", "CalAttendees", "CalAttendee", "CalAttendee_Email", - "CalAttendee_Name", "CalBody", "CalBodyTruncated", "CalBusyStatus", "CalCategories", - "CalCategory", "CalCompressed_RTF", "CalDTStamp", "CalEndTime", "CalExeption", - "CalExceptions", "CalException_IsDeleted", "CalException_StartTime", "CalLocation", - "CalMeetingStatus", "CalOrganizer_Email", "CalOrganizer_Name", "CalRecurrence", - "CalRecurrence_Type", "CalRecurrence_Until", "CalRecurrence_Occurrences", - "CalRecurrence_Interval", "CalRecurrence_DayOfWeek", "CalRecurrence_DayOfMonth", - "CalRecurrence_WeekOfMonth", "CalRecurrence_MonthOfYear", "CalReminder_MinsBefore", - "CalSensitivity", "CalSubject", "CalStartTime", "CalUID", "CalAttendee_Status", - "CalAttendee_Type" - }, - { - // 0x05 Move - "MoveItems", "Move", "SrcMsgId", "SrcFldId", "DstFldId", "Response", "Status", - "DstMsgId" - }, - { - // 0x06 ItemEstimate - }, - { - // 0x07 FolderHierarchy - "Folders", "Folder", "FolderDisplayName", "FolderServerId", "FolderParentId", "Type", - "Response", "Status", "ContentClass", "Changes", "FolderAdd", "FolderDelete", - "FolderUpdate", "FolderSyncKey", "FolderCreate", "FolderDelete", "FolderUpdate", - "FolderSync", "Count", "Version" - }, - { - // 0x08 MeetingResponse - }, - { - // 0x09 Tasks - }, - { - // 0x0A ResolveRecipients - }, - { - // 0x0B ValidateCert - }, - { - // 0x0C Contacts2 - }, - { - // 0x0D Ping - "Ping", "AutdState", "Status", "HeartbeatInterval", "PingFolders", "PingFolder", - "PingId", "PingClass", "MaxFolders" - }, - { - // 0x0E Provision - "Provision", "Policies", "Policy", "PolicyType", "PolicyKey", "Data", "Status", - "RemoteWipe", "EASProvidionDoc", "DevicePasswordEnabled", - "AlphanumericDevicePasswordRequired", - "DeviceEncryptionEnabled", "-unused-", "AttachmentsEnabled", "MinDevicePasswordLength", - "MaxInactivityTimeDeviceLock", "MaxDevicePasswordFailedAttempts", "MaxAttachmentSize", - "AllowSimpleDevicePassword", "DevicePasswordExpiration", "DevicePasswordHistory", - "AllowStorageCard", "AllowCamera", "RequireDeviceEncryption", - "AllowUnsignedApplications", "AllowUnsignedInstallationPackages", - "MinDevicePasswordComplexCharacters", "AllowWiFi", "AllowTextMessaging", - "AllowPOPIMAPEmail", "AllowBluetooth", "AllowIrDA", "RequireManualSyncWhenRoaming", - "AllowDesktopSync", - "MaxCalendarAgeFilder", "AllowHTMLEmail", "MaxEmailAgeFilder", - "MaxEmailBodyTruncationSize", "MaxEmailHTMLBodyTruncationSize", - "RequireSignedSMIMEMessages", "RequireEncryptedSMIMEMessages", - "RequireSignedSMIMEAlgorithm", "RequireEncryptionSMIMEAlgorithm", - "AllowSMIMEEncryptionAlgorithmNegotiation", "AllowSMIMESoftCerts", "AllowBrowser", - "AllowConsumerEmail", "AllowRemoteDesktop", "AllowInternetSharing", - "UnapprovedInROMApplicationList", "ApplicationName", "ApprovedApplicationList", "Hash" - }, - { - // 0x0F Search - }, - { - // 0x10 Gal - "DisplayName", "Phone", "Office", "Title", "Company", "Alias", "FirstName", "LastName", - "HomePhone", "MobilePhone", "EmailAddress" - }, - { - // 0x11 AirSyncBase - "BodyPreference", "BodyPreferenceType", "BodyPreferenceTruncationSize", "AllOrNone", - "Body", "Data", "EstimatedDataSize", "Truncated", "Attachments", "Attachment", - "DisplayName", "FileReference", "Method", "ContentId", "ContentLocation", "IsInline", - "NativeBodyType", "ContentType" - }, - { - // 0x12 Settings - }, - { - // 0x13 DocumentLibrary - }, - { - // 0x14 ItemOperations - } - }; -} diff --git a/src/com/android/exchange/EmailContent.java b/src/com/android/exchange/EmailContent.java index e75a5342d..5998b25cc 100644 --- a/src/com/android/exchange/EmailContent.java +++ b/src/com/android/exchange/EmailContent.java @@ -14,6 +14,12 @@ * limitations under the License. */ +/** + * This is a local copy of com.android.email.EmailProvider + * + * Last copied from com.android.email.EmailProvider on 7/2/09 + */ + package com.android.exchange; import com.android.email.R; @@ -223,10 +229,7 @@ public abstract class EmailContent { public String mHtmlContent; public String mTextContent; - /** - * no public constructor since this is a utility class - */ - private Body() { + public Body() { mBaseUri = CONTENT_URI; } @@ -283,7 +286,7 @@ public abstract class EmailContent { @Override @SuppressWarnings("unchecked") public EmailContent.Body restore(Cursor c) { - mBaseUri = EmailContent.Message.CONTENT_URI; + mBaseUri = EmailContent.Body.CONTENT_URI; mMessageKey = c.getLong(CONTENT_MESSAGE_KEY_COLUMN); mHtmlContent = c.getString(CONTENT_HTML_CONTENT_COLUMN); mTextContent = c.getString(CONTENT_TEXT_CONTENT_COLUMN); @@ -352,8 +355,17 @@ public abstract class EmailContent { public static final class Message extends EmailContent implements SyncColumns, MessageColumns { public static final String TABLE_NAME = "Message"; - public static final String UPDATES_TABLE_NAME = "Message_Updates"; + public static final String UPDATED_TABLE_NAME = "Message_Updates"; + public static final String DELETED_TABLE_NAME = "Message_Deletes"; + + // To refer to a specific message, use ContentUris.withAppendedId(CONTENT_URI, id) public static final Uri CONTENT_URI = Uri.parse(EmailContent.CONTENT_URI + "/message"); + public static final Uri SYNCED_CONTENT_URI = + Uri.parse(EmailContent.CONTENT_URI + "/syncedMessage"); + public static final Uri DELETED_CONTENT_URI = + Uri.parse(EmailContent.CONTENT_URI + "/deletedMessage"); + public static final Uri UPDATED_CONTENT_URI = + Uri.parse(EmailContent.CONTENT_URI + "/updatedMessage"); public static final String KEY_TIMESTAMP_DESC = MessageColumns.TIMESTAMP + " desc"; @@ -489,9 +501,6 @@ public abstract class EmailContent { mBaseUri = CONTENT_URI; } - public static final Uri UPDATED_CONTENT_URI = - Uri.parse(EmailContent.CONTENT_URI + "/updatedMessage"); - @Override public ContentValues toContentValues() { ContentValues values = new ContentValues(); @@ -500,6 +509,7 @@ public abstract class EmailContent { values.put(MessageColumns.DISPLAY_NAME, mDisplayName); values.put(MessageColumns.TIMESTAMP, mTimeStamp); values.put(MessageColumns.SUBJECT, mSubject); + values.put(MessageColumns.PREVIEW, mPreview); values.put(MessageColumns.FLAG_READ, mFlagRead); values.put(MessageColumns.FLAG_LOADED, mFlagLoaded); values.put(MessageColumns.FLAG_FAVORITE, mFlagFavorite); @@ -517,6 +527,7 @@ public abstract class EmailContent { values.put(MessageColumns.CLIENT_ID, mClientId); values.put(MessageColumns.MESSAGE_ID, mMessageId); + values.put(MessageColumns.THREAD_ID, mThreadId); values.put(MessageColumns.MAILBOX_KEY, mMailboxKey); values.put(MessageColumns.ACCOUNT_KEY, mAccountKey); @@ -744,6 +755,7 @@ public abstract class EmailContent { public static final int CHECK_INTERVAL_NEVER = -1; public static final int CHECK_INTERVAL_PUSH = -2; + public static final int CHECK_INTERVAL_PING = -3; public static final int SYNC_WINDOW_USER = -1; @@ -1653,6 +1665,12 @@ public abstract class EmailContent { // Holds junk mail public static final int TYPE_JUNK = 7; + // Types after this are used for non-mail mailboxes (as in EAS) + public static final int TYPE_NOT_EMAIL = 0x40; + public static final int TYPE_CALENDAR = 0x41; + public static final int TYPE_CONTACTS = 0x42; + public static final int TYPE_TASKS = 0x43; + // Bit field flags public static final int FLAG_HAS_CHILDREN = 1<<0; public static final int FLAG_CHILDREN_VISIBLE = 1<<1; @@ -1746,9 +1764,9 @@ public abstract class EmailContent { public static final String TABLE_NAME = "HostAuth"; public static final Uri CONTENT_URI = Uri.parse(EmailContent.CONTENT_URI + "/hostauth"); - private static final int FLAG_SSL = 1; - private static final int FLAG_TLS = 2; - private static final int FLAG_AUTHENTICATE = 4; + public static final int FLAG_SSL = 1; + public static final int FLAG_TLS = 2; + public static final int FLAG_AUTHENTICATE = 4; public String mProtocol; public String mAddress; diff --git a/src/com/android/exchange/EofException.java b/src/com/android/exchange/EofException.java deleted file mode 100644 index b9d850455..000000000 --- a/src/com/android/exchange/EofException.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright (C) 2008-2009 Marc Blank - * Licensed to 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 java.io.IOException; - -public class EofException extends IOException { - private static final long serialVersionUID = 1L; -} diff --git a/src/com/android/exchange/InteractiveSyncService.java b/src/com/android/exchange/InteractiveSyncService.java new file mode 100644 index 000000000..23b116fc4 --- /dev/null +++ b/src/com/android/exchange/InteractiveSyncService.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2008-2009 Marc Blank + * Licensed to 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 android.content.Context; + +import com.android.exchange.EmailContent.Attachment; +import com.android.exchange.EmailContent.Mailbox; + +/** + * The parent class of all SyncServices that are interactive (i.e. need to + * respond to user input in a timely way. The abstract methods for the most part + * track the service methods available in the ISyncManager interface. The + * SyncManager is responsible for ensuring that an InteractiveSyncService has + * been started, and then passes the appropriate call into it. Each ISS will + * interpret/handle the method as it deems appropriate. + */ +public abstract class InteractiveSyncService extends AbstractSyncService { + + public InteractiveSyncService(Context _context, Mailbox _mailbox) { + super(_context, _mailbox); + } + + public InteractiveSyncService(String prefix) { + super(prefix); + } + + public abstract void startSync(); + + public abstract void stopSync(); + + public abstract void reloadFolderList(); + + public abstract void loadAttachment(Attachment att, ISyncManagerCallback cb); +} diff --git a/src/com/android/exchange/KeepAliveReceiver.java b/src/com/android/exchange/MailboxAlarmReceiver.java similarity index 66% rename from src/com/android/exchange/KeepAliveReceiver.java rename to src/com/android/exchange/MailboxAlarmReceiver.java index 1d4aa538f..61af05ae3 100644 --- a/src/com/android/exchange/KeepAliveReceiver.java +++ b/src/com/android/exchange/MailboxAlarmReceiver.java @@ -1,6 +1,5 @@ /* -/* - * Copyright (C) 2008-2009 Marc Blank + * Copyright (C) 2008-2009 Marc Blank * Licensed to The Android Open Source Project. * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -22,15 +21,19 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; -public class KeepAliveReceiver extends BroadcastReceiver { +/** + * MailboxAlarmReceiver is used to "wake up" the SyncManager at the appropriate time(s). It may + * also be used for individual sync adapters, but this isn't implemented at the present time. + * + */ +public class MailboxAlarmReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { long mid = intent.getLongExtra("mailbox", -1); - if (mid < 0) { - SyncManager.kick(); - } - else { - SyncManager.ping(mid); + if (SyncManager.INSTANCE != null) { + SyncManager.INSTANCE.log("Alarm received for: " + mid); } + SyncManager.ping(mid); } } + diff --git a/src/com/android/exchange/ProtocolService.java b/src/com/android/exchange/ProtocolService.java deleted file mode 100644 index 57cc7d320..000000000 --- a/src/com/android/exchange/ProtocolService.java +++ /dev/null @@ -1,243 +0,0 @@ -/* - * Copyright (C) 2008-2009 Marc Blank - * Licensed to 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 java.util.ArrayList; - -import com.android.email.Email; -import com.android.email.mail.MessagingException; -import com.android.exchange.EmailContent.Account; -import com.android.exchange.EmailContent.Mailbox; - -import android.content.Context; -import android.net.ConnectivityManager; -import android.net.NetworkInfo; -import android.net.NetworkInfo.DetailedState; -import android.util.Log; - -// Base class for all protocol services -// Some common functionality is included here; note that each protocol will implement run() individually -// MailService (extends Service, implements Runnable) instantiates subclasses when it's time to run a sync -// (either timed, or push, or mail placed in outbox, etc.) -// Current subclasses are IMAPService, EASService, and SMTPService, with POP3Service to come... -public abstract class ProtocolService implements Runnable { - - public static final String TAG = "ProtocolService"; - - public static final String SUMMARY_PROTOCOL = "_SUMMARY_"; - public static final String SYNCED_PROTOCOL = "_SYNCING_"; - public static final String MOVE_FAVORITES_PROTOCOL = "_MOVE_FAVORITES_"; - - public static final int CONNECT_TIMEOUT = 30000; - public static final int NETWORK_WAIT = 15000; - - public static final int SECS = 1000; - public static final int MINS = 60*SECS; - public static final int HRS = 60*MINS; - public static final int DAYS = 24*HRS; - - public static final String IMAP_PROTOCOL = "imap"; - public static final String EAS_PROTOCOL = "eas"; - - // Making SSL connections is so slow that I'd prefer that only one be executed at a time - // Kindly subclasses will synchronize on this before making an SSL connection - public static Object sslGovernorToken = new Object(); - - protected Mailbox mMailbox; - protected long mMailboxId; - protected Thread mThread; - protected String mMailboxName; - protected Account mAccount; - protected Context mContext; - protected long mRequestTime; - protected ArrayList mPartRequests = new ArrayList(); - protected PartRequest mPendingPartRequest = null; - - // Stop is sent by the MailService to request that the service stop itself cleanly. An example - // would be for the implementation of sleep hours - public abstract void stop (); - // Ping is sent by the MailService to indicate that a user request requiring service has been added to - // request queue; response is service dependent - public abstract void ping (); - // MailService calls this to determine the sync state of the protocol service. By default, - // this is "SYNC", but it might, for example, be "IDLE" (i.e. push), in which case the method will be - // overridden. Could be abstract, but ... nah. - public int getSyncStatus() { - return 0; - //return MailService.SyncStatus.SYNC; - } - - public ProtocolService (Context _context, Mailbox _mailbox) { - mContext = _context; - mMailbox = _mailbox; - mMailboxId = _mailbox.mId; - mMailboxName = _mailbox.mServerId; - mAccount = Account.restoreAccountWithId(_context, _mailbox.mAccountKey); - } - - // Will be required when subclasses are instantiated by name - public ProtocolService (String prefix) { - } - - public abstract void validateAccount (String host, String userName, String password, - int port, boolean ssl, Context context) throws MessagingException; - - static public void validate (Class klass, String host, - String userName, String password, int port, boolean ssl, Context context) - throws MessagingException { - ProtocolService svc; - try { - svc = klass.newInstance(); - svc.validateAccount(host, userName, password, port, ssl, context); - } catch (IllegalAccessException e) { - throw new MessagingException("internal error", e); - } catch (InstantiationException e) { - throw new MessagingException("internal error", e); - } - } - - public static class ValidationResult { - static final int NO_FAILURE = 0; - static final int CONNECTION_FAILURE = 1; - static final int VALIDATION_FAILURE = 2; - static final int EXCEPTION = 3; - static final ValidationResult succeeded = new ValidationResult(true, NO_FAILURE, null); - boolean success; - int failure = NO_FAILURE; - String reason = null; - Exception exception = null; - - ValidationResult (boolean _success, int _failure, String _reason) { - success = _success; - failure = _failure; - reason = _reason; - } - - ValidationResult (boolean _success) { - success = _success; - } - - ValidationResult (Exception e) { - success = false; - failure = EXCEPTION; - exception = e; - } - - public boolean isSuccess () { - return success; - } - - public String getReason () { - return reason; - } - } - - public final void runAwake () { - //MailService.runAwake(mMailboxId); - } - - public final void runAsleep (long millis) { - //MailService.runAsleep(mMailboxId, millis); - } - - // Common call used by the various protocols to send a "mail" message to the UI - protected void updateUI () { - } - - protected void log (String str) { - if (Email.DEBUG) { - Log.v(Email.LOG_TAG, str); - } - } - - // Delay until there is some kind of network connectivity - // Subclasses should allow some number of retries before failing, and kicking the ball back to MailService - public int waitForConnectivity () { - ConnectivityManager cm = (ConnectivityManager)mContext - .getSystemService(Context.CONNECTIVITY_SERVICE); - while (true) { - NetworkInfo info = cm.getActiveNetworkInfo(); - if (info != null && info.isConnected()) { - DetailedState state = info.getDetailedState(); - if (state == DetailedState.CONNECTED) { - return info.getType(); - } else { - // TODO Happens sometimes; find out why... - log("Not quite connected? Pause 1 second"); - } - pause(1000); - } else { - log("Not connected; waiting 15 seconds"); - pause(NETWORK_WAIT); - } - } - } - - // Convenience - private void pause (int ms) { - try { - Thread.sleep(ms); - } catch (InterruptedException e) { - } - } - - // PartRequest handling (common functionality) - // Can be overridden if desired, but IMAP/EAS both use the next three methods as-is - public void addPartRequest (PartRequest req) { - synchronized(mPartRequests) { - mPartRequests.add(req); - } - } - - public void removePartRequest (PartRequest req) { - synchronized(mPartRequests) { - mPartRequests.remove(req); - } - } - - public PartRequest hasPartRequest(long emailId, String part) { - synchronized(mPartRequests) { - for (PartRequest pr: mPartRequests) { - if (pr.emailId == emailId && pr.loc.equals(part)) - return pr; - } - } - return null; - } - - // CancelPartRequest is sent in response to user input to stop a request (attachment load at this point) - // that is in progress. This will almost certainly require code overriding the base functionality, as - // sockets may need to be closed, etc. and this functionality will be service dependent. This returns - // the canceled PartRequest or null - public PartRequest cancelPartRequest(long emailId, String part) { - synchronized(mPartRequests) { - PartRequest p = null; - for (PartRequest pr: mPartRequests) { - if (pr.emailId == emailId && pr.loc.equals(part)) { - p = pr; - break; - } - } - if (p != null) { - mPartRequests.remove(p); - return p; - } - } - return null; - } -} diff --git a/src/com/android/exchange/EasParserException.java b/src/com/android/exchange/StaleFolderListException.java similarity index 87% rename from src/com/android/exchange/EasParserException.java rename to src/com/android/exchange/StaleFolderListException.java index ee10c2328..70ac03259 100644 --- a/src/com/android/exchange/EasParserException.java +++ b/src/com/android/exchange/StaleFolderListException.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2008-2009 Marc Blank + * Copyright (C) 2008-2009 Marc Blank * Licensed to The Android Open Source Project. * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,6 +17,6 @@ package com.android.exchange; -public class EasParserException extends Exception { +public class StaleFolderListException extends EasException { private static final long serialVersionUID = 1L; } diff --git a/src/com/android/exchange/SyncManager.java b/src/com/android/exchange/SyncManager.java index adc8f942c..906564f82 100644 --- a/src/com/android/exchange/SyncManager.java +++ b/src/com/android/exchange/SyncManager.java @@ -23,7 +23,6 @@ import android.os.IBinder; import android.util.Log; import java.util.ArrayList; -import java.util.GregorianCalendar; import java.util.HashMap; import java.util.List; @@ -33,6 +32,9 @@ import com.android.exchange.EmailContent.Account; import com.android.exchange.EmailContent.HostAuth; import com.android.exchange.EmailContent.Mailbox; import com.android.exchange.EmailContent.Message; +import com.android.exchange.EmailContent.MessageColumns; +import com.android.exchange.EmailContent.SyncColumns; +import com.android.exchange.adapter.EasOutboxService; import android.app.AlarmManager; import android.app.PendingIntent; @@ -47,91 +49,116 @@ import android.net.NetworkInfo; import android.net.Uri; import android.net.NetworkInfo.State; import android.os.Bundle; -import android.os.Debug; import android.os.Handler; import android.os.PowerManager; import android.os.RemoteCallbackList; import android.os.RemoteException; import android.os.PowerManager.WakeLock; -import android.preference.PreferenceManager; import android.database.ContentObserver; +/** + * The SyncManager handles all aspects of starting, maintaining, and stopping the various sync + * adapters used by Exchange. However, it is capable of handing any kind of email sync, and it + * would be appropriate to use for IMAP push, when that functionality is added to the Email + * application. + * + * The Email application communicates with EAS sync adapters via SyncManager's binder interface, + * which exposes UI-related functionality to the application (see the definitions below) + * + * SyncManager uses ContentObservers to detect changes to accounts, mailboxes, and messages in + * order to maintain proper 2-way syncing of data. (More documentation to follow) + * + */ public class SyncManager extends Service implements Runnable { - public static final int AWAKE = 0; - public static final int SLEEP_WEEKEND = 1; - public static final int SLEEP_HOURS = 2; - public static final int OFFLINE = 3; + public static final String TAG = "EAS SyncManager"; public static final int DEFAULT_WINDOW = Integer.MIN_VALUE; - public static final int SECS = 1000; - public static final int MINS = 60*SECS; - + public static final int MINS = 60 * SECS; static SyncManager INSTANCE; - static int mStatus = AWAKE; - static boolean mToothpicks = false; static Object mSyncToken = new Object(); static Thread mServiceThread = null; - - HashMap serviceMap = new HashMap (); + HashMap mServiceMap = new HashMap(); + HashMap mSyncErrorMap = new HashMap(); boolean mStop = false; SharedPreferences mSettings; Handler mHandler = new Handler(); AccountObserver mAccountObserver; MailboxObserver mMailboxObserver; + SyncedMessageObserver mSyncedMessageObserver; + String mNextWaitReason; - final RemoteCallbackList mCallbacks - = new RemoteCallbackList(); + static private HashMap mWakeLocks = new HashMap(); + static private HashMap mPendingIntents = + new HashMap(); + static private WakeLock mWakeLock = null; + + final RemoteCallbackList mCallbacks = + new RemoteCallbackList(); private final ISyncManager.Stub mBinder = new ISyncManager.Stub() { public int validate(String protocol, String host, String userName, String password, int port, boolean ssl) throws RemoteException { try { - ProtocolService.validate(EasService.class, host, userName, password, port, ssl, + AbstractSyncService.validate(EasSyncService.class, host, userName, password, port, ssl, SyncManager.this); return MessagingException.NO_ERROR; } catch (MessagingException e) { return e.getExceptionType(); } } + public void registerCallback(ISyncManagerCallback cb) { - if (cb != null) mCallbacks.register(cb); + if (cb != null) { + mCallbacks.register(cb); + } } + public void unregisterCallback(ISyncManagerCallback cb) { - if (cb != null) mCallbacks.unregister(cb); + if (cb != null) { + mCallbacks.unregister(cb); + } } + public boolean startSync(long mailboxId) throws RemoteException { // TODO Auto-generated method stub return false; } + public boolean stopSync(long mailboxId) throws RemoteException { // TODO Auto-generated method stub return false; } + public boolean updateFolderList(long accountId) throws RemoteException { // TODO Auto-generated method stub return false; } + public boolean loadMore(long messageId, ISyncManagerCallback cb) throws RemoteException { // TODO Auto-generated method stub return false; } + public boolean createFolder(long accountId, String name) throws RemoteException { // TODO Auto-generated method stub return false; } + public boolean deleteFolder(long accountId, String name) throws RemoteException { // TODO Auto-generated method stub return false; } + public boolean renameFolder(long accountId, String oldName, String newName) - throws RemoteException { + throws RemoteException { // TODO Auto-generated method stub return false; } + public boolean loadAttachment(long messageId, Attachment att, ISyncManagerCallback cb) - throws RemoteException { + throws RemoteException { // TODO Auto-generated method stub return false; } @@ -145,38 +172,38 @@ public class SyncManager extends Service implements Runnable { Context context = getContext(); // At startup, we want to see what EAS accounts exist and cache them - Cursor c = getContentResolver().query(Account.CONTENT_URI, - Account.CONTENT_PROJECTION, null, null, null); + Cursor c = getContentResolver().query(Account.CONTENT_URI, Account.CONTENT_PROJECTION, + null, null, null); try { collectEasAccounts(c, mAccountIds); } finally { c.close(); } - for (long accountId: mAccountIds) { - int cnt = Mailbox.count(context, Mailbox.CONTENT_URI, - "accountKey=" + accountId, null); + for (long accountId : mAccountIds) { + int cnt = Mailbox.count(context, Mailbox.CONTENT_URI, "accountKey=" + accountId, + null); if (cnt == 0) { initializeAccount(accountId); } } } - public void onChange (boolean selfChange) { + public void onChange(boolean selfChange) { // A change to the list requires us to scan for deletions (to stop running syncs) // At startup, we want to see what accounts exist and cache them ArrayList currentIds = new ArrayList(); - Cursor c = getContentResolver().query(Account.CONTENT_URI, - Account.CONTENT_PROJECTION, null, null, null); + Cursor c = getContentResolver().query(Account.CONTENT_URI, Account.CONTENT_PROJECTION, + null, null, null); try { collectEasAccounts(c, currentIds); - for (long accountId: mAccountIds) { + for (long accountId : mAccountIds) { if (!currentIds.contains(accountId)) { // This is a deletion; shut down any account-related syncs accountDeleted(accountId); } } - for (long accountId: currentIds) { + for (long accountId : currentIds) { if (!mAccountIds.contains(accountId)) { // This is an addition; create our magic hidden mailbox... initializeAccount(accountId); @@ -191,7 +218,7 @@ public class SyncManager extends Service implements Runnable { kick(); } - private void collectEasAccounts (Cursor c, ArrayList ids) { + void collectEasAccounts(Cursor c, ArrayList ids) { Context context = getContext(); while (c.moveToNext()) { long hostAuthId = c.getLong(Account.CONTENT_HOST_AUTH_KEY_RECV_COLUMN); @@ -204,7 +231,7 @@ public class SyncManager extends Service implements Runnable { } } - private void initializeAccount (long acctId) { + void initializeAccount(long acctId) { Account acct = Account.restoreAccountWithId(getContext(), acctId); Mailbox main = new Mailbox(); main.mDisplayName = "_main"; @@ -217,15 +244,14 @@ public class SyncManager extends Service implements Runnable { INSTANCE.log("Initializing account: " + acct.mDisplayName); } - private void accountDeleted (long acctId) { + void accountDeleted(long acctId) { synchronized (mSyncToken) { List deletedBoxes = new ArrayList(); - for (Long mid : INSTANCE.serviceMap.keySet()) { - Mailbox box = - Mailbox.restoreMailboxWithId(INSTANCE, mid); + for (Long mid : INSTANCE.mServiceMap.keySet()) { + Mailbox box = Mailbox.restoreMailboxWithId(INSTANCE, mid); if (box != null) { if (box.mAccountKey == acctId) { - ProtocolService svc = INSTANCE.serviceMap.get(mid); + AbstractSyncService svc = INSTANCE.mServiceMap.get(mid); if (svc != null) { svc.stop(); svc.mThread.interrupt(); @@ -235,7 +261,7 @@ public class SyncManager extends Service implements Runnable { } } for (Long mid : deletedBoxes) { - INSTANCE.serviceMap.remove(mid); + INSTANCE.mServiceMap.remove(mid); } } } @@ -246,27 +272,86 @@ public class SyncManager extends Service implements Runnable { super(handler); } - public void onChange (boolean selfChange) { + public void onChange(boolean selfChange) { // See if there's anything to do... kick(); } } + class SyncedMessageObserver extends ContentObserver { + long maxChangedId = 0; + long maxDeletedId = 0; + Intent syncAlarmIntent = new Intent(INSTANCE, UserSyncAlarmReceiver.class); + PendingIntent syncAlarmPendingIntent = + PendingIntent.getBroadcast(INSTANCE, 0, syncAlarmIntent, 0); + AlarmManager alarmManager = (AlarmManager)INSTANCE.getSystemService(Context.ALARM_SERVICE); + final String[] MAILBOX_DATA_PROJECTION = {MessageColumns.MAILBOX_KEY, SyncColumns.DATA}; + + public SyncedMessageObserver(Handler handler) { + super(handler); + } + + public void onChange(boolean selfChange) { + INSTANCE.log("SyncedMessage changed: (re)setting alarm for 10s"); + alarmManager.set(AlarmManager.RTC_WAKEUP, + System.currentTimeMillis() + (10*SECS), syncAlarmPendingIntent); + } + } + + public class SyncStatus { + static public final int NOT_RUNNING = 0; + + static public final int DIED = 1; + + static public final int SYNC = 2; + + static public final int IDLE = 3; + } + + class SyncError { + int reason; + boolean fatal = false; + long holdEndTime; + long holdDelay = 0; + + SyncError(int _reason, boolean _fatal) { + reason = _reason; + fatal = _fatal; + escalate(); + } + + /** + * We increase the hold on I/O errors in 30 second increments to 5 minutes + */ + void escalate() { + if (holdDelay < 5*MINS) { + holdDelay += 30*SECS; + } + holdEndTime = System.currentTimeMillis() + holdDelay; + } + } + @Override public IBinder onBind(Intent arg0) { return mBinder; } - public void log (String str) { - Log.v("EmailApp:MailService", str); + public void log(String str) { + if (Eas.USER_DEBUG) { + Log.d(TAG, str); + } } @Override - public void onCreate () { + public void onCreate() { + if (INSTANCE != null) { + throw new RuntimeException("\n************ ALREADY RUNNING *************\n"); + } INSTANCE = this; mAccountObserver = new AccountObserver(mHandler); mMailboxObserver = new MailboxObserver(mHandler); + mSyncedMessageObserver = new SyncedMessageObserver(mHandler); // Start our thread... if (mServiceThread == null || !mServiceThread.isAlive()) { @@ -278,19 +363,29 @@ public class SyncManager extends Service implements Runnable { } } - static private HashMap mWakeLocks = new HashMap(); - static private HashMap mPendingIntents = - new HashMap(); - static private WakeLock mWakeLock = null; + /** + * Informs SyncManager that an account has a new folder list; as a result, any existing folder + * might have become invalid. Therefore, we act as if the account has been deleted, and then + * we reinitialize it. + * + * @param acctId + */ + static public void folderListReloaded(long acctId) { + if (INSTANCE != null) { + AccountObserver obs = INSTANCE.mAccountObserver; + obs.accountDeleted(acctId); + obs.initializeAccount(acctId); + } + } - static public void acquireWakeLock (long id) { + static public void acquireWakeLock(long id) { synchronized (mWakeLocks) { Boolean lock = mWakeLocks.get(id); if (lock == null) { INSTANCE.log("+WakeLock requested for " + id); if (mWakeLock == null) { - PowerManager pm = - (PowerManager) INSTANCE.getSystemService(Context.POWER_SERVICE); + PowerManager pm = (PowerManager)INSTANCE + .getSystemService(Context.POWER_SERVICE); mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "MAIL_SERVICE"); mWakeLock.acquire(); INSTANCE.log("+WAKE LOCK ACQUIRED"); @@ -300,7 +395,7 @@ public class SyncManager extends Service implements Runnable { } } - static public void releaseWakeLock (long id) { + static public void releaseWakeLock(long id) { synchronized (mWakeLocks) { Boolean lock = mWakeLocks.get(id); if (lock != null) { @@ -313,21 +408,21 @@ public class SyncManager extends Service implements Runnable { } } } - } - - static private String alarmOwner (long id) { - if (id == -1) { - return "MailService"; - } - else return "Mailbox " + Long.toString(id); } - static private void clearAlarm (long id) { + static private String alarmOwner(long id) { + if (id == -1) { + return "MailService"; + } else + return "Mailbox " + Long.toString(id); + } + + static private void clearAlarm(long id) { synchronized (mPendingIntents) { PendingIntent pi = mPendingIntents.get(id); if (pi != null) { - AlarmManager alarmManager = - (AlarmManager)INSTANCE.getSystemService(Context.ALARM_SERVICE); + AlarmManager alarmManager = (AlarmManager)INSTANCE + .getSystemService(Context.ALARM_SERVICE); alarmManager.cancel(pi); INSTANCE.log("+Alarm cleared for " + alarmOwner(id)); mPendingIntents.remove(id); @@ -335,25 +430,25 @@ public class SyncManager extends Service implements Runnable { } } - static private void setAlarm (long id, long millis) { + static private void setAlarm(long id, long millis) { synchronized (mPendingIntents) { PendingIntent pi = mPendingIntents.get(id); if (pi == null) { - Intent i = new Intent(INSTANCE, KeepAliveReceiver.class); + Intent i = new Intent(INSTANCE, MailboxAlarmReceiver.class); i.putExtra("mailbox", id); i.setData(Uri.parse("Box" + id)); pi = PendingIntent.getBroadcast(INSTANCE, 0, i, 0); mPendingIntents.put(id, pi); - AlarmManager alarmManager = - (AlarmManager)INSTANCE.getSystemService(Context.ALARM_SERVICE); + AlarmManager alarmManager = (AlarmManager)INSTANCE + .getSystemService(Context.ALARM_SERVICE); alarmManager.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + millis, pi); INSTANCE.log("+Alarm set for " + alarmOwner(id) + ", " + millis + "ms"); } } } - static private void clearAlarms () { + static private void clearAlarms() { AlarmManager alarmManager = (AlarmManager)INSTANCE.getSystemService(Context.ALARM_SERVICE); synchronized (mPendingIntents) { for (PendingIntent pi : mPendingIntents.values()) { @@ -361,33 +456,36 @@ public class SyncManager extends Service implements Runnable { } mPendingIntents.clear(); } - } + } - static public void runAwake (long id) { + static public void runAwake(long id) { acquireWakeLock(id); clearAlarm(id); } - static public void runAsleep (long id, long millis) { + static public void runAsleep(long id, long millis) { setAlarm(id, millis); releaseWakeLock(id); } - static public void ping (long id) { - ProtocolService service = INSTANCE.serviceMap.get(id); - if (service != null) { - Mailbox m = Mailbox.restoreMailboxWithId(INSTANCE, id); - if (m != null) { - service.mAccount = - Account.restoreAccountWithId(INSTANCE, m.mAccountKey); - service.mMailbox = m; - service.ping(); + static public void ping(long id) { + if (id < 0) { + kick(); + } else { + AbstractSyncService service = INSTANCE.mServiceMap.get(id); + if (service != null) { + Mailbox m = Mailbox.restoreMailboxWithId(INSTANCE, id); + if (m != null) { + service.mAccount = Account.restoreAccountWithId(INSTANCE, m.mAccountKey); + service.mMailbox = m; + service.ping(); + } } } } @Override - public void onDestroy () { + public void onDestroy() { log("!!! MaiLService onDestroy"); if (mWakeLock != null) { mWakeLock.release(); @@ -398,7 +496,7 @@ public class SyncManager extends Service implements Runnable { public class ConnectivityReceiver extends BroadcastReceiver { @Override - public void onReceive (Context context, Intent intent) { + public void onReceive(Context context, Intent intent) { Bundle b = intent.getExtras(); if (b != null) { NetworkInfo a = (NetworkInfo)b.get("networkInfo"); @@ -406,10 +504,12 @@ public class SyncManager extends Service implements Runnable { State state = a.getState(); if (state == State.CONNECTED) { info += " CONNECTED"; + kick(); } else if (state == State.CONNECTING) { info += " CONNECTING"; } else if (state == State.DISCONNECTED) { info += " DISCONNECTED"; + kick(); } else if (state == State.DISCONNECTING) { info += " DISCONNECTING"; } else if (state == State.SUSPENDED) { @@ -422,71 +522,56 @@ public class SyncManager extends Service implements Runnable { } } - private void pause (int ms) { + private void pause(int ms) { try { Thread.sleep(ms); } catch (InterruptedException e) { } } - private void startService (ProtocolService service, Mailbox m) { + private void startService(AbstractSyncService service, Mailbox m) { synchronized (mSyncToken) { String mailboxName = m.mDisplayName; String accountName = service.mAccount.mDisplayName; Thread thread = new Thread(service, mailboxName + "(" + accountName + ")"); log("Starting thread for " + mailboxName + " in account " + accountName); thread.start(); - serviceMap.put(m.mId, service); + mServiceMap.put(m.mId, service); } } - private void startService (Mailbox m) { + private void startService(Mailbox m) { synchronized (mSyncToken) { - Account acct = - Account.restoreAccountWithId(this, m.mAccountKey); + Account acct = Account.restoreAccountWithId(this, m.mAccountKey); if (acct != null) { - ProtocolService service; - service = new EasService(this, m); + AbstractSyncService service; + service = new EasSyncService(this, m); startService(service, m); } } } - private void startSleep () { + private void stopServices() { synchronized (mSyncToken) { - // Shut everything down - boolean stoppedOne = false; - // Keep track of which services we've stopped ArrayList toStop = new ArrayList(); - // Shut down all of our running services - for (Long mid : serviceMap.keySet()) { - toStop.add(mid); - stoppedOne = true; + + // Keep track of which services to stop + for (Long mailboxId : mServiceMap.keySet()) { + toStop.add(mailboxId); } - for (Long mid: toStop) { - ProtocolService svc = serviceMap.get(mid); - log("Going to sleep: shutting down " + svc.mAccount.mDisplayName + - "/" + svc.mMailboxName); + // Shut down all of those running services + for (Long mailboxId : toStop) { + AbstractSyncService svc = mServiceMap.get(mailboxId); + log("Shutting down " + svc.mAccount.mDisplayName + '/' + svc.mMailbox.mDisplayName); svc.stop(); svc.mThread.interrupt(); - stoppedOne = true; - } - // Remove the stopped services from the map - //for (Long mid : stopped) - // serviceMap.remove(mid); - // Let the UI know - if (stoppedOne) { } } } - private void broadcastSleep () { - } - - public void run () { - log("MailService: run"); - Debug.waitForDebugger(); + public void run() { + log("Running"); mStop = false; runAwake(-1); @@ -494,229 +579,157 @@ public class SyncManager extends Service implements Runnable { ContentResolver resolver = getContentResolver(); resolver.registerContentObserver(Account.CONTENT_URI, false, mAccountObserver); resolver.registerContentObserver(Mailbox.CONTENT_URI, false, mMailboxObserver); + resolver.registerContentObserver(Message.SYNCED_CONTENT_URI, true, mSyncedMessageObserver); ConnectivityReceiver cr = new ConnectivityReceiver(); registerReceiver(cr, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)); ConnectivityManager cm = - (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); - - mSettings = PreferenceManager.getDefaultSharedPreferences(this); - GregorianCalendar calendar = new GregorianCalendar(); - - mStatus = AWAKE; + (ConnectivityManager)getSystemService(Context.CONNECTIVITY_SERVICE); try { while (!mStop) { runAwake(-1); - log("%%MailService heartbeat"); + log("Looking for something to do..."); + int cnt = 0; while (!mStop) { NetworkInfo info = cm.getActiveNetworkInfo(); if (info != null && info.isConnected()) { break; } else { + if (cnt++ == 2) { + stopServices(); + } pause(10*SECS); } } - - long nextWait = 10*MINS; - long now = System.currentTimeMillis(); - - // We could send notices of sleep time changes and otherwise cache all of this... - long sleepHours = mSettings.getLong("sleep_hours", 0); - if (sleepHours != 0) { - boolean wantSleep = false; - calendar.setTimeInMillis(now); - int nowHour = calendar.get(GregorianCalendar.HOUR_OF_DAY); - int nowMinute = calendar.get(GregorianCalendar.MINUTE); - - long sleepStart = sleepHours >> 32; - int startHour = (int)(sleepStart / 100); - int startMinute = (int)(sleepStart % 100); - - long sleepEnd = sleepHours & 0x00000000FFFFFFFFL; - int endHour = (int)(sleepEnd / 100); - int endMinute = (int)(sleepEnd % 100); - - if (sleepStart > sleepEnd) { - if ((nowHour > startHour) || - (nowHour == startHour && nowMinute >= startMinute) || - (nowHour < endHour) || - (nowHour == endHour && nowMinute <= endMinute)) - wantSleep = true; - } else if (((startHour < nowHour || - (startHour == nowHour && nowMinute >= startMinute)) && - ((nowHour < endHour) || - (nowHour == endHour && nowMinute <= endMinute)))) - wantSleep = true; - - if (wantSleep && (mStatus == AWAKE)) { - mStatus = SLEEP_HOURS; - startSleep(); - broadcastSleep(); - } else if (!wantSleep && (mStatus == SLEEP_HOURS)) { - mStatus = AWAKE; - broadcastSleep(); - } - } - - boolean sleepWeekends = mSettings.getBoolean("sleep_weekends", false); - if ((mStatus != SLEEP_HOURS) && ((mStatus != AWAKE) || sleepWeekends)) { - boolean wantSleep = false; - calendar.setTimeInMillis(now); - int day = calendar.get(GregorianCalendar.DAY_OF_WEEK); - if (sleepWeekends && - (day == GregorianCalendar.SATURDAY || - day == GregorianCalendar.SUNDAY)) { - wantSleep = true; - } - if ((mStatus == AWAKE) && wantSleep) { - mStatus = SLEEP_WEEKEND; - startSleep(); - broadcastSleep(); - } else if ((mStatus != AWAKE) && !wantSleep) { - // Wake up!! - mStatus = AWAKE; - broadcastSleep(); - } - } - - boolean offline = mSettings.getBoolean("offline", false); - if (mStatus == AWAKE || mStatus == OFFLINE) { - boolean wantSleep = offline; - if ((mStatus == AWAKE) && wantSleep) { - mStatus = OFFLINE; - startSleep(); - broadcastSleep(); - } else if ((mStatus == OFFLINE) && !wantSleep) { - // Wake up!! - mStatus = AWAKE; - broadcastSleep(); - } - } - - if (!mStop && ((mStatus == AWAKE) || mToothpicks)) { - // Start up threads that need it... + if (!mStop) { + mNextWaitReason = "Heartbeat"; + long nextWait = checkMailboxes(); try { - Cursor c = getContentResolver().query(Mailbox.CONTENT_URI, - Mailbox.CONTENT_PROJECTION, null, null, null); - while (c.moveToNext()) { - // TODO Could be much faster - just get cursor of ones we're watching... - long aid = c.getLong(Mailbox.CONTENT_ACCOUNT_KEY_COLUMN); - // Only check mailboxes for EAS accounts - if (!mAccountObserver.mAccountIds.contains(aid)) { - continue; + synchronized (INSTANCE) { + if (nextWait < 0) { + log("Negative wait? Setting to 1s"); + nextWait = 1*SECS; } - long mid = c.getLong(Mailbox.CONTENT_ID_COLUMN); - ProtocolService service = serviceMap.get(mid); - if (service == null) { - long freq = c.getInt(Mailbox.CONTENT_SYNC_FREQUENCY_COLUMN); - if (freq == Account.CHECK_INTERVAL_PUSH) { - Mailbox m = - EmailContent.getContent(c, Mailbox.class); - // Either push, or 30 mins (default for idle timeout) - if (((m.mFlags & Mailbox.FLAG_CANT_PUSH) == 0) - || ((now - m.mSyncTime) > (1000 * 60 * 30L))) { - startService(m); - } - } else if (freq == -19) { - // See if we've got anything to do... - int cnt = EmailContent.count(this, - Message.CONTENT_URI, "mailboxKey=" + - mid + " and syncServerId=0", null); - if (cnt > 0) { - Mailbox m = EmailContent.getContent(c, Mailbox.class); - startService(new EasOutboxService(this, m), m); - } - } else if (freq > 0 && freq <= 1440) { - long lastSync = - c.getLong(Mailbox.CONTENT_SYNC_TIME_COLUMN); - if (now - lastSync > (freq * 60000L)) { - Mailbox m = EmailContent.getContent(c, Mailbox.class); - startService(m); - } - } - } else { - Thread thread = service.mThread; - if (!thread.isAlive()) { - serviceMap.remove(mid); - // Restart this if necessary - if (nextWait > 3*SECS) { - nextWait = 3*SECS; - } - } else { - long requestTime = service.mRequestTime; - if (requestTime > 0) { - long timeToRequest = requestTime - now; - if (service instanceof ProtocolService && - timeToRequest <= 0) { - service.mRequestTime = 0; - service.ping(); - } else if (requestTime > 0 && timeToRequest < nextWait) { - if (timeToRequest < 11*MINS) { - nextWait = - timeToRequest < 250 ? 250 : timeToRequest; - } else { - log("Illegal timeToRequest: " + timeToRequest); - } - } - } - } + if (nextWait > (30*SECS)) { + runAsleep(-1, nextWait - 1000); } + log("Next awake in " + (nextWait / 1000) + "s: " + mNextWaitReason); + INSTANCE.wait(nextWait); } - c.close(); - - } catch (Exception e1) { - log("Exception to follow..."); + } catch (InterruptedException e) { + // Needs to be caught, but causes no problem } - } - - try { - synchronized (INSTANCE) { - if (nextWait < 0) { - System.err.println("WTF?"); - nextWait = 1*SECS; - } - if (nextWait > 30*SECS) { - runAsleep(-1, nextWait - 1000); - } - - log("%%MailService sleeping for " + (nextWait / 1000) + " s"); - INSTANCE.wait(nextWait); - } - } catch (InterruptedException e) { - log("IOException to follow..."); - } - - if (mStop) { - startSleep(); - log("Shutdown requested."); + } else { + stopServices(); + log("Shutdown requested"); return; } } - } catch (Throwable e) { - log("MailService crashed."); } finally { - log("Goodbye."); + log("Goodbye"); } startService(new Intent(this, SyncManager.class)); throw new RuntimeException("MailService crash; please restart me..."); } - static public void serviceRequest (Mailbox m) { - serviceRequest(m.mId, 10*SECS); - } - - static public void serviceRequest (long mailboxId) { - serviceRequest(mailboxId, 10*SECS); - } - - static public void serviceRequest (long mailboxId, long ms) { + long checkMailboxes () { + long nextWait = 10*MINS; + long now = System.currentTimeMillis(); + // Start up threads that need it... + Cursor c = getContentResolver().query(Mailbox.CONTENT_URI, + Mailbox.CONTENT_PROJECTION, null, null, null); try { - if (INSTANCE == null) + while (c.moveToNext()) { + // TODO Could be much faster - just get cursor of + // ones we're watching... + long aid = c.getLong(Mailbox.CONTENT_ACCOUNT_KEY_COLUMN); + // Only check mailboxes for EAS accounts + if (!mAccountObserver.mAccountIds.contains(aid)) { + continue; + } + long mid = c.getLong(Mailbox.CONTENT_ID_COLUMN); + AbstractSyncService service = mServiceMap.get(mid); + if (service == null) { + // Check whether we're in a hold (temporary or permanent) + SyncError syncError = mSyncErrorMap.get(mid); + if (syncError != null && (syncError.fatal || now < syncError.holdEndTime)) { + if (!syncError.fatal) { + if (syncError.holdEndTime < (now + nextWait)) { + nextWait = syncError.holdEndTime - now; + mNextWaitReason = "Release hold"; + } + } + continue; + } + long freq = c.getInt(Mailbox.CONTENT_SYNC_FREQUENCY_COLUMN); + if (freq == Account.CHECK_INTERVAL_PUSH) { + Mailbox m = EmailContent.getContent(c, Mailbox.class); + startService(m); + } else if (c.getInt(Mailbox.CONTENT_TYPE_COLUMN) == Mailbox.TYPE_OUTBOX) { + int cnt = EmailContent.count(this, Message.CONTENT_URI, + "mailboxKey=" + mid + " and syncServerId=0", null); + if (cnt > 0) { + Mailbox m = EmailContent.getContent(c, Mailbox.class); + startService(new EasOutboxService(this, m), m); + } + } else if (freq > 0 && freq <= 1440) { + long lastSync = c.getLong(Mailbox.CONTENT_SYNC_TIME_COLUMN); + if (now - lastSync > (freq*MINS)) { + Mailbox m = EmailContent.getContent(c, Mailbox.class); + startService(m); + } + } + } else { + Thread thread = service.mThread; + if (!thread.isAlive()) { + mServiceMap.remove(mid); + // Restart this if necessary + if (nextWait > 3*SECS) { + nextWait = 3*SECS; + mNextWaitReason = "Clean up dead thread(s)"; + } + } else { + long requestTime = service.mRequestTime; + if (requestTime > 0) { + long timeToRequest = requestTime - now; + if (service instanceof AbstractSyncService && timeToRequest <= 0) { + service.mRequestTime = 0; + service.ping(); + } else if (requestTime > 0 && timeToRequest < nextWait) { + if (timeToRequest < 11*MINS) { + nextWait = timeToRequest < 250 ? 250 : timeToRequest; + mNextWaitReason = "Sync data change"; + } else { + log("Illegal timeToRequest: " + timeToRequest); + } + } + } + } + } + } + } finally { + c.close(); + } + return nextWait; + } + + static public void serviceRequest(Mailbox m) { + serviceRequest(m.mId, 5*SECS); + } + + static public void serviceRequest(long mailboxId) { + serviceRequest(mailboxId, 5*SECS); + } + + static public void serviceRequest(long mailboxId, long ms) { + try { + if (INSTANCE == null) { return; - ProtocolService service = INSTANCE.serviceMap.get(mailboxId); + } + AbstractSyncService service = INSTANCE.mServiceMap.get(mailboxId); if (service != null) { service.mRequestTime = System.currentTimeMillis() + ms; kick(); @@ -728,10 +741,10 @@ public class SyncManager extends Service implements Runnable { } } - static public void serviceRequestImmediate (long mailboxId) { - ProtocolService service = INSTANCE.serviceMap.get(mailboxId); + static public void serviceRequestImmediate(long mailboxId) { + AbstractSyncService service = INSTANCE.mServiceMap.get(mailboxId); if (service != null) { - service.mRequestTime = System.currentTimeMillis() ; + service.mRequestTime = System.currentTimeMillis(); Mailbox m = Mailbox.restoreMailboxWithId(INSTANCE, mailboxId); service.mAccount = Account.restoreAccountWithId(INSTANCE, m.mAccountKey); service.mMailbox = m; @@ -739,16 +752,17 @@ public class SyncManager extends Service implements Runnable { } } - static public void partRequest (PartRequest req) { + static public void partRequest(PartRequest req) { Message msg = Message.restoreMessageWithId(INSTANCE, req.emailId); if (msg == null) { return; } long mailboxId = msg.mMailboxKey; - ProtocolService service = INSTANCE.serviceMap.get(mailboxId); + AbstractSyncService service = INSTANCE.mServiceMap.get(mailboxId); - if (service == null) + if (service == null) { service = startManualSync(mailboxId); + } if (service != null) { service.mRequestTime = System.currentTimeMillis(); @@ -763,7 +777,7 @@ public class SyncManager extends Service implements Runnable { return null; } long mailboxId = msg.mMailboxKey; - ProtocolService service = INSTANCE.serviceMap.get(mailboxId); + AbstractSyncService service = INSTANCE.mServiceMap.get(mailboxId); if (service != null) { service.mRequestTime = System.currentTimeMillis(); return service.hasPartRequest(emailId, part); @@ -777,26 +791,38 @@ public class SyncManager extends Service implements Runnable { return; } long mailboxId = msg.mMailboxKey; - ProtocolService service = INSTANCE.serviceMap.get(mailboxId); + AbstractSyncService service = INSTANCE.mServiceMap.get(mailboxId); if (service != null) { service.mRequestTime = System.currentTimeMillis(); service.cancelPartRequest(emailId, part); } } - public class SyncStatus { - static public final int NOT_RUNNING = 0; - static public final int DIED = 1; - static public final int SYNC = 2; - static public final int IDLE = 3; + /** + * Determine whether a given Mailbox can be synced, i.e. is not already syncing and is not in + * an error state + * + * @param mailboxId + * @return whether or not the Mailbox is available for syncing (i.e. is a valid push target) + */ + static public boolean canSync(long mailboxId) { + // Already syncing... + if (INSTANCE.mServiceMap.get(mailboxId) != null) { + return false; + } + // Blocked from syncing (transient or permanent) + if (INSTANCE.mSyncErrorMap.get(mailboxId) != null) { + return false; + } + return true; } - - static public int getSyncStatus (long mid) { + + static public int getSyncStatus(long mailboxId) { synchronized (mSyncToken) { - if (INSTANCE == null || INSTANCE.serviceMap == null) { + if (INSTANCE == null || INSTANCE.mServiceMap == null) { return SyncStatus.NOT_RUNNING; } - ProtocolService svc = INSTANCE.serviceMap.get(mid); + AbstractSyncService svc = INSTANCE.mServiceMap.get(mailboxId); if (svc == null) { return SyncStatus.NOT_RUNNING; } else { @@ -809,27 +835,29 @@ public class SyncManager extends Service implements Runnable { } } - static public ProtocolService startManualSync (long mid) { - if (INSTANCE == null || INSTANCE.serviceMap == null) + static public AbstractSyncService startManualSync(long mailboxId) { + if (INSTANCE == null || INSTANCE.mServiceMap == null) { return null; + } INSTANCE.log("startManualSync"); synchronized (mSyncToken) { - if (INSTANCE.serviceMap.get(mid) == null) { - Mailbox m = Mailbox.restoreMailboxWithId(INSTANCE, mid); + if (INSTANCE.mServiceMap.get(mailboxId) == null) { + INSTANCE.mSyncErrorMap.remove(mailboxId); + Mailbox m = Mailbox.restoreMailboxWithId(INSTANCE, mailboxId); INSTANCE.log("Starting sync for " + m.mDisplayName); INSTANCE.startService(m); } } - return INSTANCE.serviceMap.get(mid); + return INSTANCE.mServiceMap.get(mailboxId); } // DO NOT CALL THIS IN A LOOP ON THE SERVICEMAP - static public void stopManualSync (long mid) { - if (INSTANCE == null || INSTANCE.serviceMap == null) { + static public void stopManualSync(long mailboxId) { + if (INSTANCE == null || INSTANCE.mServiceMap == null) { return; } synchronized (mSyncToken) { - ProtocolService svc = INSTANCE.serviceMap.get(mid); + AbstractSyncService svc = INSTANCE.mServiceMap.get(mailboxId); if (svc != null) { INSTANCE.log("Stopping sync for " + svc.mMailboxName); svc.stop(); @@ -838,7 +866,7 @@ public class SyncManager extends Service implements Runnable { } } - static public void kick () { + static public void kick() { if (INSTANCE == null) { return; } @@ -848,20 +876,19 @@ public class SyncManager extends Service implements Runnable { } } - static public void kick (long mid) { - Mailbox m = Mailbox.restoreMailboxWithId(INSTANCE, mid); + static public void kick(long mailboxId) { + Mailbox m = Mailbox.restoreMailboxWithId(INSTANCE, mailboxId); int syncType = m.mSyncFrequency; if (syncType == Account.CHECK_INTERVAL_PUSH) { - SyncManager.serviceRequestImmediate(mid); + SyncManager.serviceRequestImmediate(mailboxId); } else { - SyncManager.startManualSync(mid); + SyncManager.startManualSync(mailboxId); } } - - static public void accountUpdated (long acctId) { + static public void accountUpdated(long acctId) { synchronized (mSyncToken) { - for (ProtocolService svc : INSTANCE.serviceMap.values()) { + for (AbstractSyncService svc : INSTANCE.mServiceMap.values()) { if (svc.mAccount.mId == acctId) { svc.mAccount = Account.restoreAccountWithId(INSTANCE, acctId); } @@ -869,38 +896,42 @@ public class SyncManager extends Service implements Runnable { } } - static public int status () { - return mStatus; + static public void done(AbstractSyncService svc) { + long mailboxId = svc.mMailboxId; + HashMap errorMap = INSTANCE.mSyncErrorMap; + SyncError syncError = errorMap.get(mailboxId); + INSTANCE.mServiceMap.remove(mailboxId); + int exitStatus = svc.mExitStatus; + switch (exitStatus) { + case AbstractSyncService.EXIT_DONE: + errorMap.remove(mailboxId); + break; + case AbstractSyncService.EXIT_IO_ERROR: + if (syncError != null) { + syncError.escalate(); + } else { + errorMap.put(mailboxId, INSTANCE.new SyncError(exitStatus, false)); + } + kick(); + break; + case AbstractSyncService.EXIT_LOGIN_FAILURE: + case AbstractSyncService.EXIT_EXCEPTION: + errorMap.put(mailboxId, INSTANCE.new SyncError(exitStatus, true)); + break; + } } - static public boolean isSleeping () { - return (mStatus == SLEEP_HOURS || mStatus == SLEEP_WEEKEND); - } - - static public void forceAwake (boolean wake) { - mToothpicks = wake; - kick(); - } - - static public boolean isForceAwake () { - return mToothpicks; - } - - static public void done (ProtocolService svc) { - INSTANCE.serviceMap.remove(svc.mMailboxId); - } - - public static void shutdown () { + public static void shutdown() { INSTANCE.mStop = true; kick(); INSTANCE.stopSelf(); } - static public String serviceName (long id) { + static public String serviceName(long id) { if (id < 0) { - return "MailService"; + return "SyncManager"; } else { - ProtocolService service = INSTANCE.serviceMap.get(id); + AbstractSyncService service = INSTANCE.mServiceMap.get(id); if (service != null) { return service.mThread.getName(); } else { @@ -909,7 +940,7 @@ public class SyncManager extends Service implements Runnable { } } - static public Context getContext () { + static public Context getContext() { if (INSTANCE == null) { return null; } diff --git a/src/com/android/exchange/UserSyncAlarmReceiver.java b/src/com/android/exchange/UserSyncAlarmReceiver.java new file mode 100644 index 000000000..c93e713cb --- /dev/null +++ b/src/com/android/exchange/UserSyncAlarmReceiver.java @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2008-2009 Marc Blank + * Licensed to 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 java.util.ArrayList; + +import com.android.exchange.EmailContent.Message; +import com.android.exchange.EmailContent.MessageColumns; +import com.android.exchange.EmailContent.SyncColumns; + +import android.content.BroadcastReceiver; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.database.Cursor; +import android.util.Log; + +/** + * UserSyncAlarmReceiver (USAR) is used by the SyncManager to start up-syncs of user-modified data + * back to the Exchange server. + * + * Here's how this works for Email, for example: + * + * 1) User modifies or deletes an email from the UI. + * 2) SyncManager, which has a ContentObserver watching the Message class, is alerted to a change + * 3) SyncManager sets an alarm (to be received by USAR) for a few seconds in the + * future (currently 15), the delay preventing excess syncing (think of it as a debounce mechanism). + * 4) USAR Receiver's onReceive method is called + * 5) USAR goes through all change and deletion records and compiles a list of mailboxes which have + * changes to be uploaded. + * 6) USAR calls SyncManager to start syncs of those mailboxes + * + */ +public class UserSyncAlarmReceiver extends BroadcastReceiver { + final String[] MAILBOX_DATA_PROJECTION = {MessageColumns.MAILBOX_KEY, SyncColumns.DATA}; + private static String TAG = "UserSyncAlarm"; + + @Override + public void onReceive(Context context, Intent intent) { + Log.v(TAG, "onReceive"); + ArrayList mailboxesToNotify = new ArrayList(); + ContentResolver cr = context.getContentResolver(); + int messageCount = 0; + // Find all of the deletions + Cursor c = cr.query(Message.DELETED_CONTENT_URI, MAILBOX_DATA_PROJECTION, + null, null, null); + try { + // Keep track of which mailboxes to notify; we'll only notify each one once + while (c.moveToNext()) { + messageCount++; + long mailboxId = c.getLong(0); + if (!mailboxesToNotify.contains(mailboxId)) { + mailboxesToNotify.add(mailboxId); + } + } + } finally { + c.close(); + } + + // Now, find changed messages + c = cr.query(Message.UPDATED_CONTENT_URI, MAILBOX_DATA_PROJECTION, + null, null, null); + try { + // Keep track of which mailboxes to notify; we'll only notify each one once + while (c.moveToNext()) { + messageCount++; + long mailboxId = c.getLong(0); + if (!mailboxesToNotify.contains(mailboxId)) { + mailboxesToNotify.add(mailboxId); + } + } + } finally { + c.close(); + } + + // Request service from the mailbox + for (Long mailboxId: mailboxesToNotify) { + SyncManager.serviceRequest(mailboxId); + } + Log.v(TAG, "Changed/Deleted messages: " + messageCount + ", mailboxes: " + + mailboxesToNotify.size()); + } +} diff --git a/src/com/android/exchange/adapter/EasCalendarSyncAdapter.java b/src/com/android/exchange/adapter/EasCalendarSyncAdapter.java new file mode 100644 index 000000000..f400a0c09 --- /dev/null +++ b/src/com/android/exchange/adapter/EasCalendarSyncAdapter.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2008-2009 Marc Blank + * Licensed to 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 java.io.ByteArrayInputStream; +import java.io.IOException; + +import com.android.exchange.EasSyncService; +import com.android.exchange.EmailContent.Mailbox; + +/** + * Sync adapter class for EAS calendars + * + */ +public class EasCalendarSyncAdapter extends EasSyncAdapter { + + public EasCalendarSyncAdapter(Mailbox mailbox) { + super(mailbox); + } + + @Override + public boolean parse(ByteArrayInputStream is, EasSyncService service) throws IOException { + // TODO Auto-generated method stub + return false; + } + + @Override + public String getCollectionName() { + return "Calendar"; + } + + @Override + public boolean sendLocalChanges(EasSerializer s, EasSyncService service) throws IOException { + // TODO Auto-generated method stub + return false; + } +} diff --git a/src/com/android/exchange/adapter/EasContactsSyncAdapter.java b/src/com/android/exchange/adapter/EasContactsSyncAdapter.java new file mode 100644 index 000000000..1fb4876d4 --- /dev/null +++ b/src/com/android/exchange/adapter/EasContactsSyncAdapter.java @@ -0,0 +1,385 @@ +/* + * Copyright (C) 2008-2009 Marc Blank + * Licensed to 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 java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; + +import android.content.ContentProviderOperation; +import android.content.ContentUris; +import android.content.ContentValues; +import android.database.Cursor; +import android.net.Uri; +import android.provider.Contacts; +import android.provider.Contacts.People; + +import com.android.exchange.EasSyncService; +import com.android.exchange.EmailContent.Mailbox; + +/** + * Sync adapter for EAS Contacts + * + */ +public class EasContactsSyncAdapter extends EasSyncAdapter { + + private static final String WHERE_SERVER_ID_AND_ACCOUNT = "_sync_id=?"; + + ArrayList mDeletedIdList = new ArrayList(); + + ArrayList mUpdatedIdList = new ArrayList(); + + public EasContactsSyncAdapter(Mailbox mailbox) { + super(mailbox); + } + + @Override + public boolean parse(ByteArrayInputStream is, EasSyncService service) throws IOException { + EasContactsSyncParser p = new EasContactsSyncParser(is, service); + return p.parse(); + } + + class EasContactsSyncParser extends EasContentParser { + + String[] mBindArgument = new String[1]; + + String mMailboxIdAsString; + + StringBuilder mExtraData = new StringBuilder(1024); + + public EasContactsSyncParser(InputStream in, EasSyncService service) throws IOException { + super(in, service); + //setDebug(true); // DON'T CHECK IN WITH THIS UNCOMMENTED + } + + class ContactMethod { + ContentValues values = new ContentValues(); + + ContactMethod(int kind, int type, String value) { + values.put(Contacts.ContactMethods.KIND, kind); + values.put(Contacts.ContactMethods.TYPE, type); + values.put(Contacts.ContactMethods.DATA, value); + } + } + + class Phone { + ContentValues values = new ContentValues(); + + Phone(int type, String value) { + values.put(Contacts.Phones.TYPE, type); + values.put(Contacts.Phones.NUMBER, value); + } + } + + @Override + public void wipe() { + // TODO Auto-generated method stub + } + + void saveExtraData (int tag) throws IOException { + mExtraData.append(name); + mExtraData.append("~"); + mExtraData.append(getValue()); + mExtraData.append('~'); + } + + public void addData(String serverId, ArrayList ops) + throws IOException { + String firstName = null; + String lastName = null; + String companyName = null; + ArrayList contactMethods = new ArrayList(); + ArrayList phones = new ArrayList(); + while (nextTag(EasTags.SYNC_APPLICATION_DATA) != END) { + switch (tag) { + case EasTags.CONTACTS_FIRST_NAME: + firstName = getValue(); + break; + case EasTags.CONTACTS_LAST_NAME: + lastName = getValue(); + break; + case EasTags.CONTACTS_COMPANY_NAME: + companyName = getValue(); + break; + case EasTags.CONTACTS_EMAIL1_ADDRESS: + case EasTags.CONTACTS_EMAIL2_ADDRESS: + case EasTags.CONTACTS_EMAIL3_ADDRESS: + contactMethods.add(new ContactMethod(Contacts.KIND_EMAIL, + Contacts.ContactMethods.TYPE_OTHER, getValue())); + break; + case EasTags.CONTACTS_BUSINESS2_TELEPHONE_NUMBER: + case EasTags.CONTACTS_BUSINESS_TELEPHONE_NUMBER: + phones.add(new Phone(Contacts.Phones.TYPE_WORK, getValue())); + break; + case EasTags.CONTACTS_BUSINESS_FAX_NUMBER: + phones.add(new Phone(Contacts.Phones.TYPE_FAX_WORK, getValue())); + break; + case EasTags.CONTACTS_HOME_FAX_NUMBER: + phones.add(new Phone(Contacts.Phones.TYPE_FAX_HOME, getValue())); + break; + case EasTags.CONTACTS_HOME_TELEPHONE_NUMBER: + case EasTags.CONTACTS_HOME2_TELEPHONE_NUMBER: + phones.add(new Phone(Contacts.Phones.TYPE_HOME, getValue())); + break; + case EasTags.CONTACTS_MOBILE_TELEPHONE_NUMBER: + case EasTags.CONTACTS_CAR_TELEPHONE_NUMBER: + phones.add(new Phone(Contacts.Phones.TYPE_MOBILE, getValue())); + break; + case EasTags.CONTACTS_PAGER_NUMBER: + phones.add(new Phone(Contacts.Phones.TYPE_PAGER, getValue())); + break; + // All tags that we don't use (except for a few like picture and body) need to + // be saved, even if we're not using them. Otherwise, when we upload changes, + // those items will be deleted back on the server. + case EasTags.CONTACTS_ANNIVERSARY: + case EasTags.CONTACTS_ASSISTANT_NAME: + case EasTags.CONTACTS_ASSISTANT_TELEPHONE_NUMBER: + case EasTags.CONTACTS_BIRTHDAY: + case EasTags.CONTACTS_BUSINESS_ADDRESS_CITY: + case EasTags.CONTACTS_BUSINESS_ADDRESS_COUNTRY: + case EasTags.CONTACTS_BUSINESS_ADDRESS_POSTAL_CODE: + case EasTags.CONTACTS_BUSINESS_ADDRESS_STATE: + case EasTags.CONTACTS_BUSINESS_ADDRESS_STREET: + case EasTags.CONTACTS_CATEGORIES: + case EasTags.CONTACTS_CATEGORY: + case EasTags.CONTACTS_CHILDREN: + case EasTags.CONTACTS_CHILD: + case EasTags.CONTACTS_DEPARTMENT: + case EasTags.CONTACTS_FILE_AS: + case EasTags.CONTACTS_HOME_ADDRESS_CITY: + case EasTags.CONTACTS_HOME_ADDRESS_COUNTRY: + case EasTags.CONTACTS_HOME_ADDRESS_POSTAL_CODE: + case EasTags.CONTACTS_HOME_ADDRESS_STATE: + case EasTags.CONTACTS_HOME_ADDRESS_STREET: + case EasTags.CONTACTS_JOB_TITLE: + case EasTags.CONTACTS_MIDDLE_NAME: + case EasTags.CONTACTS_OFFICE_LOCATION: + case EasTags.CONTACTS_OTHER_ADDRESS_CITY: + case EasTags.CONTACTS_OTHER_ADDRESS_COUNTRY: + case EasTags.CONTACTS_OTHER_ADDRESS_POSTAL_CODE: + case EasTags.CONTACTS_OTHER_ADDRESS_STATE: + case EasTags.CONTACTS_OTHER_ADDRESS_STREET: + case EasTags.CONTACTS_RADIO_TELEPHONE_NUMBER: + case EasTags.CONTACTS_SPOUSE: + case EasTags.CONTACTS_SUFFIX: + case EasTags.CONTACTS_TITLE: + case EasTags.CONTACTS_WEBPAGE: + case EasTags.CONTACTS_YOMI_COMPANY_NAME: + case EasTags.CONTACTS_YOMI_FIRST_NAME: + case EasTags.CONTACTS_YOMI_LAST_NAME: + case EasTags.CONTACTS_COMPRESSED_RTF: + //case EasTags.CONTACTS_PICTURE: + case EasTags.CONTACTS2_CUSTOMER_ID: + case EasTags.CONTACTS2_GOVERNMENT_ID: + case EasTags.CONTACTS2_IM_ADDRESS: + case EasTags.CONTACTS2_IM_ADDRESS_2: + case EasTags.CONTACTS2_IM_ADDRESS_3: + case EasTags.CONTACTS2_MANAGER_NAME: + case EasTags.CONTACTS2_COMPANY_MAIN_PHONE: + case EasTags.CONTACTS2_ACCOUNT_NAME: + case EasTags.CONTACTS2_NICKNAME: + case EasTags.CONTACTS2_MMS: + saveExtraData(tag); + break; + default: + skipTag(); + } + } + + // Ok, ready to create our contact... + // First pass, no batch... Eventually, move to changesParser + ContentValues values = new ContentValues(); + + // TODO Do something with the extras (i.e. find a home for them) + String extraData = mExtraData.toString(); + mService.userLog(extraData); + + // We must have first name, last name, or company name + String name; + if (firstName != null || lastName != null) { + if (firstName == null) { + name = lastName; + } else if (lastName == null) { + name = firstName; + } else { + name = firstName + ' ' + lastName; + } + } else if (companyName != null) { + name = companyName; + } else { + return; + } + + values.put(Contacts.People.NAME, name); + values.put("_sync_id", serverId); + // TODO Use proper value here; need to ask jham + //values.put("_sync_account", "EAS"); + Uri contactUri = + Contacts.People.createPersonInMyContactsGroup(mContentResolver, values); + + Uri contactMethodsUri = Uri.withAppendedPath(contactUri, + Contacts.People.ContactMethods.CONTENT_DIRECTORY); + for (ContactMethod cm: contactMethods) { + mContentResolver.insert(contactMethodsUri, cm.values); + //ops.add(ContentProviderOperation + // .newInsert(contactMethodsUri).withValues(cm.values).build()); + } + + Uri phoneUri = Uri.withAppendedPath(contactUri, People.Phones.CONTENT_DIRECTORY); + for (Phone phone: phones) { + mContentResolver.insert(phoneUri, phone.values); + //ops.add(ContentProviderOperation + // .newInsert(phoneUri).withValues(phone.values).build()); + } + } + + public void addParser(ArrayList ops) throws IOException { + String serverId = null; + while (nextTag(EasTags.SYNC_ADD) != END) { + switch (tag) { + case EasTags.SYNC_SERVER_ID: // same as + serverId = getValue(); + break; + case EasTags.SYNC_APPLICATION_DATA: + addData(serverId, ops); + break; + default: + skipTag(); + } + } + } + + private Cursor getServerIdCursor(String serverId) { + mBindArgument[0] = serverId; + //bindArguments[1] = "EAS"; + // TODO Find proper constant for _id + return mContentResolver.query(Contacts.People.CONTENT_URI, new String[] {"_id"}, + WHERE_SERVER_ID_AND_ACCOUNT, mBindArgument, null); + } + + public void deleteParser(ArrayList ops) throws IOException { + while (nextTag(EasTags.SYNC_DELETE) != END) { + switch (tag) { + case EasTags.SYNC_SERVER_ID: + String serverId = getValue(); + // Find the message in this mailbox with the given serverId + Cursor c = getServerIdCursor(serverId); + try { + if (c.moveToFirst()) { + mService.userLog("Deleting " + serverId); + mContentResolver.delete(ContentUris + .withAppendedId(Contacts.People.CONTENT_URI, c.getLong(0)), + null, null); + //ops.add(ContentProviderOperation.newDelete( + // ContentUris.withAppendedId(Contacts.People.CONTENT_URI, + // c.getLong(0))).build()); + } + } finally { + c.close(); + } + break; + default: + skipTag(); + } + } + } + + class ServerChange { + long id; + boolean read; + + ServerChange(long _id, boolean _read) { + id = _id; + read = _read; + } + } + + /** + * A change operation on a contact is implemented as a delete followed by an add, since the + * change data is always a full contact. + * + * @param ops the array of pending ContactProviderOperations. + * @throws IOException + */ + public void changeParser(ArrayList ops) throws IOException { + String serverId = null; + while (nextTag(EasTags.SYNC_CHANGE) != END) { + switch (tag) { + case EasTags.SYNC_SERVER_ID: + serverId = getValue(); + Cursor c = getServerIdCursor(serverId); + try { + if (c.moveToFirst()) { + mContentResolver.delete(ContentUris + .withAppendedId(Contacts.People.CONTENT_URI, c.getLong(0)), + null, null); + //ops.add(ContentProviderOperation.newDelete( + // ContentUris.withAppendedId(Contacts.People.CONTENT_URI, + // c.getLong(0))).build()); + mService.userLog("Changing " + serverId); + } + } finally { + c.close(); + } + break; + case EasTags.SYNC_APPLICATION_DATA: + addData(serverId, ops); + default: + skipTag(); + } + } + } + + public void commandsParser() throws IOException { + ArrayList ops = new ArrayList(); + while (nextTag(EasTags.SYNC_COMMANDS) != END) { + if (tag == EasTags.SYNC_ADD) { + addParser(ops); + } else if (tag == EasTags.SYNC_DELETE) { + deleteParser(ops); + } else if (tag == EasTags.SYNC_CHANGE) { + changeParser(ops); + } else + skipTag(); + } + + // Batch provider operations here +// try { +// mService.mContext.getContentResolver() +// .applyBatch(ContactsProvider.EMAIL_AUTHORITY, ops); +// } 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 +// } + + mService.userLog("SyncKey confirmed as: " + mMailbox.mSyncKey); + } + } + + @Override + public String getCollectionName() { + return "Contacts"; + } + + @Override + public boolean sendLocalChanges(EasSerializer s, EasSyncService service) throws IOException { + return false; + } +} diff --git a/src/com/android/exchange/adapter/EasContentParser.java b/src/com/android/exchange/adapter/EasContentParser.java new file mode 100644 index 000000000..88385bcc8 --- /dev/null +++ b/src/com/android/exchange/adapter/EasContentParser.java @@ -0,0 +1,141 @@ +/* + * Copyright (C) 2008-2009 Marc Blank + * Licensed to 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 java.io.IOException; +import java.io.InputStream; + +import android.content.ContentResolver; +import android.content.Context; + +import com.android.exchange.EasSyncService; +import com.android.exchange.EmailContent.Account; +import com.android.exchange.EmailContent.Mailbox; + +/** + * Base class for the Email and PIM sync parsers + * Handles the basic flow of syncKeys, looping to get more data, handling errors, etc. + * Each subclass must implement a handful of methods that relate specifically to the data type + * + */ +public abstract class EasContentParser extends EasParser { + + EasSyncService mService; + + Mailbox mMailbox; + + Account mAccount; + + Context mContext; + + ContentResolver mContentResolver; + + public EasContentParser(InputStream in, EasSyncService _service) throws IOException { + super(in); + mService = _service; + mContext = mService.mContext; + mContentResolver = mContext.getContentResolver(); + mMailbox = mService.mMailbox; + mAccount = mService.mAccount; + } + + /** + * Read, parse, and act on incoming commands from the Exchange server + * @throws IOException if the connection is broken + */ + public abstract void commandsParser() throws IOException; + + /** + * Read, parse, and act on server responses + * Email doesn't have any, so this isn't yet implemented anywhere. It will become abstract, + * in the near future, however. + * @throws IOException + */ + public void responsesParser() throws IOException { + // Placeholder until needed; will become an abstract method + } + + /** + * Delete all records of this class in this account + */ + public abstract void wipe(); + + /** + * Loop through the top-level structure coming from the Exchange server + * Sync keys and the more available flag are handled here, whereas specific data parsing + * is handled by abstract methods implemented for each data class (e.g. Email, Contacts, etc.) + */ + public boolean parse() throws IOException { + int status; + boolean moreAvailable = false; + // If we're not at the top of the xml tree, throw an exception + if (nextTag(START_DOCUMENT) != EasTags.SYNC_SYNC) { + throw new IOException(); + } + // Loop here through the remaining xml + while (nextTag(START_DOCUMENT) != END_DOCUMENT) { + if (tag == EasTags.SYNC_COLLECTION || tag == EasTags.SYNC_COLLECTIONS) { + // Ignore these tags, since we've only got one collection syncing in this loop + } else if (tag == EasTags.SYNC_STATUS) { + // Status = 1 is success; everything else is a failure + status = getValueInt(); + if (status != 1) { + mService.errorLog("Sync failed: " + status); + // Status = 3 means invalid sync key + if (status == 3) { + // Must delete all of the data and start over with syncKey of "0" + mMailbox.mSyncKey = "0"; + // Make this a push box through the first sync + // TODO Make frequency conditional on user settings! + mMailbox.mSyncFrequency = Account.CHECK_INTERVAL_PUSH; + mService.errorLog("Bad sync key; RESET and delete contacts"); + wipe(); + // Indicate there's more so that we'll start syncing again + moreAvailable = true; + } + } + } else if (tag == EasTags.SYNC_COMMANDS) { + commandsParser(); + } else if (tag == EasTags.SYNC_RESPONSES) { + responsesParser(); + } else if (tag == EasTags.SYNC_MORE_AVAILABLE) { + moreAvailable = true; + } else if (tag == EasTags.SYNC_SYNC_KEY) { + if (mMailbox.mSyncKey.equals("0")) + moreAvailable = true; + String newKey = getValue(); + mService.userLog("New sync key: " + newKey); + mMailbox.mSyncKey = newKey; + // If we were pushing (i.e. auto-start), now we'll become ping-triggered + if (mMailbox.mSyncFrequency == Account.CHECK_INTERVAL_PUSH) { + mMailbox.mSyncFrequency = Account.CHECK_INTERVAL_PING; + } + } else { + skipTag(); + } + } + + // Make sure we save away the new syncKey, syncFrequency, etc. + mMailbox.saveOrUpdate(mContext); + + // Let the caller know that there's more to do + return moreAvailable; + } + + +} diff --git a/src/com/android/exchange/adapter/EasEmailSyncAdapter.java b/src/com/android/exchange/adapter/EasEmailSyncAdapter.java new file mode 100644 index 000000000..dd3bde22d --- /dev/null +++ b/src/com/android/exchange/adapter/EasEmailSyncAdapter.java @@ -0,0 +1,436 @@ +/* + * Copyright (C) 2008-2009 Marc Blank + * Licensed to 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 java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.GregorianCalendar; +import java.util.TimeZone; + +import android.content.ContentProviderOperation; +import android.content.ContentResolver; +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.OperationApplicationException; +import android.database.Cursor; +import android.os.RemoteException; + +import com.android.email.provider.EmailProvider; +import com.android.exchange.EasSyncService; +import com.android.exchange.EmailContent.Attachment; +import com.android.exchange.EmailContent.Mailbox; +import com.android.exchange.EmailContent.Message; +import com.android.exchange.EmailContent.MessageColumns; +import com.android.exchange.EmailContent.SyncColumns; + +/** + * Sync adapter for EAS email + * + */ +public class EasEmailSyncAdapter extends EasSyncAdapter { + + private static final String[] UPDATES_PROJECTION = {MessageColumns.FLAG_READ}; + + ArrayList mDeletedIdList = new ArrayList(); + + ArrayList mUpdatedIdList = new ArrayList(); + + public EasEmailSyncAdapter(Mailbox mailbox) { + super(mailbox); + } + + @Override + public boolean parse(ByteArrayInputStream is, EasSyncService service) throws IOException { + EasEmailSyncParser p = new EasEmailSyncParser(is, service); + return p.parse(); + } + + class EasEmailSyncParser extends EasContentParser { + + private static final String WHERE_SERVER_ID_AND_MAILBOX_KEY = + SyncColumns.SERVER_ID + "=? and " + MessageColumns.MAILBOX_KEY + "=?"; + + private String mMailboxIdAsString; + + String[] bindArguments = new String[2]; + + public EasEmailSyncParser(InputStream in, EasSyncService service) throws IOException { + super(in, service); + mMailboxIdAsString = Long.toString(mMailbox.mId); + //setDebug(true); // DON'T CHECK IN WITH THIS + } + + public void wipe() { + mContentResolver.delete(Message.CONTENT_URI, + Message.MAILBOX_KEY + "=" + mMailbox.mId, null); + mContentResolver.delete(Message.DELETED_CONTENT_URI, + Message.MAILBOX_KEY + "=" + mMailbox.mId, null); + mContentResolver.delete(Message.UPDATED_CONTENT_URI, + Message.MAILBOX_KEY + "=" + mMailbox.mId, null); + } + + public void addData (Message msg) throws IOException { + String to = ""; + String from = ""; + String cc = ""; + String replyTo = ""; + int size = 0; + + ArrayList atts = new ArrayList(); + + while (nextTag(EasTags.SYNC_APPLICATION_DATA) != END) { + switch (tag) { + case EasTags.EMAIL_ATTACHMENTS: + break; + case EasTags.EMAIL_ATTACHMENT: + attachmentParser(atts, msg); + break; + case EasTags.EMAIL_TO: + to = getValue(); + break; + case EasTags.EMAIL_FROM: + from = getValue(); + String sender = from; + int q = from.indexOf('\"'); + if (q >= 0) { + int qq = from.indexOf('\"', q + 1); + if (qq > 0) { + sender = from.substring(q + 1, qq); + } + } + msg.mDisplayName = sender; + break; + case EasTags.EMAIL_CC: + cc = getValue(); + break; + case EasTags.EMAIL_REPLY_TO: + replyTo = getValue(); + break; + case EasTags.EMAIL_DATE_RECEIVED: + String date = getValue(); + // 2009-02-11T18:03:03.627Z + GregorianCalendar cal = new GregorianCalendar(); + cal.set(Integer.parseInt(date.substring(0, 4)), Integer.parseInt(date + .substring(5, 7)) - 1, Integer.parseInt(date.substring(8, 10)), + Integer.parseInt(date.substring(11, 13)), Integer.parseInt(date + .substring(14, 16)), Integer.parseInt(date + .substring(17, 19))); + cal.setTimeZone(TimeZone.getTimeZone("GMT")); + msg.mTimeStamp = cal.getTimeInMillis(); + break; + case EasTags.EMAIL_SUBJECT: + msg.mSubject = getValue(); + break; + case EasTags.EMAIL_READ: + msg.mFlagRead = getValueInt() == 1; + break; + case EasTags.EMAIL_BODY: + msg.mTextInfo = "X;X;8;" + size; // location;encoding;charset;size + msg.mText = getValue(); + // For now... + msg.mPreview = "Fake preview"; // Messages.previewFromText(body); + break; + default: + skipTag(); + } + } + + msg.mTo = to; + msg.mFrom = from; + msg.mCc = cc; + msg.mReplyTo = replyTo; + if (atts.size() > 0) { + msg.mAttachments = atts; + } + + } + + public void addParser(ArrayList emails) throws IOException { + Message msg = new Message(); + msg.mAccountKey = mAccount.mId; + msg.mMailboxKey = mMailbox.mId; + msg.mFlagLoaded = Message.LOADED; + + while (nextTag(EasTags.SYNC_ADD) != END) { + switch (tag) { + case EasTags.SYNC_SERVER_ID: + msg.mServerId = getValue(); + break; + case EasTags.SYNC_APPLICATION_DATA: + addData(msg); + break; + default: + skipTag(); + } + } + + // Tell the provider that this is synced back + msg.mServerVersion = mMailbox.mSyncKey; + emails.add(msg); + } + + public void attachmentParser(ArrayList atts, Message msg) throws IOException { + String fileName = null; + String length = null; + String lvl = null; + + while (nextTag(EasTags.EMAIL_ATTACHMENT) != END) { + switch (tag) { + case EasTags.EMAIL_DISPLAY_NAME: + fileName = getValue(); + break; + case EasTags.EMAIL_ATT_NAME: + lvl = getValue(); + break; + case EasTags.EMAIL_ATT_SIZE: + length = getValue(); + break; + default: + skipTag(); + } + } + + if (fileName != null && length != null && lvl != null) { + Attachment att = new Attachment(); + att.mEncoding = "base64"; + att.mSize = Long.parseLong(length); + att.mFileName = fileName; + atts.add(att); + msg.mFlagAttachment = true; + } + } + + private Cursor getServerIdCursor(String serverId, String[] projection) { + bindArguments[0] = serverId; + bindArguments[1] = mMailboxIdAsString; + return mContentResolver.query(Message.CONTENT_URI, projection, + WHERE_SERVER_ID_AND_MAILBOX_KEY, bindArguments, null); + } + + public void deleteParser(ArrayList deletes) throws IOException { + while (nextTag(EasTags.SYNC_DELETE) != END) { + switch (tag) { + case EasTags.SYNC_SERVER_ID: + String serverId = getValue(); + // Find the message in this mailbox with the given serverId + Cursor c = getServerIdCursor(serverId, Message.ID_COLUMN_PROJECTION); + try { + if (c.moveToFirst()) { + mService.userLog("Deleting " + serverId); + deletes.add(c.getLong(Message.ID_COLUMNS_ID_COLUMN)); + } + } finally { + c.close(); + } + break; + default: + skipTag(); + } + } + } + + class ServerChange { + long id; + boolean read; + + ServerChange(long _id, boolean _read) { + id = _id; + read = _read; + } + } + + public void changeParser(ArrayList changes) throws IOException { + String serverId = null; + boolean oldRead = false; + boolean read = true; + long id = 0; + while (nextTag(EasTags.SYNC_CHANGE) != END) { + switch (tag) { + case EasTags.SYNC_SERVER_ID: + serverId = getValue(); + Cursor c = getServerIdCursor(serverId, Message.LIST_PROJECTION); + try { + if (c.moveToFirst()) { + mService.userLog("Changing " + serverId); + oldRead = c.getInt(Message.LIST_READ_COLUMN) == Message.READ; + id = c.getLong(Message.LIST_ID_COLUMN); + } + } finally { + c.close(); + } + break; + case EasTags.EMAIL_READ: + read = getValueInt() == 1; + break; + case EasTags.SYNC_APPLICATION_DATA: + break; + default: + skipTag(); + } + } + if (oldRead != read) { + changes.add(new ServerChange(id, read)); + } + } + + public void commandsParser() throws IOException { + ArrayList newEmails = new ArrayList(); + ArrayList deletedEmails = new ArrayList(); + ArrayList changedEmails = new ArrayList(); + + while (nextTag(EasTags.SYNC_COMMANDS) != END) { + if (tag == EasTags.SYNC_ADD) { + addParser(newEmails); + } else if (tag == EasTags.SYNC_DELETE) { + deleteParser(deletedEmails); + } else if (tag == EasTags.SYNC_CHANGE) { + changeParser(changedEmails); + } else + skipTag(); + } + + // Use a batch operation to handle the changes + // TODO New mail notifications? Who looks for these? + ArrayList ops = new ArrayList(); + for (Message content : newEmails) { + content.addSaveOps(ops); + } + for (Long id : deletedEmails) { + ops.add(ContentProviderOperation.newDelete( + ContentUris.withAppendedId(Message.CONTENT_URI, id)).build()); + } + if (!changedEmails.isEmpty()) { + // Server wins in a conflict... + for (ServerChange change : changedEmails) { + // For now, don't handle read->unread + ContentValues cv = new ContentValues(); + cv.put(MessageColumns.FLAG_READ, change.read); + ops.add(ContentProviderOperation.newUpdate( + ContentUris.withAppendedId(Message.CONTENT_URI, change.id)) + .withValues(cv) + .build()); + } + } + ops.add(ContentProviderOperation.newUpdate( + ContentUris.withAppendedId(Mailbox.CONTENT_URI, mMailbox.mId)).withValues( + mMailbox.toContentValues()).build()); + + // If we've sent local deletions, clear out the deleted table + for (Long id: mDeletedIdList) { + ops.add(ContentProviderOperation.newDelete( + ContentUris.withAppendedId(Message.DELETED_CONTENT_URI, id)).build()); + } + + try { + mService.mContext.getContentResolver() + .applyBatch(EmailProvider.EMAIL_AUTHORITY, ops); + } 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 + } + + mService.userLog("SyncKey confirmed as: " + mMailbox.mSyncKey); + } + } + + @Override + public String getCollectionName() { + return "Email"; + } + + @Override + public boolean sendLocalChanges(EasSerializer s, EasSyncService service) throws IOException { + ContentResolver cr = service.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); + 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()) { + if (first) { + s.start("Commands"); + first = false; + } + // Send the command to delete this message + s.start("Delete") + .data("ServerId", c.getString(Message.LIST_SERVER_ID_COLUMN)) + .end("Delete"); + mDeletedIdList.add(c.getLong(Message.LIST_ID_COLUMN)); + } + } finally { + c.close(); + } + + // Do the same now for updated items + 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 so that we can remove them from the + // deleted table after the server receives our command + mUpdatedIdList.clear(); + try { + while (c.moveToNext()) { + long id = c.getLong(Message.LIST_ID_COLUMN); + // Say we've handled this update + mUpdatedIdList.add(id); + // We have the id of the changed item. But first, we have to find out its current + // state, since the updated table saves the opriginal state + Cursor currentCursor = cr.query(ContentUris.withAppendedId(Message.CONTENT_URI, id), + UPDATES_PROJECTION, null, null, null); + try { + // If this item no longer exists (shouldn't be possible), just move along + if (!currentCursor.moveToFirst()) { + continue; + } + int read = currentCursor.getInt(0); + if (read == c.getInt(Message.LIST_READ_COLUMN)) { + // The read state hasn't really changed, so move on... + continue; + } + if (first) { + s.start("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) + s.start("Change") + .data("ServerId", c.getString(Message.LIST_SERVER_ID_COLUMN)) + .start("ApplicationData") + .data("Read", Integer.toString(read)) + .end("ApplicationData") + .end("Change"); + } finally { + currentCursor.close(); + } + } + } finally { + c.close(); + } + + if (!first) { + s.end("Commands"); + } + return false; + } +} diff --git a/src/com/android/exchange/adapter/EasFolderSyncParser.java b/src/com/android/exchange/adapter/EasFolderSyncParser.java new file mode 100644 index 000000000..0e0b07f43 --- /dev/null +++ b/src/com/android/exchange/adapter/EasFolderSyncParser.java @@ -0,0 +1,333 @@ +/* + * Copyright (C) 2008-2009 Marc Blank + * Licensed to 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 java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import com.android.email.provider.EmailProvider; +import com.android.exchange.Eas; +import com.android.exchange.EasSyncService; +import com.android.exchange.MockParserStream; +import com.android.exchange.SyncManager; +import com.android.exchange.EmailContent.Account; +import com.android.exchange.EmailContent.Mailbox; +import com.android.exchange.EmailContent.MailboxColumns; + +import android.content.ContentProviderOperation; +import android.content.ContentResolver; +import android.content.ContentUris; +import android.content.Context; +import android.content.OperationApplicationException; +import android.database.Cursor; +import android.os.RemoteException; +import android.util.Log; + +/** + * Parse the result of a FolderSync command + * + * Handles the addition, deletion, and changes to folders in the user's Exchange account. + **/ + +public class EasFolderSyncParser extends EasParser { + + public static final String TAG = "FolderSyncParser"; + + // These are defined by the EAS protocol + public static final int USER_FOLDER_TYPE = 1; + public static final int INBOX_TYPE = 2; + public static final int DRAFTS_TYPE = 3; + public static final int DELETED_TYPE = 4; + public static final int SENT_TYPE = 5; + public static final int OUTBOX_TYPE = 6; + public static final int TASKS_TYPE = 7; + public static final int CALENDAR_TYPE = 8; + public static final int CONTACTS_TYPE = 9; + public static final int NOTES_TYPE = 10; + public static final int JOURNAL_TYPE = 11; + public static final int USER_MAILBOX_TYPE = 12; + + public static final List mValidFolderTypes = Arrays.asList(INBOX_TYPE, DRAFTS_TYPE, + DELETED_TYPE, SENT_TYPE, OUTBOX_TYPE, USER_MAILBOX_TYPE, CALENDAR_TYPE, CONTACTS_TYPE); + + private static final String WHERE_SERVER_ID_AND_ACCOUNT = MailboxColumns.SERVER_ID + "=? and " + + MailboxColumns.ACCOUNT_KEY + "=?"; + + private static final String WHERE_DISPLAY_NAME_AND_ACCOUNT = MailboxColumns.DISPLAY_NAME + + "=? and " + MailboxColumns.ACCOUNT_KEY + "=?"; + + private static final String WHERE_PARENT_SERVER_ID_AND_ACCOUNT = + MailboxColumns.PARENT_SERVER_ID +"=? and " + MailboxColumns.ACCOUNT_KEY + "=?"; + + private static final String[] MAILBOX_ID_COLUMNS_PROJECTION = + new String[] {MailboxColumns.ID, MailboxColumns.SERVER_ID}; + + private Account mAccount; + + private long mAccountId; + + private String mAccountIdAsString; + + private EasSyncService mService; + + private Context mContext; + + private ContentResolver mContentResolver; + + private MockParserStream mMock = null; + + private String[] mBindArguments = new String[2]; + + public EasFolderSyncParser(InputStream in, EasSyncService service) throws IOException { + super(in); + mService = service; + mAccount = service.mAccount; + mAccountId = mAccount.mId; + mAccountIdAsString = Long.toString(mAccountId); + mContext = service.mContext; + mContentResolver = mContext.getContentResolver(); + if (in instanceof MockParserStream) { + mMock = (MockParserStream)in; + } + setDebug(true); + } + + public boolean parse() throws IOException { + int status; + boolean res = false; + if (nextTag(START_DOCUMENT) != EasTags.FOLDER_FOLDER_SYNC) + throw new IOException(); + while (nextTag(START_DOCUMENT) != END_DOCUMENT) { + if (tag == EasTags.FOLDER_STATUS) { + status = getValueInt(); + if (status != Eas.FOLDER_STATUS_OK) { + mService.errorLog("FolderSync failed: " + status); + if (status == Eas.FOLDER_STATUS_INVALID_KEY) { + mAccount.mSyncKey = "0"; + mService.errorLog("Bad sync key; RESET and delete all folders"); + mContentResolver.delete(Mailbox.CONTENT_URI, + MailboxColumns.ACCOUNT_KEY + '=' + mAccountId, null); + // Stop existing syncs and reconstruct _main + SyncManager.folderListReloaded(mAccountId); + res = true; + } else { + // Other errors are at the server, so let's throw an error that will + // cause this sync to be retried at a later time + mService.errorLog("Throwing IOException; will retry later"); + throw new IOException(); + } + } + } else if (tag == EasTags.FOLDER_SYNC_KEY) { + mAccount.mSyncKey = getValue(); + mService.userLog("New Account SyncKey: " + mAccount.mSyncKey); + } else if (tag == EasTags.FOLDER_CHANGES) { + changesParser(); + } else + skipTag(); + } + + mAccount.saveOrUpdate(mContext); + return res; + } + + private Cursor getServerIdCursor(String serverId) { + mBindArguments[0] = serverId; + mBindArguments[1] = mAccountIdAsString; + return mContentResolver.query(Mailbox.CONTENT_URI, new String[] {MailboxColumns.ID}, + WHERE_SERVER_ID_AND_ACCOUNT, mBindArguments, null); + } + + public void deleteParser(ArrayList ops) throws IOException { + while (nextTag(EasTags.SYNC_DELETE) != END) { + switch (tag) { + case EasTags.FOLDER_SERVER_ID: + String serverId = getValue(); + // Find the mailbox in this account with the given serverId + Cursor c = getServerIdCursor(serverId); + try { + if (c.moveToFirst()) { + mService.userLog("Deleting " + serverId); + ops.add(ContentProviderOperation.newDelete( + ContentUris.withAppendedId(Mailbox.CONTENT_URI, + c.getLong(0))).build()); + } + } finally { + c.close(); + } + break; + default: + skipTag(); + } + } + } + + public void addParser(ArrayList ops) throws IOException { + String name = null; + String serverId = null; + String parentId = null; + int type = 0; + + while (nextTag(EasTags.FOLDER_ADD) != END) { + switch (tag) { + case EasTags.FOLDER_DISPLAY_NAME: { + name = getValue(); + break; + } + case EasTags.FOLDER_TYPE: { + type = getValueInt(); + break; + } + case EasTags.FOLDER_PARENT_ID: { + parentId = getValue(); + break; + } + case EasTags.FOLDER_SERVER_ID: { + serverId = getValue(); + break; + } + default: + skipTag(); + } + } + if (mValidFolderTypes.contains(type)) { + Mailbox m = new Mailbox(); + m.mDisplayName = name; + m.mServerId = serverId; + m.mAccountKey = mAccountId; + m.mSyncFrequency = Account.CHECK_INTERVAL_NEVER; + switch (type) { + case INBOX_TYPE: + m.mSyncFrequency = Account.CHECK_INTERVAL_PUSH; + m.mType = Mailbox.TYPE_INBOX; + break; + case OUTBOX_TYPE: + m.mSyncFrequency = Account.CHECK_INTERVAL_NEVER; + // TYPE_OUTBOX mailboxes are known by SyncManager to sync whenever they aren't + // empty. The value of mSyncFrequency is ignored for this kind of mailbox. + m.mType = Mailbox.TYPE_OUTBOX; + break; + case SENT_TYPE: + m.mType = Mailbox.TYPE_SENT; + break; + case DRAFTS_TYPE: + m.mType = Mailbox.TYPE_DRAFTS; + break; + case DELETED_TYPE: + m.mType = Mailbox.TYPE_TRASH; + break; + case CALENDAR_TYPE: + m.mType = Mailbox.TYPE_CALENDAR; + // TODO This could be push, depending on settings + // For now, no sync, since it's not yet implemented + break; + case CONTACTS_TYPE: + m.mType = Mailbox.TYPE_CONTACTS; + // TODO Frequency below should depend on settings + m.mSyncFrequency = Account.CHECK_INTERVAL_PUSH; + break; + } + + // Make boxes like Contacts and Calendar invisible in the folder list + m.mFlagVisible = (m.mType < Mailbox.TYPE_NOT_EMAIL); + + if (!parentId.equals("0")) { + m.mParentServerId = parentId; + } + + Log.v(TAG, "Adding mailbox: " + m.mDisplayName); + ops.add(ContentProviderOperation + .newInsert(Mailbox.CONTENT_URI).withValues(m.toContentValues()).build()); + } + + return; + } + + public void changesParser() throws IOException { + // Keep track of new boxes, deleted boxes, updated boxes + ArrayList ops = new ArrayList(); + + while (nextTag(EasTags.FOLDER_CHANGES) != END) { + // TODO Handle FOLDER_CHANGE and FOLDER_DELETE + if (tag == EasTags.FOLDER_ADD) { + addParser(ops); + } else if (tag == EasTags.FOLDER_DELETE) { + deleteParser(ops); + } else if (tag == EasTags.FOLDER_COUNT) { + getValueInt(); + } else + skipTag(); + } + + // The mock stream is used for junit tests, so that the parsing code can be tested + // separately from the provider code. + // TODO Change tests to not require this; remove references to the mock stream + if (mMock != null) { + mMock.setResult(null); + return; + } + + // Create the new mailboxes in a single batch operation + if (!ops.isEmpty()) { + mService.userLog("Applying " + ops.size() + " mailbox operations."); + + // Then, we create an update for the account (most importantly, updating the syncKey) + ops.add(ContentProviderOperation.newUpdate( + ContentUris.withAppendedId(Account.CONTENT_URI, mAccountId)).withValues( + mAccount.toContentValues()).build()); + + // Finally, we execute the batch + try { + mService.mContext.getContentResolver() + .applyBatch(EmailProvider.EMAIL_AUTHORITY, ops); + mService.userLog("New syncKey: " + mAccount.mSyncKey); + } 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 + } + + // Look for sync issues and its children and delete them + // I'm not aware of any other way to deal with this properly + mBindArguments[0] = "Sync Issues"; + mBindArguments[1] = mAccountIdAsString; + Cursor c = mContentResolver.query(Mailbox.CONTENT_URI, MAILBOX_ID_COLUMNS_PROJECTION, + WHERE_DISPLAY_NAME_AND_ACCOUNT, mBindArguments, null); + String parentServerId = null; + long id = 0; + try { + if (c.moveToFirst()) { + id = c.getLong(0); + parentServerId = c.getString(1); + } + } finally { + c.close(); + } + if (parentServerId != null) { + mContentResolver.delete(ContentUris.withAppendedId(Mailbox.CONTENT_URI, id), + null, null); + mBindArguments[0] = parentServerId; + mContentResolver.delete(Mailbox.CONTENT_URI, WHERE_PARENT_SERVER_ID_AND_ACCOUNT, + mBindArguments); + } + } + } + +} diff --git a/src/com/android/exchange/EasMoveParser.java b/src/com/android/exchange/adapter/EasMoveParser.java similarity index 72% rename from src/com/android/exchange/EasMoveParser.java rename to src/com/android/exchange/adapter/EasMoveParser.java index 3ec8c221a..a3a23fa87 100644 --- a/src/com/android/exchange/EasMoveParser.java +++ b/src/com/android/exchange/adapter/EasMoveParser.java @@ -15,32 +15,39 @@ * limitations under the License. */ -package com.android.exchange; +package com.android.exchange.adapter; import java.io.IOException; import java.io.InputStream; import android.util.Log; +import com.android.exchange.EasSyncService; import com.android.exchange.EmailContent.Mailbox; +/** + * Parse the result of a Move command + * + * This is currently unused, as "move to folder" is not implemented in the application. + **/ public class EasMoveParser extends EasParser { private static final String TAG = "EasMoveParser"; - private EasService mService; + private EasSyncService mService; private Mailbox mMailbox; protected boolean mMoreAvailable = false; - public EasMoveParser(InputStream in, EasService service) throws IOException { + public EasMoveParser(InputStream in, EasSyncService service) throws IOException { super(in); mService = service; mMailbox = service.mMailbox; - setDebug(true); + //setDebug(true); } - public void parse() throws IOException { + public boolean parse() throws IOException { int status; - if (nextTag(START_DOCUMENT) != EasTags.MOVE_MOVE_ITEMS) + if (nextTag(START_DOCUMENT) != EasTags.MOVE_MOVE_ITEMS) { throw new IOException(); + } while (nextTag(START_DOCUMENT) != END_DOCUMENT) { if (tag == EasTags.MOVE_RESPONSE) { // Ignore @@ -50,10 +57,13 @@ public class EasMoveParser extends EasParser { Log.e(TAG, "Sync failed (3 is success): " + status); } } else if (tag == EasTags.SYNC_RESPONSES) { + // TODO See if any of these cases need to be handled skipTag(); - } else + } else { skipTag(); + } } mMailbox.save(mService.mContext); + return false; } } diff --git a/src/com/android/exchange/EasOutboxService.java b/src/com/android/exchange/adapter/EasOutboxService.java similarity index 71% rename from src/com/android/exchange/EasOutboxService.java rename to src/com/android/exchange/adapter/EasOutboxService.java index 21e1302e6..461654b8f 100644 --- a/src/com/android/exchange/EasOutboxService.java +++ b/src/com/android/exchange/adapter/EasOutboxService.java @@ -15,20 +15,25 @@ * limitations under the License. */ -package com.android.exchange; +package com.android.exchange.adapter; +import java.io.IOException; import java.net.HttpURLConnection; +import com.android.exchange.EasSyncService; +import com.android.exchange.SyncManager; import com.android.exchange.EmailContent.HostAuth; import com.android.exchange.EmailContent.Mailbox; import com.android.exchange.EmailContent.Message; +import com.android.exchange.EmailContent.MessageColumns; +import com.android.exchange.utility.Rfc822Formatter; import android.content.ContentUris; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; -public class EasOutboxService extends EasService { +public class EasOutboxService extends EasSyncService { public EasOutboxService(Context _context, Mailbox _mailbox) { super(_context, _mailbox); @@ -39,26 +44,24 @@ public class EasOutboxService extends EasService { mPassword = ha.mPassword; } - public void run () { + public void run() { mThread = Thread.currentThread(); String uniqueId = android.provider.Settings.System.getString(mContext.getContentResolver(), android.provider.Settings.System.ANDROID_ID); try { Cursor c = mContext.getContentResolver().query(Message.CONTENT_URI, - Message.CONTENT_PROJECTION, "mMailbox=" + mMailbox, null, null); + Message.CONTENT_PROJECTION, MessageColumns.MAILBOX_KEY + '=' + mMailbox, + null, null); try { - if (c.moveToFirst()) { + while (c.moveToNext()) { Message msg = new Message().restore(c); if (msg != null) { String data = Rfc822Formatter - .writeEmailAsRfc822String(mContext, mAccount, msg, uniqueId); + .writeEmailAsRfc822String(mContext, mAccount, msg, uniqueId); HttpURLConnection uc = sendEASPostCommand("SendMail&SaveInSent=T", data); int code = uc.getResponseCode(); - //Intent intent = new Intent(MessageListView.MAIL_UPDATE); - //intent.putExtra("type", "toast"); if (code == HttpURLConnection.HTTP_OK) { - //intent.putExtra("text", "Your message with subject \"" + msg.mSubject + "\" has been sent."); - log("Deleting message..."); + userLog("Deleting message..."); mContext.getContentResolver().delete(ContentUris.withAppendedId( Message.CONTENT_URI, msg.mId), null, null); } else { @@ -66,19 +69,21 @@ public class EasOutboxService extends EasService { cv.put("uid", 1); Message.update(mContext, Message.CONTENT_URI, msg.mId, cv); - //intent.putExtra("text", "WHOA! Your message with subject \"" + msg.mSubject + "\" failed to send."); } - //mContext.sendBroadcast(intent); - updateUI(); + // TODO How will the user know that the message sent or not? } } - } catch (Exception e) { - e.printStackTrace(); } finally { c.close(); } - } catch (RuntimeException e1) { - e1.printStackTrace(); + } catch (IOException e) { + userLog("Caught IOException"); + mExitStatus = EXIT_IO_ERROR; + } catch (Exception e) { + mExitStatus = EXIT_EXCEPTION; + } finally { + userLog(mMailbox.mDisplayName + ": sync finished"); + SyncManager.done(this); } } } \ No newline at end of file diff --git a/src/com/android/exchange/adapter/EasParser.java b/src/com/android/exchange/adapter/EasParser.java new file mode 100644 index 000000000..190ed0368 --- /dev/null +++ b/src/com/android/exchange/adapter/EasParser.java @@ -0,0 +1,470 @@ +/* + * Copyright (C) 2008-2009 Marc Blank + * Licensed to 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 java.io.*; +import java.util.ArrayList; + +import com.android.exchange.EasException; + +import android.content.Context; +import android.util.Log; + +/** + * Extremely fast and lightweight WBXML parser, implementing only the subset of WBXML that + * EAS uses (as defined in the EAS specification) + * + */ +public abstract class EasParser { + + private static final String TAG = "EasParser"; + + // The following constants are Wbxml standard + public static final int START_DOCUMENT = 0; + public static final int DONE = 1; + public static final int START = 2; + public static final int END = 3; + public static final int TEXT = 4; + public static final int END_DOCUMENT = 3; + private static final int NOT_FETCHED = Integer.MIN_VALUE; + private static final int NOT_ENDED = Integer.MIN_VALUE; + private static final int EOF_BYTE = -1; + private boolean debug = false; + private boolean capture = false; + + private ArrayList captureArray; + + // The input stream for this parser + private InputStream in; + + // The current tag depth + private int depth; + + // The upcoming (saved) id from the stream + private int nextId = NOT_FETCHED; + + // The current tag table (i.e. the tag table for the current page) + private String[] tagTable; + + // An array of tag tables, as defined in EasTags + static private String[][] tagTables = new String[24][]; + + // The stack of names of tags being processed; used when debug = true + private String[] nameArray = new String[32]; + + // The stack of tags being processed + private int[] startTagArray = new int[32]; + + // The following vars are available to all to avoid method calls that represent the state of + // the parser at any given time + public int endTag = NOT_ENDED; + + public int startTag; + + // The type of the last token read + public int type; + + // The current page + public int page; + + // The current tag + public int tag; + + // The name of the current tag + public String name; + + // Whether the current tag is associated with content (a value) + private boolean noContent; + + // The value read, as a String. Only one of text or num will be valid, depending on whether the + // value was requested as a String or an int (to avoid wasted effort in parsing) + public String text; + + // The value read, as an int + public int num; + + public class EofException extends IOException { + private static final long serialVersionUID = 1L; + } + + public class EodException extends IOException { + private static final long serialVersionUID = 1L; + } + + public class EasParserException extends IOException { + private static final long serialVersionUID = 1L; + } + + public boolean parse() throws IOException, EasException { + return false; + } + + /** + * Initialize the tag tables; they are constant + * + */ + { + String[][] pages = EasTags.pages; + for (int i = 0; i < pages.length; i++) { + String[] page = pages[i]; + if (page.length > 0) { + tagTables[i] = page; + } + } + } + + public EasParser(InputStream in) throws IOException { + setInput(in); + } + + /** + * Set the debug state of the parser. When debugging is on, every token is logged (Log.v) to + * the console. + * + * @param val the desired state for debug output + */ + public void setDebug(boolean val) { + debug = val; + } + + /** + * Turns on data capture; this is used to create test streams that represent "live" data and + * can be used against the various parsers. + */ + public void captureOn() { + capture = true; + captureArray = new ArrayList(); + } + + /** + * Turns off data capture; writes the captured data to a specified file. + */ + public void captureOff(Context context, String file) { + try { + FileOutputStream out = context.openFileOutput(file, Context.MODE_WORLD_WRITEABLE); + out.write(captureArray.toString().getBytes()); + out.close(); + } catch (FileNotFoundException e) { + // This is debug code; exceptions aren't interesting. + } catch (IOException e) { + // This is debug code; exceptions aren't interesting. + } + } + + /** + * Return the value of the current tag, as a String + * + * @return the String value of the current tag + * @throws IOException + */ + public String getValue() throws IOException { + // The false argument tells getNext to return the value as a String + getNext(false); + // Save the value + String val = text; + // Read the next token; it had better be the end of the current tag + getNext(false); + // If not, throw an exception + if (type != END) { + throw new IOException("No END found!"); + } + endTag = startTag; + return val; + } + + /** + * Return the value of the current tag, as an integer + * + * @return the integer value of the current tag + * @throws IOException + */ + public int getValueInt() throws IOException { + // The true argument to getNext indicates the desire for an integer return value + getNext(true); + // Save the value + int val = num; + // Read the next token; it had better be the end of the current tag + getNext(false); + // If not, throw an exception + if (type != END) { + throw new IOException("No END found!"); + } + endTag = startTag; + return val; + } + + /** + * Return the next tag found in the stream; special tags END and END_DOCUMENT are used to + * mark the end of the current tag and end of document. If we hit end of document without + * looking for it, generate an EodException. The tag returned consists of the page number + * shifted PAGE_SHIFT bits OR'd with the tag retrieved from the stream. Thus, all tags returned + * are unique. + * + * @param endingTag the tag that would represent the end of the tag we're processing + * @return the next tag found + * @throws IOException + */ + public int nextTag(int endingTag) throws IOException { + // Lose the page information + endTag = endingTag &= EasTags.PAGE_MASK; + while (getNext(false) != DONE) { + // If we're a start, set tag to include the page and return it + if (type == START) { + tag = page | startTag; + return tag; + // If we're at the ending tag we're looking for, return the END signal + } else if (type == END && startTag == endTag) { + return END; + } + } + // We're at end of document here. If we're looking for it, return END_DOCUMENT + if (endTag == START_DOCUMENT) { + return END_DOCUMENT; + } + // Otherwise, we've prematurely hit end of document, so exception out + // EodException is a subclass of IOException; this will be treated as an IO error by + // SyncManager. + throw new EodException(); + } + + /** + * Skip anything found in the stream until the end of the current tag is reached. This can be + * used to ignore stretches of xml that aren't needed by the parser. + * + * @throws IOException + */ + public void skipTag() throws IOException { + int thisTag = startTag; + // Just loop until we hit the end of the current tag + while (getNext(false) != DONE) { + if (type == END && startTag == thisTag) { + return; + } + } + + // If we're at end of document, that's bad + throw new EofException(); + } + + /** + * Retrieve the next token from the input stream + * + * @return the token found + * @throws IOException + */ + public int nextToken() throws IOException { + getNext(false); + return type; + } + + /** + * Initializes the parser with an input stream; reads the first 4 bytes (which are always the + * same in EAS, and then sets the tag table to point to page 0 (by definition, the starting + * page). + * + * @param in the InputStream associated with this parser + * @throws IOException + */ + public void setInput(InputStream in) throws IOException { + this.in = in; + readByte(); // version + readInt(); // ? + readInt(); // 106 (UTF-8) + readInt(); // string table length + tagTable = tagTables[0]; + } + + /** + * Return the next piece of data from the stream. The return value indicates the type of data + * that has been retrieved - START (start of tag), END (end of tag), DONE (end of stream), or + * TEXT (the value of a tag) + * + * @param asInt whether a TEXT value should be parsed as a String or an int. + * @return the type of data retrieved + * @throws IOException + */ + private final int getNext(boolean asInt) throws IOException { + if (type == END) { + depth--; + } else { + endTag = NOT_ENDED; + } + + if (noContent) { + type = END; + noContent = false; + return type; + } + + text = null; + name = null; + + int id = nextId (); + while (id == Wbxml.SWITCH_PAGE) { + nextId = NOT_FETCHED; + // Get the new page number + int pg = readByte(); + // Save the shifted page to add into the startTag in nextTag + page = pg << EasTags.PAGE_SHIFT; + // Retrieve the current tag table + tagTable = tagTables[pg]; + id = nextId(); + } + nextId = NOT_FETCHED; + + switch (id) { + case EOF_BYTE: + // End of document + type = DONE; + break; + + case Wbxml.END: + // End of tag + type = END; + if (debug) { + name = nameArray[depth]; + Log.v(TAG, "'); + } + // Retrieve the now-current startTag from our stack + startTag = endTag = startTagArray[depth]; + break; + + case Wbxml.STR_I: + // Inline string + type = TEXT; + if (asInt) { + num = readInlineInt(); + } else { + text = readInlineString(); + } + if (debug) { + Log.v(TAG, asInt ? Integer.toString(num) : text); + } + break; + + default: + // Start of tag + type = START; + // The tag is in the low 6 bits + startTag = id & 0x3F; + // If the high bit is set, there is content (a value) to be read + noContent = (id & 0x40) == 0; + depth++; + if (debug) { + name = tagTable[startTag - 5]; + Log.v(TAG, '<' + name + '>'); + nameArray[depth] = name; + } + // Save the startTag to our stack + startTagArray[depth] = startTag; + } + + // Return the type of data we're dealing with + return type; + } + + /** + * Read an int from the input stream, and capture it if necessary for debugging. Seems a small + * price to pay... + * + * @return the int read + * @throws IOException + */ + private int read() throws IOException { + int i; + i = in.read(); + if (capture) { + captureArray.add(i); + } + return i; + } + + private int nextId() throws IOException { + if (nextId == NOT_FETCHED) { + nextId = read(); + } + return nextId; + } + + private int readByte() throws IOException { + int i = read(); + if (i == EOF_BYTE) { + throw new EofException(); + } + return i; + } + + /** + * Read an integer from the stream; this is called when the parser knows that what follows is + * an inline string representing an integer (e.g. the Read tag in Email has a value known to + * be either "0" or "1") + * + * @return the integer as parsed from the stream + * @throws IOException + */ + private int readInlineInt() throws IOException { + int result = 0; + + while (true) { + int i = readByte(); + // Inline strings are always terminated with a zero byte + if (i == 0) { + return result; + } + if (i >= '0' && i <= '9') { + result = (result * 10) + (i - '0'); + } else { + throw new IOException("Non integer"); + } + } + } + + private int readInt() throws IOException { + int result = 0; + int i; + + do { + i = readByte(); + result = (result << 7) | (i & 0x7f); + } while ((i & 0x80) != 0); + + return result; + } + + /** + * Read an inline string from the stream + * + * @return the String as parsed from the stream + * @throws IOException + */ + private String readInlineString() throws IOException { + StringBuilder sb = new StringBuilder(256); + + while (true) { + int i = read(); + if (i == 0) { + break; + } else if (i == EOF_BYTE) { + throw new EofException(); + } + sb.append((char)i); + } + String res = sb.toString(); + return res; + } +} \ No newline at end of file diff --git a/src/com/android/exchange/adapter/EasPingParser.java b/src/com/android/exchange/adapter/EasPingParser.java new file mode 100644 index 000000000..d6b0007ed --- /dev/null +++ b/src/com/android/exchange/adapter/EasPingParser.java @@ -0,0 +1,85 @@ +/* Copyright (C) 2008-2009 Marc Blank + * Licensed to 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 java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; + +import com.android.exchange.EasSyncService; +import com.android.exchange.StaleFolderListException; + +/** + * Parse the result of a Ping command. + * + * If there are folders with changes, add the serverId of those folders to the syncList array. + * If the folder list needs to be reloaded, throw a StaleFolderListException, which will be caught + * by the sync server, which will sync the updated folder list. + */ +public class EasPingParser extends EasParser { + ArrayList syncList = new ArrayList(); + + EasSyncService mService; + + public ArrayList getSyncList() { + return syncList; + } + + public EasPingParser(InputStream in, EasSyncService _service) throws IOException { + super(in); + mService = _service; + //setDebug(true); + } + + public void parsePingFolders(ArrayList syncList) throws IOException { + while (nextTag(EasTags.PING_FOLDERS) != END) { + if (tag == EasTags.PING_FOLDER) { + // Here we'll keep track of which mailboxes need syncing + syncList.add(getValue()); + } else { + skipTag(); + } + } + } + + @Override + public boolean parse() throws IOException, StaleFolderListException { + boolean res = false; + if (nextTag(START_DOCUMENT) != EasTags.PING_PING) { + throw new IOException(); + } + while (nextTag(START_DOCUMENT) != END_DOCUMENT) { + if (tag == EasTags.PING_STATUS) { + int status = getValueInt(); + mService.userLog("Ping completed, status = " + status); + if (status == 2) { + // Status = 2 indicates changes in one folder or other + res = true; + } else if (status == 7 || status == 4) { + // Status of 7 or 4 indicate a stale folder list + throw new StaleFolderListException(); + } + } else if (tag == EasTags.PING_FOLDERS) { + parsePingFolders(syncList); + } else { + skipTag(); + } + } + return res; + } +} + diff --git a/src/com/android/exchange/adapter/EasSerializer.java b/src/com/android/exchange/adapter/EasSerializer.java new file mode 100644 index 000000000..8895e9339 --- /dev/null +++ b/src/com/android/exchange/adapter/EasSerializer.java @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2008-2009 Marc Blank + * Licensed to 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 java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Hashtable; + + +/** + * This is a convenience class that simplifies the creation of Wbxml commands and allows + * multiple commands to be chained together. + * + * Each start command must pair with an end command; the values of all data fields are Strings. The + * methods here should be self-explanatory. + * + * Use toString() to obtain the output for the EAS server + */ +public class EasSerializer extends WbxmlSerializer { + ByteArrayOutputStream byteStream = new ByteArrayOutputStream(); + + static Hashtable tagTable = null; + + public EasSerializer() { + super(); + try { + setOutput(byteStream, null); + // Lazy initialization of our tag tables, as created from EasTags + if (tagTable == null) { + String[][] pages = EasTags.pages; + for (int i = 0; i < pages.length; i++) { + String[] page = pages[i]; + if (page.length > 0) { + setTagTable(i, page); + } + } + tagTable = getTagTable(); + } else { + setTagTable(tagTable); + } + startDocument("UTF-8", false); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public EasSerializer start(String tag) throws IOException { + startTag(null, tag); + return this; + } + + public EasSerializer end(String tag) throws IOException { + endTag(null, tag); + return this; + } + + public EasSerializer end() throws IOException { + endDocument(); + return this; + } + + public EasSerializer data(String tag, String value) throws IOException { + startTag(null, tag); + text(value); + endTag(null, tag); + return this; + } + + public EasSerializer tag(String tag) throws IOException { + startTag(null, tag); + endTag(null, tag); + return this; + } + + public EasSerializer text(String str) throws IOException { + super.text(str); + return this; + } + + public ByteArrayOutputStream getByteStream() { + return byteStream; + } + + public String toString() { + return byteStream.toString(); + } +} + diff --git a/src/com/android/exchange/adapter/EasSyncAdapter.java b/src/com/android/exchange/adapter/EasSyncAdapter.java new file mode 100644 index 000000000..136c54b13 --- /dev/null +++ b/src/com/android/exchange/adapter/EasSyncAdapter.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2008-2009 Marc Blank + * Licensed to 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 java.io.ByteArrayInputStream; +import java.io.IOException; + +import com.android.exchange.EasSyncService; +import com.android.exchange.EmailContent.Mailbox; + +/** + * Parent class of all sync adapters (EasMailbox, EasCalendar, and EasContacts) + * + */ +public abstract class EasSyncAdapter { + public Mailbox mMailbox; + + // Create the data for local changes that need to be sent up to the server + public abstract boolean sendLocalChanges(EasSerializer s, EasSyncService service) + throws IOException; + // Parse incoming data from the EAS server, creating, modifying, and deleting objects as + // required through the EmailProvider + public abstract boolean parse(ByteArrayInputStream is, EasSyncService service) + throws IOException; + // The name used to specify the collection type of the target (Email, Calendar, or Contacts) + public abstract String getCollectionName(); + + public EasSyncAdapter(Mailbox mailbox) { + mMailbox = mailbox; + } +} + diff --git a/src/com/android/exchange/adapter/EasTags.java b/src/com/android/exchange/adapter/EasTags.java new file mode 100644 index 000000000..4a832e105 --- /dev/null +++ b/src/com/android/exchange/adapter/EasTags.java @@ -0,0 +1,459 @@ +/* + * Copyright (C) 2008-2009 Marc Blank + * Licensed to 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; + +/** + * The wbxml tags for EAS are all defined here. + * + * The static final int's, of the form _ = are used in parsing incoming + * responses from the server (i.e. EasParser and its subclasses). + * + * The array of String arrays is used to construct server requests with EasSerializer. One thing + * we might do eventually is to "precompile" these requests, in part, although they should be + * fairly fast to begin with (each tag requires one HashMap lookup, and there aren't all that many + * of them in a given command). + * + */ +public class EasTags { + + // Wbxml page definitions for EAS + static final int AIRSYNC = 0x00; + static final int CONTACTS = 0x01; + static final int EMAIL = 0x02; + static final int CALENDAR = 0x04; + static final int MOVE = 0x05; + static final int FOLDER = 0x07; + static final int CONTACTS2 = 0x0C; + static final int PING = 0x0D; + static final int GAL = 0x10; + static final int BASE = 0x11; + + // Shift applied to page numbers to generate tag + static final int PAGE_SHIFT = 6; + static final int PAGE_MASK = 0x3F; // 6 bits + + static final int SYNC_PAGE = 0 << PAGE_SHIFT; + static final int SYNC_SYNC = SYNC_PAGE + 5; + static final int SYNC_RESPONSES = SYNC_PAGE + 6; + static final int SYNC_ADD = SYNC_PAGE + 7; + static final int SYNC_CHANGE = SYNC_PAGE + 8; + static final int SYNC_DELETE = SYNC_PAGE + 9; + static final int SYNC_FETCH = SYNC_PAGE + 0xA; + static final int SYNC_SYNC_KEY = SYNC_PAGE + 0xB; + static final int SYNC_CLIENT_ID = SYNC_PAGE + 0xC; + static final int SYNC_SERVER_ID = SYNC_PAGE + 0xD; + static final int SYNC_STATUS = SYNC_PAGE + 0xE; + static final int SYNC_COLLECTION = SYNC_PAGE + 0xF; + static final int SYNC_CLASS = SYNC_PAGE + 0x10; + static final int SYNC_VERSION = SYNC_PAGE + 0x11; + static final int SYNC_COLLECTION_ID = SYNC_PAGE + 0x12; + static final int SYNC_GET_CHANGES = SYNC_PAGE + 0x13; + static final int SYNC_MORE_AVAILABLE = SYNC_PAGE + 0x14; + static final int SYNC_WINDOW_SIZE = SYNC_PAGE + 0x15; + static final int SYNC_COMMANDS = SYNC_PAGE + 0x16; + static final int SYNC_OPTIONS = SYNC_PAGE + 0x17; + static final int SYNC_FILTER_TYPE = SYNC_PAGE + 0x18; + static final int SYNC_TRUNCATION = SYNC_PAGE + 0x19; + static final int SYNC_RTF_TRUNCATION = SYNC_PAGE + 0x1A; + static final int SYNC_CONFLICT = SYNC_PAGE + 0x1B; + static final int SYNC_COLLECTIONS = SYNC_PAGE + 0x1C; + static final int SYNC_APPLICATION_DATA = SYNC_PAGE + 0x1D; + static final int SYNC_DELETES_AS_MOVES = SYNC_PAGE + 0x1E; + static final int SYNC_NOTIFY_GUID = SYNC_PAGE + 0x1F; + static final int SYNC_SUPPORTED = SYNC_PAGE + 0x20; + static final int SYNC_SOFT_DELETE = SYNC_PAGE + 0x21; + static final int SYNC_MIME_SUPPORT = SYNC_PAGE + 0x22; + static final int SYNC_MIME_TRUNCATION = SYNC_PAGE + 0x23; + static final int SYNC_WAIT = SYNC_PAGE + 0x24; + static final int SYNC_LIMIT = SYNC_PAGE + 0x25; + static final int SYNC_PARTIAL = SYNC_PAGE + 0x26; + + static final int CONTACTS_PAGE = CONTACTS << PAGE_SHIFT; + static final int CONTACTS_ANNIVERSARY = CONTACTS_PAGE + 5; + static final int CONTACTS_ASSISTANT_NAME = CONTACTS_PAGE + 6; + static final int CONTACTS_ASSISTANT_TELEPHONE_NUMBER = CONTACTS_PAGE + 7; + static final int CONTACTS_BIRTHDAY = CONTACTS_PAGE + 8; + static final int CONTACTS_BODY = CONTACTS_PAGE + 9; + static final int CONTACTS_BODY_SIZE = CONTACTS_PAGE + 0xA; + static final int CONTACTS_BODY_TRUNCATED = CONTACTS_PAGE + 0xB; + static final int CONTACTS_BUSINESS2_TELEPHONE_NUMBER = CONTACTS_PAGE + 0xC; + static final int CONTACTS_BUSINESS_ADDRESS_CITY = CONTACTS_PAGE + 0xD; + static final int CONTACTS_BUSINESS_ADDRESS_COUNTRY = CONTACTS_PAGE + 0xE; + static final int CONTACTS_BUSINESS_ADDRESS_POSTAL_CODE = CONTACTS_PAGE + 0xF; + static final int CONTACTS_BUSINESS_ADDRESS_STATE = CONTACTS_PAGE + 0x10; + static final int CONTACTS_BUSINESS_ADDRESS_STREET = CONTACTS_PAGE + 0x11; + static final int CONTACTS_BUSINESS_FAX_NUMBER = CONTACTS_PAGE + 0x12; + static final int CONTACTS_BUSINESS_TELEPHONE_NUMBER = CONTACTS_PAGE + 0x13; + static final int CONTACTS_CAR_TELEPHONE_NUMBER = CONTACTS_PAGE + 0x14; + static final int CONTACTS_CATEGORIES = CONTACTS_PAGE + 0x15; + static final int CONTACTS_CATEGORY = CONTACTS_PAGE + 0x16; + static final int CONTACTS_CHILDREN = CONTACTS_PAGE + 0x17; + static final int CONTACTS_CHILD = CONTACTS_PAGE + 0x18; + static final int CONTACTS_COMPANY_NAME = CONTACTS_PAGE + 0x19; + static final int CONTACTS_DEPARTMENT = 0x1A; + static final int CONTACTS_EMAIL1_ADDRESS = CONTACTS_PAGE + 0x1B; + static final int CONTACTS_EMAIL2_ADDRESS = CONTACTS_PAGE + 0x1C; + static final int CONTACTS_EMAIL3_ADDRESS = CONTACTS_PAGE + 0x1D; + static final int CONTACTS_FILE_AS = CONTACTS_PAGE + 0x1E; + static final int CONTACTS_FIRST_NAME = CONTACTS_PAGE + 0x1F; + static final int CONTACTS_HOME2_TELEPHONE_NUMBER = CONTACTS_PAGE + 0x20; + static final int CONTACTS_HOME_ADDRESS_CITY = CONTACTS_PAGE + 0x21; + static final int CONTACTS_HOME_ADDRESS_COUNTRY = CONTACTS_PAGE + 0x22; + static final int CONTACTS_HOME_ADDRESS_POSTAL_CODE = CONTACTS_PAGE + 0x23; + static final int CONTACTS_HOME_ADDRESS_STATE = CONTACTS_PAGE + 0x24; + static final int CONTACTS_HOME_ADDRESS_STREET = CONTACTS_PAGE + 0x25; + static final int CONTACTS_HOME_FAX_NUMBER = CONTACTS_PAGE + 0x26; + static final int CONTACTS_HOME_TELEPHONE_NUMBER = CONTACTS_PAGE + 0x27; + static final int CONTACTS_JOB_TITLE = CONTACTS_PAGE + 0x28; + static final int CONTACTS_LAST_NAME = CONTACTS_PAGE + 0x29; + static final int CONTACTS_MIDDLE_NAME = CONTACTS_PAGE + 0x2A; + static final int CONTACTS_MOBILE_TELEPHONE_NUMBER = CONTACTS_PAGE + 0x2B; + static final int CONTACTS_OFFICE_LOCATION = CONTACTS_PAGE + 0x2C; + static final int CONTACTS_OTHER_ADDRESS_CITY = CONTACTS_PAGE + 0x2D; + static final int CONTACTS_OTHER_ADDRESS_COUNTRY = CONTACTS_PAGE + 0x2E; + static final int CONTACTS_OTHER_ADDRESS_POSTAL_CODE = CONTACTS_PAGE + 0x2F; + static final int CONTACTS_OTHER_ADDRESS_STATE = CONTACTS_PAGE + 0x30; + static final int CONTACTS_OTHER_ADDRESS_STREET = CONTACTS_PAGE + 0x31; + static final int CONTACTS_PAGER_NUMBER = CONTACTS_PAGE + 0x32; + static final int CONTACTS_RADIO_TELEPHONE_NUMBER = CONTACTS_PAGE + 0x33; + static final int CONTACTS_SPOUSE = CONTACTS_PAGE + 0x34; + static final int CONTACTS_SUFFIX = CONTACTS_PAGE + 0x35; + static final int CONTACTS_TITLE = CONTACTS_PAGE + 0x36; + static final int CONTACTS_WEBPAGE = CONTACTS_PAGE + 0x37; + static final int CONTACTS_YOMI_COMPANY_NAME = CONTACTS_PAGE + 0x38; + static final int CONTACTS_YOMI_FIRST_NAME = CONTACTS_PAGE + 0x39; + static final int CONTACTS_YOMI_LAST_NAME = CONTACTS_PAGE + 0x3A; + static final int CONTACTS_COMPRESSED_RTF = CONTACTS_PAGE + 0x3B; + static final int CONTACTS_PICTURE = CONTACTS_PAGE + 0x3C; + + static final int CALENDAR_PAGE = CALENDAR << PAGE_SHIFT; + static final int CALENDAR_TIME_ZONE = CALENDAR_PAGE + 5; + static final int CALENDAR_ALL_DAY_EVENT = CALENDAR_PAGE + 6; + static final int CALENDAR_ATTENDEES = CALENDAR_PAGE + 7; + static final int CALENDAR_ATTENDEE = CALENDAR_PAGE + 8; + static final int CALENDAR_ATTENDEE_EMAIL = CALENDAR_PAGE + 9; + static final int CALENDAR_ATTENDEE_NAME = CALENDAR_PAGE + 0xA; + static final int CALENDAR_BODY = CALENDAR_PAGE + 0xB; + static final int CALENDAR_BODY_TRUNCATED = CALENDAR_PAGE + 0xC; + static final int CALENDAR_BUSY_STATUS = CALENDAR_PAGE + 0xD; + static final int CALENDAR_CATEGORIES = CALENDAR_PAGE + 0xE; + static final int CALENDAR_CATEGORY = CALENDAR_PAGE + 0xF; + static final int CALENDAR_COMPRESSED_RTF = CALENDAR_PAGE + 0x10; + static final int CALENDAR_DTSTAMP = CALENDAR_PAGE + 0x11; + static final int CALENDAR_END_TIME = CALENDAR_PAGE + 0x12; + static final int CALENDAR_EXCEPTION = CALENDAR_PAGE + 0x13; + static final int CALENDAR_EXCEPTIONS = CALENDAR_PAGE + 0x14; + static final int CALENDAR_EXCEPTION_IS_DELETED = CALENDAR_PAGE + 0x15; + static final int CALENDAR_EXCEPTION_START_TIME = CALENDAR_PAGE + 0x16; + static final int CALENDAR_LOCATION = CALENDAR_PAGE + 0x17; + static final int CALENDAR_MEETING_STATUS = CALENDAR_PAGE + 0x18; + static final int CALENDAR_ORGANIZER_EMAIL = CALENDAR_PAGE + 0x19; + static final int CALENDAR_ORGANIZER_NAME = CALENDAR_PAGE + 0x1A; + static final int CALENDAR_RECURRENCE = CALENDAR_PAGE + 0x1B; + static final int CALENDAR_RECURRENCE_TYPE = CALENDAR_PAGE + 0x1C; + static final int CALENDAR_RECURRENCE_UNTIL = CALENDAR_PAGE + 0x1D; + static final int CALENDAR_RECURRENCE_OCCURRENCES = CALENDAR_PAGE + 0x1E; + static final int CALENDAR_RECURRENCE_INTERVAL = CALENDAR_PAGE + 0x1F; + static final int CALENDAR_RECURRENCE_DAYOFWEEK = CALENDAR_PAGE + 0x20; + static final int CALENDAR_RECURRENCE_DAYOFMONTH = CALENDAR_PAGE + 0x21; + static final int CALENDAR_RECURRENCE_WEEKOFMONTH = CALENDAR_PAGE + 0x22; + static final int CALENDAR_RECURRENCE_MONTHOFYEAR = CALENDAR_PAGE + 0x23; + static final int CALENDAR_REMINDER_MINS_BEFORE = CALENDAR_PAGE + 0x24; + static final int CALENDAR_SENSITIVITY = CALENDAR_PAGE + 0x25; + static final int CALENDAR_SUBJECT = CALENDAR_PAGE + 0x26; + static final int CALENDAR_START_TIME = CALENDAR_PAGE + 0x27; + static final int CALENDAR_UID = CALENDAR_PAGE + 0x28; + static final int CALENDAR_ATTENDEE_STATUS = CALENDAR_PAGE + 0x29; + static final int CALENDAR_ATTENDEE_TYPE = CALENDAR_PAGE + 0x2A; + + static final int FOLDER_PAGE = FOLDER << PAGE_SHIFT; + static final int FOLDER_FOLDERS = FOLDER_PAGE + 5; + static final int FOLDER_FOLDER = FOLDER_PAGE + 6; + static final int FOLDER_DISPLAY_NAME = FOLDER_PAGE + 7; + static final int FOLDER_SERVER_ID = FOLDER_PAGE + 8; + static final int FOLDER_PARENT_ID = FOLDER_PAGE + 9; + static final int FOLDER_TYPE = FOLDER_PAGE + 0xA; + static final int FOLDER_RESPONSE = FOLDER_PAGE + 0xB; + static final int FOLDER_STATUS = FOLDER_PAGE + 0xC; + static final int FOLDER_CONTENT_CLASS = FOLDER_PAGE + 0xD; + static final int FOLDER_CHANGES = FOLDER_PAGE + 0xE; + static final int FOLDER_ADD = FOLDER_PAGE + 0xF; + static final int FOLDER_DELETE = FOLDER_PAGE + 0x10; + static final int FOLDER_UPDATE = FOLDER_PAGE + 0x11; + static final int FOLDER_SYNC_KEY = FOLDER_PAGE + 0x12; + static final int FOLDER_FOLDER_CREATE = FOLDER_PAGE + 0x13; + static final int FOLDER_FOLDER_DELETE= FOLDER_PAGE + 0x14; + static final int FOLDER_FOLDER_UPDATE = FOLDER_PAGE + 0x15; + static final int FOLDER_FOLDER_SYNC = FOLDER_PAGE + 0x16; + static final int FOLDER_COUNT = FOLDER_PAGE + 0x17; + static final int FOLDER_VERSION = FOLDER_PAGE + 0x18; + + static final int EMAIL_PAGE = EMAIL << PAGE_SHIFT; + static final int EMAIL_ATTACHMENT = EMAIL_PAGE + 5; + static final int EMAIL_ATTACHMENTS = EMAIL_PAGE + 6; + static final int EMAIL_ATT_NAME = EMAIL_PAGE + 7; + static final int EMAIL_ATT_SIZE = EMAIL_PAGE + 8; + static final int EMAIL_ATT0ID = EMAIL_PAGE + 9; + static final int EMAIL_ATT_METHOD = EMAIL_PAGE + 0xA; + static final int EMAIL_ATT_REMOVED = EMAIL_PAGE + 0xB; + static final int EMAIL_BODY = EMAIL_PAGE + 0xC; + static final int EMAIL_BODY_SIZE = EMAIL_PAGE + 0xD; + static final int EMAIL_BODY_TRUNCATED = EMAIL_PAGE + 0xE; + static final int EMAIL_DATE_RECEIVED = EMAIL_PAGE + 0xF; + static final int EMAIL_DISPLAY_NAME = EMAIL_PAGE + 0x10; + static final int EMAIL_DISPLAY_TO = EMAIL_PAGE + 0x11; + static final int EMAIL_IMPORTANCE = EMAIL_PAGE + 0x12; + static final int EMAIL_MESSAGE_CLASS = EMAIL_PAGE + 0x13; + static final int EMAIL_SUBJECT = EMAIL_PAGE + 0x14; + static final int EMAIL_READ = EMAIL_PAGE + 0x15; + static final int EMAIL_TO = EMAIL_PAGE + 0x16; + static final int EMAIL_CC = EMAIL_PAGE + 0x17; + static final int EMAIL_FROM = EMAIL_PAGE + 0x18; + static final int EMAIL_REPLY_TO = EMAIL_PAGE + 0x19; + static final int EMAIL_ALL_DAY_EVENT = EMAIL_PAGE + 0x1A; + static final int EMAIL_CATEGORIES = EMAIL_PAGE + 0x1B; + static final int EMAIL_CATEGORY = EMAIL_PAGE + 0x1C; + static final int EMAIL_DTSTAMP = EMAIL_PAGE + 0x1D; + static final int EMAIL_END_TIME = EMAIL_PAGE + 0x1E; + static final int EMAIL_INSTANCE_TYPE = EMAIL_PAGE + 0x1F; + static final int EMAIL_INTD_BUSY_STATUS = EMAIL_PAGE + 0x20; + static final int EMAIL_LOCATION = EMAIL_PAGE + 0x21; + static final int EMAIL_MEETING_REQUEST = EMAIL_PAGE + 0x22; + static final int EMAIL_ORGANIZER = EMAIL_PAGE + 0x23; + static final int EMAIL_RECURRENCE_ID = EMAIL_PAGE + 0x24; + static final int EMAIL_REMINDER = EMAIL_PAGE + 0x25; + static final int EMAIL_RESPONSE_REQUESTED = EMAIL_PAGE + 0x26; + static final int EMAIL_RECURRENCES = EMAIL_PAGE + 0x27; + static final int EMAIL_RECURRENCE = EMAIL_PAGE + 0x28; + static final int EMAIL_RECURRENCE_TYPE = EMAIL_PAGE + 0x29; + static final int EMAIL_RECURRENCE_UNTIL = EMAIL_PAGE + 0x2A; + static final int EMAIL_RECURRENCE_OCCURRENCES = EMAIL_PAGE + 0x2B; + static final int EMAIL_RECURRENCE_INTERVAL = EMAIL_PAGE + 0x2C; + static final int EMAIL_RECURRENCE_DAYOFWEEK = EMAIL_PAGE + 0x2D; + static final int EMAIL_RECURRENCE_DAYOFMONTH = EMAIL_PAGE + 0x2E; + static final int EMAIL_RECURRENCE_WEEKOFMONTH = EMAIL_PAGE + 0x2F; + static final int EMAIL_RECURRENCE_MONTHOFYEAR = EMAIL_PAGE + 0x30; + static final int EMAIL_START_TIME = EMAIL_PAGE + 0x31; + static final int EMAIL_SENSITIVITY = EMAIL_PAGE + 0x32; + static final int EMAIL_TIME_ZONE = EMAIL_PAGE + 0x33; + static final int EMAIL_GLOBAL_OBJID = EMAIL_PAGE + 0x34; + static final int EMAIL_THREAD_TOPIC = EMAIL_PAGE + 0x35; + static final int EMAIL_MIME_DATA = EMAIL_PAGE + 0x36; + static final int EMAIL_MIME_TRUNCATED = EMAIL_PAGE + 0x37; + static final int EMAIL_MIME_SIZE = EMAIL_PAGE + 0x38; + static final int EMAIL_INTERNET_CPID = EMAIL_PAGE + 0x39; + static final int EMAIL_FLAG = EMAIL_PAGE + 0x3A; + static final int EMAIL_FLAG_STATUS = EMAIL_PAGE + 0x3B; + static final int EMAIL_CONTENT_CLASS = EMAIL_PAGE + 0x3C; + static final int EMAIL_FLAG_TYPE = EMAIL_PAGE + 0x3D; + static final int EMAIL_COMPLETE_TIME = EMAIL_PAGE + 0x3E; + + static final int MOVE_PAGE = MOVE << PAGE_SHIFT; + static final int MOVE_MOVE_ITEMS = MOVE_PAGE + 5; + static final int MOVE_MOVE = MOVE_PAGE + 6; + static final int MOVE_SRCMSGID = MOVE_PAGE + 7; + static final int MOVE_SRCFLDID = MOVE_PAGE + 8; + static final int MOVE_DSTFLDID = MOVE_PAGE + 9; + static final int MOVE_RESPONSE = MOVE_PAGE + 0xA; + static final int MOVE_STATUS = MOVE_PAGE + 0xB; + static final int MOVE_DSTMSGID = MOVE_PAGE + 0xC; + + static final int CONTACTS2_PAGE = CONTACTS2 << PAGE_SHIFT; + static final int CONTACTS2_CUSTOMER_ID = CONTACTS2_PAGE + 5; + static final int CONTACTS2_GOVERNMENT_ID = CONTACTS2_PAGE + 6; + static final int CONTACTS2_IM_ADDRESS = CONTACTS2_PAGE + 7; + static final int CONTACTS2_IM_ADDRESS_2 = CONTACTS2_PAGE + 8; + static final int CONTACTS2_IM_ADDRESS_3 = CONTACTS2_PAGE + 9; + static final int CONTACTS2_MANAGER_NAME = CONTACTS2_PAGE + 0xA; + static final int CONTACTS2_COMPANY_MAIN_PHONE = CONTACTS2_PAGE + 0xB; + static final int CONTACTS2_ACCOUNT_NAME = CONTACTS2_PAGE + 0xC; + static final int CONTACTS2_NICKNAME = CONTACTS2_PAGE + 0xD; + static final int CONTACTS2_MMS = CONTACTS2_PAGE + 0xE; + + // The Ping constants are used by EasSyncService, and need to be public + static final int PING_PAGE = PING << PAGE_SHIFT; + public static final int PING_PING = PING_PAGE + 5; + public static final int PING_AUTD_STATE = PING_PAGE + 6; + public static final int PING_STATUS = PING_PAGE + 7; + public static final int PING_HEARTBEAT_INTERVAL = PING_PAGE + 8; + public static final int PING_FOLDERS = PING_PAGE + 9; + public static final int PING_FOLDER = PING_PAGE + 0xA; + public static final int PING_ID = PING_PAGE + 0xB; + public static final int PING_CLASS = PING_PAGE + 0xC; + public static final int PING_MAX_FOLDERS = PING_PAGE + 0xD; + + static final int BASE_PAGE = BASE << PAGE_SHIFT; + static final int BASE_BODY_PREFERENCE = BASE_PAGE + 5; + static final int BASE_TYPE = BASE_PAGE + 6; + static final int BASE_TRUNCATION_SIZE = BASE_PAGE + 7; + static final int BASE_ALL_OR_NONE = BASE_PAGE + 8; + static final int BASE_RESERVED = BASE_PAGE + 9; + static final int BASE_BODY = BASE_PAGE + 0xA; + static final int BASE_DATA = BASE_PAGE + 0xB; + static final int BASE_ESTIMATED_DATA_SIZE = BASE_PAGE + 0xC; + static final int BASE_TRUNCATED = BASE_PAGE + 0xD; + static final int BASE_ATTACHMENTS = BASE_PAGE + 0xE; + static final int BASE_ATTACHMENT = BASE_PAGE + 0xF; + static final int BASE_DISPLAY_NAME = BASE_PAGE + 0x10; + static final int BASE_FILE_REFERENCE = BASE_PAGE + 0x11; + static final int BASE_METHOD = BASE_PAGE + 0x12; + static final int BASE_CONTENT_ID = BASE_PAGE + 0x13; + static final int BASE_CONTENT_LOCATION = BASE_PAGE + 0x14; + static final int BASE_IS_INLINE = BASE_PAGE + 0x15; + static final int BASE_NATIVE_BODY_TYPE = BASE_PAGE + 0x16; + static final int BASE_CONTENT_TYPE = BASE_PAGE + 0x17; + + static public String[][] pages = { + { // 0x00 AirSync + "Sync", "Responses", "Add", "Change", "Delete", "Fetch", "SyncKey", "ClientId", + "ServerId", "Status", "Collection", "Class", "Version", "CollectionId", "GetChanges", + "MoreAvailable", "WindowSize", "Commands", "Options", "FilterType", "Truncation", + "RTFTruncation", "Conflict", "Collections", "ApplicationData", "DeletesAsMoves", + "NotifyGUID", "Supported", "SoftDelete", "MIMESupport", "MIMETruncation", "Wait", + "Limit", "Partial" + }, + { + // 0x01 Contacts + "Anniversary", "AssistantName", "AssistantTelephoneNumber", "Birthday", "Body", + "BodySize", "BodyTruncated", "Business2TelephoneNumber", "BusinessAddressCity", + "BusinessAddressCountry", "BusinessAddressPostalCode", "BusinessAddressState", + "BusinessAddressStreet", "BusinessFaxNumber", "BusinessTelephoneNumber", + "CarTelephoneNumber", "ContactsCategories", "ContactsCategory", "Children", "Child", + "CompanyName", "Department", "Email1Address", "Email2Address", "Email3Address", + "FileAs", "FirstName", "Home2TelephoneNumber", "HomeAddressCity", "HomeAddressCountry", + "HomeAddressPostalCode", "HomeAddressState", "HomeAddressStreet", "HomeFaxNumber", + "HomeTelephoneNumber", "JobTitle", "LastName", "MiddleName", "MobileTelephoneNumber", + "OfficeLocation", "OfficeAddressCity", "OfficeAddressCountry", + "OfficeAddressPostalCode", "OfficeAddressState", "OfficeAddressStreet", "PagerNumber", + "RadioTelephoneNumber", "Spouse", "Suffix", "Title", "Webpage", "YomiCompanyName", + "YomiFirstName", "YomiLastName", "CompressedRTF", "Picture" + }, + { + // 0x02 Email + "Attachment", "Attachments", "AttName", "AttSize", "Add0Id", "AttMethod", "AttRemoved", + "Body", "BodySize", "BodyTruncated", "DateReceived", "DisplayName", "DisplayTo", + "Importance", "MessageClass", "Subject", "Read", "To", "CC", "From", "ReplyTo", + "AllDayEvent", "Categories", "Category", "DTStamp", "EndTime", "InstanceType", + "IntDBusyStatus", "Location", "MeetingRequest", "Organizer", "RecurrenceId", "Reminder", + "ResponseRequested", "Recurrences", "Recurence", "Recurrence_Type", "Recurrence_Until", + "Recurrence_Occurrences", "Recurrence_Interval", "Recurrence_DayOfWeek", + "Recurrence_DayOfMonth", "Recurrence_WeekOfMonth", "Recurrence_MonthOfYear", + "StartTime", "Sensitivity", "TimeZone", "GlobalObjId", "ThreadTopic", "MIMEData", + "MIMETruncated", "MIMESize", "InternetCPID", "Flag", "FlagStatus", "ContentClass", + "FlagType", "CompleteTime" + }, + { + // 0x03 AirNotify + }, + { + // 0x04 Calendar + "CalTimeZone", "CalAllDayEvent", "CalAttendees", "CalAttendee", "CalAttendee_Email", + "CalAttendee_Name", "CalBody", "CalBodyTruncated", "CalBusyStatus", "CalCategories", + "CalCategory", "CalCompressed_RTF", "CalDTStamp", "CalEndTime", "CalExeption", + "CalExceptions", "CalException_IsDeleted", "CalException_StartTime", "CalLocation", + "CalMeetingStatus", "CalOrganizer_Email", "CalOrganizer_Name", "CalRecurrence", + "CalRecurrence_Type", "CalRecurrence_Until", "CalRecurrence_Occurrences", + "CalRecurrence_Interval", "CalRecurrence_DayOfWeek", "CalRecurrence_DayOfMonth", + "CalRecurrence_WeekOfMonth", "CalRecurrence_MonthOfYear", "CalReminder_MinsBefore", + "CalSensitivity", "CalSubject", "CalStartTime", "CalUID", "CalAttendee_Status", + "CalAttendee_Type" + }, + { + // 0x05 Move + "MoveItems", "Move", "SrcMsgId", "SrcFldId", "DstFldId", "Response", "Status", + "DstMsgId" + }, + { + // 0x06 ItemEstimate + }, + { + // 0x07 FolderHierarchy + "Folders", "Folder", "FolderDisplayName", "FolderServerId", "FolderParentId", "Type", + "Response", "Status", "ContentClass", "Changes", "FolderAdd", "FolderDelete", + "FolderUpdate", "FolderSyncKey", "FolderCreate", "FolderDelete", "FolderUpdate", + "FolderSync", "Count", "Version" + }, + { + // 0x08 MeetingResponse + }, + { + // 0x09 Tasks + }, + { + // 0x0A ResolveRecipients + }, + { + // 0x0B ValidateCert + }, + { + // 0x0C Contacts2 + "CustomerId", "GovernmentId", "IMAddress", "IMAddress2", "IMAddress3", "ManagerName", + "CompanyMainPhone", "AccountName", "NickName", "MMS" + }, + { + // 0x0D Ping + "Ping", "AutdState", "Status", "HeartbeatInterval", "PingFolders", "PingFolder", + "PingId", "PingClass", "MaxFolders" + }, + { + // 0x0E Provision + "Provision", "Policies", "Policy", "PolicyType", "PolicyKey", "Data", "Status", + "RemoteWipe", "EASProvidionDoc", "DevicePasswordEnabled", + "AlphanumericDevicePasswordRequired", + "DeviceEncryptionEnabled", "-unused-", "AttachmentsEnabled", "MinDevicePasswordLength", + "MaxInactivityTimeDeviceLock", "MaxDevicePasswordFailedAttempts", "MaxAttachmentSize", + "AllowSimpleDevicePassword", "DevicePasswordExpiration", "DevicePasswordHistory", + "AllowStorageCard", "AllowCamera", "RequireDeviceEncryption", + "AllowUnsignedApplications", "AllowUnsignedInstallationPackages", + "MinDevicePasswordComplexCharacters", "AllowWiFi", "AllowTextMessaging", + "AllowPOPIMAPEmail", "AllowBluetooth", "AllowIrDA", "RequireManualSyncWhenRoaming", + "AllowDesktopSync", + "MaxCalendarAgeFilder", "AllowHTMLEmail", "MaxEmailAgeFilder", + "MaxEmailBodyTruncationSize", "MaxEmailHTMLBodyTruncationSize", + "RequireSignedSMIMEMessages", "RequireEncryptedSMIMEMessages", + "RequireSignedSMIMEAlgorithm", "RequireEncryptionSMIMEAlgorithm", + "AllowSMIMEEncryptionAlgorithmNegotiation", "AllowSMIMESoftCerts", "AllowBrowser", + "AllowConsumerEmail", "AllowRemoteDesktop", "AllowInternetSharing", + "UnapprovedInROMApplicationList", "ApplicationName", "ApprovedApplicationList", "Hash" + }, + { + // 0x0F Search + }, + { + // 0x10 Gal + "DisplayName", "Phone", "Office", "Title", "Company", "Alias", "FirstName", "LastName", + "HomePhone", "MobilePhone", "EmailAddress" + }, + { + // 0x11 AirSyncBase + "BodyPreference", "BodyPreferenceType", "BodyPreferenceTruncationSize", "AllOrNone", + "Body", "Data", "EstimatedDataSize", "Truncated", "Attachments", "Attachment", + "DisplayName", "FileReference", "Method", "ContentId", "ContentLocation", "IsInline", + "NativeBodyType", "ContentType" + }, + { + // 0x12 Settings + }, + { + // 0x13 DocumentLibrary + }, + { + // 0x14 ItemOperations + } + }; +} diff --git a/src/com/android/exchange/Wbxml.java b/src/com/android/exchange/adapter/Wbxml.java similarity index 98% rename from src/com/android/exchange/Wbxml.java rename to src/com/android/exchange/adapter/Wbxml.java index 07b4d9aca..55d87355b 100644 --- a/src/com/android/exchange/Wbxml.java +++ b/src/com/android/exchange/adapter/Wbxml.java @@ -18,7 +18,7 @@ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS * IN THE SOFTWARE. */ -package com.android.exchange; +package com.android.exchange.adapter; /** contains the WBXML constants */ diff --git a/src/com/android/exchange/WbxmlSerializer.java b/src/com/android/exchange/adapter/WbxmlSerializer.java similarity index 99% rename from src/com/android/exchange/WbxmlSerializer.java rename to src/com/android/exchange/adapter/WbxmlSerializer.java index af3a4f177..1363d8597 100644 --- a/src/com/android/exchange/WbxmlSerializer.java +++ b/src/com/android/exchange/adapter/WbxmlSerializer.java @@ -1,4 +1,3 @@ - /* Copyright (c) 2002,2003, Stefan Haustein, Oberhausen, Rhld., Germany * * Permission is hereby granted, free of charge, to any person obtaining a copy @@ -22,7 +21,7 @@ //Contributors: Jonathan Cox, Bogdan Onoiu, Jerry Tian //Simplified for Google, Inc. by Marc Blank -package com.android.exchange; +package com.android.exchange.adapter; import java.io.*; import java.util.*; @@ -31,6 +30,7 @@ import org.xmlpull.v1.*; + /** * A class for writing WBXML. * diff --git a/src/com/android/exchange/Base64.java b/src/com/android/exchange/utility/Base64.java similarity index 99% rename from src/com/android/exchange/Base64.java rename to src/com/android/exchange/utility/Base64.java index 71cb7a088..2ca76ccf7 100644 --- a/src/com/android/exchange/Base64.java +++ b/src/com/android/exchange/utility/Base64.java @@ -11,7 +11,7 @@ * @author rob@iharder.net * @version 2.2.2 */ -package com.android.exchange; +package com.android.exchange.utility; import java.io.Writer; diff --git a/src/com/android/exchange/QuotedPrintable.java b/src/com/android/exchange/utility/QuotedPrintable.java similarity index 90% rename from src/com/android/exchange/QuotedPrintable.java rename to src/com/android/exchange/utility/QuotedPrintable.java index 08ebdd547..a0b0928b4 100644 --- a/src/com/android/exchange/QuotedPrintable.java +++ b/src/com/android/exchange/utility/QuotedPrintable.java @@ -15,8 +15,13 @@ * limitations under the License. */ -package com.android.exchange; +package com.android.exchange.utility; +/** + * Encode and decode QuotedPrintable text, according to the specification. Since the Email + * application already does this elsewhere, the goal would be to use its functionality here. + * + */ public class QuotedPrintable { static public String toString (String str) { int len = str.length(); @@ -34,7 +39,9 @@ public class QuotedPrintable { if (n == '\n') { continue; } else { - System.err.println("Not valid QP"); + // This isn't valid QuotedPrintable, but what to do? + // Let's just ignore it because 1) it's extremely unlikely to + // happen, and 2) an exception is frankly no better. } } else { // Must be less than 0x80, right? diff --git a/src/com/android/exchange/Rfc822Formatter.java b/src/com/android/exchange/utility/Rfc822Formatter.java similarity index 94% rename from src/com/android/exchange/Rfc822Formatter.java rename to src/com/android/exchange/utility/Rfc822Formatter.java index fdd2178c6..550880722 100644 --- a/src/com/android/exchange/Rfc822Formatter.java +++ b/src/com/android/exchange/utility/Rfc822Formatter.java @@ -15,7 +15,7 @@ * limitations under the License. */ -package com.android.exchange; +package com.android.exchange.utility; import java.io.File; import java.io.FileInputStream; @@ -38,6 +38,12 @@ import android.text.Html; import android.text.SpannedString; import android.util.Log; +/** + * Generates RFC822 formatted message data from a Message object. This functionality is also needed + * by the SMTP code, so we should use a single piece of code for this purpose. stadler is currently + * planning on rewriting SMTP code to handle this task, and we will use that code when it is ready. + * + */ public class Rfc822Formatter { static final SimpleDateFormat rfc822DateFormat = new SimpleDateFormat("dd MMM yy HH:mm:ss Z"); @@ -49,14 +55,14 @@ public class Rfc822Formatter { static final String CRLF = "\r\n"; - static public String writeEmailAsRfc822String (Context context, Account acct, + static public String writeEmailAsRfc822String(Context context, Account acct, Message msg, String uniqueId) throws IOException { StringWriter w = new StringWriter(); writeEmailAsRfc822(context, acct, msg, w, uniqueId); return w.toString(); } - static public boolean writeEmailAsRfc822 (Context context, Account acct, + static public boolean writeEmailAsRfc822(Context context, Account acct, Message msg, Writer writer, String uniqueId) throws IOException { // For now, multi-part alternative means an HTML reply... boolean alternativeParts = false;