diff --git a/Android.mk b/Android.mk index 2081c3615..3fb1b4271 100644 --- a/Android.mk +++ b/Android.mk @@ -23,7 +23,7 @@ include $(CLEAR_VARS) chips_dir := ../../../frameworks/ex/chips/res unified_email_dir := ../UnifiedEmail photo_dir := ../../../frameworks/ex/photoviewer/res -res_dir := $(chips_dir) res $(unified_email_dir)/res $(photo_dir) +res_dir := $(chips_dir) res $(unified_email_dir)/res $(photo_dir) build/res LOCAL_MODULE_TAGS := optional diff --git a/AndroidManifest.xml b/AndroidManifest.xml index b28fde849..8c67d247d 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -16,8 +16,8 @@ --> + android:versionCode="500005" + android:versionName="4.2-005" > @@ -31,7 +31,9 @@ + + @@ -413,7 +415,7 @@ @@ -423,20 +425,35 @@ + + + + + + + + android:resource="@xml/syncadapter_pop3" /> @@ -496,7 +513,7 @@ @@ -533,7 +550,7 @@ /> @@ -543,7 +560,7 @@ @@ -555,7 +572,7 @@ android:name="android.content.SyncAdapter" /> + android:resource="@xml/syncadapter_imap" /> + + + + + + + + + + + + + + + + + + + diff --git a/build/res/values/strings.xml b/build/res/values/strings.xml new file mode 100644 index 000000000..99bb9c228 --- /dev/null +++ b/build/res/values/strings.xml @@ -0,0 +1,25 @@ + + + + + com.android.exchange + com.android.pop3 + com.android.imap + com.android.email.EXCHANGE_INTENT + com.android.email.provider + imap + pop3 + diff --git a/res/xml/providers.xml b/build/res/xml/providers.xml similarity index 100% rename from res/xml/providers.xml rename to build/res/xml/providers.xml diff --git a/res/xml/services.xml b/build/res/xml/services.xml similarity index 100% rename from res/xml/services.xml rename to build/res/xml/services.xml diff --git a/emailcommon/src/com/android/emailcommon/provider/Account.java b/emailcommon/src/com/android/emailcommon/provider/Account.java index 7c7710a4d..af663a973 100755 --- a/emailcommon/src/com/android/emailcommon/provider/Account.java +++ b/emailcommon/src/com/android/emailcommon/provider/Account.java @@ -40,15 +40,6 @@ import java.util.UUID; public final class Account extends EmailContent implements AccountColumns, Parcelable { public static final String TABLE_NAME = "Account"; - public static final Uri CONTENT_URI = Uri.parse(EmailContent.CONTENT_URI + "/account"); - public static final Uri ADD_TO_FIELD_URI = - Uri.parse(EmailContent.CONTENT_URI + "/accountIdAddToField"); - public static final Uri RESET_NEW_MESSAGE_COUNT_URI = - Uri.parse(EmailContent.CONTENT_URI + "/resetNewMessageCount"); - public static final Uri NOTIFIER_URI = - Uri.parse(EmailContent.CONTENT_NOTIFIER_URI + "/account"); - public static final Uri DEFAULT_ACCOUNT_ID_URI = - Uri.parse(EmailContent.CONTENT_URI + "/account/default"); // Define all pseudo account IDs here to avoid conflict with one another. /** @@ -109,6 +100,19 @@ public final class Account extends EmailContent implements AccountColumns, Parce public static final int CHECK_INTERVAL_NEVER = -1; public static final int CHECK_INTERVAL_PUSH = -2; + public static Uri CONTENT_URI; + public static Uri ADD_TO_FIELD_URI; + public static Uri RESET_NEW_MESSAGE_COUNT_URI; + public static Uri NOTIFIER_URI; + public static Uri DEFAULT_ACCOUNT_ID_URI; + + public static void initAccount() { + CONTENT_URI = Uri.parse(EmailContent.CONTENT_URI + "/account"); + ADD_TO_FIELD_URI = Uri.parse(EmailContent.CONTENT_URI + "/accountIdAddToField"); + RESET_NEW_MESSAGE_COUNT_URI = Uri.parse(EmailContent.CONTENT_URI + "/resetNewMessageCount"); + NOTIFIER_URI = Uri.parse(EmailContent.CONTENT_NOTIFIER_URI + "/account"); + DEFAULT_ACCOUNT_ID_URI = Uri.parse(EmailContent.CONTENT_URI + "/account/default"); + } public String mDisplayName; public String mEmailAddress; public String mSyncKey; @@ -495,7 +499,7 @@ public final class Account extends EmailContent implements AccountColumns, Parce public static long getAccountIdFromShortcutSafeUri(Context context, Uri uri) { // Make sure the URI is in the correct format. if (!"content".equals(uri.getScheme()) - || !AUTHORITY.equals(uri.getAuthority())) { + || !EmailContent.AUTHORITY.equals(uri.getAuthority())) { return -1; } @@ -699,7 +703,7 @@ public final class Account extends EmailContent implements AccountColumns, Parce .newUpdate(ContentUris.withAppendedId(CONTENT_URI, mId)) .withValues(cv).build()); try { - context.getContentResolver().applyBatch(AUTHORITY, ops); + context.getContentResolver().applyBatch(EmailContent.AUTHORITY, ops); return 1; } catch (RemoteException e) { // There is nothing to be done here; fail by returning 0 @@ -779,7 +783,7 @@ public final class Account extends EmailContent implements AccountColumns, Parce try { ContentProviderResult[] results = - context.getContentResolver().applyBatch(AUTHORITY, ops); + context.getContentResolver().applyBatch(EmailContent.AUTHORITY, ops); // If saving, set the mId's of the various saved objects if (recvIndex >= 0) { long newId = getId(results[recvIndex].uri); diff --git a/emailcommon/src/com/android/emailcommon/provider/EmailContent.java b/emailcommon/src/com/android/emailcommon/provider/EmailContent.java index 265935591..1747fb741 100755 --- a/emailcommon/src/com/android/emailcommon/provider/EmailContent.java +++ b/emailcommon/src/com/android/emailcommon/provider/EmailContent.java @@ -29,6 +29,7 @@ import android.os.Environment; import android.os.Parcel; import android.os.Parcelable; import android.os.RemoteException; +import android.util.Log; import com.android.emailcommon.utility.TextUtilities; import com.android.emailcommon.utility.Utility; @@ -59,39 +60,12 @@ import java.util.ArrayList; * */ public abstract class EmailContent { - - public static final String AUTHORITY = "com.android.email.provider"; - // The notifier authority is used to send notifications regarding changes to messages (insert, - // delete, or update) and is intended as an optimization for use by clients of message list - // cursors (initially, the email AppWidget). - public static final String NOTIFIER_AUTHORITY = "com.android.email.notifier"; - - public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY); - public static final String PARAMETER_LIMIT = "limit"; - - public static final Uri CONTENT_NOTIFIER_URI = Uri.parse("content://" + NOTIFIER_AUTHORITY); - - public static final Uri PICK_TRASH_FOLDER_URI = - Uri.parse("content://" + EmailContent.AUTHORITY + "/pickTrashFolder"); - public static final Uri PICK_SENT_FOLDER_URI = - Uri.parse("content://" + EmailContent.AUTHORITY + "/pickSentFolder"); - - public static final Uri MAILBOX_NOTIFICATION_URI = - Uri.parse("content://" + EmailContent.AUTHORITY + "/mailboxNotification"); public static final String[] NOTIFICATION_PROJECTION = new String[] {MailboxColumns.ID, MailboxColumns.UNREAD_COUNT, MailboxColumns.MESSAGE_COUNT}; public static final int NOTIFICATION_MAILBOX_ID_COLUMN = 0; public static final int NOTIFICATION_MAILBOX_UNREAD_COUNT_COLUMN = 1; public static final int NOTIFICATION_MAILBOX_MESSAGE_COUNT_COLUMN = 2; - public static final Uri MAILBOX_MOST_RECENT_MESSAGE_URI = - Uri.parse("content://" + EmailContent.AUTHORITY + "/mailboxMostRecentMessage"); - - public static final Uri ACCOUNT_CHECK_URI = - Uri.parse("content://" + EmailContent.AUTHORITY + "/accountCheck"); - - public static final String PROVIDER_PERMISSION = "com.android.email.permission.ACCESS_PROVIDER"; - // All classes share this public static final String RECORD_ID = "_id"; @@ -139,6 +113,57 @@ public abstract class EmailContent { // Read the Content from a ContentCursor public abstract void restore (Cursor cursor); + + public static String PACKAGE_NAME; + public static String EMAIL_PACKAGE_NAME; + public static String AUTHORITY; + // The notifier authority is used to send notifications regarding changes to messages (insert, + // delete, or update) and is intended as an optimization for use by clients of message list + // cursors (initially, the email AppWidget). + public static String NOTIFIER_AUTHORITY; + public static Uri CONTENT_URI; + public static final String PARAMETER_LIMIT = "limit"; + public static Uri CONTENT_NOTIFIER_URI; + public static Uri PICK_TRASH_FOLDER_URI; + public static Uri PICK_SENT_FOLDER_URI; + public static Uri MAILBOX_NOTIFICATION_URI; + public static Uri MAILBOX_MOST_RECENT_MESSAGE_URI; + public static Uri ACCOUNT_CHECK_URI; + public static String PROVIDER_PERMISSION; + + public static void init(Context context) { + if (AUTHORITY == null) { + PACKAGE_NAME = context.getPackageName(); + EMAIL_PACKAGE_NAME = PACKAGE_NAME; + // If our package is com...exchange, the provider is com...email.provider + if (PACKAGE_NAME.endsWith("exchange")) { + int lastDot = EMAIL_PACKAGE_NAME.lastIndexOf('.'); + EMAIL_PACKAGE_NAME = PACKAGE_NAME.substring(0, lastDot + 1) + "email"; + } + AUTHORITY = EMAIL_PACKAGE_NAME + ".provider"; + Log.d("EmailContent", "init for " + AUTHORITY); + NOTIFIER_AUTHORITY = EMAIL_PACKAGE_NAME + ".notifier"; + CONTENT_URI = Uri.parse("content://" + AUTHORITY); + CONTENT_NOTIFIER_URI = Uri.parse("content://" + NOTIFIER_AUTHORITY); + PICK_TRASH_FOLDER_URI = Uri.parse("content://" + AUTHORITY + "/pickTrashFolder"); + PICK_SENT_FOLDER_URI = Uri.parse("content://" + AUTHORITY + "/pickSendFolder"); + MAILBOX_NOTIFICATION_URI = Uri.parse("content://" + AUTHORITY + "/mailboxNotification"); + MAILBOX_MOST_RECENT_MESSAGE_URI = Uri.parse("content://" + AUTHORITY + + "/mailboxMostRecentMessage"); + ACCOUNT_CHECK_URI = Uri.parse("content://" + AUTHORITY + "/accountCheck"); + PROVIDER_PERMISSION = EMAIL_PACKAGE_NAME + ".permission.ACCESS_PROVIDER"; + // Initialize subclasses + Account.initAccount(); + Mailbox.initMailbox(); + QuickResponse.initQuickResponse(); + HostAuth.initHostAuth(); + Policy.initPolicy(); + Message.initMessage(); + Body.initBody(); + Attachment.initAttachment(); + } + } + // The Uri is lazily initialized public Uri getUri() { if (mUri == null) { @@ -287,8 +312,11 @@ public abstract class EmailContent { public static final class Body extends EmailContent implements BodyColumns { public static final String TABLE_NAME = "Body"; - @SuppressWarnings("hiding") - public static final Uri CONTENT_URI = Uri.parse(EmailContent.CONTENT_URI + "/body"); + public static Uri CONTENT_URI; + + public static void initBody() { + CONTENT_URI = Uri.parse(EmailContent.CONTENT_URI + "/body"); + } public static final int CONTENT_ID_COLUMN = 0; public static final int CONTENT_MESSAGE_KEY_COLUMN = 1; @@ -330,7 +358,7 @@ public abstract class EmailContent { public static final String[] COMMON_PROJECTION_SOURCE = new String[] { RECORD_ID, BodyColumns.SOURCE_MESSAGE_KEY }; - public static final int COMMON_PROJECTION_COLUMN_TEXT = 1; + public static final int COMMON_PROJECTION_COLUMN_TEXT = 1; private static final String[] PROJECTION_SOURCE_KEY = new String[] { BodyColumns.SOURCE_MESSAGE_KEY }; @@ -558,18 +586,28 @@ public abstract class EmailContent { 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 CONTENT_URI_LIMIT_1 = uriWithLimit(CONTENT_URI, 1); - public static final Uri SYNCED_CONTENT_URI = - Uri.parse(EmailContent.CONTENT_URI + "/syncedMessage"); - public static final Uri SELECTED_MESSAGE_CONTENT_URI = - Uri.parse(EmailContent.CONTENT_URI + "/messageBySelection"); - 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 Uri NOTIFIER_URI = - Uri.parse(EmailContent.CONTENT_NOTIFIER_URI + "/message"); + public static Uri CONTENT_URI; + public static Uri CONTENT_URI_LIMIT_1; + public static Uri SYNCED_CONTENT_URI; + public static Uri SELECTED_MESSAGE_CONTENT_URI ; + public static Uri DELETED_CONTENT_URI; + public static Uri UPDATED_CONTENT_URI; + public static Uri NOTIFIER_URI; + + public static void initMessage() { + CONTENT_URI = Uri.parse(EmailContent.CONTENT_URI + "/message"); + CONTENT_URI_LIMIT_1 = uriWithLimit(CONTENT_URI, 1); + SYNCED_CONTENT_URI = + Uri.parse(EmailContent.CONTENT_URI + "/syncedMessage"); + SELECTED_MESSAGE_CONTENT_URI = + Uri.parse(EmailContent.CONTENT_URI + "/messageBySelection"); + DELETED_CONTENT_URI = + Uri.parse(EmailContent.CONTENT_URI + "/deletedMessage"); + UPDATED_CONTENT_URI = + Uri.parse(EmailContent.CONTENT_URI + "/updatedMessage"); + NOTIFIER_URI = + Uri.parse(EmailContent.CONTENT_NOTIFIER_URI + "/message"); + } public static final String KEY_TIMESTAMP_DESC = MessageColumns.TIMESTAMP + " desc"; @@ -1143,11 +1181,16 @@ public abstract class EmailContent { public static final class Attachment extends EmailContent implements AttachmentColumns, Parcelable { public static final String TABLE_NAME = "Attachment"; - @SuppressWarnings("hiding") - public static final Uri CONTENT_URI = Uri.parse(EmailContent.CONTENT_URI + "/attachment"); + + public static Uri CONTENT_URI; // This must be used with an appended id: ContentUris.withAppendedId(MESSAGE_ID_URI, id) - public static final Uri MESSAGE_ID_URI = Uri.parse( - EmailContent.CONTENT_URI + "/attachment/message"); + public static Uri MESSAGE_ID_URI; + + public static void initAttachment() { + CONTENT_URI = Uri.parse(EmailContent.CONTENT_URI + "/attachment"); + MESSAGE_ID_URI = Uri.parse( + EmailContent.CONTENT_URI + "/attachment/message"); + } public String mFileName; public String mMimeType; diff --git a/emailcommon/src/com/android/emailcommon/provider/HostAuth.java b/emailcommon/src/com/android/emailcommon/provider/HostAuth.java index 424d8f03b..f6be38616 100644 --- a/emailcommon/src/com/android/emailcommon/provider/HostAuth.java +++ b/emailcommon/src/com/android/emailcommon/provider/HostAuth.java @@ -34,7 +34,11 @@ import java.net.URISyntaxException; public final class HostAuth extends EmailContent implements HostAuthColumns, Parcelable { public static final String TABLE_NAME = "HostAuth"; - public static final Uri CONTENT_URI = Uri.parse(EmailContent.CONTENT_URI + "/hostauth"); + public static Uri CONTENT_URI; + + public static void initHostAuth() { + CONTENT_URI = Uri.parse(EmailContent.CONTENT_URI + "/hostauth"); + } // These legacy constants should be used in code created prior to Email2 public static final String LEGACY_SCHEME_IMAP = "imap"; @@ -42,11 +46,6 @@ public final class HostAuth extends EmailContent implements HostAuthColumns, Par public static final String LEGACY_SCHEME_EAS = "eas"; public static final String LEGACY_SCHEME_SMTP = "smtp"; - // These constants should, over time, be replaced by information in services.xml - public static final String SCHEME_IMAP = "imap"; - public static final String SCHEME_POP3 = "pop3"; - public static final String SCHEME_EAS = "eas"; - public static final String SCHEME_TRUST_ALL_CERTS = "trustallcerts"; public static final int PORT_UNKNOWN = -1; @@ -284,10 +283,6 @@ public final class HostAuth extends EmailContent implements HostAuthColumns, Par mClientCertAlias = clientCertAlias; } - /** Returns {@code true} if this is an EAS connection; otherwise, {@code false}. */ - public boolean isEasConnection() { - return SCHEME_EAS.equals(mProtocol); - } /** Convenience method to determine if SSL is used. */ public boolean shouldUseSsl() { diff --git a/emailcommon/src/com/android/emailcommon/provider/Mailbox.java b/emailcommon/src/com/android/emailcommon/provider/Mailbox.java index 0c2513508..7307dbd47 100644 --- a/emailcommon/src/com/android/emailcommon/provider/Mailbox.java +++ b/emailcommon/src/com/android/emailcommon/provider/Mailbox.java @@ -33,11 +33,17 @@ import com.android.emailcommon.utility.Utility; public class Mailbox extends EmailContent implements SyncColumns, MailboxColumns, Parcelable { public static final String TABLE_NAME = "Mailbox"; - public static final Uri CONTENT_URI = Uri.parse(EmailContent.CONTENT_URI + "/mailbox"); - public static final Uri ADD_TO_FIELD_URI = - Uri.parse(EmailContent.CONTENT_URI + "/mailboxIdAddToField"); - public static final Uri FROM_ACCOUNT_AND_TYPE_URI = - Uri.parse(EmailContent.CONTENT_URI + "/mailboxIdFromAccountAndType"); + + public static Uri CONTENT_URI; + public static Uri ADD_TO_FIELD_URI; + public static Uri FROM_ACCOUNT_AND_TYPE_URI; + + public static void initMailbox() { + CONTENT_URI = Uri.parse(EmailContent.CONTENT_URI + "/mailbox"); + ADD_TO_FIELD_URI = Uri.parse(EmailContent.CONTENT_URI + "/mailboxIdAddToField"); + FROM_ACCOUNT_AND_TYPE_URI = Uri.parse(EmailContent.CONTENT_URI + + "/mailboxIdFromAccountAndType"); + } public String mDisplayName; public String mServerId; diff --git a/emailcommon/src/com/android/emailcommon/provider/Policy.java b/emailcommon/src/com/android/emailcommon/provider/Policy.java index d43290f9f..0c2410d34 100755 --- a/emailcommon/src/com/android/emailcommon/provider/Policy.java +++ b/emailcommon/src/com/android/emailcommon/provider/Policy.java @@ -40,8 +40,11 @@ public final class Policy extends EmailContent implements EmailContent.PolicyCol public static final String TAG = "Email/Policy"; public static final String TABLE_NAME = "Policy"; - @SuppressWarnings("hiding") - public static final Uri CONTENT_URI = Uri.parse(EmailContent.CONTENT_URI + "/policy"); + public static Uri CONTENT_URI; + + public static void initPolicy() { + CONTENT_URI = Uri.parse(EmailContent.CONTENT_URI + "/policy"); + } /* Convert days to mSec (used for password expiration) */ private static final long DAYS_TO_MSEC = 24 * 60 * 60 * 1000; diff --git a/emailcommon/src/com/android/emailcommon/provider/QuickResponse.java b/emailcommon/src/com/android/emailcommon/provider/QuickResponse.java index e88e01d95..3df082442 100644 --- a/emailcommon/src/com/android/emailcommon/provider/QuickResponse.java +++ b/emailcommon/src/com/android/emailcommon/provider/QuickResponse.java @@ -17,10 +17,6 @@ package com.android.emailcommon.provider; -import com.android.emailcommon.provider.EmailContent; -import com.android.emailcommon.provider.EmailContent.QuickResponseColumns; -import com.google.common.base.Objects; - import android.content.ContentUris; import android.content.ContentValues; import android.content.Context; @@ -29,6 +25,9 @@ import android.net.Uri; import android.os.Parcel; import android.os.Parcelable; +import com.android.emailcommon.provider.EmailContent.QuickResponseColumns; +import com.google.common.base.Objects; + /** * A user-modifiable message that may be quickly inserted into the body while user is composing * a message. Tied to a specific account. @@ -36,11 +35,13 @@ import android.os.Parcelable; public final class QuickResponse extends EmailContent implements QuickResponseColumns, Parcelable { public static final String TABLE_NAME = "QuickResponse"; - @SuppressWarnings("hiding") - public static final Uri CONTENT_URI = Uri.parse(EmailContent.CONTENT_URI - + "/quickresponse"); - public static final Uri ACCOUNT_ID_URI = Uri.parse( - EmailContent.CONTENT_URI + "/quickresponse/account"); + public static Uri CONTENT_URI; + public static Uri ACCOUNT_ID_URI; + + public static void initQuickResponse() { + CONTENT_URI = Uri.parse(EmailContent.CONTENT_URI + "/quickresponse"); + ACCOUNT_ID_URI = Uri.parse(EmailContent.CONTENT_URI + "/quickresponse/account"); + } private String mText; private long mAccountKey; diff --git a/emailcommon/src/com/android/emailcommon/service/AccountServiceProxy.java b/emailcommon/src/com/android/emailcommon/service/AccountServiceProxy.java index d65473894..1667295cb 100644 --- a/emailcommon/src/com/android/emailcommon/service/AccountServiceProxy.java +++ b/emailcommon/src/com/android/emailcommon/service/AccountServiceProxy.java @@ -24,14 +24,13 @@ import android.os.RemoteException; public class AccountServiceProxy extends ServiceProxy implements IAccountService { - public static final String ACCOUNT_INTENT = "com.android.email.ACCOUNT_INTENT"; public static final int DEFAULT_ACCOUNT_COLOR = 0xFF0000FF; private IAccountService mService = null; private Object mReturn; public AccountServiceProxy(Context _context) { - super(_context, new Intent(ACCOUNT_INTENT)); + super(_context, getIntentForEmailPackage(_context, "ACCOUNT_INTENT")); } @Override diff --git a/emailcommon/src/com/android/emailcommon/service/EmailServiceProxy.java b/emailcommon/src/com/android/emailcommon/service/EmailServiceProxy.java index 36d3b77e9..c7c77b897 100644 --- a/emailcommon/src/com/android/emailcommon/service/EmailServiceProxy.java +++ b/emailcommon/src/com/android/emailcommon/service/EmailServiceProxy.java @@ -475,6 +475,23 @@ public class EmailServiceProxy extends ServiceProxy implements IEmailService { return (Integer)mReturn; } } + /** + * Request that the account be updated for this service; this call is synchronous + * + * @param the email address of the account to be updated + */ + @Override + public void serviceUpdated(final String emailAddress) throws RemoteException { + setTask(new ProxyTask() { + @Override + public void run() throws RemoteException{ + if (mCallback != null) mService.setCallback(mCallback); + mService.serviceUpdated(emailAddress); + } + }, "settingsUpdate"); + waitForCompletion(); + } + @Override public IBinder asBinder() { diff --git a/emailcommon/src/com/android/emailcommon/service/IEmailService.aidl b/emailcommon/src/com/android/emailcommon/service/IEmailService.aidl index 5c48c0fb1..44b79dee5 100644 --- a/emailcommon/src/com/android/emailcommon/service/IEmailService.aidl +++ b/emailcommon/src/com/android/emailcommon/service/IEmailService.aidl @@ -63,4 +63,6 @@ interface IEmailService { // API level 3 int getCapabilities(in Account acct); + + void serviceUpdated(String emailAddress); } diff --git a/emailcommon/src/com/android/emailcommon/service/PolicyServiceProxy.java b/emailcommon/src/com/android/emailcommon/service/PolicyServiceProxy.java index 26e820dee..78a354329 100755 --- a/emailcommon/src/com/android/emailcommon/service/PolicyServiceProxy.java +++ b/emailcommon/src/com/android/emailcommon/service/PolicyServiceProxy.java @@ -17,7 +17,6 @@ package com.android.emailcommon.service; import android.content.Context; -import android.content.Intent; import android.os.IBinder; import android.os.RemoteException; import android.util.Log; @@ -29,14 +28,11 @@ public class PolicyServiceProxy extends ServiceProxy implements IPolicyService { private static final boolean DEBUG_PROXY = false; // DO NOT CHECK THIS IN SET TO TRUE private static final String TAG = "PolicyServiceProxy"; - // The intent used by sync adapter services to connect to the PolicyService - public static final String POLICY_INTENT = "com.android.email.POLICY_INTENT"; - private IPolicyService mService = null; private Object mReturn = null; public PolicyServiceProxy(Context _context) { - super(_context, new Intent(POLICY_INTENT)); + super(_context, getIntentForEmailPackage(_context, "POLICY_INTENT")); } @Override diff --git a/emailcommon/src/com/android/emailcommon/service/ServiceProxy.java b/emailcommon/src/com/android/emailcommon/service/ServiceProxy.java index 4d8b81217..f93ce9d65 100644 --- a/emailcommon/src/com/android/emailcommon/service/ServiceProxy.java +++ b/emailcommon/src/com/android/emailcommon/service/ServiceProxy.java @@ -56,6 +56,16 @@ public abstract class ServiceProxy { private long mStartTime; private boolean mDead = false; + public static Intent getIntentForEmailPackage(Context context, String actionName) { + String packageName = context.getPackageName(); + int lastDot = packageName.lastIndexOf('.'); + String intentString = + packageName.substring(0, lastDot + 1) + "email." + actionName; + // STOPSHIP Remove logging + Log.d("ServiceProxy", actionName + " -> " + intentString); + return new Intent(intentString); + } + public abstract void onConnected(IBinder binder); public ServiceProxy(Context _context, Intent _intent) { diff --git a/emailcommon/src/com/android/emailcommon/utility/AttachmentUtilities.java b/emailcommon/src/com/android/emailcommon/utility/AttachmentUtilities.java index f43458323..564e9a8ee 100644 --- a/emailcommon/src/com/android/emailcommon/utility/AttachmentUtilities.java +++ b/emailcommon/src/com/android/emailcommon/utility/AttachmentUtilities.java @@ -30,6 +30,7 @@ import android.util.Log; import android.webkit.MimeTypeMap; import com.android.emailcommon.Logging; +import com.android.emailcommon.provider.EmailContent; import com.android.emailcommon.provider.EmailContent.Attachment; import com.android.emailcommon.provider.EmailContent.AttachmentColumns; import com.android.emailcommon.provider.EmailContent.Body; @@ -47,8 +48,6 @@ import java.io.InputStream; import java.io.OutputStream; public class AttachmentUtilities { - public static final String AUTHORITY = "com.android.email.attachmentprovider"; - public static final Uri CONTENT_URI = Uri.parse( "content://" + AUTHORITY); public static final String FORMAT_RAW = "RAW"; public static final String FORMAT_THUMBNAIL = "THUMBNAIL"; @@ -137,23 +136,17 @@ public class AttachmentUtilities { */ public static final int MAX_ATTACHMENT_UPLOAD_SIZE = (5 * 1024 * 1024); + private static Uri sUri; public static Uri getAttachmentUri(long accountId, long id) { - return CONTENT_URI.buildUpon() - .appendPath(Long.toString(accountId)) - .appendPath(Long.toString(id)) - .appendPath(FORMAT_RAW) - .build(); - } - - public static Uri getAttachmentThumbnailUri(long accountId, long id, - int width, int height) { - return CONTENT_URI.buildUpon() - .appendPath(Long.toString(accountId)) - .appendPath(Long.toString(id)) - .appendPath(FORMAT_THUMBNAIL) - .appendPath(Integer.toString(width)) - .appendPath(Integer.toString(height)) - .build(); + if (sUri == null) { + sUri = Uri.parse("content://" + EmailContent.EMAIL_PACKAGE_NAME + + ".attachmentprovider"); + } + return sUri.buildUpon() + .appendPath(Long.toString(accountId)) + .appendPath(Long.toString(id)) + .appendPath(FORMAT_RAW) + .build(); } /** diff --git a/emailsync/src/com/android/emailsync/AbstractSyncService.java b/emailsync/src/com/android/emailsync/AbstractSyncService.java index c9b151a4f..f64224feb 100644 --- a/emailsync/src/com/android/emailsync/AbstractSyncService.java +++ b/emailsync/src/com/android/emailsync/AbstractSyncService.java @@ -17,16 +17,16 @@ package com.android.emailsync; -import com.android.emailcommon.provider.Account; -import com.android.emailcommon.provider.HostAuth; -import com.android.emailcommon.provider.Mailbox; - import android.content.Context; import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.os.Bundle; import android.util.Log; +import com.android.emailcommon.provider.Account; +import com.android.emailcommon.provider.HostAuth; +import com.android.emailcommon.provider.Mailbox; + import java.util.concurrent.LinkedBlockingQueue; /** @@ -48,7 +48,6 @@ public abstract class AbstractSyncService implements Runnable { public static final int CONNECT_TIMEOUT = 30*SECONDS; public static final int NETWORK_WAIT = 15*SECONDS; - 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; @@ -289,7 +288,9 @@ public abstract class AbstractSyncService implements Runnable { */ public void addRequest(Request req) { - mRequestQueue.offer(req); + if (!mRequestQueue.contains(req)) { + mRequestQueue.offer(req); + } } public void removeRequest(Request req) { diff --git a/emailsync/src/com/android/emailsync/SyncManager.java b/emailsync/src/com/android/emailsync/SyncManager.java index 4a518d939..886d13419 100644 --- a/emailsync/src/com/android/emailsync/SyncManager.java +++ b/emailsync/src/com/android/emailsync/SyncManager.java @@ -1357,6 +1357,7 @@ public abstract class SyncManager extends Service implements Runnable { @Override public void onCreate() { TAG = getClass().getSimpleName(); + EmailContent.init(this); Utility.runAsync(new Runnable() { @Override public void run() { diff --git a/res/values/attrs.xml b/res/values/attrs.xml index 26e2f8e38..e24c4ee7c 100644 --- a/res/values/attrs.xml +++ b/res/values/attrs.xml @@ -64,5 +64,6 @@ + diff --git a/res/values/strings.xml b/res/values/strings.xml index 5c4bae4ec..20b0b3c71 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -1310,8 +1310,9 @@ as %s. No messages. - - Push IMAP + + IMAP + POP3 Picky, picky, picky! @@ -1319,6 +1320,6 @@ as %s. Select server sent items folder Create folder - + diff --git a/res/xml/authenticator_alternate.xml b/res/xml/authenticator_alternate.xml index 44b756667..003e7f720 100644 --- a/res/xml/authenticator_alternate.xml +++ b/res/xml/authenticator_alternate.xml @@ -23,9 +23,9 @@ diff --git a/res/xml/authenticator_eas.xml b/res/xml/authenticator_eas.xml new file mode 100644 index 000000000..4ee83d2c1 --- /dev/null +++ b/res/xml/authenticator_eas.xml @@ -0,0 +1,29 @@ + + + + + + + diff --git a/res/xml/imap2_authenticator.xml b/res/xml/authenticator_imap.xml similarity index 94% rename from res/xml/imap2_authenticator.xml rename to res/xml/authenticator_imap.xml index 622add2cc..a8129ae10 100644 --- a/res/xml/imap2_authenticator.xml +++ b/res/xml/authenticator_imap.xml @@ -21,7 +21,7 @@ diff --git a/res/xml/authenticator_legacy_email.xml b/res/xml/authenticator_legacy_email.xml new file mode 100644 index 000000000..5aad049c8 --- /dev/null +++ b/res/xml/authenticator_legacy_email.xml @@ -0,0 +1,29 @@ + + + + + + + diff --git a/res/xml/pop_imap_authenticator.xml b/res/xml/authenticator_pop3.xml similarity index 91% rename from res/xml/pop_imap_authenticator.xml rename to res/xml/authenticator_pop3.xml index ca34a8c9f..47e022106 100644 --- a/res/xml/pop_imap_authenticator.xml +++ b/res/xml/authenticator_pop3.xml @@ -21,9 +21,9 @@ diff --git a/res/xml/senders.xml b/res/xml/senders.xml index 81ef68fea..138bcd8a6 100644 --- a/res/xml/senders.xml +++ b/res/xml/senders.xml @@ -31,5 +31,4 @@ - diff --git a/res/xml/syncadapter_imap2.xml b/res/xml/syncadapter_imap.xml similarity index 87% rename from res/xml/syncadapter_imap2.xml rename to res/xml/syncadapter_imap.xml index 278f4ad4e..1171a1f7d 100644 --- a/res/xml/syncadapter_imap2.xml +++ b/res/xml/syncadapter_imap.xml @@ -21,7 +21,7 @@ diff --git a/res/xml/syncadapter_pop_imap.xml b/res/xml/syncadapter_pop3.xml similarity index 88% rename from res/xml/syncadapter_pop_imap.xml rename to res/xml/syncadapter_pop3.xml index f4a7120fc..2ce611d6e 100644 --- a/res/xml/syncadapter_pop_imap.xml +++ b/res/xml/syncadapter_pop3.xml @@ -21,8 +21,8 @@ diff --git a/src/com/android/email/NotificationController.java b/src/com/android/email/NotificationController.java index 12477069f..99565d624 100644 --- a/src/com/android/email/NotificationController.java +++ b/src/com/android/email/NotificationController.java @@ -124,6 +124,7 @@ public class NotificationController { @VisibleForTesting NotificationController(Context context, Clock clock) { mContext = context.getApplicationContext(); + EmailContent.init(context); mNotificationManager = (NotificationManager) context.getSystemService( Context.NOTIFICATION_SERVICE); mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); diff --git a/src/com/android/email/activity/setup/AccountCheckSettingsFragment.java b/src/com/android/email/activity/setup/AccountCheckSettingsFragment.java index 4bcd7c1ec..8646e02f1 100644 --- a/src/com/android/email/activity/setup/AccountCheckSettingsFragment.java +++ b/src/com/android/email/activity/setup/AccountCheckSettingsFragment.java @@ -33,6 +33,8 @@ import android.util.Log; import com.android.email.R; import com.android.email.mail.Sender; import com.android.email.mail.Store; +import com.android.email.service.EmailServiceUtils; +import com.android.email.service.EmailServiceUtils.EmailServiceInfo; import com.android.emailcommon.Logging; import com.android.emailcommon.mail.MessagingException; import com.android.emailcommon.provider.Account; @@ -473,6 +475,11 @@ public class AccountCheckSettingsFragment extends Fragment { if (bundle != null) { resultCode = bundle.getInt( EmailServiceProxy.VALIDATE_BUNDLE_RESULT_CODE); + // Only show "policies required" if this is a new account setup + if (resultCode == MessagingException.SECURITY_POLICIES_REQUIRED && + mAccount.isSaved()) { + resultCode = MessagingException.NO_ERROR; + } } if (resultCode == MessagingException.SECURITY_POLICIES_REQUIRED) { SetupData.setPolicy((Policy)bundle.getParcelable( @@ -491,8 +498,11 @@ public class AccountCheckSettingsFragment extends Fragment { } } + String protocol = mAccount.mHostAuthRecv.mProtocol; + EmailServiceInfo info = EmailServiceUtils.getServiceInfo(mContext, protocol); + // Check Outgoing Settings - if ((mMode & SetupData.CHECK_OUTGOING) != 0) { + if (info.usesSmtp && (mMode & SetupData.CHECK_OUTGOING) != 0) { if (isCancelled()) return null; Log.d(Logging.LOG_TAG, "Begin check of outgoing email settings"); publishProgress(STATE_CHECK_OUTGOING); diff --git a/src/com/android/email/activity/setup/AccountSettingsFragment.java b/src/com/android/email/activity/setup/AccountSettingsFragment.java index 7c5fc74dc..bc07cc82d 100644 --- a/src/com/android/email/activity/setup/AccountSettingsFragment.java +++ b/src/com/android/email/activity/setup/AccountSettingsFragment.java @@ -41,7 +41,6 @@ import com.android.email.SecurityPolicy; import com.android.email.service.EmailServiceUtils; import com.android.email.service.EmailServiceUtils.EmailServiceInfo; import com.android.email2.ui.MailActivityEmail; -import com.android.emailcommon.AccountManagerTypes; import com.android.emailcommon.Logging; import com.android.emailcommon.provider.Account; import com.android.emailcommon.provider.EmailContent; @@ -727,7 +726,7 @@ public class AccountSettingsFragment extends EmailPreferenceFragment EmailServiceUtils.getServiceInfo(mContext, mAccount.getProtocol(mContext)); if (info.syncContacts || info.syncCalendar) { android.accounts.Account acct = new android.accounts.Account(mAccount.mEmailAddress, - AccountManagerTypes.TYPE_EXCHANGE); + info.accountType); ContentResolver.setSyncAutomatically(acct, ContactsContract.AUTHORITY, mSyncContacts.isChecked()); ContentResolver.setSyncAutomatically(acct, CalendarContract.AUTHORITY, diff --git a/src/com/android/email/activity/setup/AccountSetupBasics.java b/src/com/android/email/activity/setup/AccountSetupBasics.java index f6336bd0b..9117167db 100644 --- a/src/com/android/email/activity/setup/AccountSetupBasics.java +++ b/src/com/android/email/activity/setup/AccountSetupBasics.java @@ -457,12 +457,6 @@ public class AccountSetupBasics extends AccountSetupActivity HostAuth recvAuth = account.getOrCreateHostAuthRecv(this); HostAuth.setHostAuthFromString(recvAuth, mProvider.incomingUri); - // STOPSHIP; Use for Imap2 testing - if (HostAuth.LEGACY_SCHEME_IMAP.equals(recvAuth.mProtocol) && - EmailServiceUtils.isServiceAvailable(this, "imap2")) { - recvAuth.mProtocol = "imap2"; - } - recvAuth.setLogin(mProvider.incomingUsername, password); EmailServiceInfo info = EmailServiceUtils.getServiceInfo(this, recvAuth.mProtocol); recvAuth.mPort = diff --git a/src/com/android/email/activity/setup/SetupData.java b/src/com/android/email/activity/setup/SetupData.java index 80e5dc752..c753cb562 100644 --- a/src/com/android/email/activity/setup/SetupData.java +++ b/src/com/android/email/activity/setup/SetupData.java @@ -42,10 +42,6 @@ public class SetupData implements Parcelable { public static final int FLOW_MODE_RETURN_NO_ACCOUNTS_RESULT = 7; public static final int FLOW_MODE_NO_ACCOUNTS = 8; - // For debug logging - private static final String[] FLOW_MODES = {"normal", "eas", "pop/imap", "edit", "force", - "rtc", "rtl"}; - // Mode bits for AccountSetupCheckSettings, indicating the type of check requested public static final int CHECK_INCOMING = 1; public static final int CHECK_OUTGOING = 2; @@ -271,7 +267,6 @@ public class SetupData implements Parcelable { public static String debugString() { StringBuilder sb = new StringBuilder("SetupData"); SetupData data = getInstance(); - sb.append(":flow=" + FLOW_MODES[data.mFlowMode]); sb.append(":acct=" + (data.mAccount == null ? "none" : data.mAccount.mId)); if (data.mUsername != null) { sb.append(":user=" + data.mUsername); diff --git a/src/com/android/email/imap2/AttachmentLoader.java b/src/com/android/email/imap2/AttachmentLoader.java index 2e776be29..a8c4e47a1 100644 --- a/src/com/android/email/imap2/AttachmentLoader.java +++ b/src/com/android/email/imap2/AttachmentLoader.java @@ -22,8 +22,8 @@ import com.android.email.imap2.Imap2SyncService.Connection; import com.android.emailcommon.provider.EmailContent.Attachment; import com.android.emailcommon.provider.EmailContent.Message; import com.android.emailcommon.service.EmailServiceStatus; -import com.android.emailsync.PartRequest; import com.android.emailcommon.utility.AttachmentUtilities; +import com.android.emailsync.PartRequest; import com.android.mail.providers.UIProvider; import org.apache.james.mime4j.decoder.Base64InputStream; @@ -177,7 +177,7 @@ public class AttachmentLoader { OutputStream os = null; File tmpFile = null; try { - tmpFile = File.createTempFile("imap2_", "tmp", mContext.getCacheDir()); + tmpFile = File.createTempFile("imap_", "tmp", mContext.getCacheDir()); os = new FileOutputStream(tmpFile); String tag = mService.writeCommand(conn.writer, "uid fetch " + mMessage.mServerId + " body[" + mAttachment.mLocation + ']'); diff --git a/src/com/android/email/imap2/EmailSyncAdapterService.java b/src/com/android/email/imap2/EmailSyncAdapterService.java index 4366d0f96..eead7af27 100644 --- a/src/com/android/email/imap2/EmailSyncAdapterService.java +++ b/src/com/android/email/imap2/EmailSyncAdapterService.java @@ -16,11 +16,6 @@ package com.android.email.imap2; -import com.android.emailcommon.provider.EmailContent; -import com.android.emailcommon.provider.EmailContent.AccountColumns; -import com.android.emailcommon.provider.EmailContent.MailboxColumns; -import com.android.emailcommon.provider.Mailbox; - import android.accounts.Account; import android.accounts.OperationCanceledException; import android.app.Service; @@ -35,8 +30,13 @@ import android.os.Bundle; import android.os.IBinder; import android.util.Log; +import com.android.emailcommon.provider.EmailContent; +import com.android.emailcommon.provider.EmailContent.AccountColumns; +import com.android.emailcommon.provider.EmailContent.MailboxColumns; +import com.android.emailcommon.provider.Mailbox; + public class EmailSyncAdapterService extends Service { - private static final String TAG = "Imap2 EmailSyncAdapterService"; + private static final String TAG = "Imap EmailSyncAdapterService"; private static SyncAdapterImpl sSyncAdapter = null; private static final Object sSyncAdapterLock = new Object(); diff --git a/src/com/android/email/imap2/Imap2SyncManager.java b/src/com/android/email/imap2/Imap2SyncManager.java index e52ee339a..7b8fddc58 100644 --- a/src/com/android/email/imap2/Imap2SyncManager.java +++ b/src/com/android/email/imap2/Imap2SyncManager.java @@ -28,9 +28,11 @@ import android.os.IBinder; import android.os.RemoteCallbackList; import android.os.RemoteException; +import com.android.email.R; import com.android.emailcommon.Api; import com.android.emailcommon.provider.Account; import com.android.emailcommon.provider.EmailContent; +import com.android.emailcommon.provider.EmailContent.AccountColumns; import com.android.emailcommon.provider.EmailContent.Attachment; import com.android.emailcommon.provider.EmailContent.MailboxColumns; import com.android.emailcommon.provider.HostAuth; @@ -42,10 +44,10 @@ import com.android.emailcommon.service.IEmailService; import com.android.emailcommon.service.IEmailServiceCallback; import com.android.emailcommon.service.IEmailServiceCallback.Stub; import com.android.emailcommon.service.SearchParams; +import com.android.emailcommon.service.SyncWindow; import com.android.emailsync.AbstractSyncService; import com.android.emailsync.PartRequest; import com.android.emailsync.SyncManager; -import com.android.email.R; import com.android.mail.providers.UIProvider; import com.android.mail.providers.UIProvider.AccountCapabilities; import com.android.mail.providers.UIProvider.LastSyncResult; @@ -64,8 +66,7 @@ public class Imap2SyncManager extends SyncManager { private Intent mIntent; - private static final String IMAP2_ACCOUNT_TYPE = "com.android.imap2"; - private static final String PROTOCOL = "imap2"; + private static String PROTOCOL; /** * Create our EmailService implementation here. @@ -221,6 +222,30 @@ public class Imap2SyncManager extends SyncManager { AccountCapabilities.FOLDER_SERVER_SEARCH | AccountCapabilities.UNDO; } + + @Override + public void serviceUpdated(String emailAddress) throws RemoteException { + SyncManager ssm = INSTANCE; + if (ssm == null) return; + log("serviceUpdated called for " + emailAddress); + Cursor c = ssm.getContentResolver().query(Account.CONTENT_URI, Account.ID_PROJECTION, + AccountColumns.EMAIL_ADDRESS + "=?", new String[] { emailAddress }, null); + if (c == null) return; + try { + if (c.moveToNext()) { + long accountId = c.getLong(0); + ContentValues values = new ContentValues(); + values.put(AccountColumns.SYNC_INTERVAL, Account.CHECK_INTERVAL_PUSH); + values.put(AccountColumns.SYNC_LOOKBACK, SyncWindow.SYNC_WINDOW_AUTO); + // Say we can push (at least, we'll try) + mResolver.update(ContentUris.withAppendedId(Account.CONTENT_URI, accountId), + values, null, null); + log("Sync interval and lookback set for " + emailAddress); + } + } finally { + c.close(); + } + } }; static public IEmailServiceCallback callback() { @@ -305,11 +330,14 @@ public class Imap2SyncManager extends SyncManager { // We must throw here; callers might use the information we provide for reconciliation, etc. if (c == null) throw new ProviderUnavailableException(); try { + if (PROTOCOL == null) { + PROTOCOL = getString(R.string.protocol_imap); + } while (c.moveToNext()) { long hostAuthId = c.getLong(Account.CONTENT_HOST_AUTH_KEY_RECV_COLUMN); if (hostAuthId > 0) { HostAuth ha = HostAuth.restoreHostAuthWithId(context, hostAuthId); - if (ha != null && ha.mProtocol.equals("imap2")) { + if (ha != null && ha.mProtocol.equals(PROTOCOL)) { Account account = new Account(); account.restore(c); account.mHostAuthRecv = ha; @@ -325,7 +353,7 @@ public class Imap2SyncManager extends SyncManager { @Override public String getAccountManagerType() { - return IMAP2_ACCOUNT_TYPE; + return getString(R.string.account_manager_type_imap); } @Override @@ -344,7 +372,8 @@ public class Imap2SyncManager extends SyncManager { @Override protected void runAccountReconcilerSync(Context context) { alwaysLog("Reconciling accounts..."); - new AccountServiceProxy(context).reconcileAccounts(PROTOCOL, getAccountManagerType()); + new AccountServiceProxy(context).reconcileAccounts( + getString(R.string.protocol_imap), getAccountManagerType()); } @Override diff --git a/src/com/android/email/imap2/Imap2SyncService.java b/src/com/android/email/imap2/Imap2SyncService.java index 65a55b4c6..18696491a 100644 --- a/src/com/android/email/imap2/Imap2SyncService.java +++ b/src/com/android/email/imap2/Imap2SyncService.java @@ -149,7 +149,7 @@ public class Imap2SyncService extends AbstractSyncService { private final String[] MAILBOX_SERVER_ID_ARGS = new String[2]; public Imap2SyncService() { - this("Imap2 Validation"); + this("Imap Validation"); } private final ArrayList SERVER_DELETES = new ArrayList(); @@ -186,7 +186,7 @@ public class Imap2SyncService extends AbstractSyncService { } public Imap2SyncService(Context _context, Account _account) { - this("Imap2 Account"); + this("Imap Account"); mContext = _context; mResolver = _context.getContentResolver(); mAccount = _account; @@ -1512,8 +1512,9 @@ public class Imap2SyncService extends AbstractSyncService { if (andClause != null) { ac = ac + andClause; } + // Add "+0" to the sort order to coerce the text field to an integer Cursor c = mResolver.query(Message.CONTENT_URI, UID_PROJECTION, - ac, new String[] {Long.toString(mMailboxId)}, SyncColumns.SERVER_ID); + ac, new String[] {Long.toString(mMailboxId)}, SyncColumns.SERVER_ID + "+0"); if (c != null) { try { int[] uids = new int[c.getCount()]; @@ -1521,6 +1522,7 @@ public class Imap2SyncService extends AbstractSyncService { do { uids[offs++] = c.getInt(0); } while (c.moveToNext()); + System.err.println(uids); return uids; } } finally { diff --git a/src/com/android/email/mail/Store.java b/src/com/android/email/mail/Store.java index ea024a46d..ec8113493 100644 --- a/src/com/android/email/mail/Store.java +++ b/src/com/android/email/mail/Store.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2008 The Android Open Source Project + * Copyright (C) 2008 The Android Open Source P-roject * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,7 +20,7 @@ import android.content.Context; import android.os.Bundle; import android.util.Log; -import com.android.email.mail.store.ImapStore; +import com.android.email.R; import com.android.email.mail.store.Pop3Store; import com.android.email.mail.store.ServiceStore; import com.android.email2.ui.MailActivityEmail; @@ -58,11 +58,6 @@ public abstract class Store { static final HashMap> sStoreClasses = new HashMap>(); - static { - sStoreClasses.put(HostAuth.LEGACY_SCHEME_IMAP, ImapStore.class); - sStoreClasses.put(HostAuth.LEGACY_SCHEME_POP3, Pop3Store.class); - } - /** * Static named constructor. It should be overrode by extending class. * Because this method will be called through reflection, it can not be protected. @@ -87,6 +82,9 @@ public abstract class Store { */ public synchronized static Store getInstance(Account account, Context context) throws MessagingException { + if (sStores.isEmpty()) { + sStoreClasses.put(context.getString(R.string.protocol_pop3), Pop3Store.class); + } HostAuth hostAuth = account.getOrCreateHostAuthRecv(context); // An existing account might have been deleted if (hostAuth == null) return null; diff --git a/src/com/android/email/mail/store/ImapConnection.java b/src/com/android/email/mail/store/ImapConnection.java deleted file mode 100644 index a6d4fe605..000000000 --- a/src/com/android/email/mail/store/ImapConnection.java +++ /dev/null @@ -1,520 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.email.mail.store; - -import android.text.TextUtils; -import android.util.Log; - -import com.android.email.mail.store.ImapStore.ImapException; -import com.android.email.mail.store.imap.ImapConstants; -import com.android.email.mail.store.imap.ImapList; -import com.android.email.mail.store.imap.ImapResponse; -import com.android.email.mail.store.imap.ImapResponseParser; -import com.android.email.mail.store.imap.ImapUtility; -import com.android.email.mail.transport.DiscourseLogger; -import com.android.email.mail.transport.MailTransport; -import com.android.email2.ui.MailActivityEmail; -import com.android.emailcommon.Logging; -import com.android.emailcommon.mail.AuthenticationFailedException; -import com.android.emailcommon.mail.CertificateValidationException; -import com.android.emailcommon.mail.MessagingException; -import com.android.emailcommon.mail.Transport; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.concurrent.atomic.AtomicInteger; - -import javax.net.ssl.SSLException; - -/** - * A cacheable class that stores the details for a single IMAP connection. - */ -class ImapConnection { - // Always check in FALSE - private static final boolean DEBUG_FORCE_SEND_ID = false; - - /** ID capability per RFC 2971*/ - public static final int CAPABILITY_ID = 1 << 0; - /** NAMESPACE capability per RFC 2342 */ - public static final int CAPABILITY_NAMESPACE = 1 << 1; - /** STARTTLS capability per RFC 3501 */ - public static final int CAPABILITY_STARTTLS = 1 << 2; - /** UIDPLUS capability per RFC 4315 */ - public static final int CAPABILITY_UIDPLUS = 1 << 3; - - /** The capabilities supported; a set of CAPABILITY_* values. */ - private int mCapabilities; - private static final String IMAP_REDACTED_LOG = "[IMAP command redacted]"; - Transport mTransport; - private ImapResponseParser mParser; - private ImapStore mImapStore; - private String mUsername; - private String mLoginPhrase; - private String mIdPhrase = null; - /** # of command/response lines to log upon crash. */ - private static final int DISCOURSE_LOGGER_SIZE = 64; - private final DiscourseLogger mDiscourse = new DiscourseLogger(DISCOURSE_LOGGER_SIZE); - /** - * Next tag to use. All connections associated to the same ImapStore instance share the same - * counter to make tests simpler. - * (Some of the tests involve multiple connections but only have a single counter to track the - * tag.) - */ - private final AtomicInteger mNextCommandTag = new AtomicInteger(0); - - - // Keep others from instantiating directly - ImapConnection(ImapStore store, String username, String password) { - setStore(store, username, password); - } - - void setStore(ImapStore store, String username, String password) { - if (username != null && password != null) { - mUsername = username; - - // build the LOGIN string once (instead of over-and-over again.) - // apply the quoting here around the built-up password - mLoginPhrase = ImapConstants.LOGIN + " " + mUsername + " " - + ImapUtility.imapQuoted(password); - } - mImapStore = store; - } - void open() throws IOException, MessagingException { - if (mTransport != null && mTransport.isOpen()) { - return; - } - - try { - // copy configuration into a clean transport, if necessary - if (mTransport == null) { - mTransport = mImapStore.cloneTransport(); - } - - mTransport.open(); - mTransport.setSoTimeout(MailTransport.SOCKET_READ_TIMEOUT); - - createParser(); - - // BANNER - mParser.readResponse(); - - // CAPABILITY - ImapResponse capabilities = queryCapabilities(); - - boolean hasStartTlsCapability = - capabilities.contains(ImapConstants.STARTTLS); - - // TLS - ImapResponse newCapabilities = doStartTls(hasStartTlsCapability); - if (newCapabilities != null) { - capabilities = newCapabilities; - } - - // NOTE: An IMAP response MUST be processed before issuing any new IMAP - // requests. Subsequent requests may destroy previous response data. As - // such, we save away capability information here for future use. - setCapabilities(capabilities); - String capabilityString = capabilities.flatten(); - - // ID - doSendId(isCapable(CAPABILITY_ID), capabilityString); - - // LOGIN - doLogin(); - - // NAMESPACE (only valid in the Authenticated state) - doGetNamespace(isCapable(CAPABILITY_NAMESPACE)); - - // Gets the path separator from the server - doGetPathSeparator(); - - mImapStore.ensurePrefixIsValid(); - } catch (SSLException e) { - if (MailActivityEmail.DEBUG) { - Log.d(Logging.LOG_TAG, e.toString()); - } - throw new CertificateValidationException(e.getMessage(), e); - } catch (IOException ioe) { - // NOTE: Unlike similar code in POP3, I'm going to rethrow as-is. There is a lot - // of other code here that catches IOException and I don't want to break it. - // This catch is only here to enhance logging of connection-time issues. - if (MailActivityEmail.DEBUG) { - Log.d(Logging.LOG_TAG, ioe.toString()); - } - throw ioe; - } finally { - destroyResponses(); - } - } - - /** - * Closes the connection and releases all resources. This connection can not be used again - * until {@link #setStore(ImapStore, String, String)} is called. - */ - void close() { - if (mTransport != null) { - mTransport.close(); - mTransport = null; - } - destroyResponses(); - mParser = null; - mImapStore = null; - } - - /** - * Returns whether or not the specified capability is supported by the server. - */ - private boolean isCapable(int capability) { - return (mCapabilities & capability) != 0; - } - - /** - * Sets the capability flags according to the response provided by the server. - * Note: We only set the capability flags that we are interested in. There are many IMAP - * capabilities that we do not track. - */ - private void setCapabilities(ImapResponse capabilities) { - if (capabilities.contains(ImapConstants.ID)) { - mCapabilities |= CAPABILITY_ID; - } - if (capabilities.contains(ImapConstants.NAMESPACE)) { - mCapabilities |= CAPABILITY_NAMESPACE; - } - if (capabilities.contains(ImapConstants.UIDPLUS)) { - mCapabilities |= CAPABILITY_UIDPLUS; - } - if (capabilities.contains(ImapConstants.STARTTLS)) { - mCapabilities |= CAPABILITY_STARTTLS; - } - } - - /** - * Create an {@link ImapResponseParser} from {@code mTransport.getInputStream()} and - * set it to {@link #mParser}. - * - * If we already have an {@link ImapResponseParser}, we - * {@link #destroyResponses()} and throw it away. - */ - private void createParser() { - destroyResponses(); - mParser = new ImapResponseParser(mTransport.getInputStream(), mDiscourse); - } - - void destroyResponses() { - if (mParser != null) { - mParser.destroyResponses(); - } - } - - boolean isTransportOpenForTest() { - return mTransport != null ? mTransport.isOpen() : false; - } - - ImapResponse readResponse() throws IOException, MessagingException { - return mParser.readResponse(); - } - - /** - * Send a single command to the server. The command will be preceded by an IMAP command - * tag and followed by \r\n (caller need not supply them). - * - * @param command The command to send to the server - * @param sensitive If true, the command will not be logged - * @return Returns the command tag that was sent - */ - String sendCommand(String command, boolean sensitive) - throws MessagingException, IOException { - open(); - String tag = Integer.toString(mNextCommandTag.incrementAndGet()); - String commandToSend = tag + " " + command; - mTransport.writeLine(commandToSend, sensitive ? IMAP_REDACTED_LOG : null); - mDiscourse.addSentCommand(sensitive ? IMAP_REDACTED_LOG : commandToSend); - return tag; - } - - - /** - * Send a single, complex command to the server. The command will be preceded by an IMAP - * command tag and followed by \r\n (caller need not supply them). After each piece of the - * command, a response will be read which MUST be a continuation request. - * - * @param commands An array of Strings comprising the command to be sent to the server - * @return Returns the command tag that was sent - */ - String sendComplexCommand(List commands, boolean sensitive) throws MessagingException, - IOException { - open(); - String tag = Integer.toString(mNextCommandTag.incrementAndGet()); - int len = commands.size(); - for (int i = 0; i < len; i++) { - String commandToSend = commands.get(i); - // The first part of the command gets the tag - if (i == 0) { - commandToSend = tag + " " + commandToSend; - } else { - // Otherwise, read the response from the previous part of the command - ImapResponse response = readResponse(); - // If it isn't a continuation request, that's an error - if (!response.isContinuationRequest()) { - throw new MessagingException("Expected continuation request"); - } - } - // Send the command - mTransport.writeLine(commandToSend, null); - mDiscourse.addSentCommand(sensitive ? IMAP_REDACTED_LOG : commandToSend); - } - return tag; - } - - List executeSimpleCommand(String command) throws IOException, - MessagingException { - return executeSimpleCommand(command, false); - } - - /** - * Read and return all of the responses from the most recent command sent to the server - * - * @return a list of ImapResponses - * @throws IOException - * @throws MessagingException - */ - List getCommandResponses() throws IOException, MessagingException { - ArrayList responses = new ArrayList(); - ImapResponse response; - do { - response = mParser.readResponse(); - responses.add(response); - } while (!response.isTagged()); - if (!response.isOk()) { - final String toString = response.toString(); - final String alert = response.getAlertTextOrEmpty().getString(); - destroyResponses(); - throw new ImapException(toString, alert); - } - return responses; - } - - /** - * Execute a simple command at the server, a simple command being one that is sent in a single - * line of text - * - * @param command the command to send to the server - * @param sensitive whether the command should be redacted in logs (used for login) - * @return a list of ImapResponses - * @throws IOException - * @throws MessagingException - */ - List executeSimpleCommand(String command, boolean sensitive) - throws IOException, MessagingException { - sendCommand(command, sensitive); - return getCommandResponses(); - } - - /** - * Execute a complex command at the server, a complex command being one that must be sent in - * multiple lines due to the use of string literals - * - * @param commands a list of strings that comprise the command to be sent to the server - * @param sensitive whether the command should be redacted in logs (used for login) - * @return a list of ImapResponses - * @throws IOException - * @throws MessagingException - */ - List executeComplexCommand(List commands, boolean sensitive) - throws IOException, MessagingException { - sendComplexCommand(commands, sensitive); - return getCommandResponses(); - } - - /** - * Query server for capabilities. - */ - private ImapResponse queryCapabilities() throws IOException, MessagingException { - ImapResponse capabilityResponse = null; - for (ImapResponse r : executeSimpleCommand(ImapConstants.CAPABILITY)) { - if (r.is(0, ImapConstants.CAPABILITY)) { - capabilityResponse = r; - break; - } - } - if (capabilityResponse == null) { - throw new MessagingException("Invalid CAPABILITY response received"); - } - return capabilityResponse; - } - - /** - * Sends client identification information to the IMAP server per RFC 2971. If - * the server does not support the ID command, this will perform no operation. - * - * Interoperability hack: Never send ID to *.secureserver.net, which sends back a - * malformed response that our parser can't deal with. - */ - private void doSendId(boolean hasIdCapability, String capabilities) - throws MessagingException { - if (!hasIdCapability) return; - - // Never send ID to *.secureserver.net - String host = mTransport.getHost(); - if (host.toLowerCase().endsWith(".secureserver.net")) return; - - // Assign user-agent string (for RFC2971 ID command) - String mUserAgent = - ImapStore.getImapId(mImapStore.getContext(), mUsername, host, capabilities); - - if (mUserAgent != null) { - mIdPhrase = ImapConstants.ID + " (" + mUserAgent + ")"; - } else if (DEBUG_FORCE_SEND_ID) { - mIdPhrase = ImapConstants.ID + " " + ImapConstants.NIL; - } - // else: mIdPhrase = null, no ID will be emitted - - // Send user-agent in an RFC2971 ID command - if (mIdPhrase != null) { - try { - executeSimpleCommand(mIdPhrase); - } catch (ImapException ie) { - // Log for debugging, but this is not a fatal problem. - if (MailActivityEmail.DEBUG) { - Log.d(Logging.LOG_TAG, ie.toString()); - } - } catch (IOException ioe) { - // Special case to handle malformed OK responses and ignore them. - // A true IOException will recur on the following login steps - // This can go away after the parser is fixed - see bug 2138981 - } - } - } - - /** - * Gets the user's Personal Namespace from the IMAP server per RFC 2342. If the user - * explicitly sets a namespace (using setup UI) or if the server does not support the - * namespace command, this will perform no operation. - */ - private void doGetNamespace(boolean hasNamespaceCapability) throws MessagingException { - // user did not specify a hard-coded prefix; try to get it from the server - if (hasNamespaceCapability && !mImapStore.isUserPrefixSet()) { - List responseList = Collections.emptyList(); - - try { - responseList = executeSimpleCommand(ImapConstants.NAMESPACE); - } catch (ImapException ie) { - // Log for debugging, but this is not a fatal problem. - if (MailActivityEmail.DEBUG) { - Log.d(Logging.LOG_TAG, ie.toString()); - } - } catch (IOException ioe) { - // Special case to handle malformed OK responses and ignore them. - } - - for (ImapResponse response: responseList) { - if (response.isDataResponse(0, ImapConstants.NAMESPACE)) { - ImapList namespaceList = response.getListOrEmpty(1); - ImapList namespace = namespaceList.getListOrEmpty(0); - String namespaceString = namespace.getStringOrEmpty(0).getString(); - if (!TextUtils.isEmpty(namespaceString)) { - mImapStore.setPathPrefix(ImapStore.decodeFolderName(namespaceString, null)); - mImapStore.setPathSeparator(namespace.getStringOrEmpty(1).getString()); - } - } - } - } - } - - /** - * Logs into the IMAP server - */ - private void doLogin() - throws IOException, MessagingException, AuthenticationFailedException { - try { - // TODO eventually we need to add additional authentication - // options such as SASL - executeSimpleCommand(mLoginPhrase, true); - } catch (ImapException ie) { - if (MailActivityEmail.DEBUG) { - Log.d(Logging.LOG_TAG, ie.toString()); - } - throw new AuthenticationFailedException(ie.getAlertText(), ie); - - } catch (MessagingException me) { - throw new AuthenticationFailedException(null, me); - } - } - - /** - * Gets the path separator per the LIST command in RFC 3501. If the path separator - * was obtained while obtaining the namespace or there is no prefix defined, this - * will perform no operation. - */ - private void doGetPathSeparator() throws MessagingException { - // user did not specify a hard-coded prefix; try to get it from the server - if (mImapStore.isUserPrefixSet()) { - List responseList = Collections.emptyList(); - - try { - responseList = executeSimpleCommand(ImapConstants.LIST + " \"\" \"\""); - } catch (ImapException ie) { - // Log for debugging, but this is not a fatal problem. - if (MailActivityEmail.DEBUG) { - Log.d(Logging.LOG_TAG, ie.toString()); - } - } catch (IOException ioe) { - // Special case to handle malformed OK responses and ignore them. - } - - for (ImapResponse response: responseList) { - if (response.isDataResponse(0, ImapConstants.LIST)) { - mImapStore.setPathSeparator(response.getStringOrEmpty(2).getString()); - } - } - } - } - - /** - * Starts a TLS session with the IMAP server per RFC 3501. If the user has not opted - * to use TLS or the server does not support the TLS capability, this will perform - * no operation. - */ - private ImapResponse doStartTls(boolean hasStartTlsCapability) - throws IOException, MessagingException { - if (mTransport.canTryTlsSecurity()) { - if (hasStartTlsCapability) { - // STARTTLS - executeSimpleCommand(ImapConstants.STARTTLS); - - mTransport.reopenTls(); - mTransport.setSoTimeout(MailTransport.SOCKET_READ_TIMEOUT); - createParser(); - // Per RFC requirement (3501-6.2.1) gather new capabilities - return(queryCapabilities()); - } else { - if (MailActivityEmail.DEBUG) { - Log.d(Logging.LOG_TAG, "TLS not supported but required"); - } - throw new MessagingException(MessagingException.TLS_REQUIRED); - } - } - return null; - } - - /** @see DiscourseLogger#logLastDiscourse() */ - void logLastDiscourse() { - mDiscourse.logLastDiscourse(); - } -} \ No newline at end of file diff --git a/src/com/android/email/mail/store/ImapFolder.java b/src/com/android/email/mail/store/ImapFolder.java deleted file mode 100644 index e13366153..000000000 --- a/src/com/android/email/mail/store/ImapFolder.java +++ /dev/null @@ -1,1134 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.email.mail.store; - -import android.content.Context; -import android.text.TextUtils; -import android.util.Base64DataException; -import android.util.Log; - -import com.android.email.mail.store.ImapStore.ImapException; -import com.android.email.mail.store.ImapStore.ImapMessage; -import com.android.email.mail.store.imap.ImapConstants; -import com.android.email.mail.store.imap.ImapElement; -import com.android.email.mail.store.imap.ImapList; -import com.android.email.mail.store.imap.ImapResponse; -import com.android.email.mail.store.imap.ImapString; -import com.android.email.mail.store.imap.ImapUtility; -import com.android.email2.ui.MailActivityEmail; -import com.android.emailcommon.Logging; -import com.android.emailcommon.internet.BinaryTempFileBody; -import com.android.emailcommon.internet.MimeBodyPart; -import com.android.emailcommon.internet.MimeHeader; -import com.android.emailcommon.internet.MimeMultipart; -import com.android.emailcommon.internet.MimeUtility; -import com.android.emailcommon.mail.AuthenticationFailedException; -import com.android.emailcommon.mail.Body; -import com.android.emailcommon.mail.FetchProfile; -import com.android.emailcommon.mail.Flag; -import com.android.emailcommon.mail.Folder; -import com.android.emailcommon.mail.Message; -import com.android.emailcommon.mail.MessagingException; -import com.android.emailcommon.mail.Part; -import com.android.emailcommon.provider.Mailbox; -import com.android.emailcommon.service.SearchParams; -import com.android.emailcommon.utility.CountingOutputStream; -import com.android.emailcommon.utility.EOLConvertingOutputStream; -import com.android.emailcommon.utility.Utility; -import com.google.common.annotations.VisibleForTesting; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Date; -import java.util.HashMap; -import java.util.LinkedHashSet; -import java.util.List; - -class ImapFolder extends Folder { - private final static Flag[] PERMANENT_FLAGS = - { Flag.DELETED, Flag.SEEN, Flag.FLAGGED, Flag.ANSWERED }; - private static final int COPY_BUFFER_SIZE = 16*1024; - - private final ImapStore mStore; - private final String mName; - private int mMessageCount = -1; - private ImapConnection mConnection; - private OpenMode mMode; - private boolean mExists; - /** The local mailbox associated with this remote folder */ - Mailbox mMailbox; - /** A set of hashes that can be used to track dirtiness */ - Object mHash[]; - - /*package*/ ImapFolder(ImapStore store, String name) { - mStore = store; - mName = name; - } - - private void destroyResponses() { - if (mConnection != null) { - mConnection.destroyResponses(); - } - } - - @Override - public void open(OpenMode mode) - throws MessagingException { - try { - if (isOpen()) { - if (mMode == mode) { - // Make sure the connection is valid. - // If it's not we'll close it down and continue on to get a new one. - try { - mConnection.executeSimpleCommand(ImapConstants.NOOP); - return; - - } catch (IOException ioe) { - ioExceptionHandler(mConnection, ioe); - } finally { - destroyResponses(); - } - } else { - // Return the connection to the pool, if exists. - close(false); - } - } - synchronized (this) { - mConnection = mStore.getConnection(); - } - // * FLAGS (\Answered \Flagged \Deleted \Seen \Draft NonJunk - // $MDNSent) - // * OK [PERMANENTFLAGS (\Answered \Flagged \Deleted \Seen \Draft - // NonJunk $MDNSent \*)] Flags permitted. - // * 23 EXISTS - // * 0 RECENT - // * OK [UIDVALIDITY 1125022061] UIDs valid - // * OK [UIDNEXT 57576] Predicted next UID - // 2 OK [READ-WRITE] Select completed. - try { - doSelect(); - } catch (IOException ioe) { - throw ioExceptionHandler(mConnection, ioe); - } finally { - destroyResponses(); - } - } catch (AuthenticationFailedException e) { - // Don't cache this connection, so we're forced to try connecting/login again - mConnection = null; - close(false); - throw e; - } catch (MessagingException e) { - mExists = false; - close(false); - throw e; - } - } - - @Override - @VisibleForTesting - public boolean isOpen() { - return mExists && mConnection != null; - } - - @Override - public OpenMode getMode() { - return mMode; - } - - @Override - public void close(boolean expunge) { - // TODO implement expunge - mMessageCount = -1; - synchronized (this) { - mStore.poolConnection(mConnection); - mConnection = null; - } - } - - @Override - public String getName() { - return mName; - } - - @Override - public boolean exists() throws MessagingException { - if (mExists) { - return true; - } - /* - * This method needs to operate in the unselected mode as well as the selected mode - * so we must get the connection ourselves if it's not there. We are specifically - * not calling checkOpen() since we don't care if the folder is open. - */ - ImapConnection connection = null; - synchronized(this) { - if (mConnection == null) { - connection = mStore.getConnection(); - } else { - connection = mConnection; - } - } - try { - connection.executeSimpleCommand(String.format( - ImapConstants.STATUS + " \"%s\" (" + ImapConstants.UIDVALIDITY + ")", - ImapStore.encodeFolderName(mName, mStore.mPathPrefix))); - mExists = true; - return true; - - } catch (MessagingException me) { - // Treat IOERROR messaging exception as IOException - if (me.getExceptionType() == MessagingException.IOERROR) { - throw me; - } - return false; - - } catch (IOException ioe) { - throw ioExceptionHandler(connection, ioe); - - } finally { - connection.destroyResponses(); - if (mConnection == null) { - mStore.poolConnection(connection); - } - } - } - - // IMAP supports folder creation - @Override - public boolean canCreate(FolderType type) { - return true; - } - - @Override - public boolean create(FolderType type) throws MessagingException { - /* - * This method needs to operate in the unselected mode as well as the selected mode - * so we must get the connection ourselves if it's not there. We are specifically - * not calling checkOpen() since we don't care if the folder is open. - */ - ImapConnection connection = null; - synchronized(this) { - if (mConnection == null) { - connection = mStore.getConnection(); - } else { - connection = mConnection; - } - } - try { - connection.executeSimpleCommand(String.format(ImapConstants.CREATE + " \"%s\"", - ImapStore.encodeFolderName(mName, mStore.mPathPrefix))); - return true; - - } catch (MessagingException me) { - return false; - - } catch (IOException ioe) { - throw ioExceptionHandler(connection, ioe); - - } finally { - connection.destroyResponses(); - if (mConnection == null) { - mStore.poolConnection(connection); - } - } - } - - @Override - public void copyMessages(Message[] messages, Folder folder, - MessageUpdateCallbacks callbacks) throws MessagingException { - checkOpen(); - try { - List responseList = mConnection.executeSimpleCommand( - String.format(ImapConstants.UID_COPY + " %s \"%s\"", - ImapStore.joinMessageUids(messages), - ImapStore.encodeFolderName(folder.getName(), mStore.mPathPrefix))); - // Build a message map for faster UID matching - HashMap messageMap = new HashMap(); - boolean handledUidPlus = false; - for (Message m : messages) { - messageMap.put(m.getUid(), m); - } - // Process response to get the new UIDs - for (ImapResponse response : responseList) { - // All "BAD" responses are bad. Only "NO", tagged responses are bad. - if (response.isBad() || (response.isNo() && response.isTagged())) { - String responseText = response.getStatusResponseTextOrEmpty().getString(); - throw new MessagingException(responseText); - } - // Skip untagged responses; they're just status - if (!response.isTagged()) { - continue; - } - // No callback provided to report of UID changes; nothing more to do here - // NOTE: We check this here to catch any server errors - if (callbacks == null) { - continue; - } - ImapList copyResponse = response.getListOrEmpty(1); - String responseCode = copyResponse.getStringOrEmpty(0).getString(); - if (ImapConstants.COPYUID.equals(responseCode)) { - handledUidPlus = true; - String origIdSet = copyResponse.getStringOrEmpty(2).getString(); - String newIdSet = copyResponse.getStringOrEmpty(3).getString(); - String[] origIdArray = ImapUtility.getImapSequenceValues(origIdSet); - String[] newIdArray = ImapUtility.getImapSequenceValues(newIdSet); - // There has to be a 1:1 mapping between old and new IDs - if (origIdArray.length != newIdArray.length) { - throw new MessagingException("Set length mis-match; orig IDs \"" + - origIdSet + "\" new IDs \"" + newIdSet + "\""); - } - for (int i = 0; i < origIdArray.length; i++) { - final String id = origIdArray[i]; - final Message m = messageMap.get(id); - if (m != null) { - callbacks.onMessageUidChange(m, newIdArray[i]); - } - } - } - } - // If the server doesn't support UIDPLUS, try a different way to get the new UID(s) - if (callbacks != null && !handledUidPlus) { - ImapFolder newFolder = (ImapFolder)folder; - try { - // Temporarily select the destination folder - newFolder.open(OpenMode.READ_WRITE); - // Do the search(es) ... - for (Message m : messages) { - String searchString = "HEADER Message-Id \"" + m.getMessageId() + "\""; - String[] newIdArray = newFolder.searchForUids(searchString); - if (newIdArray.length == 1) { - callbacks.onMessageUidChange(m, newIdArray[0]); - } - } - } catch (MessagingException e) { - // Log, but, don't abort; failures here don't need to be propagated - Log.d(Logging.LOG_TAG, "Failed to find message", e); - } finally { - newFolder.close(false); - } - // Re-select the original folder - doSelect(); - } - } catch (IOException ioe) { - throw ioExceptionHandler(mConnection, ioe); - } finally { - destroyResponses(); - } - } - - @Override - public int getMessageCount() { - return mMessageCount; - } - - @Override - public int getUnreadMessageCount() throws MessagingException { - checkOpen(); - try { - int unreadMessageCount = 0; - List responses = mConnection.executeSimpleCommand(String.format( - ImapConstants.STATUS + " \"%s\" (" + ImapConstants.UNSEEN + ")", - ImapStore.encodeFolderName(mName, mStore.mPathPrefix))); - // S: * STATUS mboxname (MESSAGES 231 UIDNEXT 44292) - for (ImapResponse response : responses) { - if (response.isDataResponse(0, ImapConstants.STATUS)) { - unreadMessageCount = response.getListOrEmpty(2) - .getKeyedStringOrEmpty(ImapConstants.UNSEEN).getNumberOrZero(); - } - } - return unreadMessageCount; - } catch (IOException ioe) { - throw ioExceptionHandler(mConnection, ioe); - } finally { - destroyResponses(); - } - } - - @Override - public void delete(boolean recurse) { - throw new Error("ImapStore.delete() not yet implemented"); - } - - String[] getSearchUids(List responses) { - // S: * SEARCH 2 3 6 - final ArrayList uids = new ArrayList(); - for (ImapResponse response : responses) { - if (!response.isDataResponse(0, ImapConstants.SEARCH)) { - continue; - } - // Found SEARCH response data - for (int i = 1; i < response.size(); i++) { - ImapString s = response.getStringOrEmpty(i); - if (s.isString()) { - uids.add(s.getString()); - } - } - } - return uids.toArray(Utility.EMPTY_STRINGS); - } - - @VisibleForTesting - String[] searchForUids(String searchCriteria) throws MessagingException { - checkOpen(); - try { - try { - String command = ImapConstants.UID_SEARCH + " " + searchCriteria; - return getSearchUids(mConnection.executeSimpleCommand(command)); - } catch (ImapException e) { - Log.d(Logging.LOG_TAG, "ImapException in search: " + searchCriteria); - return Utility.EMPTY_STRINGS; // not found; - } catch (IOException ioe) { - throw ioExceptionHandler(mConnection, ioe); - } - } finally { - destroyResponses(); - } - } - - @Override - @VisibleForTesting - public Message getMessage(String uid) throws MessagingException { - checkOpen(); - - String[] uids = searchForUids(ImapConstants.UID + " " + uid); - for (int i = 0; i < uids.length; i++) { - if (uids[i].equals(uid)) { - return new ImapMessage(uid, this); - } - } - return null; - } - - @VisibleForTesting - protected static boolean isAsciiString(String str) { - int len = str.length(); - for (int i = 0; i < len; i++) { - char c = str.charAt(i); - if (c >= 128) return false; - } - return true; - } - - /** - * Retrieve messages based on search parameters. We search FROM, TO, CC, SUBJECT, and BODY - * We send: SEARCH OR FROM "foo" (OR TO "foo" (OR CC "foo" (OR SUBJECT "foo" BODY "foo"))), but - * with the additional CHARSET argument and sending "foo" as a literal (e.g. {3}foo} - */ - @Override - @VisibleForTesting - public Message[] getMessages(SearchParams params, MessageRetrievalListener listener) - throws MessagingException { - List commands = new ArrayList(); - String filter = params.mFilter; - // All servers MUST accept US-ASCII, so we'll send this as the CHARSET unless we're really - // dealing with a string that contains non-ascii characters - String charset = "US-ASCII"; - if (!isAsciiString(filter)) { - charset = "UTF-8"; - } - // This is the length of the string in octets (bytes), formatted as a string literal {n} - String octetLength = "{" + filter.getBytes().length + "}"; - // Break the command up into pieces ending with the string literal length - commands.add(ImapConstants.UID_SEARCH + " CHARSET " + charset + " OR FROM " + octetLength); - commands.add(filter + " (OR TO " + octetLength); - commands.add(filter + " (OR CC " + octetLength); - commands.add(filter + " (OR SUBJECT " + octetLength); - commands.add(filter + " BODY " + octetLength); - commands.add(filter + ")))"); - return getMessagesInternal(complexSearchForUids(commands), listener); - } - - /* package */ String[] complexSearchForUids(List commands) throws MessagingException { - checkOpen(); - try { - try { - return getSearchUids(mConnection.executeComplexCommand(commands, false)); - } catch (ImapException e) { - return Utility.EMPTY_STRINGS; // not found; - } catch (IOException ioe) { - throw ioExceptionHandler(mConnection, ioe); - } - } finally { - destroyResponses(); - } - } - - @Override - @VisibleForTesting - public Message[] getMessages(int start, int end, MessageRetrievalListener listener) - throws MessagingException { - if (start < 1 || end < 1 || end < start) { - throw new MessagingException(String.format("Invalid range: %d %d", start, end)); - } - return getMessagesInternal( - searchForUids(String.format("%d:%d NOT DELETED", start, end)), listener); - } - - @Override - @VisibleForTesting - public Message[] getMessages(String[] uids, MessageRetrievalListener listener) - throws MessagingException { - if (uids == null) { - uids = searchForUids("1:* NOT DELETED"); - } - return getMessagesInternal(uids, listener); - } - - public Message[] getMessagesInternal(String[] uids, MessageRetrievalListener listener) { - final ArrayList messages = new ArrayList(uids.length); - for (int i = 0; i < uids.length; i++) { - final String uid = uids[i]; - final ImapMessage message = new ImapMessage(uid, this); - messages.add(message); - if (listener != null) { - listener.messageRetrieved(message); - } - } - return messages.toArray(Message.EMPTY_ARRAY); - } - - @Override - public void fetch(Message[] messages, FetchProfile fp, MessageRetrievalListener listener) - throws MessagingException { - try { - fetchInternal(messages, fp, listener); - } catch (RuntimeException e) { // Probably a parser error. - Log.w(Logging.LOG_TAG, "Exception detected: " + e.getMessage()); - if (mConnection != null) { - mConnection.logLastDiscourse(); - } - throw e; - } - } - - public void fetchInternal(Message[] messages, FetchProfile fp, - MessageRetrievalListener listener) throws MessagingException { - if (messages.length == 0) { - return; - } - checkOpen(); - HashMap messageMap = new HashMap(); - for (Message m : messages) { - messageMap.put(m.getUid(), m); - } - - /* - * Figure out what command we are going to run: - * FLAGS - UID FETCH (FLAGS) - * ENVELOPE - UID FETCH (INTERNALDATE UID RFC822.SIZE FLAGS BODY.PEEK[ - * HEADER.FIELDS (date subject from content-type to cc)]) - * STRUCTURE - UID FETCH (BODYSTRUCTURE) - * BODY_SANE - UID FETCH (BODY.PEEK[]<0.N>) where N = max bytes returned - * BODY - UID FETCH (BODY.PEEK[]) - * Part - UID FETCH (BODY.PEEK[ID]) where ID = mime part ID - */ - - final LinkedHashSet fetchFields = new LinkedHashSet(); - - fetchFields.add(ImapConstants.UID); - if (fp.contains(FetchProfile.Item.FLAGS)) { - fetchFields.add(ImapConstants.FLAGS); - } - if (fp.contains(FetchProfile.Item.ENVELOPE)) { - fetchFields.add(ImapConstants.INTERNALDATE); - fetchFields.add(ImapConstants.RFC822_SIZE); - fetchFields.add(ImapConstants.FETCH_FIELD_HEADERS); - } - if (fp.contains(FetchProfile.Item.STRUCTURE)) { - fetchFields.add(ImapConstants.BODYSTRUCTURE); - } - - if (fp.contains(FetchProfile.Item.BODY_SANE)) { - fetchFields.add(ImapConstants.FETCH_FIELD_BODY_PEEK_SANE); - } - if (fp.contains(FetchProfile.Item.BODY)) { - fetchFields.add(ImapConstants.FETCH_FIELD_BODY_PEEK); - } - - final Part fetchPart = fp.getFirstPart(); - if (fetchPart != null) { - String[] partIds = - fetchPart.getHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA); - if (partIds != null) { - fetchFields.add(ImapConstants.FETCH_FIELD_BODY_PEEK_BARE - + "[" + partIds[0] + "]"); - } - } - - try { - mConnection.sendCommand(String.format( - ImapConstants.UID_FETCH + " %s (%s)", ImapStore.joinMessageUids(messages), - Utility.combine(fetchFields.toArray(new String[fetchFields.size()]), ' ') - ), false); - ImapResponse response; - int messageNumber = 0; - do { - response = null; - try { - response = mConnection.readResponse(); - - if (!response.isDataResponse(1, ImapConstants.FETCH)) { - continue; // Ignore - } - final ImapList fetchList = response.getListOrEmpty(2); - final String uid = fetchList.getKeyedStringOrEmpty(ImapConstants.UID) - .getString(); - if (TextUtils.isEmpty(uid)) continue; - - ImapMessage message = (ImapMessage) messageMap.get(uid); - if (message == null) continue; - - if (fp.contains(FetchProfile.Item.FLAGS)) { - final ImapList flags = - fetchList.getKeyedListOrEmpty(ImapConstants.FLAGS); - for (int i = 0, count = flags.size(); i < count; i++) { - final ImapString flag = flags.getStringOrEmpty(i); - if (flag.is(ImapConstants.FLAG_DELETED)) { - message.setFlagInternal(Flag.DELETED, true); - } else if (flag.is(ImapConstants.FLAG_ANSWERED)) { - message.setFlagInternal(Flag.ANSWERED, true); - } else if (flag.is(ImapConstants.FLAG_SEEN)) { - message.setFlagInternal(Flag.SEEN, true); - } else if (flag.is(ImapConstants.FLAG_FLAGGED)) { - message.setFlagInternal(Flag.FLAGGED, true); - } - } - } - if (fp.contains(FetchProfile.Item.ENVELOPE)) { - final Date internalDate = fetchList.getKeyedStringOrEmpty( - ImapConstants.INTERNALDATE).getDateOrNull(); - final int size = fetchList.getKeyedStringOrEmpty( - ImapConstants.RFC822_SIZE).getNumberOrZero(); - final String header = fetchList.getKeyedStringOrEmpty( - ImapConstants.BODY_BRACKET_HEADER, true).getString(); - - message.setInternalDate(internalDate); - message.setSize(size); - message.parse(Utility.streamFromAsciiString(header)); - } - if (fp.contains(FetchProfile.Item.STRUCTURE)) { - ImapList bs = fetchList.getKeyedListOrEmpty( - ImapConstants.BODYSTRUCTURE); - if (!bs.isEmpty()) { - try { - parseBodyStructure(bs, message, ImapConstants.TEXT); - } catch (MessagingException e) { - if (Logging.LOGD) { - Log.v(Logging.LOG_TAG, "Error handling message", e); - } - message.setBody(null); - } - } - } - if (fp.contains(FetchProfile.Item.BODY) - || fp.contains(FetchProfile.Item.BODY_SANE)) { - // Body is keyed by "BODY[]...". - // Previously used "BODY[..." but this can be confused with "BODY[HEADER..." - // TODO Should we accept "RFC822" as well?? - ImapString body = fetchList.getKeyedStringOrEmpty("BODY[]", true); - InputStream bodyStream = body.getAsStream(); - message.parse(bodyStream); - } - if (fetchPart != null && fetchPart.getSize() > 0) { - InputStream bodyStream = - fetchList.getKeyedStringOrEmpty("BODY[", true).getAsStream(); - String contentTransferEncoding = fetchPart.getHeader( - MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING)[0]; - - // TODO Don't create 2 temp files. - // decodeBody creates BinaryTempFileBody, but we could avoid this - // if we implement ImapStringBody. - // (We'll need to share a temp file. Protect it with a ref-count.) - fetchPart.setBody(decodeBody(bodyStream, contentTransferEncoding, - fetchPart.getSize(), listener)); - } - - if (listener != null) { - listener.messageRetrieved(message); - } - } finally { - destroyResponses(); - } - } while (!response.isTagged()); - } catch (IOException ioe) { - throw ioExceptionHandler(mConnection, ioe); - } - } - - /** - * Removes any content transfer encoding from the stream and returns a Body. - * This code is taken/condensed from MimeUtility.decodeBody - */ - private Body decodeBody(InputStream in, String contentTransferEncoding, int size, - MessageRetrievalListener listener) throws IOException { - // Get a properly wrapped input stream - in = MimeUtility.getInputStreamForContentTransferEncoding(in, contentTransferEncoding); - BinaryTempFileBody tempBody = new BinaryTempFileBody(); - OutputStream out = tempBody.getOutputStream(); - try { - byte[] buffer = new byte[COPY_BUFFER_SIZE]; - int n = 0; - int count = 0; - while (-1 != (n = in.read(buffer))) { - out.write(buffer, 0, n); - count += n; - if (listener != null) { - listener.loadAttachmentProgress(count * 100 / size); - } - } - } catch (Base64DataException bde) { - String warning = "\n\n" + MailActivityEmail.getMessageDecodeErrorString(); - out.write(warning.getBytes()); - } finally { - out.close(); - } - return tempBody; - } - - @Override - public Flag[] getPermanentFlags() { - return PERMANENT_FLAGS; - } - - /** - * Handle any untagged responses that the caller doesn't care to handle themselves. - * @param responses - */ - private void handleUntaggedResponses(List responses) { - for (ImapResponse response : responses) { - handleUntaggedResponse(response); - } - } - - /** - * Handle an untagged response that the caller doesn't care to handle themselves. - * @param response - */ - private void handleUntaggedResponse(ImapResponse response) { - if (response.isDataResponse(1, ImapConstants.EXISTS)) { - mMessageCount = response.getStringOrEmpty(0).getNumberOrZero(); - } - } - - private static void parseBodyStructure(ImapList bs, Part part, String id) - throws MessagingException { - if (bs.getElementOrNone(0).isList()) { - /* - * This is a multipart/* - */ - MimeMultipart mp = new MimeMultipart(); - for (int i = 0, count = bs.size(); i < count; i++) { - ImapElement e = bs.getElementOrNone(i); - if (e.isList()) { - /* - * For each part in the message we're going to add a new BodyPart and parse - * into it. - */ - MimeBodyPart bp = new MimeBodyPart(); - if (id.equals(ImapConstants.TEXT)) { - parseBodyStructure(bs.getListOrEmpty(i), bp, Integer.toString(i + 1)); - - } else { - parseBodyStructure(bs.getListOrEmpty(i), bp, id + "." + (i + 1)); - } - mp.addBodyPart(bp); - - } else { - if (e.isString()) { - mp.setSubType(bs.getStringOrEmpty(i).getString().toLowerCase()); - } - break; // Ignore the rest of the list. - } - } - part.setBody(mp); - } else { - /* - * This is a body. We need to add as much information as we can find out about - * it to the Part. - */ - - /* - body type - body subtype - body parameter parenthesized list - body id - body description - body encoding - body size - */ - - final ImapString type = bs.getStringOrEmpty(0); - final ImapString subType = bs.getStringOrEmpty(1); - final String mimeType = - (type.getString() + "/" + subType.getString()).toLowerCase(); - - final ImapList bodyParams = bs.getListOrEmpty(2); - final ImapString cid = bs.getStringOrEmpty(3); - final ImapString encoding = bs.getStringOrEmpty(5); - final int size = bs.getStringOrEmpty(6).getNumberOrZero(); - - if (MimeUtility.mimeTypeMatches(mimeType, MimeUtility.MIME_TYPE_RFC822)) { - // A body type of type MESSAGE and subtype RFC822 - // contains, immediately after the basic fields, the - // envelope structure, body structure, and size in - // text lines of the encapsulated message. - // [MESSAGE, RFC822, [NAME, filename.eml], NIL, NIL, 7BIT, 5974, NIL, - // [INLINE, [FILENAME*0, Fwd: Xxx..., FILENAME*1, filename.eml]], NIL] - /* - * This will be caught by fetch and handled appropriately. - */ - throw new MessagingException("BODYSTRUCTURE " + MimeUtility.MIME_TYPE_RFC822 - + " not yet supported."); - } - - /* - * Set the content type with as much information as we know right now. - */ - final StringBuilder contentType = new StringBuilder(mimeType); - - /* - * If there are body params we might be able to get some more information out - * of them. - */ - for (int i = 1, count = bodyParams.size(); i < count; i += 2) { - - // TODO We need to convert " into %22, but - // because MimeUtility.getHeaderParameter doesn't recognize it, - // we can't fix it for now. - contentType.append(String.format(";\n %s=\"%s\"", - bodyParams.getStringOrEmpty(i - 1).getString(), - bodyParams.getStringOrEmpty(i).getString())); - } - - part.setHeader(MimeHeader.HEADER_CONTENT_TYPE, contentType.toString()); - - // Extension items - final ImapList bodyDisposition; - - if (type.is(ImapConstants.TEXT) && bs.getElementOrNone(9).isList()) { - // If media-type is TEXT, 9th element might be: [body-fld-lines] := number - // So, if it's not a list, use 10th element. - // (Couldn't find evidence in the RFC if it's ALWAYS 10th element.) - bodyDisposition = bs.getListOrEmpty(9); - } else { - bodyDisposition = bs.getListOrEmpty(8); - } - - final StringBuilder contentDisposition = new StringBuilder(); - - if (bodyDisposition.size() > 0) { - final String bodyDisposition0Str = - bodyDisposition.getStringOrEmpty(0).getString().toLowerCase(); - if (!TextUtils.isEmpty(bodyDisposition0Str)) { - contentDisposition.append(bodyDisposition0Str); - } - - final ImapList bodyDispositionParams = bodyDisposition.getListOrEmpty(1); - if (!bodyDispositionParams.isEmpty()) { - /* - * If there is body disposition information we can pull some more - * information about the attachment out. - */ - for (int i = 1, count = bodyDispositionParams.size(); i < count; i += 2) { - - // TODO We need to convert " into %22. See above. - contentDisposition.append(String.format(";\n %s=\"%s\"", - bodyDispositionParams.getStringOrEmpty(i - 1) - .getString().toLowerCase(), - bodyDispositionParams.getStringOrEmpty(i).getString())); - } - } - } - - if ((size > 0) - && (MimeUtility.getHeaderParameter(contentDisposition.toString(), "size") - == null)) { - contentDisposition.append(String.format(";\n size=%d", size)); - } - - if (contentDisposition.length() > 0) { - /* - * Set the content disposition containing at least the size. Attachment - * handling code will use this down the road. - */ - part.setHeader(MimeHeader.HEADER_CONTENT_DISPOSITION, - contentDisposition.toString()); - } - - /* - * Set the Content-Transfer-Encoding header. Attachment code will use this - * to parse the body. - */ - if (!encoding.isEmpty()) { - part.setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, - encoding.getString()); - } - - /* - * Set the Content-ID header. - */ - if (!cid.isEmpty()) { - part.setHeader(MimeHeader.HEADER_CONTENT_ID, cid.getString()); - } - - if (size > 0) { - if (part instanceof ImapMessage) { - ((ImapMessage) part).setSize(size); - } else if (part instanceof MimeBodyPart) { - ((MimeBodyPart) part).setSize(size); - } else { - throw new MessagingException("Unknown part type " + part.toString()); - } - } - part.setHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA, id); - } - - } - - /** - * Appends the given messages to the selected folder. This implementation also determines - * the new UID of the given message on the IMAP server and sets the Message's UID to the - * new server UID. - */ - @Override - public void appendMessages(Message[] messages) throws MessagingException { - checkOpen(); - try { - for (Message message : messages) { - // Create output count - CountingOutputStream out = new CountingOutputStream(); - EOLConvertingOutputStream eolOut = new EOLConvertingOutputStream(out); - message.writeTo(eolOut); - eolOut.flush(); - // Create flag list (most often this will be "\SEEN") - String flagList = ""; - Flag[] flags = message.getFlags(); - if (flags.length > 0) { - StringBuilder sb = new StringBuilder(); - for (int i = 0, count = flags.length; i < count; i++) { - Flag flag = flags[i]; - if (flag == Flag.SEEN) { - sb.append(" " + ImapConstants.FLAG_SEEN); - } else if (flag == Flag.FLAGGED) { - sb.append(" " + ImapConstants.FLAG_FLAGGED); - } - } - if (sb.length() > 0) { - flagList = sb.substring(1); - } - } - - mConnection.sendCommand( - String.format(ImapConstants.APPEND + " \"%s\" (%s) {%d}", - ImapStore.encodeFolderName(mName, mStore.mPathPrefix), - flagList, - out.getCount()), false); - ImapResponse response; - do { - response = mConnection.readResponse(); - if (response.isContinuationRequest()) { - eolOut = new EOLConvertingOutputStream( - mConnection.mTransport.getOutputStream()); - message.writeTo(eolOut); - eolOut.write('\r'); - eolOut.write('\n'); - eolOut.flush(); - } else if (!response.isTagged()) { - handleUntaggedResponse(response); - } - } while (!response.isTagged()); - - // TODO Why not check the response? - - /* - * Try to recover the UID of the message from an APPENDUID response. - * e.g. 11 OK [APPENDUID 2 238268] APPEND completed - */ - final ImapList appendList = response.getListOrEmpty(1); - if ((appendList.size() >= 3) && appendList.is(0, ImapConstants.APPENDUID)) { - String serverUid = appendList.getStringOrEmpty(2).getString(); - if (!TextUtils.isEmpty(serverUid)) { - message.setUid(serverUid); - continue; - } - } - - /* - * Try to find the UID of the message we just appended using the - * Message-ID header. If there are more than one response, take the - * last one, as it's most likely the newest (the one we just uploaded). - */ - String messageId = message.getMessageId(); - if (messageId == null || messageId.length() == 0) { - continue; - } - // Most servers don't care about parenthesis in the search query [and, some - // fail to work if they are used] - String[] uids = searchForUids(String.format("HEADER MESSAGE-ID %s", messageId)); - if (uids.length > 0) { - message.setUid(uids[0]); - } - // However, there's at least one server [AOL] that fails to work unless there - // are parenthesis, so, try this as a last resort - uids = searchForUids(String.format("(HEADER MESSAGE-ID %s)", messageId)); - if (uids.length > 0) { - message.setUid(uids[0]); - } - } - } catch (IOException ioe) { - throw ioExceptionHandler(mConnection, ioe); - } finally { - destroyResponses(); - } - } - - @Override - public Message[] expunge() throws MessagingException { - checkOpen(); - try { - handleUntaggedResponses(mConnection.executeSimpleCommand(ImapConstants.EXPUNGE)); - } catch (IOException ioe) { - throw ioExceptionHandler(mConnection, ioe); - } finally { - destroyResponses(); - } - return null; - } - - @Override - public void setFlags(Message[] messages, Flag[] flags, boolean value) - throws MessagingException { - checkOpen(); - - String allFlags = ""; - if (flags.length > 0) { - StringBuilder flagList = new StringBuilder(); - for (int i = 0, count = flags.length; i < count; i++) { - Flag flag = flags[i]; - if (flag == Flag.SEEN) { - flagList.append(" " + ImapConstants.FLAG_SEEN); - } else if (flag == Flag.DELETED) { - flagList.append(" " + ImapConstants.FLAG_DELETED); - } else if (flag == Flag.FLAGGED) { - flagList.append(" " + ImapConstants.FLAG_FLAGGED); - } else if (flag == Flag.ANSWERED) { - flagList.append(" " + ImapConstants.FLAG_ANSWERED); - } - } - allFlags = flagList.substring(1); - } - try { - mConnection.executeSimpleCommand(String.format( - ImapConstants.UID_STORE + " %s %s" + ImapConstants.FLAGS_SILENT + " (%s)", - ImapStore.joinMessageUids(messages), - value ? "+" : "-", - allFlags)); - - } catch (IOException ioe) { - throw ioExceptionHandler(mConnection, ioe); - } finally { - destroyResponses(); - } - } - - /** - * Persists this folder. We will always perform the proper database operation (e.g. - * 'save' or 'update'). As an optimization, if a folder has not been modified, no - * database operations are performed. - */ - void save(Context context) { - final Mailbox mailbox = mMailbox; - if (!mailbox.isSaved()) { - mailbox.save(context); - mHash = mailbox.getHashes(); - } else { - Object[] hash = mailbox.getHashes(); - if (!Arrays.equals(mHash, hash)) { - mailbox.update(context, mailbox.toContentValues()); - mHash = hash; // Save updated hash - } - } - } - - /** - * Selects the folder for use. Before performing any operations on this folder, it - * must be selected. - */ - private void doSelect() throws IOException, MessagingException { - List responses = mConnection.executeSimpleCommand( - String.format(ImapConstants.SELECT + " \"%s\"", - ImapStore.encodeFolderName(mName, mStore.mPathPrefix))); - - // Assume the folder is opened read-write; unless we are notified otherwise - mMode = OpenMode.READ_WRITE; - int messageCount = -1; - for (ImapResponse response : responses) { - if (response.isDataResponse(1, ImapConstants.EXISTS)) { - messageCount = response.getStringOrEmpty(0).getNumberOrZero(); - } else if (response.isOk()) { - final ImapString responseCode = response.getResponseCodeOrEmpty(); - if (responseCode.is(ImapConstants.READ_ONLY)) { - mMode = OpenMode.READ_ONLY; - } else if (responseCode.is(ImapConstants.READ_WRITE)) { - mMode = OpenMode.READ_WRITE; - } - } else if (response.isTagged()) { // Not OK - throw new MessagingException("Can't open mailbox: " - + response.getStatusResponseTextOrEmpty()); - } - } - if (messageCount == -1) { - throw new MessagingException("Did not find message count during select"); - } - mMessageCount = messageCount; - mExists = true; - } - - private void checkOpen() throws MessagingException { - if (!isOpen()) { - throw new MessagingException("Folder " + mName + " is not open."); - } - } - - private MessagingException ioExceptionHandler(ImapConnection connection, IOException ioe) { - if (MailActivityEmail.DEBUG) { - Log.d(Logging.LOG_TAG, "IO Exception detected: ", ioe); - } - connection.close(); - if (connection == mConnection) { - mConnection = null; // To prevent close() from returning the connection to the pool. - close(false); - } - return new MessagingException("IO Error", ioe); - } - - @Override - public boolean equals(Object o) { - if (o instanceof ImapFolder) { - return ((ImapFolder)o).mName.equals(mName); - } - return super.equals(o); - } - - @Override - public Message createMessage(String uid) { - return new ImapMessage(uid, this); - } -} diff --git a/src/com/android/email/mail/store/ImapStore.java b/src/com/android/email/mail/store/ImapStore.java deleted file mode 100644 index 039c2e931..000000000 --- a/src/com/android/email/mail/store/ImapStore.java +++ /dev/null @@ -1,618 +0,0 @@ -/* - * Copyright (C) 2008 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.email.mail.store; - -import android.content.Context; -import android.os.Build; -import android.os.Bundle; -import android.telephony.TelephonyManager; -import android.text.TextUtils; -import android.util.Base64; -import android.util.Log; - -import com.android.email.LegacyConversions; -import com.android.email.Preferences; -import com.android.email.R; -import com.android.email.mail.Store; -import com.android.email.mail.store.imap.ImapConstants; -import com.android.email.mail.store.imap.ImapResponse; -import com.android.email.mail.store.imap.ImapString; -import com.android.email.mail.transport.MailTransport; -import com.android.emailcommon.Logging; -import com.android.emailcommon.VendorPolicyLoader; -import com.android.emailcommon.internet.MimeMessage; -import com.android.emailcommon.mail.AuthenticationFailedException; -import com.android.emailcommon.mail.Flag; -import com.android.emailcommon.mail.Folder; -import com.android.emailcommon.mail.Message; -import com.android.emailcommon.mail.MessagingException; -import com.android.emailcommon.mail.Transport; -import com.android.emailcommon.provider.Account; -import com.android.emailcommon.provider.HostAuth; -import com.android.emailcommon.provider.Mailbox; -import com.android.emailcommon.service.EmailServiceProxy; -import com.android.emailcommon.utility.Utility; -import com.beetstra.jutf7.CharsetProvider; -import com.google.common.annotations.VisibleForTesting; - -import java.io.IOException; -import java.io.InputStream; -import java.nio.ByteBuffer; -import java.nio.charset.Charset; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.util.Collection; -import java.util.HashMap; -import java.util.List; -import java.util.Set; -import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.regex.Pattern; - - -/** - *
- * TODO Need to start keeping track of UIDVALIDITY
- * TODO Need a default response handler for things like folder updates
- * TODO In fetch(), if we need a ImapMessage and were given
- *      something else we can try to do a pre-fetch first.
- * TODO Collect ALERT messages and show them to users.
- *
- * ftp://ftp.isi.edu/in-notes/rfc2683.txt When a client asks for
- * certain information in a FETCH command, the server may return the requested
- * information in any order, not necessarily in the order that it was requested.
- * Further, the server may return the information in separate FETCH responses
- * and may also return information that was not explicitly requested (to reflect
- * to the client changes in the state of the subject message).
- * 
- */ -public class ImapStore extends Store { - /** Charset used for converting folder names to and from UTF-7 as defined by RFC 3501. */ - private static final Charset MODIFIED_UTF_7_CHARSET = - new CharsetProvider().charsetForName("X-RFC-3501"); - - @VisibleForTesting static String sImapId = null; - @VisibleForTesting String mPathPrefix; - @VisibleForTesting String mPathSeparator; - - private final ConcurrentLinkedQueue mConnectionPool = - new ConcurrentLinkedQueue(); - - /** - * Static named constructor. - */ - public static Store newInstance(Account account, Context context) throws MessagingException { - return new ImapStore(context, account); - } - - /** - * Creates a new store for the given account. Always use - * {@link #newInstance(Account, Context)} to create an IMAP store. - */ - private ImapStore(Context context, Account account) throws MessagingException { - mContext = context; - mAccount = account; - - HostAuth recvAuth = account.getOrCreateHostAuthRecv(context); - if (recvAuth == null || !HostAuth.LEGACY_SCHEME_IMAP.equalsIgnoreCase(recvAuth.mProtocol)) { - throw new MessagingException("Unsupported protocol"); - } - mTransport = new MailTransport(context, "IMAP", recvAuth); - - String[] userInfo = recvAuth.getLogin(); - if (userInfo != null) { - mUsername = userInfo[0]; - mPassword = userInfo[1]; - } else { - mUsername = null; - mPassword = null; - } - mPathPrefix = recvAuth.mDomain; - } - - @VisibleForTesting - Collection getConnectionPoolForTest() { - return mConnectionPool; - } - - /** - * For testing only. Injects a different root transport (it will be copied using - * newInstanceWithConfiguration() each time IMAP sets up a new channel). The transport - * should already be set up and ready to use. Do not use for real code. - * @param testTransport The Transport to inject and use for all future communication. - */ - @VisibleForTesting - void setTransportForTest(Transport testTransport) { - mTransport = testTransport; - } - - /** - * Return, or create and return, an string suitable for use in an IMAP ID message. - * This is constructed similarly to the way the browser sets up its user-agent strings. - * See RFC 2971 for more details. The output of this command will be a series of key-value - * pairs delimited by spaces (there is no point in returning a structured result because - * this will be sent as-is to the IMAP server). No tokens, parenthesis or "ID" are included, - * because some connections may append additional values. - * - * The following IMAP ID keys may be included: - * name Android package name of the program - * os "android" - * os-version "version; model; build-id" - * vendor Vendor of the client/server - * x-android-device-model Model (only revealed if release build) - * x-android-net-operator Mobile network operator (if known) - * AGUID A device+account UID - * - * In addition, a vendor policy .apk can append key/value pairs. - * - * @param userName the username of the account - * @param host the host (server) of the account - * @param capabilities a list of the capabilities from the server - * @return a String for use in an IMAP ID message. - */ - public static String getImapId(Context context, String userName, String host, - String capabilities) { - // The first section is global to all IMAP connections, and generates the fixed - // values in any IMAP ID message - synchronized (ImapStore.class) { - if (sImapId == null) { - TelephonyManager tm = - (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); - String networkOperator = tm.getNetworkOperatorName(); - if (networkOperator == null) networkOperator = ""; - - sImapId = makeCommonImapId(context.getPackageName(), Build.VERSION.RELEASE, - Build.VERSION.CODENAME, Build.MODEL, Build.ID, Build.MANUFACTURER, - networkOperator); - } - } - - // This section is per Store, and adds in a dynamic elements like UID's. - // We don't cache the result of this work, because the caller does anyway. - StringBuilder id = new StringBuilder(sImapId); - - // Optionally add any vendor-supplied id keys - String vendorId = VendorPolicyLoader.getInstance(context).getImapIdValues(userName, host, - capabilities); - if (vendorId != null) { - id.append(' '); - id.append(vendorId); - } - - // Generate a UID that mixes a "stable" device UID with the email address - try { - String devUID = Preferences.getPreferences(context).getDeviceUID(); - MessageDigest messageDigest; - messageDigest = MessageDigest.getInstance("SHA-1"); - messageDigest.update(userName.getBytes()); - messageDigest.update(devUID.getBytes()); - byte[] uid = messageDigest.digest(); - String hexUid = Base64.encodeToString(uid, Base64.NO_WRAP); - id.append(" \"AGUID\" \""); - id.append(hexUid); - id.append('\"'); - } catch (NoSuchAlgorithmException e) { - Log.d(Logging.LOG_TAG, "couldn't obtain SHA-1 hash for device UID"); - } - return id.toString(); - } - - /** - * Helper function that actually builds the static part of the IMAP ID string. This is - * separated from getImapId for testability. There is no escaping or encoding in IMAP ID so - * any rogue chars must be filtered here. - * - * @param packageName context.getPackageName() - * @param version Build.VERSION.RELEASE - * @param codeName Build.VERSION.CODENAME - * @param model Build.MODEL - * @param id Build.ID - * @param vendor Build.MANUFACTURER - * @param networkOperator TelephonyManager.getNetworkOperatorName() - * @return the static (never changes) portion of the IMAP ID - */ - @VisibleForTesting - static String makeCommonImapId(String packageName, String version, - String codeName, String model, String id, String vendor, String networkOperator) { - - // Before building up IMAP ID string, pre-filter the input strings for "legal" chars - // This is using a fairly arbitrary char set intended to pass through most reasonable - // version, model, and vendor strings: a-z A-Z 0-9 - _ + = ; : . , / - // The most important thing is *not* to pass parens, quotes, or CRLF, which would break - // the format of the IMAP ID list. - Pattern p = Pattern.compile("[^a-zA-Z0-9-_\\+=;:\\.,/ ]"); - packageName = p.matcher(packageName).replaceAll(""); - version = p.matcher(version).replaceAll(""); - codeName = p.matcher(codeName).replaceAll(""); - model = p.matcher(model).replaceAll(""); - id = p.matcher(id).replaceAll(""); - vendor = p.matcher(vendor).replaceAll(""); - networkOperator = p.matcher(networkOperator).replaceAll(""); - - // "name" "com.android.email" - StringBuffer sb = new StringBuffer("\"name\" \""); - sb.append(packageName); - sb.append("\""); - - // "os" "android" - sb.append(" \"os\" \"android\""); - - // "os-version" "version; build-id" - sb.append(" \"os-version\" \""); - if (version.length() > 0) { - sb.append(version); - } else { - // default to "1.0" - sb.append("1.0"); - } - // add the build ID or build # - if (id.length() > 0) { - sb.append("; "); - sb.append(id); - } - sb.append("\""); - - // "vendor" "the vendor" - if (vendor.length() > 0) { - sb.append(" \"vendor\" \""); - sb.append(vendor); - sb.append("\""); - } - - // "x-android-device-model" the device model (on release builds only) - if ("REL".equals(codeName)) { - if (model.length() > 0) { - sb.append(" \"x-android-device-model\" \""); - sb.append(model); - sb.append("\""); - } - } - - // "x-android-mobile-net-operator" "name of network operator" - if (networkOperator.length() > 0) { - sb.append(" \"x-android-mobile-net-operator\" \""); - sb.append(networkOperator); - sb.append("\""); - } - - return sb.toString(); - } - - - @Override - public Folder getFolder(String name) { - return new ImapFolder(this, name); - } - - /** - * Creates a mailbox hierarchy out of the flat data provided by the server. - */ - @VisibleForTesting - static void createHierarchy(HashMap mailboxes) { - Set pathnames = mailboxes.keySet(); - for (String path : pathnames) { - final ImapFolder folder = mailboxes.get(path); - final Mailbox mailbox = folder.mMailbox; - int delimiterIdx = mailbox.mServerId.lastIndexOf(mailbox.mDelimiter); - long parentKey = Mailbox.NO_MAILBOX; - if (delimiterIdx != -1) { - String parentPath = path.substring(0, delimiterIdx); - final ImapFolder parentFolder = mailboxes.get(parentPath); - final Mailbox parentMailbox = (parentFolder == null) ? null : parentFolder.mMailbox; - if (parentMailbox != null) { - parentKey = parentMailbox.mId; - parentMailbox.mFlags - |= (Mailbox.FLAG_HAS_CHILDREN | Mailbox.FLAG_CHILDREN_VISIBLE); - } - } - mailbox.mParentKey = parentKey; - } - } - - /** - * Creates a {@link Folder} and associated {@link Mailbox}. If the folder does not already - * exist in the local database, a new row will immediately be created in the mailbox table. - * Otherwise, the existing row will be used. Any changes to existing rows, will not be stored - * to the database immediately. - * @param accountId The ID of the account the mailbox is to be associated with - * @param mailboxPath The path of the mailbox to add - * @param delimiter A path delimiter. May be {@code null} if there is no delimiter. - * @param selectable If {@code true}, the mailbox can be selected and used to store messages. - */ - private ImapFolder addMailbox(Context context, long accountId, String mailboxPath, - char delimiter, boolean selectable) { - ImapFolder folder = (ImapFolder) getFolder(mailboxPath); - Mailbox mailbox = Mailbox.getMailboxForPath(context, accountId, mailboxPath); - if (mailbox.isSaved()) { - // existing mailbox - // mailbox retrieved from database; save hash _before_ updating fields - folder.mHash = mailbox.getHashes(); - } - updateMailbox(mailbox, accountId, mailboxPath, delimiter, selectable, - LegacyConversions.inferMailboxTypeFromName(context, mailboxPath)); - if (folder.mHash == null) { - // new mailbox - // save hash after updating. allows tracking changes if the mailbox is saved - // outside of #saveMailboxList() - folder.mHash = mailbox.getHashes(); - // We must save this here to make sure we have a valid ID for later - mailbox.save(mContext); - } - folder.mMailbox = mailbox; - return folder; - } - - /** - * Persists the folders in the given list. - */ - private static void saveMailboxList(Context context, HashMap folderMap) { - for (ImapFolder imapFolder : folderMap.values()) { - imapFolder.save(context); - } - } - - @Override - public Folder[] updateFolders() throws MessagingException { - ImapConnection connection = getConnection(); - try { - HashMap mailboxes = new HashMap(); - // Establish a connection to the IMAP server; if necessary - // This ensures a valid prefix if the prefix is automatically set by the server - connection.executeSimpleCommand(ImapConstants.NOOP); - String imapCommand = ImapConstants.LIST + " \"\" \"*\""; - if (mPathPrefix != null) { - imapCommand = ImapConstants.LIST + " \"\" \"" + mPathPrefix + "*\""; - } - List responses = connection.executeSimpleCommand(imapCommand); - for (ImapResponse response : responses) { - // S: * LIST (\Noselect) "/" ~/Mail/foo - if (response.isDataResponse(0, ImapConstants.LIST)) { - // Get folder name. - ImapString encodedFolder = response.getStringOrEmpty(3); - if (encodedFolder.isEmpty()) continue; - - String folderName = decodeFolderName(encodedFolder.getString(), mPathPrefix); - if (ImapConstants.INBOX.equalsIgnoreCase(folderName)) continue; - - // Parse attributes. - boolean selectable = - !response.getListOrEmpty(1).contains(ImapConstants.FLAG_NO_SELECT); - String delimiter = response.getStringOrEmpty(2).getString(); - char delimiterChar = '\0'; - if (!TextUtils.isEmpty(delimiter)) { - delimiterChar = delimiter.charAt(0); - } - ImapFolder folder = - addMailbox(mContext, mAccount.mId, folderName, delimiterChar, selectable); - mailboxes.put(folderName, folder); - } - } - String inboxName = mContext.getString(R.string.mailbox_name_display_inbox); - Folder newFolder = - addMailbox(mContext, mAccount.mId, inboxName, '\0', true /*selectable*/); - mailboxes.put(ImapConstants.INBOX, (ImapFolder)newFolder); - createHierarchy(mailboxes); - saveMailboxList(mContext, mailboxes); - return mailboxes.values().toArray(new Folder[] {}); - } catch (IOException ioe) { - connection.close(); - throw new MessagingException("Unable to get folder list.", ioe); - } catch (AuthenticationFailedException afe) { - // We do NOT want this connection pooled, or we will continue to send NOOP and SELECT - // commands to the server - connection.destroyResponses(); - connection = null; - throw afe; - } finally { - if (connection != null) { - poolConnection(connection); - } - } - } - - @Override - public Bundle checkSettings() throws MessagingException { - int result = MessagingException.NO_ERROR; - Bundle bundle = new Bundle(); - ImapConnection connection = new ImapConnection(this, mUsername, mPassword); - try { - connection.open(); - connection.close(); - } catch (IOException ioe) { - bundle.putString(EmailServiceProxy.VALIDATE_BUNDLE_ERROR_MESSAGE, ioe.getMessage()); - result = MessagingException.IOERROR; - } finally { - connection.destroyResponses(); - } - bundle.putInt(EmailServiceProxy.VALIDATE_BUNDLE_RESULT_CODE, result); - return bundle; - } - - /** - * Returns whether or not the prefix has been set by the user. This can be determined by - * the fact that the prefix is set, but, the path separator is not set. - */ - boolean isUserPrefixSet() { - return TextUtils.isEmpty(mPathSeparator) && !TextUtils.isEmpty(mPathPrefix); - } - - /** Sets the path separator */ - void setPathSeparator(String pathSeparator) { - mPathSeparator = pathSeparator; - } - - /** Sets the prefix */ - void setPathPrefix(String pathPrefix) { - mPathPrefix = pathPrefix; - } - - /** Gets the context for this store */ - Context getContext() { - return mContext; - } - - /** Returns a clone of the transport associated with this store. */ - Transport cloneTransport() { - return mTransport.clone(); - } - - /** - * Fixes the path prefix, if necessary. The path prefix must always end with the - * path separator. - */ - void ensurePrefixIsValid() { - // Make sure the path prefix ends with the path separator - if (!TextUtils.isEmpty(mPathPrefix) && !TextUtils.isEmpty(mPathSeparator)) { - if (!mPathPrefix.endsWith(mPathSeparator)) { - mPathPrefix = mPathPrefix + mPathSeparator; - } - } - } - - /** - * Gets a connection if one is available from the pool, or creates a new one if not. - */ - ImapConnection getConnection() { - ImapConnection connection = null; - while ((connection = mConnectionPool.poll()) != null) { - try { - connection.setStore(this, mUsername, mPassword); - connection.executeSimpleCommand(ImapConstants.NOOP); - break; - } catch (MessagingException e) { - // Fall through - } catch (IOException e) { - // Fall through - } - connection.close(); - connection = null; - } - if (connection == null) { - connection = new ImapConnection(this, mUsername, mPassword); - } - return connection; - } - - /** - * Save a {@link ImapConnection} in the pool for reuse. Any responses associated with the - * connection are destroyed before adding the connection to the pool. - */ - void poolConnection(ImapConnection connection) { - if (connection != null) { - connection.destroyResponses(); - mConnectionPool.add(connection); - } - } - - /** - * Prepends the folder name with the given prefix and UTF-7 encodes it. - */ - static String encodeFolderName(String name, String prefix) { - // do NOT add the prefix to the special name "INBOX" - if (ImapConstants.INBOX.equalsIgnoreCase(name)) return name; - - // Prepend prefix - if (prefix != null) { - name = prefix + name; - } - - // TODO bypass the conversion if name doesn't have special char. - ByteBuffer bb = MODIFIED_UTF_7_CHARSET.encode(name); - byte[] b = new byte[bb.limit()]; - bb.get(b); - - return Utility.fromAscii(b); - } - - /** - * UTF-7 decodes the folder name and removes the given path prefix. - */ - static String decodeFolderName(String name, String prefix) { - // TODO bypass the conversion if name doesn't have special char. - String folder; - folder = MODIFIED_UTF_7_CHARSET.decode(ByteBuffer.wrap(Utility.toAscii(name))).toString(); - if ((prefix != null) && folder.startsWith(prefix)) { - folder = folder.substring(prefix.length()); - } - return folder; - } - - /** - * Returns UIDs of Messages joined with "," as the separator. - */ - static String joinMessageUids(Message[] messages) { - StringBuilder sb = new StringBuilder(); - boolean notFirst = false; - for (Message m : messages) { - if (notFirst) { - sb.append(','); - } - sb.append(m.getUid()); - notFirst = true; - } - return sb.toString(); - } - - static class ImapMessage extends MimeMessage { - ImapMessage(String uid, ImapFolder folder) { - mUid = uid; - mFolder = folder; - } - - public void setSize(int size) { - mSize = size; - } - - @Override - public void parse(InputStream in) throws IOException, MessagingException { - super.parse(in); - } - - public void setFlagInternal(Flag flag, boolean set) throws MessagingException { - super.setFlag(flag, set); - } - - @Override - public void setFlag(Flag flag, boolean set) throws MessagingException { - super.setFlag(flag, set); - mFolder.setFlags(new Message[] { this }, new Flag[] { flag }, set); - } - } - - static class ImapException extends MessagingException { - private static final long serialVersionUID = 1L; - - String mAlertText; - - public ImapException(String message, String alertText, Throwable throwable) { - super(message, throwable); - mAlertText = alertText; - } - - public ImapException(String message, String alertText) { - super(message); - mAlertText = alertText; - } - - public String getAlertText() { - return mAlertText; - } - - public void setAlertText(String alertText) { - mAlertText = alertText; - } - } -} diff --git a/src/com/android/email/mail/store/Pop3Store.java b/src/com/android/email/mail/store/Pop3Store.java index 99f54f2af..4cbea9dc8 100644 --- a/src/com/android/email/mail/store/Pop3Store.java +++ b/src/com/android/email/mail/store/Pop3Store.java @@ -30,10 +30,10 @@ import com.android.emailcommon.mail.AuthenticationFailedException; import com.android.emailcommon.mail.FetchProfile; import com.android.emailcommon.mail.Flag; import com.android.emailcommon.mail.Folder; -import com.android.emailcommon.mail.Transport; import com.android.emailcommon.mail.Folder.OpenMode; import com.android.emailcommon.mail.Message; import com.android.emailcommon.mail.MessagingException; +import com.android.emailcommon.mail.Transport; import com.android.emailcommon.provider.Account; import com.android.emailcommon.provider.HostAuth; import com.android.emailcommon.provider.Mailbox; @@ -77,9 +77,6 @@ public class Pop3Store extends Store { mAccount = account; HostAuth recvAuth = account.getOrCreateHostAuthRecv(context); - if (recvAuth == null || !HostAuth.LEGACY_SCHEME_POP3.equalsIgnoreCase(recvAuth.mProtocol)) { - throw new MessagingException("Unsupported protocol"); - } mTransport = new MailTransport(context, "POP3", recvAuth); String[] userInfoParts = recvAuth.getLogin(); if (userInfoParts != null) { diff --git a/src/com/android/email/mail/store/imap/ImapConstants.java b/src/com/android/email/mail/store/imap/ImapConstants.java deleted file mode 100644 index eee2ac44e..000000000 --- a/src/com/android/email/mail/store/imap/ImapConstants.java +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright (C) 2010 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.email.mail.store.imap; - -import com.android.email.mail.Store; - -public final class ImapConstants { - private ImapConstants() {} - - public static final String FETCH_FIELD_BODY_PEEK_BARE = "BODY.PEEK"; - public static final String FETCH_FIELD_BODY_PEEK = FETCH_FIELD_BODY_PEEK_BARE + "[]"; - public static final String FETCH_FIELD_BODY_PEEK_SANE - = String.format("BODY.PEEK[]<0.%d>", Store.FETCH_BODY_SANE_SUGGESTED_SIZE); - public static final String FETCH_FIELD_HEADERS = - "BODY.PEEK[HEADER.FIELDS (date subject from content-type to cc message-id)]"; - - public static final String ALERT = "ALERT"; - public static final String APPEND = "APPEND"; - public static final String BAD = "BAD"; - public static final String BADCHARSET = "BADCHARSET"; - public static final String BODY = "BODY"; - public static final String BODY_BRACKET_HEADER = "BODY[HEADER"; - public static final String BODYSTRUCTURE = "BODYSTRUCTURE"; - public static final String BYE = "BYE"; - public static final String CAPABILITY = "CAPABILITY"; - public static final String CHECK = "CHECK"; - public static final String CLOSE = "CLOSE"; - public static final String COPY = "COPY"; - public static final String COPYUID = "COPYUID"; - public static final String CREATE = "CREATE"; - public static final String DELETE = "DELETE"; - public static final String EXAMINE = "EXAMINE"; - public static final String EXISTS = "EXISTS"; - public static final String EXPUNGE = "EXPUNGE"; - public static final String FETCH = "FETCH"; - public static final String FLAG_ANSWERED = "\\ANSWERED"; - public static final String FLAG_DELETED = "\\DELETED"; - public static final String FLAG_FLAGGED = "\\FLAGGED"; - public static final String FLAG_NO_SELECT = "\\NOSELECT"; - public static final String FLAG_SEEN = "\\SEEN"; - public static final String FLAGS = "FLAGS"; - public static final String FLAGS_SILENT = "FLAGS.SILENT"; - public static final String ID = "ID"; - public static final String INBOX = "INBOX"; - public static final String INTERNALDATE = "INTERNALDATE"; - public static final String LIST = "LIST"; - public static final String LOGIN = "LOGIN"; - public static final String LOGOUT = "LOGOUT"; - public static final String LSUB = "LSUB"; - public static final String NAMESPACE = "NAMESPACE"; - public static final String NO = "NO"; - public static final String NOOP = "NOOP"; - public static final String OK = "OK"; - public static final String PARSE = "PARSE"; - public static final String PERMANENTFLAGS = "PERMANENTFLAGS"; - public static final String PREAUTH = "PREAUTH"; - public static final String READ_ONLY = "READ-ONLY"; - public static final String READ_WRITE = "READ-WRITE"; - public static final String RENAME = "RENAME"; - public static final String RFC822_SIZE = "RFC822.SIZE"; - public static final String SEARCH = "SEARCH"; - public static final String SELECT = "SELECT"; - public static final String STARTTLS = "STARTTLS"; - public static final String STATUS = "STATUS"; - public static final String STORE = "STORE"; - public static final String SUBSCRIBE = "SUBSCRIBE"; - public static final String TEXT = "TEXT"; - public static final String TRYCREATE = "TRYCREATE"; - public static final String UID = "UID"; - public static final String UID_COPY = "UID COPY"; - public static final String UID_FETCH = "UID FETCH"; - public static final String UID_SEARCH = "UID SEARCH"; - public static final String UID_STORE = "UID STORE"; - public static final String UIDNEXT = "UIDNEXT"; - public static final String UIDPLUS = "UIDPLUS"; - public static final String UIDVALIDITY = "UIDVALIDITY"; - public static final String UNSEEN = "UNSEEN"; - public static final String UNSUBSCRIBE = "UNSUBSCRIBE"; - public static final String APPENDUID = "APPENDUID"; - public static final String NIL = "NIL"; -} diff --git a/src/com/android/email/mail/store/imap/ImapElement.java b/src/com/android/email/mail/store/imap/ImapElement.java deleted file mode 100644 index 80bb6cd99..000000000 --- a/src/com/android/email/mail/store/imap/ImapElement.java +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Copyright (C) 2010 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.email.mail.store.imap; - -/** - * Class representing "element"s in IMAP responses. - * - *

Class hierarchy: - *

- * ImapElement
- *   |
- *   |-- ImapElement.NONE (for 'index out of range')
- *   |
- *   |-- ImapList (isList() == true)
- *   |   |
- *   |   |-- ImapList.EMPTY
- *   |   |
- *   |   --- ImapResponse
- *   |
- *   --- ImapString (isString() == true)
- *       |
- *       |-- ImapString.EMPTY
- *       |
- *       |-- ImapSimpleString
- *       |
- *       |-- ImapMemoryLiteral
- *       |
- *       --- ImapTempFileLiteral
- * 
- */ -public abstract class ImapElement { - /** - * An element that is returned by {@link ImapList#getElementOrNone} to indicate an index - * is out of range. - */ - public static final ImapElement NONE = new ImapElement() { - @Override public void destroy() { - // Don't call super.destroy(). - // It's a shared object. We don't want the mDestroyed to be set on this. - } - - @Override public boolean isList() { - return false; - } - - @Override public boolean isString() { - return false; - } - - @Override public String toString() { - return "[NO ELEMENT]"; - } - - @Override - public boolean equalsForTest(ImapElement that) { - return super.equalsForTest(that); - } - }; - - private boolean mDestroyed = false; - - public abstract boolean isList(); - - public abstract boolean isString(); - - protected boolean isDestroyed() { - return mDestroyed; - } - - /** - * Clean up the resources used by the instance. - * It's for removing a temp file used by {@link ImapTempFileLiteral}. - */ - public void destroy() { - mDestroyed = true; - } - - /** - * Throws {@link RuntimeException} if it's already destroyed. - */ - protected final void checkNotDestroyed() { - if (mDestroyed) { - throw new RuntimeException("Already destroyed"); - } - } - - /** - * Return a string that represents this object; it's purely for the debug purpose. Don't - * mistake it for {@link ImapString#getString}. - * - * Abstract to force subclasses to implement it. - */ - @Override - public abstract String toString(); - - /** - * The equals implementation that is intended to be used only for unit testing. - * (Because it may be heavy and has a special sense of "equal" for testing.) - */ - public boolean equalsForTest(ImapElement that) { - if (that == null) { - return false; - } - return this.getClass() == that.getClass(); // Has to be the same class. - } -} diff --git a/src/com/android/email/mail/store/imap/ImapList.java b/src/com/android/email/mail/store/imap/ImapList.java deleted file mode 100644 index e28355989..000000000 --- a/src/com/android/email/mail/store/imap/ImapList.java +++ /dev/null @@ -1,235 +0,0 @@ -/* - * Copyright (C) 2010 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.email.mail.store.imap; - -import java.util.ArrayList; - -/** - * Class represents an IMAP list. - */ -public class ImapList extends ImapElement { - /** - * {@link ImapList} representing an empty list. - */ - public static final ImapList EMPTY = new ImapList() { - @Override public void destroy() { - // Don't call super.destroy(). - // It's a shared object. We don't want the mDestroyed to be set on this. - } - - @Override void add(ImapElement e) { - throw new RuntimeException(); - } - }; - - private ArrayList mList = new ArrayList(); - - /* package */ void add(ImapElement e) { - if (e == null) { - throw new RuntimeException("Can't add null"); - } - mList.add(e); - } - - @Override - public final boolean isString() { - return false; - } - - @Override - public final boolean isList() { - return true; - } - - public final int size() { - return mList.size(); - } - - public final boolean isEmpty() { - return size() == 0; - } - - /** - * Return true if the element at {@code index} exists, is string, and equals to {@code s}. - * (case insensitive) - */ - public final boolean is(int index, String s) { - return is(index, s, false); - } - - /** - * Same as {@link #is(int, String)}, but does the prefix match if {@code prefixMatch}. - */ - public final boolean is(int index, String s, boolean prefixMatch) { - if (!prefixMatch) { - return getStringOrEmpty(index).is(s); - } else { - return getStringOrEmpty(index).startsWith(s); - } - } - - /** - * Return the element at {@code index}. - * If {@code index} is out of range, returns {@link ImapElement#NONE}. - */ - public final ImapElement getElementOrNone(int index) { - return (index >= mList.size()) ? ImapElement.NONE : mList.get(index); - } - - /** - * Return the element at {@code index} if it's a list. - * If {@code index} is out of range or not a list, returns {@link ImapList#EMPTY}. - */ - public final ImapList getListOrEmpty(int index) { - ImapElement el = getElementOrNone(index); - return el.isList() ? (ImapList) el : EMPTY; - } - - /** - * Return the element at {@code index} if it's a string. - * If {@code index} is out of range or not a string, returns {@link ImapString#EMPTY}. - */ - public final ImapString getStringOrEmpty(int index) { - ImapElement el = getElementOrNone(index); - return el.isString() ? (ImapString) el : ImapString.EMPTY; - } - - /** - * Return an element keyed by {@code key}. Return null if not found. {@code key} has to be - * at an even index. - */ - /* package */ final ImapElement getKeyedElementOrNull(String key, boolean prefixMatch) { - for (int i = 1; i < size(); i += 2) { - if (is(i-1, key, prefixMatch)) { - return mList.get(i); - } - } - return null; - } - - /** - * Return an {@link ImapList} keyed by {@code key}. - * Return {@link ImapList#EMPTY} if not found. - */ - public final ImapList getKeyedListOrEmpty(String key) { - return getKeyedListOrEmpty(key, false); - } - - /** - * Return an {@link ImapList} keyed by {@code key}. - * Return {@link ImapList#EMPTY} if not found. - */ - public final ImapList getKeyedListOrEmpty(String key, boolean prefixMatch) { - ImapElement e = getKeyedElementOrNull(key, prefixMatch); - return (e != null) ? ((ImapList) e) : ImapList.EMPTY; - } - - /** - * Return an {@link ImapString} keyed by {@code key}. - * Return {@link ImapString#EMPTY} if not found. - */ - public final ImapString getKeyedStringOrEmpty(String key) { - return getKeyedStringOrEmpty(key, false); - } - - /** - * Return an {@link ImapString} keyed by {@code key}. - * Return {@link ImapString#EMPTY} if not found. - */ - public final ImapString getKeyedStringOrEmpty(String key, boolean prefixMatch) { - ImapElement e = getKeyedElementOrNull(key, prefixMatch); - return (e != null) ? ((ImapString) e) : ImapString.EMPTY; - } - - /** - * Return true if it contains {@code s}. - */ - public final boolean contains(String s) { - for (int i = 0; i < size(); i++) { - if (getStringOrEmpty(i).is(s)) { - return true; - } - } - return false; - } - - @Override - public void destroy() { - if (mList != null) { - for (ImapElement e : mList) { - e.destroy(); - } - mList = null; - } - super.destroy(); - } - - @Override - public String toString() { - return mList.toString(); - } - - /** - * Return the text representations of the contents concatenated with ",". - */ - public final String flatten() { - return flatten(new StringBuilder()).toString(); - } - - /** - * Returns text representations (i.e. getString()) of contents joined together with - * "," as the separator. - * - * Only used for building the capability string passed to vendor policies. - * - * We can't use toString(), because it's for debugging (meaning the format may change any time), - * and it won't expand literals. - */ - private final StringBuilder flatten(StringBuilder sb) { - sb.append('['); - for (int i = 0; i < mList.size(); i++) { - if (i > 0) { - sb.append(','); - } - final ImapElement e = getElementOrNone(i); - if (e.isList()) { - getListOrEmpty(i).flatten(sb); - } else if (e.isString()) { - sb.append(getStringOrEmpty(i).getString()); - } - } - sb.append(']'); - return sb; - } - - @Override - public boolean equalsForTest(ImapElement that) { - if (!super.equalsForTest(that)) { - return false; - } - ImapList thatList = (ImapList) that; - if (size() != thatList.size()) { - return false; - } - for (int i = 0; i < size(); i++) { - if (!mList.get(i).equalsForTest(thatList.getElementOrNone(i))) { - return false; - } - } - return true; - } -} diff --git a/src/com/android/email/mail/store/imap/ImapMemoryLiteral.java b/src/com/android/email/mail/store/imap/ImapMemoryLiteral.java deleted file mode 100644 index ea62d52d1..000000000 --- a/src/com/android/email/mail/store/imap/ImapMemoryLiteral.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright (C) 2010 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.email.mail.store.imap; - -import com.android.email.FixedLengthInputStream; -import com.android.emailcommon.Logging; -import com.android.emailcommon.utility.Utility; - -import android.util.Log; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; - -/** - * Subclass of {@link ImapString} used for literals backed by an in-memory byte array. - */ -public class ImapMemoryLiteral extends ImapString { - private byte[] mData; - - /* package */ ImapMemoryLiteral(FixedLengthInputStream in) throws IOException { - // We could use ByteArrayOutputStream and IOUtils.copy, but it'd perform an unnecessary - // copy.... - mData = new byte[in.getLength()]; - int pos = 0; - while (pos < mData.length) { - int read = in.read(mData, pos, mData.length - pos); - if (read < 0) { - break; - } - pos += read; - } - if (pos != mData.length) { - Log.w(Logging.LOG_TAG, ""); - } - } - - @Override - public void destroy() { - mData = null; - super.destroy(); - } - - @Override - public String getString() { - return Utility.fromAscii(mData); - } - - @Override - public InputStream getAsStream() { - return new ByteArrayInputStream(mData); - } - - @Override - public String toString() { - return String.format("{%d byte literal(memory)}", mData.length); - } -} diff --git a/src/com/android/email/mail/store/imap/ImapResponse.java b/src/com/android/email/mail/store/imap/ImapResponse.java deleted file mode 100644 index 05bf594e6..000000000 --- a/src/com/android/email/mail/store/imap/ImapResponse.java +++ /dev/null @@ -1,152 +0,0 @@ -/* - * Copyright (C) 2010 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.email.mail.store.imap; - - -/** - * Class represents an IMAP response. - */ -public class ImapResponse extends ImapList { - private final String mTag; - private final boolean mIsContinuationRequest; - - /* package */ ImapResponse(String tag, boolean isContinuationRequest) { - mTag = tag; - mIsContinuationRequest = isContinuationRequest; - } - - /* package */ static boolean isStatusResponse(String symbol) { - return ImapConstants.OK.equalsIgnoreCase(symbol) - || ImapConstants.NO.equalsIgnoreCase(symbol) - || ImapConstants.BAD.equalsIgnoreCase(symbol) - || ImapConstants.PREAUTH.equalsIgnoreCase(symbol) - || ImapConstants.BYE.equalsIgnoreCase(symbol); - } - - /** - * @return whether it's a tagged response. - */ - public boolean isTagged() { - return mTag != null; - } - - /** - * @return whether it's a continuation request. - */ - public boolean isContinuationRequest() { - return mIsContinuationRequest; - } - - public boolean isStatusResponse() { - return isStatusResponse(getStringOrEmpty(0).getString()); - } - - /** - * @return whether it's an OK response. - */ - public boolean isOk() { - return is(0, ImapConstants.OK); - } - - /** - * @return whether it's an BAD response. - */ - public boolean isBad() { - return is(0, ImapConstants.BAD); - } - - /** - * @return whether it's an NO response. - */ - public boolean isNo() { - return is(0, ImapConstants.NO); - } - - /** - * @return whether it's an {@code responseType} data response. (i.e. not tagged). - * @param index where {@code responseType} should appear. e.g. 1 for "FETCH" - * @param responseType e.g. "FETCH" - */ - public final boolean isDataResponse(int index, String responseType) { - return !isTagged() && getStringOrEmpty(index).is(responseType); - } - - /** - * @return Response code (RFC 3501 7.1) if it's a status response. - * - * e.g. "ALERT" for "* OK [ALERT] System shutdown in 10 minutes" - */ - public ImapString getResponseCodeOrEmpty() { - if (!isStatusResponse()) { - return ImapString.EMPTY; // Not a status response. - } - return getListOrEmpty(1).getStringOrEmpty(0); - } - - /** - * @return Alert message it it has ALERT response code. - * - * e.g. "System shutdown in 10 minutes" for "* OK [ALERT] System shutdown in 10 minutes" - */ - public ImapString getAlertTextOrEmpty() { - if (!getResponseCodeOrEmpty().is(ImapConstants.ALERT)) { - return ImapString.EMPTY; // Not an ALERT - } - // The 3rd element contains all the rest of line. - return getStringOrEmpty(2); - } - - /** - * @return Response text in a status response. - */ - public ImapString getStatusResponseTextOrEmpty() { - if (!isStatusResponse()) { - return ImapString.EMPTY; - } - return getStringOrEmpty(getElementOrNone(1).isList() ? 2 : 1); - } - - @Override - public String toString() { - String tag = mTag; - if (isContinuationRequest()) { - tag = "+"; - } - return "#" + tag + "# " + super.toString(); - } - - @Override - public boolean equalsForTest(ImapElement that) { - if (!super.equalsForTest(that)) { - return false; - } - final ImapResponse thatResponse = (ImapResponse) that; - if (mTag == null) { - if (thatResponse.mTag != null) { - return false; - } - } else { - if (!mTag.equals(thatResponse.mTag)) { - return false; - } - } - if (mIsContinuationRequest != thatResponse.mIsContinuationRequest) { - return false; - } - return true; - } -} diff --git a/src/com/android/email/mail/store/imap/ImapResponseParser.java b/src/com/android/email/mail/store/imap/ImapResponseParser.java deleted file mode 100644 index 078cf9f76..000000000 --- a/src/com/android/email/mail/store/imap/ImapResponseParser.java +++ /dev/null @@ -1,450 +0,0 @@ -/* - * Copyright (C) 2010 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.email.mail.store.imap; - -import android.text.TextUtils; -import android.util.Log; - -import com.android.email.FixedLengthInputStream; -import com.android.email.PeekableInputStream; -import com.android.email.mail.transport.DiscourseLogger; -import com.android.email2.ui.MailActivityEmail; -import com.android.emailcommon.Logging; -import com.android.emailcommon.mail.MessagingException; -import com.android.emailcommon.utility.LoggingInputStream; - -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; - -/** - * IMAP response parser. - */ -public class ImapResponseParser { - private static final boolean DEBUG_LOG_RAW_STREAM = false; // DO NOT RELEASE AS 'TRUE' - - /** - * Literal larger than this will be stored in temp file. - */ - public static final int LITERAL_KEEP_IN_MEMORY_THRESHOLD = 2 * 1024 * 1024; - - /** Input stream */ - private final PeekableInputStream mIn; - - /** - * To log network activities when the parser crashes. - * - *

We log all bytes received from the server, except for the part sent as literals. - */ - private final DiscourseLogger mDiscourseLogger; - - private final int mLiteralKeepInMemoryThreshold; - - /** StringBuilder used by readUntil() */ - private final StringBuilder mBufferReadUntil = new StringBuilder(); - - /** StringBuilder used by parseBareString() */ - private final StringBuilder mParseBareString = new StringBuilder(); - - /** - * We store all {@link ImapResponse} in it. {@link #destroyResponses()} must be called from - * time to time to destroy them and clear it. - */ - private final ArrayList mResponsesToDestroy = new ArrayList(); - - /** - * Exception thrown when we receive BYE. It derives from IOException, so it'll be treated - * in the same way EOF does. - */ - public static class ByeException extends IOException { - public static final String MESSAGE = "Received BYE"; - public ByeException() { - super(MESSAGE); - } - } - - /** - * Public constructor for normal use. - */ - public ImapResponseParser(InputStream in, DiscourseLogger discourseLogger) { - this(in, discourseLogger, LITERAL_KEEP_IN_MEMORY_THRESHOLD); - } - - /** - * Constructor for testing to override the literal size threshold. - */ - /* package for test */ ImapResponseParser(InputStream in, DiscourseLogger discourseLogger, - int literalKeepInMemoryThreshold) { - if (DEBUG_LOG_RAW_STREAM && MailActivityEmail.DEBUG) { - in = new LoggingInputStream(in); - } - mIn = new PeekableInputStream(in); - mDiscourseLogger = discourseLogger; - mLiteralKeepInMemoryThreshold = literalKeepInMemoryThreshold; - } - - private static IOException newEOSException() { - final String message = "End of stream reached"; - if (MailActivityEmail.DEBUG) { - Log.d(Logging.LOG_TAG, message); - } - return new IOException(message); - } - - /** - * Peek next one byte. - * - * Throws IOException() if reaches EOF. As long as logical response lines end with \r\n, - * we shouldn't see EOF during parsing. - */ - private int peek() throws IOException { - final int next = mIn.peek(); - if (next == -1) { - throw newEOSException(); - } - return next; - } - - /** - * Read and return one byte from {@link #mIn}, and put it in {@link #mDiscourseLogger}. - * - * Throws IOException() if reaches EOF. As long as logical response lines end with \r\n, - * we shouldn't see EOF during parsing. - */ - private int readByte() throws IOException { - int next = mIn.read(); - if (next == -1) { - throw newEOSException(); - } - mDiscourseLogger.addReceivedByte(next); - return next; - } - - /** - * Destroy all the {@link ImapResponse}s stored in the internal storage and clear it. - * - * @see #readResponse() - */ - public void destroyResponses() { - for (ImapResponse r : mResponsesToDestroy) { - r.destroy(); - } - mResponsesToDestroy.clear(); - } - - /** - * Reads the next response available on the stream and returns an - * {@link ImapResponse} object that represents it. - * - *

When this method successfully returns an {@link ImapResponse}, the {@link ImapResponse} - * is stored in the internal storage. When the {@link ImapResponse} is no longer used - * {@link #destroyResponses} should be called to destroy all the responses in the array. - * - * @return the parsed {@link ImapResponse} object. - * @exception ByeException when detects BYE. - */ - public ImapResponse readResponse() throws IOException, MessagingException { - ImapResponse response = null; - try { - response = parseResponse(); - if (MailActivityEmail.DEBUG) { - Log.d(Logging.LOG_TAG, "<<< " + response.toString()); - } - - } catch (RuntimeException e) { - // Parser crash -- log network activities. - onParseError(e); - throw e; - } catch (IOException e) { - // Network error, or received an unexpected char. - onParseError(e); - throw e; - } - - // Handle this outside of try-catch. We don't have to dump protocol log when getting BYE. - if (response.is(0, ImapConstants.BYE)) { - Log.w(Logging.LOG_TAG, ByeException.MESSAGE); - response.destroy(); - throw new ByeException(); - } - mResponsesToDestroy.add(response); - return response; - } - - private void onParseError(Exception e) { - // Read a few more bytes, so that the log will contain some more context, even if the parser - // crashes in the middle of a response. - // This also makes sure the byte in question will be logged, no matter where it crashes. - // e.g. when parseAtom() peeks and finds at an unexpected char, it throws an exception - // before actually reading it. - // However, we don't want to read too much, because then it may get into an email message. - try { - for (int i = 0; i < 4; i++) { - int b = readByte(); - if (b == -1 || b == '\n') { - break; - } - } - } catch (IOException ignore) { - } - Log.w(Logging.LOG_TAG, "Exception detected: " + e.getMessage()); - mDiscourseLogger.logLastDiscourse(); - } - - /** - * Read next byte from stream and throw it away. If the byte is different from {@code expected} - * throw {@link MessagingException}. - */ - /* package for test */ void expect(char expected) throws IOException { - final int next = readByte(); - if (expected != next) { - throw new IOException(String.format("Expected %04x (%c) but got %04x (%c)", - (int) expected, expected, next, (char) next)); - } - } - - /** - * Read bytes until we find {@code end}, and return all as string. - * The {@code end} will be read (rather than peeked) and won't be included in the result. - */ - /* package for test */ String readUntil(char end) throws IOException { - mBufferReadUntil.setLength(0); - for (;;) { - final int ch = readByte(); - if (ch != end) { - mBufferReadUntil.append((char) ch); - } else { - return mBufferReadUntil.toString(); - } - } - } - - /** - * Read all bytes until \r\n. - */ - /* package */ String readUntilEol() throws IOException { - String ret = readUntil('\r'); - expect('\n'); // TODO Should this really be error? - return ret; - } - - /** - * Parse and return the response line. - */ - private ImapResponse parseResponse() throws IOException, MessagingException { - // We need to destroy the response if we get an exception. - // So, we first store the response that's being built in responseToDestroy, until it's - // completely built, at which point we copy it into responseToReturn and null out - // responseToDestroyt. - // If responseToDestroy is not null in finally, we destroy it because that means - // we got an exception somewhere. - ImapResponse responseToDestroy = null; - final ImapResponse responseToReturn; - - try { - final int ch = peek(); - if (ch == '+') { // Continuation request - readByte(); // skip + - expect(' '); - responseToDestroy = new ImapResponse(null, true); - - // If it's continuation request, we don't really care what's in it. - responseToDestroy.add(new ImapSimpleString(readUntilEol())); - - // Response has successfully been built. Let's return it. - responseToReturn = responseToDestroy; - responseToDestroy = null; - } else { - // Status response or response data - final String tag; - if (ch == '*') { - tag = null; - readByte(); // skip * - expect(' '); - } else { - tag = readUntil(' '); - } - responseToDestroy = new ImapResponse(tag, false); - - final ImapString firstString = parseBareString(); - responseToDestroy.add(firstString); - - // parseBareString won't eat a space after the string, so we need to skip it, - // if exists. - // If the next char is not ' ', it should be EOL. - if (peek() == ' ') { - readByte(); // skip ' ' - - if (responseToDestroy.isStatusResponse()) { // It's a status response - - // Is there a response code? - final int next = peek(); - if (next == '[') { - responseToDestroy.add(parseList('[', ']')); - if (peek() == ' ') { // Skip following space - readByte(); - } - } - - String rest = readUntilEol(); - if (!TextUtils.isEmpty(rest)) { - // The rest is free-form text. - responseToDestroy.add(new ImapSimpleString(rest)); - } - } else { // It's a response data. - parseElements(responseToDestroy, '\0'); - } - } else { - expect('\r'); - expect('\n'); - } - - // Response has successfully been built. Let's return it. - responseToReturn = responseToDestroy; - responseToDestroy = null; - } - } finally { - if (responseToDestroy != null) { - // We get an exception. - responseToDestroy.destroy(); - } - } - - return responseToReturn; - } - - private ImapElement parseElement() throws IOException, MessagingException { - final int next = peek(); - switch (next) { - case '(': - return parseList('(', ')'); - case '[': - return parseList('[', ']'); - case '"': - readByte(); // Skip " - return new ImapSimpleString(readUntil('"')); - case '{': - return parseLiteral(); - case '\r': // CR - readByte(); // Consume \r - expect('\n'); // Should be followed by LF. - return null; - case '\n': // LF // There shouldn't be a bare LF, but just in case. - readByte(); // Consume \n - return null; - default: - return parseBareString(); - } - } - - /** - * Parses an atom. - * - * Special case: If an atom contains '[', everything until the next ']' will be considered - * a part of the atom. - * (e.g. "BODY[HEADER.FIELDS ("DATE" ...)]" will become a single ImapString) - * - * If the value is "NIL", returns an empty string. - */ - private ImapString parseBareString() throws IOException, MessagingException { - mParseBareString.setLength(0); - for (;;) { - final int ch = peek(); - - // TODO Can we clean this up? (This condition is from the old parser.) - if (ch == '(' || ch == ')' || ch == '{' || ch == ' ' || - // ']' is not part of atom (it's in resp-specials) - ch == ']' || - // docs claim that flags are \ atom but atom isn't supposed to - // contain - // * and some flags contain * - // ch == '%' || ch == '*' || - ch == '%' || - // TODO probably should not allow \ and should recognize - // it as a flag instead - // ch == '"' || ch == '\' || - ch == '"' || (0x00 <= ch && ch <= 0x1f) || ch == 0x7f) { - if (mParseBareString.length() == 0) { - throw new MessagingException("Expected string, none found."); - } - String s = mParseBareString.toString(); - - // NIL will be always converted into the empty string. - if (ImapConstants.NIL.equalsIgnoreCase(s)) { - return ImapString.EMPTY; - } - return new ImapSimpleString(s); - } else if (ch == '[') { - // Eat all until next ']' - mParseBareString.append((char) readByte()); - mParseBareString.append(readUntil(']')); - mParseBareString.append(']'); // readUntil won't include the end char. - } else { - mParseBareString.append((char) readByte()); - } - } - } - - private void parseElements(ImapList list, char end) - throws IOException, MessagingException { - for (;;) { - for (;;) { - final int next = peek(); - if (next == end) { - return; - } - if (next != ' ') { - break; - } - // Skip space - readByte(); - } - final ImapElement el = parseElement(); - if (el == null) { // EOL - return; - } - list.add(el); - } - } - - private ImapList parseList(char opening, char closing) - throws IOException, MessagingException { - expect(opening); - final ImapList list = new ImapList(); - parseElements(list, closing); - expect(closing); - return list; - } - - private ImapString parseLiteral() throws IOException, MessagingException { - expect('{'); - final int size; - try { - size = Integer.parseInt(readUntil('}')); - } catch (NumberFormatException nfe) { - throw new MessagingException("Invalid length in literal"); - } - expect('\r'); - expect('\n'); - FixedLengthInputStream in = new FixedLengthInputStream(mIn, size); - if (size > mLiteralKeepInMemoryThreshold) { - return new ImapTempFileLiteral(in); - } else { - return new ImapMemoryLiteral(in); - } - } -} diff --git a/src/com/android/email/mail/store/imap/ImapSimpleString.java b/src/com/android/email/mail/store/imap/ImapSimpleString.java deleted file mode 100644 index 190c5237f..000000000 --- a/src/com/android/email/mail/store/imap/ImapSimpleString.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright (C) 2010 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.email.mail.store.imap; - -import com.android.emailcommon.utility.Utility; - -import java.io.ByteArrayInputStream; -import java.io.InputStream; - -/** - * Subclass of {@link ImapString} used for non literals. - */ -public class ImapSimpleString extends ImapString { - private String mString; - - /* package */ ImapSimpleString(String string) { - mString = (string != null) ? string : ""; - } - - @Override - public void destroy() { - mString = null; - super.destroy(); - } - - @Override - public String getString() { - return mString; - } - - @Override - public InputStream getAsStream() { - return new ByteArrayInputStream(Utility.toAscii(mString)); - } - - @Override - public String toString() { - // Purposefully not return just mString, in order to prevent using it instead of getString. - return "\"" + mString + "\""; - } -} diff --git a/src/com/android/email/mail/store/imap/ImapString.java b/src/com/android/email/mail/store/imap/ImapString.java deleted file mode 100644 index b0ee99d84..000000000 --- a/src/com/android/email/mail/store/imap/ImapString.java +++ /dev/null @@ -1,187 +0,0 @@ -/* - * Copyright (C) 2010 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.email.mail.store.imap; - -import com.android.emailcommon.Logging; - -import android.util.Log; - -import java.io.ByteArrayInputStream; -import java.io.InputStream; -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.Locale; - -/** - * Class represents an IMAP "element" that is not a list. - * - * An atom, quoted string, literal, are all represented by this. Values like OK, STATUS are too. - * Also, this class class may contain more arbitrary value like "BODY[HEADER.FIELDS ("DATE")]". - * See {@link ImapResponseParser}. - */ -public abstract class ImapString extends ImapElement { - private static final byte[] EMPTY_BYTES = new byte[0]; - - public static final ImapString EMPTY = new ImapString() { - @Override public void destroy() { - // Don't call super.destroy(). - // It's a shared object. We don't want the mDestroyed to be set on this. - } - - @Override public String getString() { - return ""; - } - - @Override public InputStream getAsStream() { - return new ByteArrayInputStream(EMPTY_BYTES); - } - - @Override public String toString() { - return ""; - } - }; - - // This is used only for parsing IMAP's FETCH ENVELOPE command, in which - // en_US-like date format is used like "01-Jan-2009 11:20:39 -0800", so this should be - // handled by Locale.US - private final static SimpleDateFormat DATE_TIME_FORMAT = - new SimpleDateFormat("dd-MMM-yyyy HH:mm:ss Z", Locale.US); - - private boolean mIsInteger; - private int mParsedInteger; - private Date mParsedDate; - - @Override - public final boolean isList() { - return false; - } - - @Override - public final boolean isString() { - return true; - } - - /** - * @return true if and only if the length of the string is larger than 0. - * - * Note: IMAP NIL is considered an empty string. See {@link ImapResponseParser - * #parseBareString}. - * On the other hand, a quoted/literal string with value NIL (i.e. "NIL" and {3}\r\nNIL) is - * treated literally. - */ - public final boolean isEmpty() { - return getString().length() == 0; - } - - public abstract String getString(); - - public abstract InputStream getAsStream(); - - /** - * @return whether it can be parsed as a number. - */ - public final boolean isNumber() { - if (mIsInteger) { - return true; - } - try { - mParsedInteger = Integer.parseInt(getString()); - mIsInteger = true; - return true; - } catch (NumberFormatException e) { - return false; - } - } - - /** - * @return value parsed as a number. - */ - public final int getNumberOrZero() { - if (!isNumber()) { - return 0; - } - return mParsedInteger; - } - - /** - * @return whether it can be parsed as a date using {@link #DATE_TIME_FORMAT}. - */ - public final boolean isDate() { - if (mParsedDate != null) { - return true; - } - if (isEmpty()) { - return false; - } - try { - mParsedDate = DATE_TIME_FORMAT.parse(getString()); - return true; - } catch (ParseException e) { - Log.w(Logging.LOG_TAG, getString() + " can't be parsed as a date."); - return false; - } - } - - /** - * @return value it can be parsed as a {@link Date}, or null otherwise. - */ - public final Date getDateOrNull() { - if (!isDate()) { - return null; - } - return mParsedDate; - } - - /** - * @return whether the value case-insensitively equals to {@code s}. - */ - public final boolean is(String s) { - if (s == null) { - return false; - } - return getString().equalsIgnoreCase(s); - } - - - /** - * @return whether the value case-insensitively starts with {@code s}. - */ - public final boolean startsWith(String prefix) { - if (prefix == null) { - return false; - } - final String me = this.getString(); - if (me.length() < prefix.length()) { - return false; - } - return me.substring(0, prefix.length()).equalsIgnoreCase(prefix); - } - - // To force subclasses to implement it. - @Override - public abstract String toString(); - - @Override - public final boolean equalsForTest(ImapElement that) { - if (!super.equalsForTest(that)) { - return false; - } - ImapString thatString = (ImapString) that; - return getString().equals(thatString.getString()); - } -} diff --git a/src/com/android/email/mail/store/imap/ImapTempFileLiteral.java b/src/com/android/email/mail/store/imap/ImapTempFileLiteral.java deleted file mode 100644 index eda1b568e..000000000 --- a/src/com/android/email/mail/store/imap/ImapTempFileLiteral.java +++ /dev/null @@ -1,124 +0,0 @@ -/* - * Copyright (C) 2010 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.email.mail.store.imap; - -import android.util.Log; - -import com.android.email.FixedLengthInputStream; -import com.android.emailcommon.Logging; -import com.android.emailcommon.TempDirectory; -import com.android.emailcommon.utility.Utility; - -import org.apache.commons.io.IOUtils; - -import java.io.ByteArrayInputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; - -/** - * Subclass of {@link ImapString} used for literals backed by a temp file. - */ -public class ImapTempFileLiteral extends ImapString { - /* package for test */ final File mFile; - - /** Size is purely for toString() */ - private final int mSize; - - /* package */ ImapTempFileLiteral(FixedLengthInputStream stream) throws IOException { - mSize = stream.getLength(); - mFile = File.createTempFile("imap", ".tmp", TempDirectory.getTempDirectory()); - - // Unfortunately, we can't really use deleteOnExit(), because temp filenames are random - // so it'd simply cause a memory leak. - // deleteOnExit() simply adds filenames to a static list and the list will never shrink. - // mFile.deleteOnExit(); - OutputStream out = new FileOutputStream(mFile); - IOUtils.copy(stream, out); - out.close(); - } - - /** - * Make sure we delete the temp file. - * - * We should always be calling {@link ImapResponse#destroy()}, but it's here as a last resort. - */ - @Override - protected void finalize() throws Throwable { - try { - destroy(); - } finally { - super.finalize(); - } - } - - @Override - public InputStream getAsStream() { - checkNotDestroyed(); - try { - return new FileInputStream(mFile); - } catch (FileNotFoundException e) { - // It's probably possible if we're low on storage and the system clears the cache dir. - Log.w(Logging.LOG_TAG, "ImapTempFileLiteral: Temp file not found"); - - // Return 0 byte stream as a dummy... - return new ByteArrayInputStream(new byte[0]); - } - } - - @Override - public String getString() { - checkNotDestroyed(); - try { - byte[] bytes = IOUtils.toByteArray(getAsStream()); - // Prevent crash from OOM; we've seen this, but only rarely and not reproducibly - if (bytes.length > ImapResponseParser.LITERAL_KEEP_IN_MEMORY_THRESHOLD) { - throw new IOException(); - } - return Utility.fromAscii(bytes); - } catch (IOException e) { - Log.w(Logging.LOG_TAG, "ImapTempFileLiteral: Error while reading temp file", e); - return ""; - } - } - - @Override - public void destroy() { - try { - if (!isDestroyed() && mFile.exists()) { - mFile.delete(); - } - } catch (RuntimeException re) { - // Just log and ignore. - Log.w(Logging.LOG_TAG, "Failed to remove temp file: " + re.getMessage()); - } - super.destroy(); - } - - @Override - public String toString() { - return String.format("{%d byte literal(file)}", mSize); - } - - public boolean tempFileExistsForTest() { - return mFile.exists(); - } -} diff --git a/src/com/android/email/mail/store/imap/ImapUtility.java b/src/com/android/email/mail/store/imap/ImapUtility.java deleted file mode 100644 index dc7e98e96..000000000 --- a/src/com/android/email/mail/store/imap/ImapUtility.java +++ /dev/null @@ -1,127 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.email.mail.store.imap; - -import com.android.emailcommon.Logging; - -import android.util.Log; - -import java.util.ArrayList; - -/** - * Utility methods for use with IMAP. - */ -public class ImapUtility { - /** - * Apply quoting rules per IMAP RFC, - * quoted = DQUOTE *QUOTED-CHAR DQUOTE - * QUOTED-CHAR = / "\" quoted-specials - * quoted-specials = DQUOTE / "\" - * - * This is used primarily for IMAP login, but might be useful elsewhere. - * - * NOTE: Not very efficient - you may wish to preflight this, or perhaps it should check - * for trouble chars before calling the replace functions. - * - * @param s The string to be quoted. - * @return A copy of the string, having undergone quoting as described above - */ - public static String imapQuoted(String s) { - - // First, quote any backslashes by replacing \ with \\ - // regex Pattern: \\ (Java string const = \\\\) - // Substitute: \\\\ (Java string const = \\\\\\\\) - String result = s.replaceAll("\\\\", "\\\\\\\\"); - - // Then, quote any double-quotes by replacing " with \" - // regex Pattern: " (Java string const = \") - // Substitute: \\" (Java string const = \\\\\") - result = result.replaceAll("\"", "\\\\\""); - - // return string with quotes around it - return "\"" + result + "\""; - } - - /** - * Gets all of the values in a sequence set per RFC 3501. Any ranges are expanded into a - * list of individual numbers. If the set is invalid, an empty array is returned. - *

-     * sequence-number = nz-number / "*"
-     * sequence-range  = sequence-number ":" sequence-number
-     * sequence-set    = (sequence-number / sequence-range) *("," sequence-set)
-     * 
- */ - public static String[] getImapSequenceValues(String set) { - ArrayList list = new ArrayList(); - if (set != null) { - String[] setItems = set.split(","); - for (String item : setItems) { - if (item.indexOf(':') == -1) { - // simple item - try { - Integer.parseInt(item); // Don't need the value; just ensure it's valid - list.add(item); - } catch (NumberFormatException e) { - Log.d(Logging.LOG_TAG, "Invalid UID value", e); - } - } else { - // range - for (String rangeItem : getImapRangeValues(item)) { - list.add(rangeItem); - } - } - } - } - String[] stringList = new String[list.size()]; - return list.toArray(stringList); - } - - /** - * Expand the given number range into a list of individual numbers. If the range is not valid, - * an empty array is returned. - *
-     * sequence-number = nz-number / "*"
-     * sequence-range  = sequence-number ":" sequence-number
-     * sequence-set    = (sequence-number / sequence-range) *("," sequence-set)
-     * 
- */ - public static String[] getImapRangeValues(String range) { - ArrayList list = new ArrayList(); - try { - if (range != null) { - int colonPos = range.indexOf(':'); - if (colonPos > 0) { - int first = Integer.parseInt(range.substring(0, colonPos)); - int second = Integer.parseInt(range.substring(colonPos + 1)); - if (first < second) { - for (int i = first; i <= second; i++) { - list.add(Integer.toString(i)); - } - } else { - for (int i = first; i >= second; i--) { - list.add(Integer.toString(i)); - } - } - } - } - } catch (NumberFormatException e) { - Log.d(Logging.LOG_TAG, "Invalid range value", e); - } - String[] stringList = new String[list.size()]; - return list.toArray(stringList); - } -} diff --git a/src/com/android/email/provider/AttachmentProvider.java b/src/com/android/email/provider/AttachmentProvider.java index 71955851e..25945e1d3 100644 --- a/src/com/android/email/provider/AttachmentProvider.java +++ b/src/com/android/email/provider/AttachmentProvider.java @@ -49,10 +49,10 @@ import java.util.List; * A simple ContentProvider that allows file access to Email's attachments. * * The URI scheme is as follows. For raw file access: - * content://com.android.email.attachmentprovider/acct#/attach#/RAW + * content://com.android.mail.attachmentprovider/acct#/attach#/RAW * * And for access to thumbnails: - * content://com.android.email.attachmentprovider/acct#/attach#/THUMBNAIL/width#/height# + * content://com.android.mail.attachmentprovider/acct#/attach#/THUMBNAIL/width#/height# * * The on-disk (storage) schema is as follows. * diff --git a/src/com/android/email/provider/DBHelper.java b/src/com/android/email/provider/DBHelper.java index b3cecfd83..f0e44bf92 100644 --- a/src/com/android/email/provider/DBHelper.java +++ b/src/com/android/email/provider/DBHelper.java @@ -29,7 +29,6 @@ import android.provider.ContactsContract; import android.util.Log; import com.android.email2.ui.MailActivityEmail; -import com.android.emailcommon.AccountManagerTypes; import com.android.emailcommon.mail.Address; import com.android.emailcommon.provider.Account; import com.android.emailcommon.provider.EmailContent; @@ -586,7 +585,7 @@ public final class DBHelper { // Versions >= 5 require that data be preserved! if (oldVersion < 5) { android.accounts.Account[] accounts = AccountManager.get(mContext) - .getAccountsByType(AccountManagerTypes.TYPE_EXCHANGE); + .getAccountsByType("eas"); for (android.accounts.Account account: accounts) { AccountManager.get(mContext).removeAccount(account, null, null); } @@ -1092,8 +1091,9 @@ public final class DBHelper { static private void createAccountManagerAccount(Context context, String login, String password) { AccountManager accountManager = AccountManager.get(context); + // STOPSHIP android.accounts.Account amAccount = - new android.accounts.Account(login, AccountManagerTypes.TYPE_POP_IMAP); + new android.accounts.Account(login, "com.android.email"); accountManager.addAccountExplicitly(amAccount, password, null); ContentResolver.setIsSyncable(amAccount, EmailContent.AUTHORITY, 1); ContentResolver.setSyncAutomatically(amAccount, EmailContent.AUTHORITY, true); @@ -1135,7 +1135,7 @@ public final class DBHelper { android.accounts.Account amAccount = new android.accounts.Account( accountCursor.getString(V21_ACCOUNT_EMAIL), - AccountManagerTypes.TYPE_EXCHANGE); + "eas"); ContentResolver.setIsSyncable(amAccount, EmailContent.AUTHORITY, 1); ContentResolver.setSyncAutomatically(amAccount, EmailContent.AUTHORITY, true); diff --git a/src/com/android/email/provider/EmailProvider.java b/src/com/android/email/provider/EmailProvider.java index 040183653..60a0dfd26 100644 --- a/src/com/android/email/provider/EmailProvider.java +++ b/src/com/android/email/provider/EmailProvider.java @@ -136,15 +136,6 @@ public class EmailProvider extends ContentProvider { public static final String EMAIL_ATTACHMENT_MIME_TYPE = "vnd.android.cursor.item/email-attachment"; - public static final Uri INTEGRITY_CHECK_URI = - Uri.parse("content://" + EmailContent.AUTHORITY + "/integrityCheck"); - public static final Uri ACCOUNT_BACKUP_URI = - Uri.parse("content://" + EmailContent.AUTHORITY + "/accountBackup"); - public static final Uri FOLDER_STATUS_URI = - Uri.parse("content://" + EmailContent.AUTHORITY + "/status"); - public static final Uri FOLDER_REFRESH_URI = - Uri.parse("content://" + EmailContent.AUTHORITY + "/refresh"); - /** Appended to the notification URI for delete operations */ public static final String NOTIFICATION_OP_DELETE = "delete"; /** Appended to the notification URI for insert operations */ @@ -310,7 +301,7 @@ public class EmailProvider extends ContentProvider { null // UI }; - private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH); + private static UriMatcher sURIMatcher = null; private static final String MAILBOX_PRE_CACHE_SELECTION = MailboxColumns.TYPE + " IN (" + Mailbox.TYPE_INBOX + "," + Mailbox.TYPE_DRAFTS + "," + Mailbox.TYPE_TRASH + "," + @@ -341,7 +332,7 @@ public class EmailProvider extends ContentProvider { private static final String ID_EQUALS = EmailContent.RECORD_ID + "=?"; - private static final ContentValues CONTENT_VALUES_RESET_NEW_MESSAGE_COUNT; + private static ContentValues CONTENT_VALUES_RESET_NEW_MESSAGE_COUNT; private static final ContentValues EMPTY_CONTENT_VALUES = new ContentValues(); public static final String MESSAGE_URI_PARAMETER_MAILBOX_ID = "mailboxId"; @@ -357,130 +348,6 @@ public class EmailProvider extends ContentProvider { private static final String SWIPE_DELETE = Integer.toString(Swipe.DELETE); private static final String SWIPE_DISABLED = Integer.toString(Swipe.DISABLED); - static { - // Email URI matching table - UriMatcher matcher = sURIMatcher; - - // All accounts - matcher.addURI(EmailContent.AUTHORITY, "account", ACCOUNT); - // A specific account - // insert into this URI causes a mailbox to be added to the account - matcher.addURI(EmailContent.AUTHORITY, "account/#", ACCOUNT_ID); - matcher.addURI(EmailContent.AUTHORITY, "account/default", ACCOUNT_DEFAULT_ID); - matcher.addURI(EmailContent.AUTHORITY, "accountCheck/#", ACCOUNT_CHECK); - - // Special URI to reset the new message count. Only update works, and content values - // will be ignored. - matcher.addURI(EmailContent.AUTHORITY, "resetNewMessageCount", - ACCOUNT_RESET_NEW_COUNT); - matcher.addURI(EmailContent.AUTHORITY, "resetNewMessageCount/#", - ACCOUNT_RESET_NEW_COUNT_ID); - - // All mailboxes - matcher.addURI(EmailContent.AUTHORITY, "mailbox", MAILBOX); - // A specific mailbox - // insert into this URI causes a message to be added to the mailbox - // ** NOTE For now, the accountKey must be set manually in the values! - matcher.addURI(EmailContent.AUTHORITY, "mailbox/#", MAILBOX_ID); - matcher.addURI(EmailContent.AUTHORITY, "mailboxIdFromAccountAndType/#/#", - MAILBOX_ID_FROM_ACCOUNT_AND_TYPE); - matcher.addURI(EmailContent.AUTHORITY, "mailboxNotification/#", MAILBOX_NOTIFICATION); - matcher.addURI(EmailContent.AUTHORITY, "mailboxMostRecentMessage/#", - MAILBOX_MOST_RECENT_MESSAGE); - - // All messages - matcher.addURI(EmailContent.AUTHORITY, "message", MESSAGE); - // A specific message - // insert into this URI causes an attachment to be added to the message - matcher.addURI(EmailContent.AUTHORITY, "message/#", MESSAGE_ID); - - // A specific attachment - matcher.addURI(EmailContent.AUTHORITY, "attachment", ATTACHMENT); - // A specific attachment (the header information) - matcher.addURI(EmailContent.AUTHORITY, "attachment/#", ATTACHMENT_ID); - // The attachments of a specific message (query only) (insert & delete TBD) - matcher.addURI(EmailContent.AUTHORITY, "attachment/message/#", - ATTACHMENTS_MESSAGE_ID); - - // All mail bodies - matcher.addURI(EmailContent.AUTHORITY, "body", BODY); - // A specific mail body - matcher.addURI(EmailContent.AUTHORITY, "body/#", BODY_ID); - - // All hostauth records - matcher.addURI(EmailContent.AUTHORITY, "hostauth", HOSTAUTH); - // A specific hostauth - matcher.addURI(EmailContent.AUTHORITY, "hostauth/*", HOSTAUTH_ID); - - // Atomically a constant value to a particular field of a mailbox/account - matcher.addURI(EmailContent.AUTHORITY, "mailboxIdAddToField/#", - MAILBOX_ID_ADD_TO_FIELD); - matcher.addURI(EmailContent.AUTHORITY, "accountIdAddToField/#", - ACCOUNT_ID_ADD_TO_FIELD); - - /** - * THIS URI HAS SPECIAL SEMANTICS - * ITS USE IS INTENDED FOR THE UI APPLICATION TO MARK CHANGES THAT NEED TO BE SYNCED BACK - * TO A SERVER VIA A SYNC ADAPTER - */ - matcher.addURI(EmailContent.AUTHORITY, "syncedMessage/#", SYNCED_MESSAGE_ID); - matcher.addURI(EmailContent.AUTHORITY, "messageBySelection", MESSAGE_SELECTION); - - /** - * THE URIs BELOW THIS POINT ARE INTENDED TO BE USED BY SYNC ADAPTERS ONLY - * THEY REFER TO DATA CREATED AND MAINTAINED BY CALLS TO THE SYNCED_MESSAGE_ID URI - * BY THE UI APPLICATION - */ - // All deleted messages - matcher.addURI(EmailContent.AUTHORITY, "deletedMessage", DELETED_MESSAGE); - // A specific deleted message - matcher.addURI(EmailContent.AUTHORITY, "deletedMessage/#", DELETED_MESSAGE_ID); - - // All updated messages - matcher.addURI(EmailContent.AUTHORITY, "updatedMessage", UPDATED_MESSAGE); - // A specific updated message - matcher.addURI(EmailContent.AUTHORITY, "updatedMessage/#", UPDATED_MESSAGE_ID); - - CONTENT_VALUES_RESET_NEW_MESSAGE_COUNT = new ContentValues(); - CONTENT_VALUES_RESET_NEW_MESSAGE_COUNT.put(Account.NEW_MESSAGE_COUNT, 0); - - matcher.addURI(EmailContent.AUTHORITY, "policy", POLICY); - matcher.addURI(EmailContent.AUTHORITY, "policy/#", POLICY_ID); - - // All quick responses - matcher.addURI(EmailContent.AUTHORITY, "quickresponse", QUICK_RESPONSE); - // A specific quick response - matcher.addURI(EmailContent.AUTHORITY, "quickresponse/#", QUICK_RESPONSE_ID); - // All quick responses associated with a particular account id - matcher.addURI(EmailContent.AUTHORITY, "quickresponse/account/#", - QUICK_RESPONSE_ACCOUNT_ID); - - matcher.addURI(EmailContent.AUTHORITY, "uifolders/#", UI_FOLDERS); - matcher.addURI(EmailContent.AUTHORITY, "uiallfolders/#", UI_ALL_FOLDERS); - matcher.addURI(EmailContent.AUTHORITY, "uisubfolders/#", UI_SUBFOLDERS); - matcher.addURI(EmailContent.AUTHORITY, "uimessages/#", UI_MESSAGES); - matcher.addURI(EmailContent.AUTHORITY, "uimessage/#", UI_MESSAGE); - matcher.addURI(EmailContent.AUTHORITY, "uisendmail/#", UI_SENDMAIL); - matcher.addURI(EmailContent.AUTHORITY, "uiundo", UI_UNDO); - matcher.addURI(EmailContent.AUTHORITY, "uisavedraft/#", UI_SAVEDRAFT); - matcher.addURI(EmailContent.AUTHORITY, "uiupdatedraft/#", UI_UPDATEDRAFT); - matcher.addURI(EmailContent.AUTHORITY, "uisenddraft/#", UI_SENDDRAFT); - matcher.addURI(EmailContent.AUTHORITY, "uirefresh/#", UI_FOLDER_REFRESH); - matcher.addURI(EmailContent.AUTHORITY, "uifolder/#", UI_FOLDER); - matcher.addURI(EmailContent.AUTHORITY, "uiaccount/#", UI_ACCOUNT); - matcher.addURI(EmailContent.AUTHORITY, "uiaccts", UI_ACCTS); - matcher.addURI(EmailContent.AUTHORITY, "uiattachments/#", UI_ATTACHMENTS); - matcher.addURI(EmailContent.AUTHORITY, "uiattachment/#", UI_ATTACHMENT); - matcher.addURI(EmailContent.AUTHORITY, "uisearch/#", UI_SEARCH); - matcher.addURI(EmailContent.AUTHORITY, "uiaccountdata/#", UI_ACCOUNT_DATA); - matcher.addURI(EmailContent.AUTHORITY, "uiloadmore/#", UI_FOLDER_LOAD_MORE); - matcher.addURI(EmailContent.AUTHORITY, "uiconversation/#", UI_CONVERSATION); - matcher.addURI(EmailContent.AUTHORITY, "uirecentfolders/#", UI_RECENT_FOLDERS); - matcher.addURI(EmailContent.AUTHORITY, "uidefaultrecentfolders/#", - UI_DEFAULT_RECENT_FOLDERS); - matcher.addURI(EmailContent.AUTHORITY, "pickTrashFolder/#", ACCOUNT_PICK_TRASH_FOLDER); - matcher.addURI(EmailContent.AUTHORITY, "pickSentFolder/#", ACCOUNT_PICK_SENT_FOLDER); - } /** * Wrap the UriMatcher call so we can throw a runtime exception if an unknown Uri is passed in @@ -497,6 +364,11 @@ public class EmailProvider extends ContentProvider { return match; } + public static Uri INTEGRITY_CHECK_URI; + public static Uri ACCOUNT_BACKUP_URI; + public static Uri FOLDER_STATUS_URI; + public static Uri FOLDER_REFRESH_URI; + private SQLiteDatabase mDatabase; private SQLiteDatabase mBodyDatabase; @@ -1171,10 +1043,147 @@ public class EmailProvider extends ContentProvider { return resultUri; } - @Override - public boolean onCreate() { - MailActivityEmail.setServicesEnabledAsync(getContext()); - checkDatabases(); + @Override + public boolean onCreate() { + Context context = getContext(); + EmailContent.init(context); + if (INTEGRITY_CHECK_URI == null) { + INTEGRITY_CHECK_URI = Uri.parse("content://" + EmailContent.AUTHORITY + + "/integrityCheck"); + ACCOUNT_BACKUP_URI = + Uri.parse("content://" + EmailContent.AUTHORITY + "/accountBackup"); + FOLDER_STATUS_URI = + Uri.parse("content://" + EmailContent.AUTHORITY + "/status"); + FOLDER_REFRESH_URI = + Uri.parse("content://" + EmailContent.AUTHORITY + "/refresh"); + } + MailActivityEmail.setServicesEnabledAsync(context); + checkDatabases(); + if (sURIMatcher == null) { + sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH); + // Email URI matching table + UriMatcher matcher = sURIMatcher; + + // All accounts + matcher.addURI(EmailContent.AUTHORITY, "account", ACCOUNT); + // A specific account + // insert into this URI causes a mailbox to be added to the account + matcher.addURI(EmailContent.AUTHORITY, "account/#", ACCOUNT_ID); + matcher.addURI(EmailContent.AUTHORITY, "account/default", ACCOUNT_DEFAULT_ID); + matcher.addURI(EmailContent.AUTHORITY, "accountCheck/#", ACCOUNT_CHECK); + + // Special URI to reset the new message count. Only update works, and content values + // will be ignored. + matcher.addURI(EmailContent.AUTHORITY, "resetNewMessageCount", + ACCOUNT_RESET_NEW_COUNT); + matcher.addURI(EmailContent.AUTHORITY, "resetNewMessageCount/#", + ACCOUNT_RESET_NEW_COUNT_ID); + + // All mailboxes + matcher.addURI(EmailContent.AUTHORITY, "mailbox", MAILBOX); + // A specific mailbox + // insert into this URI causes a message to be added to the mailbox + // ** NOTE For now, the accountKey must be set manually in the values! + matcher.addURI(EmailContent.AUTHORITY, "mailbox/#", MAILBOX_ID); + matcher.addURI(EmailContent.AUTHORITY, "mailboxIdFromAccountAndType/#/#", + MAILBOX_ID_FROM_ACCOUNT_AND_TYPE); + matcher.addURI(EmailContent.AUTHORITY, "mailboxNotification/#", MAILBOX_NOTIFICATION); + matcher.addURI(EmailContent.AUTHORITY, "mailboxMostRecentMessage/#", + MAILBOX_MOST_RECENT_MESSAGE); + + // All messages + matcher.addURI(EmailContent.AUTHORITY, "message", MESSAGE); + // A specific message + // insert into this URI causes an attachment to be added to the message + matcher.addURI(EmailContent.AUTHORITY, "message/#", MESSAGE_ID); + + // A specific attachment + matcher.addURI(EmailContent.AUTHORITY, "attachment", ATTACHMENT); + // A specific attachment (the header information) + matcher.addURI(EmailContent.AUTHORITY, "attachment/#", ATTACHMENT_ID); + // The attachments of a specific message (query only) (insert & delete TBD) + matcher.addURI(EmailContent.AUTHORITY, "attachment/message/#", + ATTACHMENTS_MESSAGE_ID); + + // All mail bodies + matcher.addURI(EmailContent.AUTHORITY, "body", BODY); + // A specific mail body + matcher.addURI(EmailContent.AUTHORITY, "body/#", BODY_ID); + + // All hostauth records + matcher.addURI(EmailContent.AUTHORITY, "hostauth", HOSTAUTH); + // A specific hostauth + matcher.addURI(EmailContent.AUTHORITY, "hostauth/*", HOSTAUTH_ID); + + // Atomically a constant value to a particular field of a mailbox/account + matcher.addURI(EmailContent.AUTHORITY, "mailboxIdAddToField/#", + MAILBOX_ID_ADD_TO_FIELD); + matcher.addURI(EmailContent.AUTHORITY, "accountIdAddToField/#", + ACCOUNT_ID_ADD_TO_FIELD); + + /** + * THIS URI HAS SPECIAL SEMANTICS + * ITS USE IS INTENDED FOR THE UI TO MARK CHANGES THAT NEED TO BE SYNCED BACK + * TO A SERVER VIA A SYNC ADAPTER + */ + matcher.addURI(EmailContent.AUTHORITY, "syncedMessage/#", SYNCED_MESSAGE_ID); + matcher.addURI(EmailContent.AUTHORITY, "messageBySelection", MESSAGE_SELECTION); + + /** + * THE URIs BELOW THIS POINT ARE INTENDED TO BE USED BY SYNC ADAPTERS ONLY + * THEY REFER TO DATA CREATED AND MAINTAINED BY CALLS TO THE SYNCED_MESSAGE_ID URI + * BY THE UI APPLICATION + */ + // All deleted messages + matcher.addURI(EmailContent.AUTHORITY, "deletedMessage", DELETED_MESSAGE); + // A specific deleted message + matcher.addURI(EmailContent.AUTHORITY, "deletedMessage/#", DELETED_MESSAGE_ID); + + // All updated messages + matcher.addURI(EmailContent.AUTHORITY, "updatedMessage", UPDATED_MESSAGE); + // A specific updated message + matcher.addURI(EmailContent.AUTHORITY, "updatedMessage/#", UPDATED_MESSAGE_ID); + + CONTENT_VALUES_RESET_NEW_MESSAGE_COUNT = new ContentValues(); + CONTENT_VALUES_RESET_NEW_MESSAGE_COUNT.put(Account.NEW_MESSAGE_COUNT, 0); + + matcher.addURI(EmailContent.AUTHORITY, "policy", POLICY); + matcher.addURI(EmailContent.AUTHORITY, "policy/#", POLICY_ID); + + // All quick responses + matcher.addURI(EmailContent.AUTHORITY, "quickresponse", QUICK_RESPONSE); + // A specific quick response + matcher.addURI(EmailContent.AUTHORITY, "quickresponse/#", QUICK_RESPONSE_ID); + // All quick responses associated with a particular account id + matcher.addURI(EmailContent.AUTHORITY, "quickresponse/account/#", + QUICK_RESPONSE_ACCOUNT_ID); + + matcher.addURI(EmailContent.AUTHORITY, "uifolders/#", UI_FOLDERS); + matcher.addURI(EmailContent.AUTHORITY, "uiallfolders/#", UI_ALL_FOLDERS); + matcher.addURI(EmailContent.AUTHORITY, "uisubfolders/#", UI_SUBFOLDERS); + matcher.addURI(EmailContent.AUTHORITY, "uimessages/#", UI_MESSAGES); + matcher.addURI(EmailContent.AUTHORITY, "uimessage/#", UI_MESSAGE); + matcher.addURI(EmailContent.AUTHORITY, "uisendmail/#", UI_SENDMAIL); + matcher.addURI(EmailContent.AUTHORITY, "uiundo", UI_UNDO); + matcher.addURI(EmailContent.AUTHORITY, "uisavedraft/#", UI_SAVEDRAFT); + matcher.addURI(EmailContent.AUTHORITY, "uiupdatedraft/#", UI_UPDATEDRAFT); + matcher.addURI(EmailContent.AUTHORITY, "uisenddraft/#", UI_SENDDRAFT); + matcher.addURI(EmailContent.AUTHORITY, "uirefresh/#", UI_FOLDER_REFRESH); + matcher.addURI(EmailContent.AUTHORITY, "uifolder/#", UI_FOLDER); + matcher.addURI(EmailContent.AUTHORITY, "uiaccount/#", UI_ACCOUNT); + matcher.addURI(EmailContent.AUTHORITY, "uiaccts", UI_ACCTS); + matcher.addURI(EmailContent.AUTHORITY, "uiattachments/#", UI_ATTACHMENTS); + matcher.addURI(EmailContent.AUTHORITY, "uiattachment/#", UI_ATTACHMENT); + matcher.addURI(EmailContent.AUTHORITY, "uisearch/#", UI_SEARCH); + matcher.addURI(EmailContent.AUTHORITY, "uiaccountdata/#", UI_ACCOUNT_DATA); + matcher.addURI(EmailContent.AUTHORITY, "uiloadmore/#", UI_FOLDER_LOAD_MORE); + matcher.addURI(EmailContent.AUTHORITY, "uiconversation/#", UI_CONVERSATION); + matcher.addURI(EmailContent.AUTHORITY, "uirecentfolders/#", UI_RECENT_FOLDERS); + matcher.addURI(EmailContent.AUTHORITY, "uidefaultrecentfolders/#", + UI_DEFAULT_RECENT_FOLDERS); + matcher.addURI(EmailContent.AUTHORITY, "pickTrashFolder/#", ACCOUNT_PICK_TRASH_FOLDER); + matcher.addURI(EmailContent.AUTHORITY, "pickSentFolder/#", ACCOUNT_PICK_SENT_FOLDER); + } return false; } @@ -2121,28 +2130,34 @@ outer: * Mapping of UIProvider columns to EmailProvider columns for the message list (called the * conversation list in UnifiedEmail) */ - private static final ProjectionMap sMessageListMap = ProjectionMap.builder() - .add(BaseColumns._ID, MessageColumns.ID) - .add(UIProvider.ConversationColumns.URI, uriWithId("uimessage")) - .add(UIProvider.ConversationColumns.MESSAGE_LIST_URI, uriWithId("uimessage")) - .add(UIProvider.ConversationColumns.SUBJECT, MessageColumns.SUBJECT) - .add(UIProvider.ConversationColumns.SNIPPET, MessageColumns.SNIPPET) - .add(UIProvider.ConversationColumns.CONVERSATION_INFO, null) - .add(UIProvider.ConversationColumns.DATE_RECEIVED_MS, MessageColumns.TIMESTAMP) - .add(UIProvider.ConversationColumns.HAS_ATTACHMENTS, MessageColumns.FLAG_ATTACHMENT) - .add(UIProvider.ConversationColumns.NUM_MESSAGES, "1") - .add(UIProvider.ConversationColumns.NUM_DRAFTS, "0") - .add(UIProvider.ConversationColumns.SENDING_STATE, - Integer.toString(ConversationSendingState.OTHER)) - .add(UIProvider.ConversationColumns.PRIORITY, Integer.toString(ConversationPriority.LOW)) - .add(UIProvider.ConversationColumns.READ, MessageColumns.FLAG_READ) - .add(UIProvider.ConversationColumns.STARRED, MessageColumns.FLAG_FAVORITE) - .add(UIProvider.ConversationColumns.FLAGS, CONVERSATION_FLAGS) - .add(UIProvider.ConversationColumns.ACCOUNT_URI, - "'content://" + EmailContent.AUTHORITY + "/uiaccount/' || " - + MessageColumns.ACCOUNT_KEY) - .add(UIProvider.ConversationColumns.SENDER_INFO, MessageColumns.FROM_LIST) - .build(); + private ProjectionMap getMessageListMap() { + if (sMessageListMap == null) { + sMessageListMap = ProjectionMap.builder() + .add(BaseColumns._ID, MessageColumns.ID) + .add(UIProvider.ConversationColumns.URI, uriWithId("uimessage")) + .add(UIProvider.ConversationColumns.MESSAGE_LIST_URI, uriWithId("uimessage")) + .add(UIProvider.ConversationColumns.SUBJECT, MessageColumns.SUBJECT) + .add(UIProvider.ConversationColumns.SNIPPET, MessageColumns.SNIPPET) + .add(UIProvider.ConversationColumns.CONVERSATION_INFO, null) + .add(UIProvider.ConversationColumns.DATE_RECEIVED_MS, MessageColumns.TIMESTAMP) + .add(UIProvider.ConversationColumns.HAS_ATTACHMENTS, MessageColumns.FLAG_ATTACHMENT) + .add(UIProvider.ConversationColumns.NUM_MESSAGES, "1") + .add(UIProvider.ConversationColumns.NUM_DRAFTS, "0") + .add(UIProvider.ConversationColumns.SENDING_STATE, + Integer.toString(ConversationSendingState.OTHER)) + .add(UIProvider.ConversationColumns.PRIORITY, + Integer.toString(ConversationPriority.LOW)) + .add(UIProvider.ConversationColumns.READ, MessageColumns.FLAG_READ) + .add(UIProvider.ConversationColumns.STARRED, MessageColumns.FLAG_FAVORITE) + .add(UIProvider.ConversationColumns.FLAGS, CONVERSATION_FLAGS) + .add(UIProvider.ConversationColumns.ACCOUNT_URI, + uriWithColumn("uiaccount", MessageColumns.ACCOUNT_KEY)) + .add(UIProvider.ConversationColumns.SENDER_INFO, MessageColumns.FROM_LIST) + .build(); + } + return sMessageListMap; + } + private static ProjectionMap sMessageListMap; /** * Generate UIProvider draft type; note the test for "reply all" must come before "reply" @@ -2167,45 +2182,53 @@ outer: * Mapping of UIProvider columns to EmailProvider columns for a detailed message view in * UnifiedEmail */ - private static final ProjectionMap sMessageViewMap = ProjectionMap.builder() - .add(BaseColumns._ID, Message.TABLE_NAME + "." + EmailContent.MessageColumns.ID) - .add(UIProvider.MessageColumns.SERVER_ID, SyncColumns.SERVER_ID) - .add(UIProvider.MessageColumns.URI, uriWithFQId("uimessage", Message.TABLE_NAME)) - .add(UIProvider.MessageColumns.CONVERSATION_ID, - uriWithFQId("uimessage", Message.TABLE_NAME)) - .add(UIProvider.MessageColumns.SUBJECT, EmailContent.MessageColumns.SUBJECT) - .add(UIProvider.MessageColumns.SNIPPET, EmailContent.MessageColumns.SNIPPET) - .add(UIProvider.MessageColumns.FROM, EmailContent.MessageColumns.FROM_LIST) - .add(UIProvider.MessageColumns.TO, EmailContent.MessageColumns.TO_LIST) - .add(UIProvider.MessageColumns.CC, EmailContent.MessageColumns.CC_LIST) - .add(UIProvider.MessageColumns.BCC, EmailContent.MessageColumns.BCC_LIST) - .add(UIProvider.MessageColumns.REPLY_TO, EmailContent.MessageColumns.REPLY_TO_LIST) - .add(UIProvider.MessageColumns.DATE_RECEIVED_MS, EmailContent.MessageColumns.TIMESTAMP) - .add(UIProvider.MessageColumns.BODY_HTML, Body.HTML_CONTENT) - .add(UIProvider.MessageColumns.BODY_TEXT, Body.TEXT_CONTENT) - .add(UIProvider.MessageColumns.REF_MESSAGE_ID, "0") - .add(UIProvider.MessageColumns.DRAFT_TYPE, NOT_A_DRAFT_STRING) - .add(UIProvider.MessageColumns.APPEND_REF_MESSAGE_CONTENT, "0") - .add(UIProvider.MessageColumns.HAS_ATTACHMENTS, EmailContent.MessageColumns.FLAG_ATTACHMENT) - .add(UIProvider.MessageColumns.ATTACHMENT_LIST_URI, - uriWithFQId("uiattachments", Message.TABLE_NAME)) - .add(UIProvider.MessageColumns.MESSAGE_FLAGS, MESSAGE_FLAGS) - .add(UIProvider.MessageColumns.SAVE_MESSAGE_URI, - uriWithFQId("uiupdatedraft", Message.TABLE_NAME)) - .add(UIProvider.MessageColumns.SEND_MESSAGE_URI, - uriWithFQId("uisenddraft", Message.TABLE_NAME)) - .add(UIProvider.MessageColumns.DRAFT_TYPE, MESSAGE_DRAFT_TYPE) - .add(UIProvider.MessageColumns.MESSAGE_ACCOUNT_URI, - uriWithColumn("account", MessageColumns.ACCOUNT_KEY)) - .add(UIProvider.MessageColumns.STARRED, EmailContent.MessageColumns.FLAG_FAVORITE) - .add(UIProvider.MessageColumns.READ, EmailContent.MessageColumns.FLAG_READ) - .add(UIProvider.MessageColumns.SPAM_WARNING_STRING, null) - .add(UIProvider.MessageColumns.SPAM_WARNING_LEVEL, - Integer.toString(UIProvider.SpamWarningLevel.NO_WARNING)) - .add(UIProvider.MessageColumns.SPAM_WARNING_LINK_TYPE, - Integer.toString(UIProvider.SpamWarningLinkType.NO_LINK)) - .add(UIProvider.MessageColumns.VIA_DOMAIN, null) - .build(); + private ProjectionMap getMessageViewMap() { + if (sMessageViewMap == null) { + sMessageViewMap = ProjectionMap.builder() + .add(BaseColumns._ID, Message.TABLE_NAME + "." + EmailContent.MessageColumns.ID) + .add(UIProvider.MessageColumns.SERVER_ID, SyncColumns.SERVER_ID) + .add(UIProvider.MessageColumns.URI, uriWithFQId("uimessage", Message.TABLE_NAME)) + .add(UIProvider.MessageColumns.CONVERSATION_ID, + uriWithFQId("uimessage", Message.TABLE_NAME)) + .add(UIProvider.MessageColumns.SUBJECT, EmailContent.MessageColumns.SUBJECT) + .add(UIProvider.MessageColumns.SNIPPET, EmailContent.MessageColumns.SNIPPET) + .add(UIProvider.MessageColumns.FROM, EmailContent.MessageColumns.FROM_LIST) + .add(UIProvider.MessageColumns.TO, EmailContent.MessageColumns.TO_LIST) + .add(UIProvider.MessageColumns.CC, EmailContent.MessageColumns.CC_LIST) + .add(UIProvider.MessageColumns.BCC, EmailContent.MessageColumns.BCC_LIST) + .add(UIProvider.MessageColumns.REPLY_TO, EmailContent.MessageColumns.REPLY_TO_LIST) + .add(UIProvider.MessageColumns.DATE_RECEIVED_MS, + EmailContent.MessageColumns.TIMESTAMP) + .add(UIProvider.MessageColumns.BODY_HTML, Body.HTML_CONTENT) + .add(UIProvider.MessageColumns.BODY_TEXT, Body.TEXT_CONTENT) + .add(UIProvider.MessageColumns.REF_MESSAGE_ID, "0") + .add(UIProvider.MessageColumns.DRAFT_TYPE, NOT_A_DRAFT_STRING) + .add(UIProvider.MessageColumns.APPEND_REF_MESSAGE_CONTENT, "0") + .add(UIProvider.MessageColumns.HAS_ATTACHMENTS, + EmailContent.MessageColumns.FLAG_ATTACHMENT) + .add(UIProvider.MessageColumns.ATTACHMENT_LIST_URI, + uriWithFQId("uiattachments", Message.TABLE_NAME)) + .add(UIProvider.MessageColumns.MESSAGE_FLAGS, MESSAGE_FLAGS) + .add(UIProvider.MessageColumns.SAVE_MESSAGE_URI, + uriWithFQId("uiupdatedraft", Message.TABLE_NAME)) + .add(UIProvider.MessageColumns.SEND_MESSAGE_URI, + uriWithFQId("uisenddraft", Message.TABLE_NAME)) + .add(UIProvider.MessageColumns.DRAFT_TYPE, MESSAGE_DRAFT_TYPE) + .add(UIProvider.MessageColumns.MESSAGE_ACCOUNT_URI, + uriWithColumn("account", MessageColumns.ACCOUNT_KEY)) + .add(UIProvider.MessageColumns.STARRED, EmailContent.MessageColumns.FLAG_FAVORITE) + .add(UIProvider.MessageColumns.READ, EmailContent.MessageColumns.FLAG_READ) + .add(UIProvider.MessageColumns.SPAM_WARNING_STRING, null) + .add(UIProvider.MessageColumns.SPAM_WARNING_LEVEL, + Integer.toString(UIProvider.SpamWarningLevel.NO_WARNING)) + .add(UIProvider.MessageColumns.SPAM_WARNING_LINK_TYPE, + Integer.toString(UIProvider.SpamWarningLinkType.NO_LINK)) + .add(UIProvider.MessageColumns.VIA_DOMAIN, null) + .build(); + } + return sMessageViewMap; + } + private static ProjectionMap sMessageViewMap; /** * Generate UIProvider folder capabilities from mailbox flags @@ -2236,50 +2259,62 @@ outer: + " WHEN " + Mailbox.TYPE_STARRED + " THEN " + R.drawable.ic_menu_star_holo_light + " ELSE -1 END"; - private static final ProjectionMap sFolderListMap = ProjectionMap.builder() - .add(BaseColumns._ID, MailboxColumns.ID) - .add(UIProvider.FolderColumns.URI, uriWithId("uifolder")) - .add(UIProvider.FolderColumns.NAME, "displayName") - .add(UIProvider.FolderColumns.HAS_CHILDREN, - MailboxColumns.FLAGS + "&" + Mailbox.FLAG_HAS_CHILDREN) - .add(UIProvider.FolderColumns.CAPABILITIES, FOLDER_CAPABILITIES) - .add(UIProvider.FolderColumns.SYNC_WINDOW, "3") - .add(UIProvider.FolderColumns.CONVERSATION_LIST_URI, uriWithId("uimessages")) - .add(UIProvider.FolderColumns.CHILD_FOLDERS_LIST_URI, uriWithId("uisubfolders")) - .add(UIProvider.FolderColumns.UNREAD_COUNT, MailboxColumns.UNREAD_COUNT) - .add(UIProvider.FolderColumns.TOTAL_COUNT, MailboxColumns.MESSAGE_COUNT) - .add(UIProvider.FolderColumns.REFRESH_URI, uriWithId("uirefresh")) - .add(UIProvider.FolderColumns.SYNC_STATUS, MailboxColumns.UI_SYNC_STATUS) - .add(UIProvider.FolderColumns.LAST_SYNC_RESULT, MailboxColumns.UI_LAST_SYNC_RESULT) - .add(UIProvider.FolderColumns.TYPE, FOLDER_TYPE) - .add(UIProvider.FolderColumns.ICON_RES_ID, FOLDER_ICON) - .add(UIProvider.FolderColumns.HIERARCHICAL_DESC, MailboxColumns.HIERARCHICAL_NAME) - .build(); + private ProjectionMap getFolderListMap() { + if (sFolderListMap == null) { + sFolderListMap = ProjectionMap.builder() + .add(BaseColumns._ID, MailboxColumns.ID) + .add(UIProvider.FolderColumns.URI, uriWithId("uifolder")) + .add(UIProvider.FolderColumns.NAME, "displayName") + .add(UIProvider.FolderColumns.HAS_CHILDREN, + MailboxColumns.FLAGS + "&" + Mailbox.FLAG_HAS_CHILDREN) + .add(UIProvider.FolderColumns.CAPABILITIES, FOLDER_CAPABILITIES) + .add(UIProvider.FolderColumns.SYNC_WINDOW, "3") + .add(UIProvider.FolderColumns.CONVERSATION_LIST_URI, uriWithId("uimessages")) + .add(UIProvider.FolderColumns.CHILD_FOLDERS_LIST_URI, uriWithId("uisubfolders")) + .add(UIProvider.FolderColumns.UNREAD_COUNT, MailboxColumns.UNREAD_COUNT) + .add(UIProvider.FolderColumns.TOTAL_COUNT, MailboxColumns.MESSAGE_COUNT) + .add(UIProvider.FolderColumns.REFRESH_URI, uriWithId("uirefresh")) + .add(UIProvider.FolderColumns.SYNC_STATUS, MailboxColumns.UI_SYNC_STATUS) + .add(UIProvider.FolderColumns.LAST_SYNC_RESULT, MailboxColumns.UI_LAST_SYNC_RESULT) + .add(UIProvider.FolderColumns.TYPE, FOLDER_TYPE) + .add(UIProvider.FolderColumns.ICON_RES_ID, FOLDER_ICON) + .add(UIProvider.FolderColumns.HIERARCHICAL_DESC, MailboxColumns.HIERARCHICAL_NAME) + .build(); + } + return sFolderListMap; + } + private static ProjectionMap sFolderListMap; - private static final ProjectionMap sAccountListMap = ProjectionMap.builder() - .add(BaseColumns._ID, AccountColumns.ID) - .add(UIProvider.AccountColumns.FOLDER_LIST_URI, uriWithId("uifolders")) - .add(UIProvider.AccountColumns.FULL_FOLDER_LIST_URI, uriWithId("uiallfolders")) - .add(UIProvider.AccountColumns.NAME, AccountColumns.DISPLAY_NAME) - .add(UIProvider.AccountColumns.SAVE_DRAFT_URI, uriWithId("uisavedraft")) - .add(UIProvider.AccountColumns.SEND_MAIL_URI, uriWithId("uisendmail")) - .add(UIProvider.AccountColumns.UNDO_URI, - ("'content://" + UIProvider.AUTHORITY + "/uiundo'")) - .add(UIProvider.AccountColumns.URI, uriWithId("uiaccount")) - .add(UIProvider.AccountColumns.SEARCH_URI, uriWithId("uisearch")) - // TODO: Is provider version used? - .add(UIProvider.AccountColumns.PROVIDER_VERSION, "1") - .add(UIProvider.AccountColumns.SYNC_STATUS, "0") - .add(UIProvider.AccountColumns.RECENT_FOLDER_LIST_URI, uriWithId("uirecentfolders")) - .add(UIProvider.AccountColumns.DEFAULT_RECENT_FOLDER_LIST_URI, - uriWithId("uidefaultrecentfolders")) - .add(UIProvider.AccountColumns.SettingsColumns.SIGNATURE, AccountColumns.SIGNATURE) - .add(UIProvider.AccountColumns.SettingsColumns.SNAP_HEADERS, - Integer.toString(UIProvider.SnapHeaderValue.ALWAYS)) - .add(UIProvider.AccountColumns.SettingsColumns.REPLY_BEHAVIOR, - Integer.toString(UIProvider.DefaultReplyBehavior.REPLY)) - .add(UIProvider.AccountColumns.SettingsColumns.CONFIRM_ARCHIVE, "0") - .build(); + private ProjectionMap getAccountListMap() { + if (sAccountListMap == null) { + sAccountListMap = ProjectionMap.builder() + .add(BaseColumns._ID, AccountColumns.ID) + .add(UIProvider.AccountColumns.FOLDER_LIST_URI, uriWithId("uifolders")) + .add(UIProvider.AccountColumns.FULL_FOLDER_LIST_URI, uriWithId("uiallfolders")) + .add(UIProvider.AccountColumns.NAME, AccountColumns.DISPLAY_NAME) + .add(UIProvider.AccountColumns.SAVE_DRAFT_URI, uriWithId("uisavedraft")) + .add(UIProvider.AccountColumns.SEND_MAIL_URI, uriWithId("uisendmail")) + .add(UIProvider.AccountColumns.UNDO_URI, + ("'content://" + UIProvider.AUTHORITY + "/uiundo'")) + .add(UIProvider.AccountColumns.URI, uriWithId("uiaccount")) + .add(UIProvider.AccountColumns.SEARCH_URI, uriWithId("uisearch")) + // TODO: Is provider version used? + .add(UIProvider.AccountColumns.PROVIDER_VERSION, "1") + .add(UIProvider.AccountColumns.SYNC_STATUS, "0") + .add(UIProvider.AccountColumns.RECENT_FOLDER_LIST_URI, uriWithId("uirecentfolders")) + .add(UIProvider.AccountColumns.DEFAULT_RECENT_FOLDER_LIST_URI, + uriWithId("uidefaultrecentfolders")) + .add(UIProvider.AccountColumns.SettingsColumns.SIGNATURE, AccountColumns.SIGNATURE) + .add(UIProvider.AccountColumns.SettingsColumns.SNAP_HEADERS, + Integer.toString(UIProvider.SnapHeaderValue.ALWAYS)) + .add(UIProvider.AccountColumns.SettingsColumns.REPLY_BEHAVIOR, + Integer.toString(UIProvider.DefaultReplyBehavior.REPLY)) + .add(UIProvider.AccountColumns.SettingsColumns.CONFIRM_ARCHIVE, "0") + .build(); + } + return sAccountListMap; + } + private static ProjectionMap sAccountListMap; /** * The "ORDER BY" clause for top level folders @@ -2298,16 +2333,23 @@ outer: /** * Mapping of UIProvider columns to EmailProvider columns for a message's attachments */ - private static final ProjectionMap sAttachmentMap = ProjectionMap.builder() - .add(UIProvider.AttachmentColumns.NAME, AttachmentColumns.FILENAME) - .add(UIProvider.AttachmentColumns.SIZE, AttachmentColumns.SIZE) - .add(UIProvider.AttachmentColumns.URI, uriWithId("uiattachment")) - .add(UIProvider.AttachmentColumns.CONTENT_TYPE, AttachmentColumns.MIME_TYPE) - .add(UIProvider.AttachmentColumns.STATE, AttachmentColumns.UI_STATE) - .add(UIProvider.AttachmentColumns.DESTINATION, AttachmentColumns.UI_DESTINATION) - .add(UIProvider.AttachmentColumns.DOWNLOADED_SIZE, AttachmentColumns.UI_DOWNLOADED_SIZE) - .add(UIProvider.AttachmentColumns.CONTENT_URI, AttachmentColumns.CONTENT_URI) - .build(); + private ProjectionMap getAttachmentMap() { + if (sAttachmentMap == null) { + sAttachmentMap = ProjectionMap.builder() + .add(UIProvider.AttachmentColumns.NAME, AttachmentColumns.FILENAME) + .add(UIProvider.AttachmentColumns.SIZE, AttachmentColumns.SIZE) + .add(UIProvider.AttachmentColumns.URI, uriWithId("uiattachment")) + .add(UIProvider.AttachmentColumns.CONTENT_TYPE, AttachmentColumns.MIME_TYPE) + .add(UIProvider.AttachmentColumns.STATE, AttachmentColumns.UI_STATE) + .add(UIProvider.AttachmentColumns.DESTINATION, AttachmentColumns.UI_DESTINATION) + .add(UIProvider.AttachmentColumns.DOWNLOADED_SIZE, + AttachmentColumns.UI_DOWNLOADED_SIZE) + .add(UIProvider.AttachmentColumns.CONTENT_URI, AttachmentColumns.CONTENT_URI) + .build(); + } + return sAttachmentMap; + } + private static ProjectionMap sAttachmentMap; /** * Generate the SELECT clause using a specified mapping and the original UI projection @@ -2469,7 +2511,7 @@ outer: "content://ui.email2.android.com/event/" + msg.mId); } } - StringBuilder sb = genSelect(sMessageViewMap, uiProjection, values); + StringBuilder sb = genSelect(getMessageViewMap(), uiProjection, values); sb.append(" FROM " + Message.TABLE_NAME + "," + Body.TABLE_NAME + " WHERE " + Body.MESSAGE_KEY + "=" + Message.TABLE_NAME + "." + Message.RECORD_ID + " AND " + Message.TABLE_NAME + "." + Message.RECORD_ID + "=?"); @@ -2484,7 +2526,7 @@ outer: * @return the SQLite query to be executed on the EmailProvider database */ private String genQueryMailboxMessages(String[] uiProjection) { - StringBuilder sb = genSelect(sMessageListMap, uiProjection); + StringBuilder sb = genSelect(getMessageListMap(), uiProjection); sb.append(" FROM " + Message.TABLE_NAME + " WHERE " + Message.MAILBOX_KEY + "=? ORDER BY " + MessageColumns.TIMESTAMP + " DESC"); return sb.toString(); @@ -2501,7 +2543,7 @@ outer: long mailboxId) { ContentValues values = new ContentValues(); values.put(UIProvider.ConversationColumns.COLOR, CONVERSATION_COLOR); - StringBuilder sb = genSelect(sMessageListMap, uiProjection, values); + StringBuilder sb = genSelect(getMessageListMap(), uiProjection, values); if (isCombinedMailbox(mailboxId)) { switch (getVirtualMailboxType(mailboxId)) { case Mailbox.TYPE_INBOX: @@ -2543,7 +2585,7 @@ outer: * @return the SQLite query to be executed on the EmailProvider database */ private String genQueryConversation(String[] uiProjection) { - StringBuilder sb = genSelect(sMessageListMap, uiProjection); + StringBuilder sb = genSelect(getMessageListMap(), uiProjection); sb.append(" FROM " + Message.TABLE_NAME + " WHERE " + Message.RECORD_ID + "=?"); return sb.toString(); } @@ -2555,7 +2597,7 @@ outer: * @return the SQLite query to be executed on the EmailProvider database */ private String genQueryAccountMailboxes(String[] uiProjection) { - StringBuilder sb = genSelect(sFolderListMap, uiProjection); + StringBuilder sb = genSelect(getFolderListMap(), uiProjection); sb.append(" FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns.ACCOUNT_KEY + "=? AND " + MailboxColumns.TYPE + " < " + Mailbox.TYPE_NOT_EMAIL + " AND " + MailboxColumns.PARENT_KEY + " < 0 ORDER BY "); @@ -2571,7 +2613,7 @@ outer: * @return the SQLite query to be executed on the EmailProvider database */ private String genQueryAccountAllMailboxes(String[] uiProjection) { - StringBuilder sb = genSelect(sFolderListMap, uiProjection); + StringBuilder sb = genSelect(getFolderListMap(), uiProjection); // Use a derived column to choose either hierarchicalName or displayName sb.append(", case when " + MailboxColumns.HIERARCHICAL_NAME + " is null then " + MailboxColumns.DISPLAY_NAME + " else " + MailboxColumns.HIERARCHICAL_NAME + @@ -2590,7 +2632,7 @@ outer: * @return the SQLite query to be executed on the EmailProvider database */ private String genQueryRecentMailboxes(String[] uiProjection) { - StringBuilder sb = genSelect(sFolderListMap, uiProjection); + StringBuilder sb = genSelect(getFolderListMap(), uiProjection); sb.append(" FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns.ACCOUNT_KEY + "=? AND " + MailboxColumns.TYPE + " < " + Mailbox.TYPE_NOT_EMAIL + " AND " + MailboxColumns.PARENT_KEY + " < 0 AND " + @@ -2656,7 +2698,7 @@ outer: getFolderCapabilities(info, mailbox.mFlags, mailbox.mType, mailboxId)); } } - StringBuilder sb = genSelect(sFolderListMap, uiProjection, values); + StringBuilder sb = genSelect(getFolderListMap(), uiProjection, values); sb.append(" FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns.ID + "=?"); return sb.toString(); } @@ -2778,7 +2820,7 @@ outer: values.put(UIProvider.AccountColumns.SettingsColumns.PRIORITY_ARROWS_ENABLED, "0"); } - final StringBuilder sb = genSelect(sAccountListMap, uiProjection, values); + final StringBuilder sb = genSelect(getAccountListMap(), uiProjection, values); sb.append(" FROM " + Account.TABLE_NAME + " WHERE " + AccountColumns.ID + "=?"); return sb.toString(); } @@ -3037,7 +3079,7 @@ outer: */ private String genQueryAttachments(String[] uiProjection, List contentTypeQueryParameters) { - StringBuilder sb = genSelect(sAttachmentMap, uiProjection); + StringBuilder sb = genSelect(getAttachmentMap(), uiProjection); sb.append(" FROM " + Attachment.TABLE_NAME + " WHERE " + AttachmentColumns.MESSAGE_KEY + " =? "); @@ -3069,7 +3111,7 @@ outer: * @return the SQLite query to be executed on the EmailProvider database */ private String genQueryAttachment(String[] uiProjection) { - StringBuilder sb = genSelect(sAttachmentMap, uiProjection); + StringBuilder sb = genSelect(getAttachmentMap(), uiProjection); sb.append(" FROM " + Attachment.TABLE_NAME + " WHERE " + AttachmentColumns.ID + " =? "); return sb.toString(); } @@ -3081,7 +3123,7 @@ outer: * @return the SQLite query to be executed on the EmailProvider database */ private String genQuerySubfolders(String[] uiProjection) { - StringBuilder sb = genSelect(sFolderListMap, uiProjection); + StringBuilder sb = genSelect(getFolderListMap(), uiProjection); sb.append(" FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns.PARENT_KEY + " =? ORDER BY "); sb.append(MAILBOX_ORDER_BY); @@ -3157,7 +3199,7 @@ outer: final String[] idAndType = { BaseColumns._ID, UIProvider.FolderColumns.TYPE }; // Sent, Drafts, and Starred are the default recents. - final StringBuilder sb = genSelect(sFolderListMap, idAndType); + final StringBuilder sb = genSelect(getFolderListMap(), idAndType); sb.append(" FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns.ACCOUNT_KEY + " = " + id + " AND " diff --git a/src/com/android/email/service/EmailBroadcastProcessorService.java b/src/com/android/email/service/EmailBroadcastProcessorService.java index ecf8569af..c7483e29e 100644 --- a/src/com/android/email/service/EmailBroadcastProcessorService.java +++ b/src/com/android/email/service/EmailBroadcastProcessorService.java @@ -203,7 +203,7 @@ public class EmailBroadcastProcessorService extends IntentService { private void onSystemAccountChanged() { Log.i(Logging.LOG_TAG, "System accounts updated."); - MailService.reconcilePopImapAccountsSync(this); + MailService.reconcilePopAccountsSync(this); // Start any remote services EmailServiceUtils.startRemoteServices(this); diff --git a/src/com/android/email/service/EmailServiceStub.java b/src/com/android/email/service/EmailServiceStub.java index fd7a217b0..04d7ce964 100644 --- a/src/com/android/email/service/EmailServiceStub.java +++ b/src/com/android/email/service/EmailServiceStub.java @@ -28,11 +28,11 @@ import android.text.TextUtils; import android.util.Log; import com.android.email.NotificationController; +import com.android.email.R; import com.android.email.mail.Sender; import com.android.email.mail.Store; import com.android.email.provider.Utilities; import com.android.email2.ui.MailActivityEmail; -import com.android.emailcommon.AccountManagerTypes; import com.android.emailcommon.Api; import com.android.emailcommon.Logging; import com.android.emailcommon.TrafficFlags; @@ -108,7 +108,7 @@ public abstract class EmailServiceStub extends IEmailService.Stub implements IEm Account account = Account.restoreAccountWithId(mContext, mailbox.mAccountKey); if (account == null) return; android.accounts.Account acct = new android.accounts.Account(account.mEmailAddress, - AccountManagerTypes.TYPE_POP_IMAP); + mContext.getString(R.string.account_manager_type_pop3)); Bundle extras = new Bundle(); extras.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true); extras.putLong(SYNC_EXTRA_MAILBOX_ID, mailboxId); @@ -450,7 +450,7 @@ public abstract class EmailServiceStub extends IEmailService.Stub implements IEm @Override public void deleteAccountPIMData(long accountId) throws RemoteException { - MailService.reconcilePopImapAccountsSync(mContext); + MailService.reconcilePopAccountsSync(mContext); } @Override diff --git a/src/com/android/email/service/EmailServiceUtils.java b/src/com/android/email/service/EmailServiceUtils.java index ec0850db2..53e3395d2 100644 --- a/src/com/android/email/service/EmailServiceUtils.java +++ b/src/com/android/email/service/EmailServiceUtils.java @@ -21,21 +21,29 @@ import android.accounts.AccountManagerFuture; import android.accounts.AuthenticatorException; import android.accounts.OperationCanceledException; import android.app.Service; +import android.content.ComponentName; +import android.content.ContentProviderClient; import android.content.ContentResolver; import android.content.ContentUris; import android.content.ContentValues; import android.content.Context; import android.content.Intent; +import android.content.pm.PackageManager; import android.content.res.Resources; import android.content.res.TypedArray; import android.content.res.XmlResourceParser; import android.database.Cursor; import android.net.Uri; +import android.os.AsyncTask; import android.os.Bundle; +import android.os.Debug; import android.os.IBinder; import android.os.RemoteException; import android.provider.CalendarContract; +import android.provider.CalendarContract.Calendars; +import android.provider.CalendarContract.SyncState; import android.provider.ContactsContract; +import android.provider.SyncStateContract; import android.util.Log; import com.android.email.R; @@ -43,8 +51,8 @@ import com.android.emailcommon.Api; import com.android.emailcommon.Logging; import com.android.emailcommon.provider.Account; import com.android.emailcommon.provider.EmailContent; -import com.android.emailcommon.provider.HostAuth; import com.android.emailcommon.provider.EmailContent.AccountColumns; +import com.android.emailcommon.provider.HostAuth; import com.android.emailcommon.service.EmailServiceProxy; import com.android.emailcommon.service.IEmailService; import com.android.emailcommon.service.IEmailServiceCallback; @@ -167,6 +175,7 @@ public class EmailServiceUtils { public CharSequence[] syncIntervals; public int defaultSyncInterval; public String inferPrefix; + public boolean requiresAccountUpdate; public String toString() { StringBuilder sb = new StringBuilder("Protocol: "); @@ -187,7 +196,14 @@ public class EmailServiceUtils { if (info == null) { Log.w(Logging.LOG_TAG, "Returning NullService for " + protocol); return new EmailServiceProxy(context, NullService.class, null); - } else if (info.klass != null) { + } else { + return getServiceFromInfo(context, callback, info); + } + } + + public static EmailServiceProxy getServiceFromInfo(Context context, + IEmailServiceCallback callback, EmailServiceInfo info) { + if (info.klass != null) { return new EmailServiceProxy(context, info.klass, callback); } else { return new EmailServiceProxy(context, info.intentAction, callback); @@ -229,30 +245,47 @@ public class EmailServiceUtils { } } - /** - * "Change" the account manager type of the account; this entails deleting the account - * and adding a new one. We can't call into AccountManager on the UI thread, but we might - * well be on it (currently no clean way of guaranteeing that we're not). - * - * @param context the caller's context - * @param amAccount the AccountManager account we're changing - * @param newType the new AccountManager type for this account - * @param newProtocol the protocol now being used - */ - private static void updateAccountManagerType(final Context context, - final android.accounts.Account amAccount, final String newType, - final String newProtocol) { - // STOPSHIP There must be a better way - Thread amThread = new Thread(new Runnable() { - @Override - public void run() { - updateAccountManagerTypeImpl(context, amAccount, newType, newProtocol); - }}); - amThread.start(); + private static class UpdateAccountManagerTask extends AsyncTask { + private final Context mContext; + private final android.accounts.Account mAccount; + private final EmailServiceInfo mOldInfo; + private final EmailServiceInfo mNewInfo; + + public UpdateAccountManagerTask(Context context, android.accounts.Account amAccount, + EmailServiceInfo oldInfo, EmailServiceInfo newInfo) { + super(); + mContext = context; + mAccount = amAccount; + mOldInfo = oldInfo; + mNewInfo = newInfo; + } + + @Override + protected Void doInBackground(Void... params) { + updateAccountManagerType(mContext, mAccount, mOldInfo, mNewInfo); + return null; + } } - private static void updateAccountManagerTypeImpl(Context context, - android.accounts.Account amAccount, String newType, String newProtocol) { + private static class DisableComponentsTask extends AsyncTask { + private final Context mContext; + + public DisableComponentsTask(Context context) { + super(); + mContext = context; + } + + @Override + protected Void doInBackground(Void... params) { + disableComponent(mContext, LegacyEmailAuthenticatorService.class); + disableComponent(mContext, LegacyEasAuthenticatorService.class); + return null; + } + } + + private static void updateAccountManagerType(Context context, + android.accounts.Account amAccount, EmailServiceInfo oldInfo, + EmailServiceInfo newInfo) { ContentResolver resolver = context.getContentResolver(); Cursor c = resolver.query(Account.CONTENT_URI, Account.CONTENT_PROJECTION, AccountColumns.EMAIL_ADDRESS + "=?", new String[] { amAccount.name }, null); @@ -260,7 +293,6 @@ public class EmailServiceUtils { if (c == null) return; try { if (c.moveToNext()) { - Log.w(Logging.LOG_TAG, "Converting " + amAccount.name + " to " + newProtocol); // Get the EmailProvider Account/HostAuth Account account = new Account(); account.restore(c); @@ -268,6 +300,14 @@ public class EmailServiceUtils { HostAuth.restoreHostAuthWithId(context, account.mHostAuthKeyRecv); if (hostAuth == null) return; + // Make sure this email address is using the expected protocol; our query to + // AccountManager doesn't know which protocol was being used (com.android.email + // was used for both pop3 and imap + if (!hostAuth.mProtocol.equals(oldInfo.protocol)) { + return; + } + Log.w(Logging.LOG_TAG, "Converting " + amAccount.name + " to " + newInfo.protocol); + ContentValues accountValues = new ContentValues(); int oldFlags = account.mFlags; @@ -280,33 +320,122 @@ public class EmailServiceUtils { // Change the HostAuth to reference the new protocol; this has to be done before // trying to create the AccountManager account (below) ContentValues hostValues = new ContentValues(); - hostValues.put(HostAuth.PROTOCOL, newProtocol); + hostValues.put(HostAuth.PROTOCOL, newInfo.protocol); resolver.update(ContentUris.withAppendedId(HostAuth.CONTENT_URI, hostAuth.mId), hostValues, null, null); + Log.w(Logging.LOG_TAG, "Updated HostAuths"); try { // Get current settings for the existing AccountManager account boolean email = ContentResolver.getSyncAutomatically(amAccount, EmailContent.AUTHORITY); + if (!email) { + // Try our old provider name + email = ContentResolver.getSyncAutomatically(amAccount, + "com.android.email.provider"); + } boolean contacts = ContentResolver.getSyncAutomatically(amAccount, ContactsContract.AUTHORITY); boolean calendar = ContentResolver.getSyncAutomatically(amAccount, CalendarContract.AUTHORITY); + Log.w(Logging.LOG_TAG, "Email: " + email + ", Contacts: " + contacts + "," + + " Calendar: " + calendar); - // Delete the AccountManager account - AccountManagerFuture amFuture = AccountManager.get(context) - .removeAccount(amAccount, null, null); - finishAccountManagerBlocker(amFuture); + // Get sync keys for calendar/contacts + String amName = amAccount.name; + String oldType = amAccount.type; + ContentProviderClient client = context.getContentResolver() + .acquireContentProviderClient(CalendarContract.CONTENT_URI); + byte[] calendarSyncKey = null; + try { + calendarSyncKey = SyncStateContract.Helpers.get(client, + asCalendarSyncAdapter(SyncState.CONTENT_URI, amName, oldType), + new android.accounts.Account(amName, oldType)); + } catch (RemoteException e) { + Log.w(Logging.LOG_TAG, "Get calendar key FAILED"); + } finally { + client.release(); + } + client = context.getContentResolver() + .acquireContentProviderClient(ContactsContract.AUTHORITY_URI); + byte[] contactsSyncKey = null; + try { + contactsSyncKey = SyncStateContract.Helpers.get(client, + ContactsContract.SyncState.CONTENT_URI, + new android.accounts.Account(amName, oldType)); + } catch (RemoteException e) { + Log.w(Logging.LOG_TAG, "Get contacts key FAILED"); + } finally { + client.release(); + } + if (calendarSyncKey != null) { + Log.w(Logging.LOG_TAG, "Got calendar key: " + new String(calendarSyncKey)); + } + if (contactsSyncKey != null) { + Log.w(Logging.LOG_TAG, "Got contacts key: " + new String(contactsSyncKey)); + } // Set up a new AccountManager account with new type and old settings - amFuture = MailService.setupAccountManagerAccount(context, account, email, - calendar, contacts, null); + AccountManagerFuture amFuture = MailService.setupAccountManagerAccount( + context, account, email, calendar, contacts, null); finishAccountManagerBlocker(amFuture); - Log.w(Logging.LOG_TAG, "Conversion complete!"); + Log.w(Logging.LOG_TAG, "Created new AccountManager account"); + + // Delete the AccountManager account + amFuture = AccountManager.get(context) + .removeAccount(amAccount, null, null); + finishAccountManagerBlocker(amFuture); + Log.w(Logging.LOG_TAG, "Deleted old AccountManager account"); + + // Restore sync keys for contacts/calendar + if (calendarSyncKey != null && calendarSyncKey.length != 0) { + client = context.getContentResolver() + .acquireContentProviderClient(CalendarContract.CONTENT_URI); + try { + SyncStateContract.Helpers.set(client, + asCalendarSyncAdapter(SyncState.CONTENT_URI, amName, + newInfo.accountType), + new android.accounts.Account(amName, newInfo.accountType), + calendarSyncKey); + Log.w(Logging.LOG_TAG, "Set calendar key..."); + } catch (RemoteException e) { + Log.w(Logging.LOG_TAG, "Set calendar key FAILED"); + } finally { + client.release(); + } + } + if (contactsSyncKey != null && contactsSyncKey.length != 0) { + client = context.getContentResolver() + .acquireContentProviderClient(ContactsContract.AUTHORITY_URI); + try { + SyncStateContract.Helpers.set(client, + ContactsContract.SyncState.CONTENT_URI, + new android.accounts.Account(amName, newInfo.accountType), + contactsSyncKey); + Log.w(Logging.LOG_TAG, "Set contacts key..."); + } catch (RemoteException e) { + Log.w(Logging.LOG_TAG, "Set contacts key FAILED"); + } + } + + if (oldInfo.requiresAccountUpdate) { + EmailServiceProxy service = + EmailServiceUtils.getServiceFromInfo(context, null, newInfo); + try { + service.serviceUpdated(amAccount.name); + Log.w(Logging.LOG_TAG, "Updated account settings"); + } catch (RemoteException e) { + // Old settings won't hurt anyone + } + } + + // That's all folks! + Log.w(Logging.LOG_TAG, "Account update completed."); } finally { // Clear the incomplete flag on the provider account accountValues.put(AccountColumns.FLAGS, oldFlags); resolver.update(accountUri, accountValues, null, null); + Log.w(Logging.LOG_TAG, "[Incomplete flag cleared]"); } } } finally { @@ -314,6 +443,14 @@ public class EmailServiceUtils { } } + private static void disableComponent(Context context, Class klass) { + Log.w(Logging.LOG_TAG, "Disabling legacy authenticator " + klass.getSimpleName()); + final ComponentName c = new ComponentName(context, klass); + context.getPackageManager().setComponentEnabledSetting(c, + PackageManager.COMPONENT_ENABLED_STATE_DISABLED, + PackageManager.DONT_KILL_APP); + } + /** * Parse services.xml file to find our available email services */ @@ -339,12 +476,14 @@ public class EmailServiceUtils { throw new IllegalStateException( "Replacement service not found: " + newProtocol); } + info.requiresAccountUpdate = ta.getBoolean( + R.styleable.EmailServiceInfo_requiresAccountUpdate, false); AccountManager am = AccountManager.get(context); android.accounts.Account[] amAccounts = am.getAccountsByType(info.accountType); for (android.accounts.Account amAccount: amAccounts) { - updateAccountManagerType(context, amAccount, newInfo.accountType, - newProtocol); + new UpdateAccountManagerTask(context, amAccount, info, newInfo) + .executeOnExecutor(AsyncTask.SERIAL_EXECUTOR); } continue; } @@ -407,6 +546,8 @@ public class EmailServiceUtils { sServiceList.add(info); } } + // Disable our legacy components + new DisableComponentsTask(context).executeOnExecutor(AsyncTask.SERIAL_EXECUTOR); } catch (XmlPullParserException e) { // ignore } catch (IOException e) { @@ -414,6 +555,12 @@ public class EmailServiceUtils { } } + private static Uri asCalendarSyncAdapter(Uri uri, String account, String accountType) { + return uri.buildUpon().appendQueryParameter(CalendarContract.CALLER_IS_SYNCADAPTER, "true") + .appendQueryParameter(Calendars.ACCOUNT_NAME, account) + .appendQueryParameter(Calendars.ACCOUNT_TYPE, accountType).build(); + } + /** * A no-op service that can be returned for non-existent/null protocols */ @@ -504,6 +651,10 @@ public class EmailServiceUtils { public void sendMail(long accountId) throws RemoteException { } + @Override + public void serviceUpdated(String emailAddress) throws RemoteException { + } + @Override public int getCapabilities(Account acct) throws RemoteException { return 0; diff --git a/src/com/android/email/service/Imap2AuthenticatorService.java b/src/com/android/email/service/ImapAuthenticatorService.java similarity index 91% rename from src/com/android/email/service/Imap2AuthenticatorService.java rename to src/com/android/email/service/ImapAuthenticatorService.java index f19104920..975583da3 100644 --- a/src/com/android/email/service/Imap2AuthenticatorService.java +++ b/src/com/android/email/service/ImapAuthenticatorService.java @@ -19,5 +19,5 @@ package com.android.email.service; /** * This service needs to be declared separately from the base service */ -public class Imap2AuthenticatorService extends AuthenticatorService { +public class ImapAuthenticatorService extends AuthenticatorService { } diff --git a/src/com/android/email/service/ImapService.java b/src/com/android/email/service/ImapService.java deleted file mode 100644 index a08936f69..000000000 --- a/src/com/android/email/service/ImapService.java +++ /dev/null @@ -1,1291 +0,0 @@ -/* - * Copyright (C) 2012 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.email.service; - -import android.app.Service; -import android.content.ContentResolver; -import android.content.ContentUris; -import android.content.ContentValues; -import android.content.Context; -import android.content.Intent; -import android.database.Cursor; -import android.net.TrafficStats; -import android.net.Uri; -import android.os.IBinder; -import android.os.RemoteCallbackList; -import android.os.RemoteException; -import android.text.TextUtils; -import android.util.Log; - -import com.android.email.LegacyConversions; -import com.android.email.NotificationController; -import com.android.email.mail.Store; -import com.android.email.provider.Utilities; -import com.android.email2.ui.MailActivityEmail; -import com.android.emailcommon.Logging; -import com.android.emailcommon.TrafficFlags; -import com.android.emailcommon.internet.MimeUtility; -import com.android.emailcommon.mail.AuthenticationFailedException; -import com.android.emailcommon.mail.FetchProfile; -import com.android.emailcommon.mail.Flag; -import com.android.emailcommon.mail.Folder; -import com.android.emailcommon.mail.Folder.FolderType; -import com.android.emailcommon.mail.Folder.MessageRetrievalListener; -import com.android.emailcommon.mail.Folder.MessageUpdateCallbacks; -import com.android.emailcommon.mail.Folder.OpenMode; -import com.android.emailcommon.mail.Message; -import com.android.emailcommon.mail.MessagingException; -import com.android.emailcommon.mail.Part; -import com.android.emailcommon.provider.Account; -import com.android.emailcommon.provider.EmailContent; -import com.android.emailcommon.provider.EmailContent.MailboxColumns; -import com.android.emailcommon.provider.EmailContent.MessageColumns; -import com.android.emailcommon.provider.EmailContent.SyncColumns; -import com.android.emailcommon.provider.Mailbox; -import com.android.emailcommon.service.EmailServiceCallback; -import com.android.emailcommon.service.EmailServiceStatus; -import com.android.emailcommon.service.IEmailServiceCallback; -import com.android.emailcommon.service.SearchParams; -import com.android.emailcommon.utility.AttachmentUtilities; -import com.android.mail.providers.UIProvider.AccountCapabilities; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Comparator; -import java.util.HashMap; -import java.util.HashSet; - -public class ImapService extends Service { - private static final String TAG = "ImapService"; - private static final int MAX_SMALL_MESSAGE_SIZE = (25 * 1024); - - private static final Flag[] FLAG_LIST_SEEN = new Flag[] { Flag.SEEN }; - private static final Flag[] FLAG_LIST_FLAGGED = new Flag[] { Flag.FLAGGED }; - private static final Flag[] FLAG_LIST_ANSWERED = new Flag[] { Flag.ANSWERED }; - - /** - * Simple cache for last search result mailbox by account and serverId, since the most common - * case will be repeated use of the same mailbox - */ - private static long mLastSearchAccountKey = Account.NO_ACCOUNT; - private static String mLastSearchServerId = null; - private static Mailbox mLastSearchRemoteMailbox = null; - - /** - * Cache search results by account; this allows for "load more" support without having to - * redo the search (which can be quite slow). SortableMessage is a smallish class, so memory - * shouldn't be an issue - */ - private static final HashMap sSearchResults = - new HashMap(); - - /** - * We write this into the serverId field of messages that will never be upsynced. - */ - private static final String LOCAL_SERVERID_PREFIX = "Local-"; - - @Override - public int onStartCommand(Intent intent, int flags, int startId) { - return Service.START_STICKY; - } - - // Callbacks as set up via setCallback - private static final RemoteCallbackList mCallbackList = - new RemoteCallbackList(); - - private static final EmailServiceCallback sCallbackProxy = - new EmailServiceCallback(mCallbackList); - - /** - * Create our EmailService implementation here. - */ - private final EmailServiceStub mBinder = new EmailServiceStub() { - - @Override - public void setCallback(IEmailServiceCallback cb) throws RemoteException { - mCallbackList.register(cb); - } - - @Override - public void loadMore(long messageId) throws RemoteException { - // We don't do "loadMore" for IMAP messages; the sync should handle this - } - - @Override - public int searchMessages(long accountId, SearchParams searchParams, long destMailboxId) { - try { - return searchMailboxImpl(getApplicationContext(), accountId, searchParams, - destMailboxId); - } catch (MessagingException e) { - } - return 0; - } - - @Override - public int getCapabilities(Account acct) throws RemoteException { - return AccountCapabilities.SYNCABLE_FOLDERS | - AccountCapabilities.FOLDER_SERVER_SEARCH | - AccountCapabilities.UNDO; - } - }; - - @Override - public IBinder onBind(Intent intent) { - mBinder.init(this, sCallbackProxy); - return mBinder; - } - - private static void sendMailboxStatus(Mailbox mailbox, int status) { - sCallbackProxy.syncMailboxStatus(mailbox.mId, status, 0); - } - - /** - * Start foreground synchronization of the specified folder. This is called by - * synchronizeMailbox or checkMail. - * TODO this should use ID's instead of fully-restored objects - * @param account - * @param folder - * @throws MessagingException - */ - public static void synchronizeMailboxSynchronous(Context context, final Account account, - final Mailbox folder) throws MessagingException { - sendMailboxStatus(folder, EmailServiceStatus.IN_PROGRESS); - - TrafficStats.setThreadStatsTag(TrafficFlags.getSyncFlags(context, account)); - if ((folder.mFlags & Mailbox.FLAG_HOLDS_MAIL) == 0) { - sendMailboxStatus(folder, EmailServiceStatus.SUCCESS); - } - NotificationController nc = NotificationController.getInstance(context); - try { - processPendingActionsSynchronous(context, account); - synchronizeMailboxGeneric(context, account, folder); - // Clear authentication notification for this account - nc.cancelLoginFailedNotification(account.mId); - sendMailboxStatus(folder, EmailServiceStatus.SUCCESS); - } catch (MessagingException e) { - if (Logging.LOGD) { - Log.v(Logging.LOG_TAG, "synchronizeMailbox", e); - } - if (e instanceof AuthenticationFailedException) { - // Generate authentication notification - nc.showLoginFailedNotification(account.mId); - } - sendMailboxStatus(folder, e.getExceptionType()); - throw e; - } - } - - /** - * Lightweight record for the first pass of message sync, where I'm just seeing if - * the local message requires sync. Later (for messages that need syncing) we'll do a full - * readout from the DB. - */ - private static class LocalMessageInfo { - private static final int COLUMN_ID = 0; - private static final int COLUMN_FLAG_READ = 1; - private static final int COLUMN_FLAG_FAVORITE = 2; - private static final int COLUMN_FLAG_LOADED = 3; - private static final int COLUMN_SERVER_ID = 4; - private static final int COLUMN_FLAGS = 7; - private static final String[] PROJECTION = new String[] { - EmailContent.RECORD_ID, - MessageColumns.FLAG_READ, MessageColumns.FLAG_FAVORITE, MessageColumns.FLAG_LOADED, - SyncColumns.SERVER_ID, MessageColumns.MAILBOX_KEY, MessageColumns.ACCOUNT_KEY, - MessageColumns.FLAGS - }; - - final long mId; - final boolean mFlagRead; - final boolean mFlagFavorite; - final int mFlagLoaded; - final String mServerId; - final int mFlags; - - public LocalMessageInfo(Cursor c) { - mId = c.getLong(COLUMN_ID); - mFlagRead = c.getInt(COLUMN_FLAG_READ) != 0; - mFlagFavorite = c.getInt(COLUMN_FLAG_FAVORITE) != 0; - mFlagLoaded = c.getInt(COLUMN_FLAG_LOADED); - mServerId = c.getString(COLUMN_SERVER_ID); - mFlags = c.getInt(COLUMN_FLAGS); - // Note: mailbox key and account key not needed - they are projected for the SELECT - } - } - - /** - * Load the structure and body of messages not yet synced - * @param account the account we're syncing - * @param remoteFolder the (open) Folder we're working on - * @param unsyncedMessages an array of Message's we've got headers for - * @param toMailbox the destination mailbox we're syncing - * @throws MessagingException - */ - static void loadUnsyncedMessages(final Context context, final Account account, - Folder remoteFolder, ArrayList messages, final Mailbox toMailbox) - throws MessagingException { - - FetchProfile fp = new FetchProfile(); - fp.add(FetchProfile.Item.STRUCTURE); - remoteFolder.fetch(messages.toArray(new Message[messages.size()]), fp, null); - for (Message message : messages) { - // Build a list of parts we are interested in. Text parts will be downloaded - // right now, attachments will be left for later. - ArrayList viewables = new ArrayList(); - ArrayList attachments = new ArrayList(); - MimeUtility.collectParts(message, viewables, attachments); - // Download the viewables immediately - for (Part part : viewables) { - fp.clear(); - fp.add(part); - remoteFolder.fetch(new Message[] { message }, fp, null); - } - // Store the updated message locally and mark it fully loaded - Utilities.copyOneMessageToProvider(context, message, account, toMailbox, - EmailContent.Message.FLAG_LOADED_COMPLETE); - } - } - - public static void downloadFlagAndEnvelope(final Context context, final Account account, - final Mailbox mailbox, Folder remoteFolder, ArrayList unsyncedMessages, - HashMap localMessageMap, final ArrayList unseenMessages) - throws MessagingException { - FetchProfile fp = new FetchProfile(); - fp.add(FetchProfile.Item.FLAGS); - fp.add(FetchProfile.Item.ENVELOPE); - - final HashMap localMapCopy; - if (localMessageMap != null) - localMapCopy = new HashMap(localMessageMap); - else { - localMapCopy = new HashMap(); - } - - remoteFolder.fetch(unsyncedMessages.toArray(new Message[0]), fp, - new MessageRetrievalListener() { - @Override - public void messageRetrieved(Message message) { - try { - // Determine if the new message was already known (e.g. partial) - // And create or reload the full message info - LocalMessageInfo localMessageInfo = - localMapCopy.get(message.getUid()); - EmailContent.Message localMessage = null; - if (localMessageInfo == null) { - localMessage = new EmailContent.Message(); - } else { - localMessage = EmailContent.Message.restoreMessageWithId( - context, localMessageInfo.mId); - } - - if (localMessage != null) { - try { - // Copy the fields that are available into the message - LegacyConversions.updateMessageFields(localMessage, - message, account.mId, mailbox.mId); - // Commit the message to the local store - Utilities.saveOrUpdate(localMessage, context); - // Track the "new" ness of the downloaded message - if (!message.isSet(Flag.SEEN) && unseenMessages != null) { - unseenMessages.add(localMessage.mId); - } - } catch (MessagingException me) { - Log.e(Logging.LOG_TAG, - "Error while copying downloaded message." + me); - } - - } - } - catch (Exception e) { - Log.e(Logging.LOG_TAG, - "Error while storing downloaded message." + e.toString()); - } - } - - @Override - public void loadAttachmentProgress(int progress) { - } - }); - - } - - /** - * Synchronizer for IMAP. - * - * TODO Break this method up into smaller chunks. - * - * @param account the account to sync - * @param mailbox the mailbox to sync - * @return results of the sync pass - * @throws MessagingException - */ - private static void synchronizeMailboxGeneric(final Context context, - final Account account, final Mailbox mailbox) throws MessagingException { - - /* - * A list of IDs for messages that were downloaded and did not have the seen flag set. - * This serves as the "true" new message count reported to the user via notification. - */ - final ArrayList unseenMessages = new ArrayList(); - - ContentResolver resolver = context.getContentResolver(); - - // 0. We do not ever sync DRAFTS or OUTBOX (down or up) - if (mailbox.mType == Mailbox.TYPE_DRAFTS || mailbox.mType == Mailbox.TYPE_OUTBOX) { - return; - } - - // 1. Get the message list from the local store and create an index of the uids - - Cursor localUidCursor = null; - HashMap localMessageMap = new HashMap(); - - try { - localUidCursor = resolver.query( - EmailContent.Message.CONTENT_URI, - LocalMessageInfo.PROJECTION, - EmailContent.MessageColumns.ACCOUNT_KEY + "=?" + - " AND " + MessageColumns.MAILBOX_KEY + "=?", - new String[] { - String.valueOf(account.mId), - String.valueOf(mailbox.mId) - }, - null); - while (localUidCursor.moveToNext()) { - LocalMessageInfo info = new LocalMessageInfo(localUidCursor); - localMessageMap.put(info.mServerId, info); - } - } finally { - if (localUidCursor != null) { - localUidCursor.close(); - } - } - - // 2. Open the remote folder and create the remote folder if necessary - - Store remoteStore = Store.getInstance(account, context); - // The account might have been deleted - if (remoteStore == null) return; - Folder remoteFolder = remoteStore.getFolder(mailbox.mServerId); - - /* - * If the folder is a "special" folder we need to see if it exists - * on the remote server. It if does not exist we'll try to create it. If we - * can't create we'll abort. This will happen on every single Pop3 folder as - * designed and on Imap folders during error conditions. This allows us - * to treat Pop3 and Imap the same in this code. - */ - if (mailbox.mType == Mailbox.TYPE_TRASH || mailbox.mType == Mailbox.TYPE_SENT - || mailbox.mType == Mailbox.TYPE_DRAFTS) { - if (!remoteFolder.exists()) { - if (!remoteFolder.create(FolderType.HOLDS_MESSAGES)) { - return; - } - } - } - - // 3, Open the remote folder. This pre-loads certain metadata like message count. - remoteFolder.open(OpenMode.READ_WRITE); - - // 4. Trash any remote messages that are marked as trashed locally. - // TODO - this comment was here, but no code was here. - - // 5. Get the remote message count. - int remoteMessageCount = remoteFolder.getMessageCount(); - ContentValues values = new ContentValues(); - values.put(MailboxColumns.TOTAL_COUNT, remoteMessageCount); - mailbox.update(context, values); - - // 6. Determine the limit # of messages to download - int visibleLimit = mailbox.mVisibleLimit; - if (visibleLimit <= 0) { - visibleLimit = MailActivityEmail.VISIBLE_LIMIT_DEFAULT; - } - - // 7. Create a list of messages to download - Message[] remoteMessages = new Message[0]; - final ArrayList unsyncedMessages = new ArrayList(); - HashMap remoteUidMap = new HashMap(); - - if (remoteMessageCount > 0) { - /* - * Message numbers start at 1. - */ - int remoteStart = Math.max(0, remoteMessageCount - visibleLimit) + 1; - int remoteEnd = remoteMessageCount; - remoteMessages = remoteFolder.getMessages(remoteStart, remoteEnd, null); - // TODO Why are we running through the list twice? Combine w/ for loop below - for (Message message : remoteMessages) { - remoteUidMap.put(message.getUid(), message); - } - - /* - * Get a list of the messages that are in the remote list but not on the - * local store, or messages that are in the local store but failed to download - * on the last sync. These are the new messages that we will download. - * Note, we also skip syncing messages which are flagged as "deleted message" sentinels, - * because they are locally deleted and we don't need or want the old message from - * the server. - */ - for (Message message : remoteMessages) { - LocalMessageInfo localMessage = localMessageMap.get(message.getUid()); - // localMessage == null -> message has never been created (not even headers) - // mFlagLoaded = UNLOADED -> message created, but none of body loaded - // mFlagLoaded = PARTIAL -> message created, a "sane" amt of body has been loaded - // mFlagLoaded = COMPLETE -> message body has been completely loaded - // mFlagLoaded = DELETED -> message has been deleted - // Only the first two of these are "unsynced", so let's retrieve them - if (localMessage == null || - (localMessage.mFlagLoaded == EmailContent.Message.FLAG_LOADED_UNLOADED) || - (localMessage.mFlagLoaded == EmailContent.Message.FLAG_LOADED_PARTIAL)) { - unsyncedMessages.add(message); - } - } - } - - // 8. Download basic info about the new/unloaded messages (if any) - /* - * Fetch the flags and envelope only of the new messages. This is intended to get us - * critical data as fast as possible, and then we'll fill in the details. - */ - if (unsyncedMessages.size() > 0) { - downloadFlagAndEnvelope(context, account, mailbox, remoteFolder, unsyncedMessages, - localMessageMap, unseenMessages); - } - - // 9. Refresh the flags for any messages in the local store that we didn't just download. - FetchProfile fp = new FetchProfile(); - fp.add(FetchProfile.Item.FLAGS); - remoteFolder.fetch(remoteMessages, fp, null); - boolean remoteSupportsSeen = false; - boolean remoteSupportsFlagged = false; - boolean remoteSupportsAnswered = false; - for (Flag flag : remoteFolder.getPermanentFlags()) { - if (flag == Flag.SEEN) { - remoteSupportsSeen = true; - } - if (flag == Flag.FLAGGED) { - remoteSupportsFlagged = true; - } - if (flag == Flag.ANSWERED) { - remoteSupportsAnswered = true; - } - } - // Update SEEN/FLAGGED/ANSWERED (star) flags (if supported remotely - e.g. not for POP3) - if (remoteSupportsSeen || remoteSupportsFlagged || remoteSupportsAnswered) { - for (Message remoteMessage : remoteMessages) { - LocalMessageInfo localMessageInfo = localMessageMap.get(remoteMessage.getUid()); - if (localMessageInfo == null) { - continue; - } - boolean localSeen = localMessageInfo.mFlagRead; - boolean remoteSeen = remoteMessage.isSet(Flag.SEEN); - boolean newSeen = (remoteSupportsSeen && (remoteSeen != localSeen)); - boolean localFlagged = localMessageInfo.mFlagFavorite; - boolean remoteFlagged = remoteMessage.isSet(Flag.FLAGGED); - boolean newFlagged = (remoteSupportsFlagged && (localFlagged != remoteFlagged)); - int localFlags = localMessageInfo.mFlags; - boolean localAnswered = (localFlags & EmailContent.Message.FLAG_REPLIED_TO) != 0; - boolean remoteAnswered = remoteMessage.isSet(Flag.ANSWERED); - boolean newAnswered = (remoteSupportsAnswered && (localAnswered != remoteAnswered)); - if (newSeen || newFlagged || newAnswered) { - Uri uri = ContentUris.withAppendedId( - EmailContent.Message.CONTENT_URI, localMessageInfo.mId); - ContentValues updateValues = new ContentValues(); - updateValues.put(MessageColumns.FLAG_READ, remoteSeen); - updateValues.put(MessageColumns.FLAG_FAVORITE, remoteFlagged); - if (remoteAnswered) { - localFlags |= EmailContent.Message.FLAG_REPLIED_TO; - } else { - localFlags &= ~EmailContent.Message.FLAG_REPLIED_TO; - } - updateValues.put(MessageColumns.FLAGS, localFlags); - resolver.update(uri, updateValues, null, null); - } - } - } - - // 10. Remove any messages that are in the local store but no longer on the remote store. - HashSet localUidsToDelete = new HashSet(localMessageMap.keySet()); - localUidsToDelete.removeAll(remoteUidMap.keySet()); - for (String uidToDelete : localUidsToDelete) { - LocalMessageInfo infoToDelete = localMessageMap.get(uidToDelete); - - // Delete associated data (attachment files) - // Attachment & Body records are auto-deleted when we delete the Message record - AttachmentUtilities.deleteAllAttachmentFiles(context, account.mId, - infoToDelete.mId); - - // Delete the message itself - Uri uriToDelete = ContentUris.withAppendedId( - EmailContent.Message.CONTENT_URI, infoToDelete.mId); - resolver.delete(uriToDelete, null, null); - - // Delete extra rows (e.g. synced or deleted) - Uri syncRowToDelete = ContentUris.withAppendedId( - EmailContent.Message.UPDATED_CONTENT_URI, infoToDelete.mId); - resolver.delete(syncRowToDelete, null, null); - Uri deletERowToDelete = ContentUris.withAppendedId( - EmailContent.Message.UPDATED_CONTENT_URI, infoToDelete.mId); - resolver.delete(deletERowToDelete, null, null); - } - - loadUnsyncedMessages(context, account, remoteFolder, unsyncedMessages, mailbox); - - // 14. Clean up and report results - remoteFolder.close(false); - } - - /** - * Find messages in the updated table that need to be written back to server. - * - * Handles: - * Read/Unread - * Flagged - * Append (upload) - * Move To Trash - * Empty trash - * TODO: - * Move - * - * @param account the account to scan for pending actions - * @throws MessagingException - */ - private static void processPendingActionsSynchronous(Context context, Account account) - throws MessagingException { - TrafficStats.setThreadStatsTag(TrafficFlags.getSyncFlags(context, account)); - String[] accountIdArgs = new String[] { Long.toString(account.mId) }; - - // Handle deletes first, it's always better to get rid of things first - processPendingDeletesSynchronous(context, account, accountIdArgs); - - // Handle uploads (currently, only to sent messages) - processPendingUploadsSynchronous(context, account, accountIdArgs); - - // Now handle updates / upsyncs - processPendingUpdatesSynchronous(context, account, accountIdArgs); - } - - /** - * Get the mailbox corresponding to the remote location of a message; this will normally be - * the mailbox whose _id is mailboxKey, except for search results, where we must look it up - * by serverId - * @param message the message in question - * @return the mailbox in which the message resides on the server - */ - private static Mailbox getRemoteMailboxForMessage(Context context, - EmailContent.Message message) { - // If this is a search result, use the protocolSearchInfo field to get the server info - if (!TextUtils.isEmpty(message.mProtocolSearchInfo)) { - long accountKey = message.mAccountKey; - String protocolSearchInfo = message.mProtocolSearchInfo; - if (accountKey == mLastSearchAccountKey && - protocolSearchInfo.equals(mLastSearchServerId)) { - return mLastSearchRemoteMailbox; - } - Cursor c = context.getContentResolver().query(Mailbox.CONTENT_URI, - Mailbox.CONTENT_PROJECTION, Mailbox.PATH_AND_ACCOUNT_SELECTION, - new String[] {protocolSearchInfo, Long.toString(accountKey)}, - null); - try { - if (c.moveToNext()) { - Mailbox mailbox = new Mailbox(); - mailbox.restore(c); - mLastSearchAccountKey = accountKey; - mLastSearchServerId = protocolSearchInfo; - mLastSearchRemoteMailbox = mailbox; - return mailbox; - } else { - return null; - } - } finally { - c.close(); - } - } else { - return Mailbox.restoreMailboxWithId(context, message.mMailboxKey); - } - } - - /** - * Scan for messages that are in the Message_Deletes table, look for differences that - * we can deal with, and do the work. - * - * @param account - * @param resolver - * @param accountIdArgs - */ - private static void processPendingDeletesSynchronous(Context context, Account account, - String[] accountIdArgs) { - Cursor deletes = context.getContentResolver().query( - EmailContent.Message.DELETED_CONTENT_URI, - EmailContent.Message.CONTENT_PROJECTION, - EmailContent.MessageColumns.ACCOUNT_KEY + "=?", accountIdArgs, - EmailContent.MessageColumns.MAILBOX_KEY); - long lastMessageId = -1; - try { - // Defer setting up the store until we know we need to access it - Store remoteStore = null; - // loop through messages marked as deleted - while (deletes.moveToNext()) { - boolean deleteFromTrash = false; - - EmailContent.Message oldMessage = - EmailContent.getContent(deletes, EmailContent.Message.class); - - if (oldMessage != null) { - lastMessageId = oldMessage.mId; - - Mailbox mailbox = getRemoteMailboxForMessage(context, oldMessage); - if (mailbox == null) { - continue; // Mailbox removed. Move to the next message. - } - deleteFromTrash = mailbox.mType == Mailbox.TYPE_TRASH; - - // Load the remote store if it will be needed - if (remoteStore == null && deleteFromTrash) { - remoteStore = Store.getInstance(account, context); - } - - // Dispatch here for specific change types - if (deleteFromTrash) { - // Move message to trash - processPendingDeleteFromTrash(context, remoteStore, account, mailbox, - oldMessage); - } - } - - // Finally, delete the update - Uri uri = ContentUris.withAppendedId(EmailContent.Message.DELETED_CONTENT_URI, - oldMessage.mId); - context.getContentResolver().delete(uri, null, null); - } - } catch (MessagingException me) { - // Presumably an error here is an account connection failure, so there is - // no point in continuing through the rest of the pending updates. - if (MailActivityEmail.DEBUG) { - Log.d(Logging.LOG_TAG, "Unable to process pending delete for id=" - + lastMessageId + ": " + me); - } - } finally { - deletes.close(); - } - } - - /** - * Scan for messages that are in Sent, and are in need of upload, - * and send them to the server. "In need of upload" is defined as: - * serverId == null (no UID has been assigned) - * or - * message is in the updated list - * - * Note we also look for messages that are moving from drafts->outbox->sent. They never - * go through "drafts" or "outbox" on the server, so we hang onto these until they can be - * uploaded directly to the Sent folder. - * - * @param account - * @param resolver - * @param accountIdArgs - */ - private static void processPendingUploadsSynchronous(Context context, Account account, - String[] accountIdArgs) { - ContentResolver resolver = context.getContentResolver(); - // Find the Sent folder (since that's all we're uploading for now - Cursor mailboxes = resolver.query(Mailbox.CONTENT_URI, Mailbox.ID_PROJECTION, - MailboxColumns.ACCOUNT_KEY + "=?" - + " and " + MailboxColumns.TYPE + "=" + Mailbox.TYPE_SENT, - accountIdArgs, null); - long lastMessageId = -1; - try { - // Defer setting up the store until we know we need to access it - Store remoteStore = null; - while (mailboxes.moveToNext()) { - long mailboxId = mailboxes.getLong(Mailbox.ID_PROJECTION_COLUMN); - String[] mailboxKeyArgs = new String[] { Long.toString(mailboxId) }; - // Demand load mailbox - Mailbox mailbox = null; - - // First handle the "new" messages (serverId == null) - Cursor upsyncs1 = resolver.query(EmailContent.Message.CONTENT_URI, - EmailContent.Message.ID_PROJECTION, - EmailContent.Message.MAILBOX_KEY + "=?" - + " and (" + EmailContent.Message.SERVER_ID + " is null" - + " or " + EmailContent.Message.SERVER_ID + "=''" + ")", - mailboxKeyArgs, - null); - try { - while (upsyncs1.moveToNext()) { - // Load the remote store if it will be needed - if (remoteStore == null) { - remoteStore = Store.getInstance(account, context); - } - // Load the mailbox if it will be needed - if (mailbox == null) { - mailbox = Mailbox.restoreMailboxWithId(context, mailboxId); - if (mailbox == null) { - continue; // Mailbox removed. Move to the next message. - } - } - // upsync the message - long id = upsyncs1.getLong(EmailContent.Message.ID_PROJECTION_COLUMN); - lastMessageId = id; - processUploadMessage(context, remoteStore, account, mailbox, id); - } - } finally { - if (upsyncs1 != null) { - upsyncs1.close(); - } - } - - // Next, handle any updates (e.g. edited in place, although this shouldn't happen) - Cursor upsyncs2 = resolver.query(EmailContent.Message.UPDATED_CONTENT_URI, - EmailContent.Message.ID_PROJECTION, - EmailContent.MessageColumns.MAILBOX_KEY + "=?", mailboxKeyArgs, - null); - try { - while (upsyncs2.moveToNext()) { - // Load the remote store if it will be needed - if (remoteStore == null) { - remoteStore = Store.getInstance(account, context); - } - // Load the mailbox if it will be needed - if (mailbox == null) { - mailbox = Mailbox.restoreMailboxWithId(context, mailboxId); - if (mailbox == null) { - continue; // Mailbox removed. Move to the next message. - } - } - // upsync the message - long id = upsyncs2.getLong(EmailContent.Message.ID_PROJECTION_COLUMN); - lastMessageId = id; - processUploadMessage(context, remoteStore, account, mailbox, id); - } - } finally { - if (upsyncs2 != null) { - upsyncs2.close(); - } - } - } - } catch (MessagingException me) { - // Presumably an error here is an account connection failure, so there is - // no point in continuing through the rest of the pending updates. - if (MailActivityEmail.DEBUG) { - Log.d(Logging.LOG_TAG, "Unable to process pending upsync for id=" - + lastMessageId + ": " + me); - } - } finally { - if (mailboxes != null) { - mailboxes.close(); - } - } - } - - /** - * Scan for messages that are in the Message_Updates table, look for differences that - * we can deal with, and do the work. - * - * @param account - * @param resolver - * @param accountIdArgs - */ - private static void processPendingUpdatesSynchronous(Context context, Account account, - String[] accountIdArgs) { - ContentResolver resolver = context.getContentResolver(); - Cursor updates = resolver.query(EmailContent.Message.UPDATED_CONTENT_URI, - EmailContent.Message.CONTENT_PROJECTION, - EmailContent.MessageColumns.ACCOUNT_KEY + "=?", accountIdArgs, - EmailContent.MessageColumns.MAILBOX_KEY); - long lastMessageId = -1; - try { - // Defer setting up the store until we know we need to access it - Store remoteStore = null; - // Demand load mailbox (note order-by to reduce thrashing here) - Mailbox mailbox = null; - // loop through messages marked as needing updates - while (updates.moveToNext()) { - boolean changeMoveToTrash = false; - boolean changeRead = false; - boolean changeFlagged = false; - boolean changeMailbox = false; - boolean changeAnswered = false; - - EmailContent.Message oldMessage = - EmailContent.getContent(updates, EmailContent.Message.class); - lastMessageId = oldMessage.mId; - EmailContent.Message newMessage = - EmailContent.Message.restoreMessageWithId(context, oldMessage.mId); - if (newMessage != null) { - mailbox = Mailbox.restoreMailboxWithId(context, newMessage.mMailboxKey); - if (mailbox == null) { - continue; // Mailbox removed. Move to the next message. - } - if (oldMessage.mMailboxKey != newMessage.mMailboxKey) { - if (mailbox.mType == Mailbox.TYPE_TRASH) { - changeMoveToTrash = true; - } else { - changeMailbox = true; - } - } - changeRead = oldMessage.mFlagRead != newMessage.mFlagRead; - changeFlagged = oldMessage.mFlagFavorite != newMessage.mFlagFavorite; - changeAnswered = (oldMessage.mFlags & EmailContent.Message.FLAG_REPLIED_TO) != - (newMessage.mFlags & EmailContent.Message.FLAG_REPLIED_TO); - } - - // Load the remote store if it will be needed - if (remoteStore == null && - (changeMoveToTrash || changeRead || changeFlagged || changeMailbox || - changeAnswered)) { - remoteStore = Store.getInstance(account, context); - } - - // Dispatch here for specific change types - if (changeMoveToTrash) { - // Move message to trash - processPendingMoveToTrash(context, remoteStore, account, mailbox, oldMessage, - newMessage); - } else if (changeRead || changeFlagged || changeMailbox || changeAnswered) { - processPendingDataChange(context, remoteStore, mailbox, changeRead, - changeFlagged, changeMailbox, changeAnswered, oldMessage, newMessage); - } - - // Finally, delete the update - Uri uri = ContentUris.withAppendedId(EmailContent.Message.UPDATED_CONTENT_URI, - oldMessage.mId); - resolver.delete(uri, null, null); - } - - } catch (MessagingException me) { - // Presumably an error here is an account connection failure, so there is - // no point in continuing through the rest of the pending updates. - if (MailActivityEmail.DEBUG) { - Log.d(Logging.LOG_TAG, "Unable to process pending update for id=" - + lastMessageId + ": " + me); - } - } finally { - updates.close(); - } - } - - /** - * Upsync an entire message. This must also unwind whatever triggered it (either by - * updating the serverId, or by deleting the update record, or it's going to keep happening - * over and over again. - * - * Note: If the message is being uploaded into an unexpected mailbox, we *do not* upload. - * This is to avoid unnecessary uploads into the trash. Although the caller attempts to select - * only the Drafts and Sent folders, this can happen when the update record and the current - * record mismatch. In this case, we let the update record remain, because the filters - * in processPendingUpdatesSynchronous() will pick it up as a move and handle it (or drop it) - * appropriately. - * - * @param resolver - * @param remoteStore - * @param account - * @param mailbox the actual mailbox - * @param messageId - */ - private static void processUploadMessage(Context context, Store remoteStore, - Account account, Mailbox mailbox, long messageId) - throws MessagingException { - EmailContent.Message newMessage = - EmailContent.Message.restoreMessageWithId(context, messageId); - boolean deleteUpdate = false; - if (newMessage == null) { - deleteUpdate = true; - Log.d(Logging.LOG_TAG, "Upsync failed for null message, id=" + messageId); - } else if (mailbox.mType == Mailbox.TYPE_DRAFTS) { - deleteUpdate = false; - Log.d(Logging.LOG_TAG, "Upsync skipped for mailbox=drafts, id=" + messageId); - } else if (mailbox.mType == Mailbox.TYPE_OUTBOX) { - deleteUpdate = false; - Log.d(Logging.LOG_TAG, "Upsync skipped for mailbox=outbox, id=" + messageId); - } else if (mailbox.mType == Mailbox.TYPE_TRASH) { - deleteUpdate = false; - Log.d(Logging.LOG_TAG, "Upsync skipped for mailbox=trash, id=" + messageId); - } else if (newMessage != null && newMessage.mMailboxKey != mailbox.mId) { - deleteUpdate = false; - Log.d(Logging.LOG_TAG, "Upsync skipped; mailbox changed, id=" + messageId); - } else { -// Log.d(Logging.LOG_TAG, "Upsyc triggered for message id=" + messageId); -// deleteUpdate = processPendingAppend(context, remoteStore, account, mailbox, - //newMessage); - } - if (deleteUpdate) { - // Finally, delete the update (if any) - Uri uri = ContentUris.withAppendedId( - EmailContent.Message.UPDATED_CONTENT_URI, messageId); - context.getContentResolver().delete(uri, null, null); - } - } - - /** - * Upsync changes to read, flagged, or mailbox - * - * @param remoteStore the remote store for this mailbox - * @param mailbox the mailbox the message is stored in - * @param changeRead whether the message's read state has changed - * @param changeFlagged whether the message's flagged state has changed - * @param changeMailbox whether the message's mailbox has changed - * @param oldMessage the message in it's pre-change state - * @param newMessage the current version of the message - */ - private static void processPendingDataChange(final Context context, Store remoteStore, - Mailbox mailbox, boolean changeRead, boolean changeFlagged, boolean changeMailbox, - boolean changeAnswered, EmailContent.Message oldMessage, - final EmailContent.Message newMessage) throws MessagingException { - // New mailbox is the mailbox this message WILL be in (same as the one it WAS in if it isn't - // being moved - Mailbox newMailbox = mailbox; - // Mailbox is the original remote mailbox (the one we're acting on) - mailbox = getRemoteMailboxForMessage(context, oldMessage); - - // 0. No remote update if the message is local-only - if (newMessage.mServerId == null || newMessage.mServerId.equals("") - || newMessage.mServerId.startsWith(LOCAL_SERVERID_PREFIX) || (mailbox == null)) { - return; - } - - // 1. No remote update for DRAFTS or OUTBOX - if (mailbox.mType == Mailbox.TYPE_DRAFTS || mailbox.mType == Mailbox.TYPE_OUTBOX) { - return; - } - - // 2. Open the remote store & folder - Folder remoteFolder = remoteStore.getFolder(mailbox.mServerId); - if (!remoteFolder.exists()) { - return; - } - remoteFolder.open(OpenMode.READ_WRITE); - if (remoteFolder.getMode() != OpenMode.READ_WRITE) { - return; - } - - // 3. Finally, apply the changes to the message - Message remoteMessage = remoteFolder.getMessage(newMessage.mServerId); - if (remoteMessage == null) { - return; - } - if (MailActivityEmail.DEBUG) { - Log.d(Logging.LOG_TAG, - "Update for msg id=" + newMessage.mId - + " read=" + newMessage.mFlagRead - + " flagged=" + newMessage.mFlagFavorite - + " answered=" - + ((newMessage.mFlags & EmailContent.Message.FLAG_REPLIED_TO) != 0) - + " new mailbox=" + newMessage.mMailboxKey); - } - Message[] messages = new Message[] { remoteMessage }; - if (changeRead) { - remoteFolder.setFlags(messages, FLAG_LIST_SEEN, newMessage.mFlagRead); - } - if (changeFlagged) { - remoteFolder.setFlags(messages, FLAG_LIST_FLAGGED, newMessage.mFlagFavorite); - } - if (changeAnswered) { - remoteFolder.setFlags(messages, FLAG_LIST_ANSWERED, - (newMessage.mFlags & EmailContent.Message.FLAG_REPLIED_TO) != 0); - } - if (changeMailbox) { - Folder toFolder = remoteStore.getFolder(newMailbox.mServerId); - if (!remoteFolder.exists()) { - return; - } - // We may need the message id to search for the message in the destination folder - remoteMessage.setMessageId(newMessage.mMessageId); - // Copy the message to its new folder - remoteFolder.copyMessages(messages, toFolder, new MessageUpdateCallbacks() { - @Override - public void onMessageUidChange(Message message, String newUid) { - ContentValues cv = new ContentValues(); - cv.put(EmailContent.Message.SERVER_ID, newUid); - // We only have one message, so, any updates _must_ be for it. Otherwise, - // we'd have to cycle through to find the one with the same server ID. - context.getContentResolver().update(ContentUris.withAppendedId( - EmailContent.Message.CONTENT_URI, newMessage.mId), cv, null, null); - } - @Override - public void onMessageNotFound(Message message) { - } - }); - // Delete the message from the remote source folder - remoteMessage.setFlag(Flag.DELETED, true); - remoteFolder.expunge(); - } - remoteFolder.close(false); - } - - /** - * Process a pending trash message command. - * - * @param remoteStore the remote store we're working in - * @param account The account in which we are working - * @param newMailbox The local trash mailbox - * @param oldMessage The message copy that was saved in the updates shadow table - * @param newMessage The message that was moved to the mailbox - */ - private static void processPendingMoveToTrash(final Context context, Store remoteStore, - Account account, Mailbox newMailbox, EmailContent.Message oldMessage, - final EmailContent.Message newMessage) throws MessagingException { - - // 0. No remote move if the message is local-only - if (newMessage.mServerId == null || newMessage.mServerId.equals("") - || newMessage.mServerId.startsWith(LOCAL_SERVERID_PREFIX)) { - return; - } - - // 1. Escape early if we can't find the local mailbox - // TODO smaller projection here - Mailbox oldMailbox = getRemoteMailboxForMessage(context, oldMessage); - if (oldMailbox == null) { - // can't find old mailbox, it may have been deleted. just return. - return; - } - // 2. We don't support delete-from-trash here - if (oldMailbox.mType == Mailbox.TYPE_TRASH) { - return; - } - - // The rest of this method handles server-side deletion - - // 4. Find the remote mailbox (that we deleted from), and open it - Folder remoteFolder = remoteStore.getFolder(oldMailbox.mServerId); - if (!remoteFolder.exists()) { - return; - } - - remoteFolder.open(OpenMode.READ_WRITE); - if (remoteFolder.getMode() != OpenMode.READ_WRITE) { - remoteFolder.close(false); - return; - } - - // 5. Find the remote original message - Message remoteMessage = remoteFolder.getMessage(oldMessage.mServerId); - if (remoteMessage == null) { - remoteFolder.close(false); - return; - } - - // 6. Find the remote trash folder, and create it if not found - Folder remoteTrashFolder = remoteStore.getFolder(newMailbox.mServerId); - if (!remoteTrashFolder.exists()) { - /* - * If the remote trash folder doesn't exist we try to create it. - */ - remoteTrashFolder.create(FolderType.HOLDS_MESSAGES); - } - - // 7. Try to copy the message into the remote trash folder - // Note, this entire section will be skipped for POP3 because there's no remote trash - if (remoteTrashFolder.exists()) { - /* - * Because remoteTrashFolder may be new, we need to explicitly open it - */ - remoteTrashFolder.open(OpenMode.READ_WRITE); - if (remoteTrashFolder.getMode() != OpenMode.READ_WRITE) { - remoteFolder.close(false); - remoteTrashFolder.close(false); - return; - } - - remoteFolder.copyMessages(new Message[] { remoteMessage }, remoteTrashFolder, - new Folder.MessageUpdateCallbacks() { - @Override - public void onMessageUidChange(Message message, String newUid) { - // update the UID in the local trash folder, because some stores will - // have to change it when copying to remoteTrashFolder - ContentValues cv = new ContentValues(); - cv.put(EmailContent.Message.SERVER_ID, newUid); - context.getContentResolver().update(newMessage.getUri(), cv, null, null); - } - - /** - * This will be called if the deleted message doesn't exist and can't be - * deleted (e.g. it was already deleted from the server.) In this case, - * attempt to delete the local copy as well. - */ - @Override - public void onMessageNotFound(Message message) { - context.getContentResolver().delete(newMessage.getUri(), null, null); - } - }); - remoteTrashFolder.close(false); - } - - // 8. Delete the message from the remote source folder - remoteMessage.setFlag(Flag.DELETED, true); - remoteFolder.expunge(); - remoteFolder.close(false); - } - - /** - * Process a pending trash message command. - * - * @param remoteStore the remote store we're working in - * @param account The account in which we are working - * @param oldMailbox The local trash mailbox - * @param oldMessage The message that was deleted from the trash - */ - private static void processPendingDeleteFromTrash(Context context, Store remoteStore, - Account account, Mailbox oldMailbox, EmailContent.Message oldMessage) - throws MessagingException { - - // 1. We only support delete-from-trash here - if (oldMailbox.mType != Mailbox.TYPE_TRASH) { - return; - } - - // 2. Find the remote trash folder (that we are deleting from), and open it - Folder remoteTrashFolder = remoteStore.getFolder(oldMailbox.mServerId); - if (!remoteTrashFolder.exists()) { - return; - } - - remoteTrashFolder.open(OpenMode.READ_WRITE); - if (remoteTrashFolder.getMode() != OpenMode.READ_WRITE) { - remoteTrashFolder.close(false); - return; - } - - // 3. Find the remote original message - Message remoteMessage = remoteTrashFolder.getMessage(oldMessage.mServerId); - if (remoteMessage == null) { - remoteTrashFolder.close(false); - return; - } - - // 4. Delete the message from the remote trash folder - remoteMessage.setFlag(Flag.DELETED, true); - remoteTrashFolder.expunge(); - remoteTrashFolder.close(false); - } - - /** - * A message and numeric uid that's easily sortable - */ - private static class SortableMessage { - private final Message mMessage; - private final long mUid; - - SortableMessage(Message message, long uid) { - mMessage = message; - mUid = uid; - } - } - - private int searchMailboxImpl(final Context context, long accountId, SearchParams searchParams, - final long destMailboxId) throws MessagingException { - final Account account = Account.restoreAccountWithId(context, accountId); - final Mailbox mailbox = Mailbox.restoreMailboxWithId(context, searchParams.mMailboxId); - final Mailbox destMailbox = Mailbox.restoreMailboxWithId(context, destMailboxId); - if (account == null || mailbox == null || destMailbox == null) { - Log.d(Logging.LOG_TAG, "Attempted search for " + searchParams - + " but account or mailbox information was missing"); - return 0; - } - - // Tell UI that we're loading messages - - Store remoteStore = Store.getInstance(account, context); - Folder remoteFolder = remoteStore.getFolder(mailbox.mServerId); - remoteFolder.open(OpenMode.READ_WRITE); - - SortableMessage[] sortableMessages = new SortableMessage[0]; - if (searchParams.mOffset == 0) { - // Get the "bare" messages (basically uid) - Message[] remoteMessages = remoteFolder.getMessages(searchParams, null); - int remoteCount = remoteMessages.length; - if (remoteCount > 0) { - sortableMessages = new SortableMessage[remoteCount]; - int i = 0; - for (Message msg : remoteMessages) { - sortableMessages[i++] = new SortableMessage(msg, Long.parseLong(msg.getUid())); - } - // Sort the uid's, most recent first - // Note: Not all servers will be nice and return results in the order of request; - // those that do will see messages arrive from newest to oldest - Arrays.sort(sortableMessages, new Comparator() { - @Override - public int compare(SortableMessage lhs, SortableMessage rhs) { - return lhs.mUid > rhs.mUid ? -1 : lhs.mUid < rhs.mUid ? 1 : 0; - } - }); - sSearchResults.put(accountId, sortableMessages); - } - } else { - sortableMessages = sSearchResults.get(accountId); - } - - final int numSearchResults = sortableMessages.length; - final int numToLoad = - Math.min(numSearchResults - searchParams.mOffset, searchParams.mLimit); - if (numToLoad <= 0) { - return 0; - } - - final ArrayList messageList = new ArrayList(); - for (int i = searchParams.mOffset; i < numToLoad + searchParams.mOffset; i++) { - messageList.add(sortableMessages[i].mMessage); - } - // Get everything in one pass, rather than two (as in sync); this starts getting us - // usable results quickly. - FetchProfile fp = new FetchProfile(); - fp.add(FetchProfile.Item.FLAGS); - fp.add(FetchProfile.Item.ENVELOPE); - fp.add(FetchProfile.Item.STRUCTURE); - fp.add(FetchProfile.Item.BODY_SANE); - remoteFolder.fetch(messageList.toArray(new Message[0]), fp, - new MessageRetrievalListener() { - @Override - public void messageRetrieved(Message message) { - try { - // Determine if the new message was already known (e.g. partial) - // And create or reload the full message info - EmailContent.Message localMessage = new EmailContent.Message(); - try { - // Copy the fields that are available into the message - LegacyConversions.updateMessageFields(localMessage, - message, account.mId, mailbox.mId); - // Commit the message to the local store - Utilities.saveOrUpdate(localMessage, context); - localMessage.mMailboxKey = destMailboxId; - // We load 50k or so; maybe it's complete, maybe not... - int flag = EmailContent.Message.FLAG_LOADED_COMPLETE; - // We store the serverId of the source mailbox into protocolSearchInfo - // This will be used by loadMessageForView, etc. to use the proper remote - // folder - localMessage.mProtocolSearchInfo = mailbox.mServerId; - if (message.getSize() > Store.FETCH_BODY_SANE_SUGGESTED_SIZE) { - flag = EmailContent.Message.FLAG_LOADED_PARTIAL; - } - Utilities.copyOneMessageToProvider(context, message, localMessage, flag); - } catch (MessagingException me) { - Log.e(Logging.LOG_TAG, - "Error while copying downloaded message." + me); - } - } catch (Exception e) { - Log.e(Logging.LOG_TAG, - "Error while storing downloaded message." + e.toString()); - } - } - - @Override - public void loadAttachmentProgress(int progress) { - } - }); - return numSearchResults; - } -} \ No newline at end of file diff --git a/src/com/android/email/service/ImapTempFileLiteral.java b/src/com/android/email/service/ImapTempFileLiteral.java deleted file mode 100644 index cc1dd5410..000000000 --- a/src/com/android/email/service/ImapTempFileLiteral.java +++ /dev/null @@ -1,127 +0,0 @@ -/* - * Copyright (C) 2010 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.email.service; - -import android.util.Log; - -import com.android.email.FixedLengthInputStream; -import com.android.email.mail.store.imap.ImapResponse; -import com.android.email.mail.store.imap.ImapResponseParser; -import com.android.email.mail.store.imap.ImapString; -import com.android.emailcommon.Logging; -import com.android.emailcommon.TempDirectory; -import com.android.emailcommon.utility.Utility; - -import org.apache.commons.io.IOUtils; - -import java.io.ByteArrayInputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; - -/** - * Subclass of {@link ImapString} used for literals backed by a temp file. - */ -public class ImapTempFileLiteral extends ImapString { - /* package for test */ final File mFile; - - /** Size is purely for toString() */ - private final int mSize; - - /* package */ ImapTempFileLiteral(FixedLengthInputStream stream) throws IOException { - mSize = stream.getLength(); - mFile = File.createTempFile("imap", ".tmp", TempDirectory.getTempDirectory()); - - // Unfortunately, we can't really use deleteOnExit(), because temp filenames are random - // so it'd simply cause a memory leak. - // deleteOnExit() simply adds filenames to a static list and the list will never shrink. - // mFile.deleteOnExit(); - OutputStream out = new FileOutputStream(mFile); - IOUtils.copy(stream, out); - out.close(); - } - - /** - * Make sure we delete the temp file. - * - * We should always be calling {@link ImapResponse#destroy()}, but it's here as a last resort. - */ - @Override - protected void finalize() throws Throwable { - try { - destroy(); - } finally { - super.finalize(); - } - } - - @Override - public InputStream getAsStream() { - checkNotDestroyed(); - try { - return new FileInputStream(mFile); - } catch (FileNotFoundException e) { - // It's probably possible if we're low on storage and the system clears the cache dir. - Log.w(Logging.LOG_TAG, "ImapTempFileLiteral: Temp file not found"); - - // Return 0 byte stream as a dummy... - return new ByteArrayInputStream(new byte[0]); - } - } - - @Override - public String getString() { - checkNotDestroyed(); - try { - byte[] bytes = IOUtils.toByteArray(getAsStream()); - // Prevent crash from OOM; we've seen this, but only rarely and not reproducibly - if (bytes.length > ImapResponseParser.LITERAL_KEEP_IN_MEMORY_THRESHOLD) { - throw new IOException(); - } - return Utility.fromAscii(bytes); - } catch (IOException e) { - Log.w(Logging.LOG_TAG, "ImapTempFileLiteral: Error while reading temp file", e); - return ""; - } - } - - @Override - public void destroy() { - try { - if (!isDestroyed() && mFile.exists()) { - mFile.delete(); - } - } catch (RuntimeException re) { - // Just log and ignore. - Log.w(Logging.LOG_TAG, "Failed to remove temp file: " + re.getMessage()); - } - super.destroy(); - } - - @Override - public String toString() { - return String.format("{%d byte literal(file)}", mSize); - } - - public boolean tempFileExistsForTest() { - return mFile.exists(); - } -} diff --git a/emailcommon/src/com/android/emailcommon/AccountManagerTypes.java b/src/com/android/email/service/LegacyEasAuthenticatorService.java similarity index 68% rename from emailcommon/src/com/android/emailcommon/AccountManagerTypes.java rename to src/com/android/email/service/LegacyEasAuthenticatorService.java index 4ccd480a5..d90497c4c 100644 --- a/emailcommon/src/com/android/emailcommon/AccountManagerTypes.java +++ b/src/com/android/email/service/LegacyEasAuthenticatorService.java @@ -1,6 +1,5 @@ /* - /* - * Copyright (C) 2011 The Android Open Source Project + * Copyright (C) 2010 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. @@ -15,9 +14,10 @@ * limitations under the License. */ -package com.android.emailcommon; +package com.android.email.service; -public class AccountManagerTypes { - public static final String TYPE_EXCHANGE = "com.android.exchange"; - public static final String TYPE_POP_IMAP = "com.android.email"; +/** + * This service needs to be declared separately from the base service + */ +public class LegacyEasAuthenticatorService extends AuthenticatorService { } diff --git a/src/com/android/email/service/LegacyEmailAuthenticatorService.java b/src/com/android/email/service/LegacyEmailAuthenticatorService.java new file mode 100644 index 000000000..c5b56444d --- /dev/null +++ b/src/com/android/email/service/LegacyEmailAuthenticatorService.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2010 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.email.service; + +/** + * This service needs to be declared separately from the base service + */ +public class LegacyEmailAuthenticatorService extends AuthenticatorService { +} diff --git a/src/com/android/email/service/MailService.java b/src/com/android/email/service/MailService.java index cc99789aa..d5c11ea26 100644 --- a/src/com/android/email/service/MailService.java +++ b/src/com/android/email/service/MailService.java @@ -26,11 +26,11 @@ import android.database.Cursor; import android.os.Bundle; import android.os.IBinder; +import com.android.email.R; import com.android.email.SingleRunningTask; import com.android.email.provider.AccountReconciler; import com.android.email.service.EmailServiceUtils.EmailServiceInfo; import com.android.email2.ui.MailActivityEmail; -import com.android.emailcommon.AccountManagerTypes; import com.android.emailcommon.provider.Account; import com.android.emailcommon.provider.HostAuth; import com.android.emailcommon.utility.EmailAsyncTask; @@ -51,7 +51,7 @@ public class MailService extends Service { EmailAsyncTask.runAsyncParallel(new Runnable() { @Override public void run() { - reconcilePopImapAccountsSync(MailService.this); + reconcilePopAccountsSync(MailService.this); } }); @@ -70,7 +70,7 @@ public class MailService extends Service { return null; } - public static ArrayList getPopImapAccountList(Context context) { + public static ArrayList getPopAccountList(Context context) { ArrayList providerAccounts = new ArrayList(); Cursor c = context.getContentResolver().query(Account.CONTENT_URI, Account.ID_PROJECTION, null, null, null); @@ -79,7 +79,8 @@ public class MailService extends Service { long accountId = c.getLong(Account.CONTENT_ID_COLUMN); String protocol = Account.getProtocol(context, accountId); EmailServiceInfo info = EmailServiceUtils.getServiceInfo(context, protocol); - if ((info != null) && info.accountType.equals(AccountManagerTypes.TYPE_POP_IMAP)) { + if ((info != null) && info.accountType.equals( + context.getString(R.string.account_manager_type_pop3))) { Account account = Account.restoreAccountWithId(context, accountId); if (account != null) { providerAccounts.add(account); @@ -92,13 +93,14 @@ public class MailService extends Service { return providerAccounts; } - private static final SingleRunningTask sReconcilePopImapAccountsSyncExecutor = + private static final SingleRunningTask sReconcilePopAccountsSyncExecutor = new SingleRunningTask("ReconcilePopImapAccountsSync") { @Override protected void runInternal(Context context) { android.accounts.Account[] accountManagerAccounts = AccountManager.get(context) - .getAccountsByType(AccountManagerTypes.TYPE_POP_IMAP); - ArrayList providerAccounts = getPopImapAccountList(context); + .getAccountsByType( + context.getString(R.string.account_manager_type_pop3)); + ArrayList providerAccounts = getPopAccountList(context); MailService.reconcileAccountsWithAccountManager(context, providerAccounts, accountManagerAccounts, context); @@ -108,20 +110,8 @@ public class MailService extends Service { /** * Reconcile POP/IMAP accounts. */ - public static void reconcilePopImapAccountsSync(Context context) { - sReconcilePopImapAccountsSyncExecutor.run(context); - } - - /** - * Determines whether or not POP/IMAP accounts need reconciling or not. This is a safe operation - * to perform on the UI thread. - */ - public static boolean hasMismatchInPopImapAccounts(Context context) { - android.accounts.Account[] accountManagerAccounts = AccountManager.get(context) - .getAccountsByType(AccountManagerTypes.TYPE_POP_IMAP); - ArrayList providerAccounts = getPopImapAccountList(context); - return AccountReconciler.accountsNeedReconciling( - context, providerAccounts, accountManagerAccounts); + public static void reconcilePopAccountsSync(Context context) { + sReconcilePopAccountsSyncExecutor.run(context); } /** diff --git a/src/com/android/email/service/PopImapAuthenticatorService.java b/src/com/android/email/service/Pop3AuthenticatorService.java similarity index 91% rename from src/com/android/email/service/PopImapAuthenticatorService.java rename to src/com/android/email/service/Pop3AuthenticatorService.java index b829be1f3..f3076ee6b 100644 --- a/src/com/android/email/service/PopImapAuthenticatorService.java +++ b/src/com/android/email/service/Pop3AuthenticatorService.java @@ -19,5 +19,5 @@ package com.android.email.service; /** * This service needs to be declared separately from the base service */ -public class PopImapAuthenticatorService extends AuthenticatorService { +public class Pop3AuthenticatorService extends AuthenticatorService { } diff --git a/src/com/android/email/service/Pop3Service.java b/src/com/android/email/service/Pop3Service.java index 0561d666a..600694445 100644 --- a/src/com/android/email/service/Pop3Service.java +++ b/src/com/android/email/service/Pop3Service.java @@ -105,6 +105,11 @@ public class Pop3Service extends Service { // We load attachments during a sync startSync(inboxId, true); } + + @Override + public void serviceUpdated(String emailAddress) throws RemoteException { + // Not required for POP3 + } }; @Override diff --git a/src/com/android/email/service/PopImapSyncAdapterService.java b/src/com/android/email/service/Pop3SyncAdapterService.java similarity index 87% rename from src/com/android/email/service/PopImapSyncAdapterService.java rename to src/com/android/email/service/Pop3SyncAdapterService.java index 9a1f185e6..8f2cc8559 100644 --- a/src/com/android/email/service/PopImapSyncAdapterService.java +++ b/src/com/android/email/service/Pop3SyncAdapterService.java @@ -38,18 +38,17 @@ import com.android.emailcommon.provider.Account; import com.android.emailcommon.provider.EmailContent; import com.android.emailcommon.provider.EmailContent.AccountColumns; import com.android.emailcommon.provider.EmailContent.Message; -import com.android.emailcommon.provider.HostAuth; import com.android.emailcommon.provider.Mailbox; import com.android.emailcommon.service.EmailServiceProxy; import java.util.ArrayList; -public class PopImapSyncAdapterService extends Service { - private static final String TAG = "PopImapSyncAdapterService"; +public class Pop3SyncAdapterService extends Service { + private static final String TAG = "Pop3SyncAdapterService"; private static SyncAdapterImpl sSyncAdapter = null; private static final Object sSyncAdapterLock = new Object(); - public PopImapSyncAdapterService() { + public Pop3SyncAdapterService() { super(); } @@ -65,7 +64,7 @@ public class PopImapSyncAdapterService extends Service { public void onPerformSync(android.accounts.Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) { try { - PopImapSyncAdapterService.performSync(mContext, account, extras, + Pop3SyncAdapterService.performSync(mContext, account, extras, authority, provider, syncResult); } catch (OperationCanceledException e) { } @@ -87,25 +86,6 @@ public class PopImapSyncAdapterService extends Service { return sSyncAdapter.getSyncAdapterBinder(); } - /** - * @return whether or not this mailbox retrieves its data from the server (as opposed to just - * a local mailbox that is never synced). - */ - public static boolean loadsFromServer(Mailbox m, String protocol) { - if (HostAuth.LEGACY_SCHEME_IMAP.equals(protocol)) { - // TODO: actually use a sync flag when creating the mailboxes. Right now we use an - // approximation for IMAP. - return m.mType != Mailbox.TYPE_DRAFTS - && m.mType != Mailbox.TYPE_OUTBOX - && m.mType != Mailbox.TYPE_SEARCH; - - } else if (HostAuth.LEGACY_SCHEME_POP3.equals(protocol)) { - return Mailbox.TYPE_INBOX == m.mType; - } - - return false; - } - private static void sync(Context context, long mailboxId, SyncResult syncResult, boolean uiRefresh) { TempDirectory.setTempDirectory(context); @@ -114,8 +94,7 @@ public class PopImapSyncAdapterService extends Service { Account account = Account.restoreAccountWithId(context, mailbox.mAccountKey); if (account == null) return; ContentResolver resolver = context.getContentResolver(); - String protocol = account.getProtocol(context); - if ((mailbox.mType != Mailbox.TYPE_OUTBOX) && !loadsFromServer(mailbox, protocol)) { + if ((mailbox.mType != Mailbox.TYPE_OUTBOX) && (mailbox.mType != Mailbox.TYPE_INBOX)) { // This is an update to a message in a non-syncing mailbox; delete this from the // updates table and return resolver.delete(Message.UPDATED_CONTENT_URI, Message.MAILBOX_KEY + "=?", @@ -134,8 +113,6 @@ public class PopImapSyncAdapterService extends Service { try { if (mailbox.mType == Mailbox.TYPE_OUTBOX) { EmailServiceStub.sendMailImpl(context, account.mId); - } else if (protocol.equals(HostAuth.LEGACY_SCHEME_IMAP)) { - ImapService.synchronizeMailboxSynchronous(context, account, mailbox); } else { Pop3Service.synchronizeMailboxSynchronous(context, account, mailbox); } diff --git a/src/com/android/mail/providers/protos/boot/AccountReceiver.java b/src/com/android/mail/providers/protos/boot/AccountReceiver.java index 16eed27d3..08b29401a 100644 --- a/src/com/android/mail/providers/protos/boot/AccountReceiver.java +++ b/src/com/android/mail/providers/protos/boot/AccountReceiver.java @@ -16,6 +16,7 @@ package com.android.mail.providers.protos.boot; +import com.android.emailcommon.provider.EmailContent; import com.android.mail.providers.MailAppProvider; import android.content.BroadcastReceiver; @@ -30,12 +31,9 @@ public class AccountReceiver extends BroadcastReceiver { public static final String ACTION_PROVIDER_CREATED = "com.android.email2.providers.protos.boot.intent.ACTION_PROVIDER_CREATED"; - private static final Uri ACCOUNTS_URI = - Uri.parse("content://com.android.email.provider/uiaccts"); - - @Override public void onReceive(Context context, Intent intent) { - MailAppProvider.addAccountsForUriAsync(ACCOUNTS_URI); + EmailContent.init(context); + MailAppProvider.addAccountsForUriAsync(Uri.parse(EmailContent.CONTENT_URI + "/uiaccts")); } } diff --git a/tests/src/com/android/emailcommon/utility/UtilityMediumTests.java b/tests/src/com/android/emailcommon/utility/UtilityMediumTests.java index 96d2a8f0b..46ea4d78e 100644 --- a/tests/src/com/android/emailcommon/utility/UtilityMediumTests.java +++ b/tests/src/com/android/emailcommon/utility/UtilityMediumTests.java @@ -134,12 +134,12 @@ public class UtilityMediumTests extends ProviderTestCase2 { public void testBuildLimitOneUri() { // EmailProvider supports "?limit=" - assertEquals(Uri.parse("content://com.android.email.provider?limit=1"), - Utility.buildLimitOneUri(Uri.parse("content://com.android.email.provider"))); + assertEquals(Uri.parse("content://com.android.mail.provider?limit=1"), + Utility.buildLimitOneUri(Uri.parse("content://com.android.mail.provider"))); // Others don't -- so don't add it. - assertEquals(Uri.parse("content://com.android.email.attachmentprovider"), - Utility.buildLimitOneUri(Uri.parse("content://com.android.email.attachmentprovider" + assertEquals(Uri.parse("content://com.android.mail.attachmentprovider"), + Utility.buildLimitOneUri(Uri.parse("content://com.android.mail.attachmentprovider" ))); assertEquals(Uri.parse("content://gmail-ls/android@gmail.com"), Utility.buildLimitOneUri(Uri.parse("content://gmail-ls/android@gmail.com"