diff options
30 files changed, 2005 insertions, 1452 deletions
diff --git a/Android.mk b/Android.mk index 451c39b75..3dd43e76d 100644 --- a/Android.mk +++ b/Android.mk @@ -21,7 +21,8 @@ LOCAL_SRC_FILES := $(call all-java-files-under, src) LOCAL_SRC_FILES += \ src/com/android/emailcommon/service/IEmailService.aidl \ src/com/android/emailcommon/service/IEmailServiceCallback.aidl \ - src/com/android/emailcommon/service/IPolicyService.aidl + src/com/android/emailcommon/service/IPolicyService.aidl \ + src/com/android/emailcommon/service/IAccountService.aidl LOCAL_STATIC_JAVA_LIBRARIES := android-common # Revive this when the app is unbundled. diff --git a/AndroidManifest.xml b/AndroidManifest.xml index f8b4b56f8..3a949df21 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -370,6 +370,17 @@ </intent-filter> </service> + <service + android:name=".service.AccountService" + android:enabled="true" + android:permission="com.android.email.permission.ACCESS_PROVIDER" + > + <intent-filter> + <action + android:name="com.android.email.ACCOUNT_INTENT" /> + </intent-filter> + </service> + <!--EXCHANGE-REMOVE-SECTION-START--> <!--Required stanza to register the EAS EmailSyncAdapterService with SyncManager --> <service diff --git a/src/com/android/email/Account.java b/src/com/android/email/Account.java index 9f4527a2c..362377554 100644 --- a/src/com/android/email/Account.java +++ b/src/com/android/email/Account.java @@ -17,6 +17,7 @@ package com.android.email; import com.android.email.mail.Store; +import com.android.emailcommon.service.SyncWindow; import android.content.Context; import android.content.SharedPreferences; @@ -37,14 +38,6 @@ public class Account { public static final int CHECK_INTERVAL_NEVER = -1; public static final int CHECK_INTERVAL_PUSH = -2; - public static final int SYNC_WINDOW_USER = -1; - public static final int SYNC_WINDOW_1_DAY = 1; - public static final int SYNC_WINDOW_3_DAYS = 2; - public static final int SYNC_WINDOW_1_WEEK = 3; - public static final int SYNC_WINDOW_2_WEEKS = 4; - public static final int SYNC_WINDOW_1_MONTH = 5; - public static final int SYNC_WINDOW_ALL = 6; - // These flags will never be seen in a "real" (legacy) account public static final int BACKUP_FLAGS_IS_BACKUP = 1; public static final int BACKUP_FLAGS_SYNC_CONTACTS = 2; @@ -109,7 +102,7 @@ public class Account { mVibrate = false; mVibrateWhenSilent = false; mRingtoneUri = "content://settings/system/notification_sound"; - mSyncWindow = SYNC_WINDOW_USER; // IMAP & POP3 + mSyncWindow = SyncWindow.SYNC_WINDOW_USER; // IMAP & POP3 mBackupFlags = 0; mProtocolVersion = null; mSecurityFlags = 0; @@ -171,7 +164,7 @@ public class Account { "content://settings/system/notification_sound"); mSyncWindow = preferences.mSharedPreferences.getInt(mUuid + KEY_SYNC_WINDOW, - SYNC_WINDOW_USER); + SyncWindow.SYNC_WINDOW_USER); mBackupFlags = preferences.mSharedPreferences.getInt(mUuid + KEY_BACKUP_FLAGS, 0); mProtocolVersion = preferences.mSharedPreferences.getString(mUuid + KEY_PROTOCOL_VERSION, diff --git a/src/com/android/email/AttachmentInfo.java b/src/com/android/email/AttachmentInfo.java index bd257da81..b8d4efe03 100644 --- a/src/com/android/email/AttachmentInfo.java +++ b/src/com/android/email/AttachmentInfo.java @@ -17,8 +17,8 @@ package com.android.email; import com.android.email.mail.internet.MimeUtility; -import com.android.email.provider.AttachmentProvider; import com.android.email.provider.EmailContent.Attachment; +import com.android.emailcommon.utility.AttachmentUtilities; import android.content.Context; import android.content.Intent; @@ -69,7 +69,7 @@ public class AttachmentInfo { public AttachmentInfo(Context context, long id, long size, String fileName, String mimeType, long accountKey) { mSize = size; - mContentType = AttachmentProvider.inferMimeType(fileName, mimeType); + mContentType = AttachmentUtilities.inferMimeType(fileName, mimeType); mName = fileName; mId = id; mAccountKey = accountKey; @@ -83,7 +83,7 @@ public class AttachmentInfo { } // Check for unacceptable attachments by filename extension - String extension = AttachmentProvider.getFilenameExtension(mName); + String extension = AttachmentUtilities.getFilenameExtension(mName); if (!TextUtils.isEmpty(extension) && Utility.arrayContains(Email.UNACCEPTABLE_ATTACHMENT_EXTENSIONS, extension)) { canView = false; @@ -91,7 +91,7 @@ public class AttachmentInfo { } // Check for installable attachments by filename extension - extension = AttachmentProvider.getFilenameExtension(mName); + extension = AttachmentUtilities.getFilenameExtension(mName); if (!TextUtils.isEmpty(extension) && Utility.arrayContains(Email.INSTALLABLE_ATTACHMENT_EXTENSIONS, extension)) { int sideloadEnabled; @@ -134,9 +134,9 @@ public class AttachmentInfo { * @return an Intent suitable for loading the attachment */ public Intent getAttachmentIntent(Context context, long accountId) { - Uri contentUri = AttachmentProvider.getAttachmentUri(accountId, mId); + Uri contentUri = AttachmentUtilities.getAttachmentUri(accountId, mId); if (accountId > 0) { - contentUri = AttachmentProvider.resolveAttachmentIdToContentUri( + contentUri = AttachmentUtilities.resolveAttachmentIdToContentUri( context.getContentResolver(), contentUri); } Intent intent = new Intent(Intent.ACTION_VIEW); @@ -154,6 +154,7 @@ public class AttachmentInfo { return mAllowView || mAllowSave; } + @Override public String toString() { return "{Attachment " + mId + ":" + mName + "," + mContentType + "," + mSize + "}"; } diff --git a/src/com/android/email/Controller.java b/src/com/android/email/Controller.java index b99c48e72..f448c8679 100644 --- a/src/com/android/email/Controller.java +++ b/src/com/android/email/Controller.java @@ -21,7 +21,6 @@ import com.android.email.mail.MessagingException; import com.android.email.mail.Store; import com.android.email.mail.Folder.MessageRetrievalListener; import com.android.email.mail.store.Pop3Store.Pop3Message; -import com.android.email.provider.AttachmentProvider; import com.android.email.provider.EmailContent; import com.android.email.provider.EmailContent.Account; import com.android.email.provider.EmailContent.Attachment; @@ -34,6 +33,7 @@ import com.android.emailcommon.Api; import com.android.emailcommon.service.EmailServiceStatus; import com.android.emailcommon.service.IEmailService; import com.android.emailcommon.service.IEmailServiceCallback; +import com.android.emailcommon.utility.AttachmentUtilities; import android.app.Service; import android.content.ContentResolver; @@ -197,7 +197,8 @@ public class Controller { while (c.moveToNext()) { long mailboxId = c.getLong(EmailContent.ID_PROJECTION_COLUMN); // Must delete attachments BEFORE messages - AttachmentProvider.deleteAllMailboxAttachmentFiles(mProviderContext, 0, mailboxId); + AttachmentUtilities.deleteAllMailboxAttachmentFiles(mProviderContext, 0, + mailboxId); resolver.delete(Message.CONTENT_URI, WHERE_MAILBOX_KEY, new String[] {Long.toString(mailboxId)}); } @@ -720,7 +721,7 @@ public class Controller { if (mailbox == null) return; // 4. Drop non-essential data for the message (e.g. attachment files) - AttachmentProvider.deleteAllAttachmentFiles(mProviderContext, account.mId, + AttachmentUtilities.deleteAllAttachmentFiles(mProviderContext, account.mId, messageId); Uri uri = ContentUris.withAppendedId(EmailContent.Message.SYNCED_CONTENT_URI, @@ -1003,7 +1004,8 @@ public class Controller { public void deleteSyncedDataSync(long accountId) { try { // Delete synced attachments - AttachmentProvider.deleteAllAccountAttachmentFiles(mProviderContext, accountId); + AttachmentUtilities.deleteAllAccountAttachmentFiles(mProviderContext, + accountId); // Delete synced email, leaving only an empty inbox. We do this in two phases: // 1. Delete all non-inbox mailboxes (which will delete all of their messages) diff --git a/src/com/android/email/LegacyConversions.java b/src/com/android/email/LegacyConversions.java index 32858eba2..f1a04e98b 100644 --- a/src/com/android/email/LegacyConversions.java +++ b/src/com/android/email/LegacyConversions.java @@ -19,20 +19,20 @@ package com.android.email; import com.android.email.mail.Address; import com.android.email.mail.Flag; import com.android.email.mail.Message; -import com.android.email.mail.Message.RecipientType; import com.android.email.mail.MessagingException; import com.android.email.mail.Part; +import com.android.email.mail.Message.RecipientType; import com.android.email.mail.internet.MimeBodyPart; import com.android.email.mail.internet.MimeHeader; import com.android.email.mail.internet.MimeMessage; import com.android.email.mail.internet.MimeMultipart; import com.android.email.mail.internet.MimeUtility; import com.android.email.mail.internet.TextBody; -import com.android.email.provider.AttachmentProvider; import com.android.email.provider.EmailContent; import com.android.email.provider.EmailContent.Attachment; import com.android.email.provider.EmailContent.AttachmentColumns; import com.android.email.provider.EmailContent.Mailbox; +import com.android.emailcommon.utility.AttachmentUtilities; import org.apache.commons.io.IOUtils; @@ -41,7 +41,6 @@ import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.net.Uri; -import android.text.TextUtils; import android.util.Log; import java.io.File; @@ -148,108 +147,6 @@ public class LegacyConversions { } /** - * Copy body text (plain and/or HTML) from MimeMessage to provider Message - */ - public static boolean updateBodyFields(EmailContent.Body body, - EmailContent.Message localMessage, ArrayList<Part> viewables) - throws MessagingException { - - body.mMessageKey = localMessage.mId; - - StringBuffer sbHtml = null; - StringBuffer sbText = null; - StringBuffer sbHtmlReply = null; - StringBuffer sbTextReply = null; - StringBuffer sbIntroText = null; - - for (Part viewable : viewables) { - String text = MimeUtility.getTextFromPart(viewable); - String[] replyTags = viewable.getHeader(MimeHeader.HEADER_ANDROID_BODY_QUOTED_PART); - String replyTag = null; - if (replyTags != null && replyTags.length > 0) { - replyTag = replyTags[0]; - } - // Deploy text as marked by the various tags - boolean isHtml = "text/html".equalsIgnoreCase(viewable.getMimeType()); - - if (replyTag != null) { - boolean isQuotedReply = BODY_QUOTED_PART_REPLY.equalsIgnoreCase(replyTag); - boolean isQuotedForward = BODY_QUOTED_PART_FORWARD.equalsIgnoreCase(replyTag); - boolean isQuotedIntro = BODY_QUOTED_PART_INTRO.equalsIgnoreCase(replyTag); - - if (isQuotedReply || isQuotedForward) { - if (isHtml) { - sbHtmlReply = appendTextPart(sbHtmlReply, text); - } else { - sbTextReply = appendTextPart(sbTextReply, text); - } - // Set message flags as well - localMessage.mFlags &= ~EmailContent.Message.FLAG_TYPE_MASK; - localMessage.mFlags |= isQuotedReply - ? EmailContent.Message.FLAG_TYPE_REPLY - : EmailContent.Message.FLAG_TYPE_FORWARD; - continue; - } - if (isQuotedIntro) { - sbIntroText = appendTextPart(sbIntroText, text); - continue; - } - } - - // Most of the time, just process regular body parts - if (isHtml) { - sbHtml = appendTextPart(sbHtml, text); - } else { - sbText = appendTextPart(sbText, text); - } - } - - // write the combined data to the body part - if (!TextUtils.isEmpty(sbText)) { - String text = sbText.toString(); - body.mTextContent = text; - localMessage.mSnippet = Snippet.fromPlainText(text); - } - if (!TextUtils.isEmpty(sbHtml)) { - String text = sbHtml.toString(); - body.mHtmlContent = text; - if (localMessage.mSnippet == null) { - localMessage.mSnippet = Snippet.fromHtmlText(text); - } - } - if (sbHtmlReply != null && sbHtmlReply.length() != 0) { - body.mHtmlReply = sbHtmlReply.toString(); - } - if (sbTextReply != null && sbTextReply.length() != 0) { - body.mTextReply = sbTextReply.toString(); - } - if (sbIntroText != null && sbIntroText.length() != 0) { - body.mIntroText = sbIntroText.toString(); - } - return true; - } - - /** - * Helper function to append text to a StringBuffer, creating it if necessary. - * Optimization: The majority of the time we are *not* appending - we should have a path - * that deals with single strings. - */ - private static StringBuffer appendTextPart(StringBuffer sb, String newText) { - if (newText == null) { - return sb; - } - else if (sb == null) { - sb = new StringBuffer(newText); - } else { - if (sb.length() > 0) { - sb.append('\n'); - } - sb.append(newText); - } - return sb; - } - - /** * Copy attachments from MimeMessage to provider Message. * * @param context a context for file operations @@ -392,11 +289,11 @@ public class LegacyConversions { InputStream in = part.getBody().getInputStream(); - File saveIn = AttachmentProvider.getAttachmentDirectory(context, accountId); + File saveIn = AttachmentUtilities.getAttachmentDirectory(context, accountId); if (!saveIn.exists()) { saveIn.mkdirs(); } - File saveAs = AttachmentProvider.getAttachmentFilename(context, accountId, + File saveAs = AttachmentUtilities.getAttachmentFilename(context, accountId, attachmentId); saveAs.createNewFile(); FileOutputStream out = new FileOutputStream(saveAs); @@ -405,7 +302,7 @@ public class LegacyConversions { out.close(); // update the attachment with the extra information we now know - String contentUriString = AttachmentProvider.getAttachmentUri( + String contentUriString = AttachmentUtilities.getAttachmentUri( accountId, attachmentId).toString(); localAttachment.mSize = copySize; diff --git a/src/com/android/email/MessagingController.java b/src/com/android/email/MessagingController.java index 8aeefc2cb..edabd53a4 100644 --- a/src/com/android/email/MessagingController.java +++ b/src/com/android/email/MessagingController.java @@ -33,7 +33,6 @@ import com.android.email.mail.internet.MimeBodyPart; import com.android.email.mail.internet.MimeHeader; import com.android.email.mail.internet.MimeMultipart; import com.android.email.mail.internet.MimeUtility; -import com.android.email.provider.AttachmentProvider; import com.android.email.provider.EmailContent; import com.android.email.provider.EmailContent.Attachment; import com.android.email.provider.EmailContent.AttachmentColumns; @@ -41,6 +40,8 @@ import com.android.email.provider.EmailContent.Mailbox; import com.android.email.provider.EmailContent.MailboxColumns; import com.android.email.provider.EmailContent.MessageColumns; import com.android.email.provider.EmailContent.SyncColumns; +import com.android.emailcommon.utility.AttachmentUtilities; +import com.android.emailcommon.utility.ConversionUtilities; import android.content.ContentResolver; import android.content.ContentUris; @@ -300,7 +301,7 @@ public class MessagingController implements Runnable { break; default: // Drop all attachment files related to this mailbox - AttachmentProvider.deleteAllMailboxAttachmentFiles( + AttachmentUtilities.deleteAllMailboxAttachmentFiles( mContext, accountId, localInfo.mId); // Delete the mailbox. Triggers will take care of // related Message, Body and Attachment records. @@ -736,7 +737,8 @@ public class MessagingController implements Runnable { // Delete associated data (attachment files) // Attachment & Body records are auto-deleted when we delete the Message record - AttachmentProvider.deleteAllAttachmentFiles(mContext, account.mId, infoToDelete.mId); + AttachmentUtilities.deleteAllAttachmentFiles(mContext, account.mId, + infoToDelete.mId); // Delete the message itself Uri uriToDelete = ContentUris.withAppendedId( @@ -1005,7 +1007,7 @@ public class MessagingController implements Runnable { ArrayList<Part> attachments = new ArrayList<Part>(); MimeUtility.collectParts(message, viewables, attachments); - LegacyConversions.updateBodyFields(body, localMessage, viewables); + ConversionUtilities.updateBodyFields(body, localMessage, viewables); // Commit the message & body to the local store immediately saveOrUpdate(localMessage, context); @@ -2046,12 +2048,13 @@ public class MessagingController implements Runnable { EmailContent.Message.restoreMessageWithId(mContext, messageId); if (msg != null && ((msg.mFlags & EmailContent.Message.FLAG_TYPE_FORWARD) != 0)) { - AttachmentProvider.deleteAllAttachmentFiles(mContext, account.mId, + AttachmentUtilities.deleteAllAttachmentFiles(mContext, account.mId, messageId); } resolver.update(syncedUri, moveToSentValues, null, null); } else { - AttachmentProvider.deleteAllAttachmentFiles(mContext, account.mId, messageId); + AttachmentUtilities.deleteAllAttachmentFiles(mContext, account.mId, + messageId); Uri uri = ContentUris.withAppendedId(EmailContent.Message.CONTENT_URI, messageId); resolver.delete(uri, null, null); diff --git a/src/com/android/email/activity/MessageViewFragmentBase.java b/src/com/android/email/activity/MessageViewFragmentBase.java index bec27d974..567987a5d 100644 --- a/src/com/android/email/activity/MessageViewFragmentBase.java +++ b/src/com/android/email/activity/MessageViewFragmentBase.java @@ -27,12 +27,12 @@ import com.android.email.Utility; import com.android.email.mail.Address; import com.android.email.mail.MessagingException; import com.android.email.mail.internet.EmailHtmlUtil; -import com.android.email.provider.AttachmentProvider; import com.android.email.provider.EmailContent.Attachment; import com.android.email.provider.EmailContent.Body; import com.android.email.provider.EmailContent.Mailbox; import com.android.email.provider.EmailContent.Message; import com.android.email.service.AttachmentDownloadService; +import com.android.emailcommon.utility.AttachmentUtilities; import org.apache.commons.io.IOUtils; @@ -677,14 +677,14 @@ public abstract class MessageViewFragmentBase extends Fragment implements View.O return; } Attachment attachment = Attachment.restoreAttachmentWithId(mContext, info.mId); - Uri attachmentUri = AttachmentProvider.getAttachmentUri(mAccountId, attachment.mId); + Uri attachmentUri = AttachmentUtilities.getAttachmentUri(mAccountId, attachment.mId); try { File downloads = Environment.getExternalStoragePublicDirectory( Environment.DIRECTORY_DOWNLOADS); downloads.mkdirs(); File file = Utility.createUniqueFile(downloads, attachment.mFileName); - Uri contentUri = AttachmentProvider.resolveAttachmentIdToContentUri( + Uri contentUri = AttachmentUtilities.resolveAttachmentIdToContentUri( mContext.getContentResolver(), attachmentUri); InputStream in = mContext.getContentResolver().openInputStream(contentUri); OutputStream out = new FileOutputStream(file); @@ -1076,7 +1076,7 @@ public abstract class MessageViewFragmentBase extends Fragment implements View.O try { return BitmapFactory.decodeStream( mContext.getContentResolver().openInputStream( - AttachmentProvider.getAttachmentThumbnailUri( + AttachmentUtilities.getAttachmentThumbnailUri( mAccountId, attachment.mId, PREVIEW_ICON_WIDTH, PREVIEW_ICON_HEIGHT))); diff --git a/src/com/android/email/activity/setup/AccountSetupOptions.java b/src/com/android/email/activity/setup/AccountSetupOptions.java index f07d62263..ac4544428 100644 --- a/src/com/android/email/activity/setup/AccountSetupOptions.java +++ b/src/com/android/email/activity/setup/AccountSetupOptions.java @@ -26,6 +26,7 @@ import com.android.email.provider.EmailContent; import com.android.email.provider.EmailContent.Account; import com.android.email.service.MailService; import com.android.emailcommon.service.PolicySet; +import com.android.emailcommon.service.SyncWindow; import android.accounts.AccountAuthenticatorResponse; import android.accounts.AccountManager; @@ -66,7 +67,7 @@ public class AccountSetupOptions extends AccountSetupActivity implements OnClick public static final int REQUEST_CODE_ACCEPT_POLICIES = 1; /** Default sync window for new EAS accounts */ - private static final int SYNC_WINDOW_EAS_DEFAULT = com.android.email.Account.SYNC_WINDOW_3_DAYS; + private static final int SYNC_WINDOW_EAS_DEFAULT = SyncWindow.SYNC_WINDOW_3_DAYS; public static void actionOptions(Activity fromActivity) { fromActivity.startActivity(new Intent(fromActivity, AccountSetupOptions.class)); diff --git a/src/com/android/email/provider/AttachmentProvider.java b/src/com/android/email/provider/AttachmentProvider.java index 30afd8fa8..e496755a0 100644 --- a/src/com/android/email/provider/AttachmentProvider.java +++ b/src/com/android/email/provider/AttachmentProvider.java @@ -20,14 +20,12 @@ import com.android.email.Email; import com.android.email.mail.internet.MimeUtility; import com.android.email.provider.EmailContent.Attachment; import com.android.email.provider.EmailContent.AttachmentColumns; -import com.android.email.provider.EmailContent.Message; -import com.android.email.provider.EmailContent.MessageColumns; +import com.android.emailcommon.utility.AttachmentUtilities; +import com.android.emailcommon.utility.AttachmentUtilities.Columns; import android.content.ContentProvider; -import android.content.ContentResolver; import android.content.ContentUris; import android.content.ContentValues; -import android.content.Context; import android.database.Cursor; import android.database.MatrixCursor; import android.graphics.Bitmap; @@ -35,9 +33,7 @@ import android.graphics.BitmapFactory; import android.net.Uri; import android.os.Binder; import android.os.ParcelFileDescriptor; -import android.text.TextUtils; import android.util.Log; -import android.webkit.MimeTypeMap; import java.io.File; import java.io.FileNotFoundException; @@ -66,19 +62,6 @@ import java.util.List; */ public class AttachmentProvider extends ContentProvider { - public static final String AUTHORITY = "com.android.email.attachmentprovider"; - public static final Uri CONTENT_URI = Uri.parse( "content://" + AUTHORITY); - - private static final String FORMAT_RAW = "RAW"; - private static final String FORMAT_THUMBNAIL = "THUMBNAIL"; - - public static class AttachmentProviderColumns { - public static final String _ID = "_id"; - public static final String DATA = "_data"; - public static final String DISPLAY_NAME = "_display_name"; - public static final String SIZE = "_size"; - } - private static final String[] MIME_TYPE_PROJECTION = new String[] { AttachmentColumns.MIME_TYPE, AttachmentColumns.FILENAME }; private static final int MIME_TYPE_COLUMN_MIME_TYPE = 0; @@ -87,47 +70,6 @@ public class AttachmentProvider extends ContentProvider { private static final String[] PROJECTION_QUERY = new String[] { AttachmentColumns.FILENAME, AttachmentColumns.SIZE, AttachmentColumns.CONTENT_URI }; - 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(); - } - - /** - * Return the filename for a given attachment. This should be used by any code that is - * going to *write* attachments. - * - * This does not create or write the file, or even the directories. It simply builds - * the filename that should be used. - */ - public static File getAttachmentFilename(Context context, long accountId, long attachmentId) { - return new File(getAttachmentDirectory(context, accountId), Long.toString(attachmentId)); - } - - /** - * Return the directory for a given attachment. This should be used by any code that is - * going to *write* attachments. - * - * This does not create or write the directory. It simply builds the pathname that should be - * used. - */ - public static File getAttachmentDirectory(Context context, long accountId) { - return context.getDatabasePath(accountId + ".db_att"); - } - @Override public boolean onCreate() { /* @@ -157,17 +99,17 @@ public class AttachmentProvider extends ContentProvider { List<String> segments = uri.getPathSegments(); String id = segments.get(1); String format = segments.get(2); - if (FORMAT_THUMBNAIL.equals(format)) { + if (AttachmentUtilities.FORMAT_THUMBNAIL.equals(format)) { return "image/png"; } else { uri = ContentUris.withAppendedId(Attachment.CONTENT_URI, Long.parseLong(id)); - Cursor c = getContext().getContentResolver().query(uri, MIME_TYPE_PROJECTION, - null, null, null); + Cursor c = getContext().getContentResolver().query(uri, MIME_TYPE_PROJECTION, null, + null, null); try { if (c.moveToFirst()) { String mimeType = c.getString(MIME_TYPE_COLUMN_MIME_TYPE); String fileName = c.getString(MIME_TYPE_COLUMN_FILENAME); - mimeType = inferMimeType(fileName, mimeType); + mimeType = AttachmentUtilities.inferMimeType(fileName, mimeType); return mimeType; } } finally { @@ -181,82 +123,6 @@ public class AttachmentProvider extends ContentProvider { } /** - * Helper to convert unknown or unmapped attachments to something useful based on filename - * extensions. The mime type is inferred based upon the table below. It's not perfect, but - * it helps. - * - * <pre> - * |---------------------------------------------------------| - * | E X T E N S I O N | - * |---------------------------------------------------------| - * | .eml | known(.png) | unknown(.abc) | none | - * | M |-----------------------------------------------------------------------| - * | I | none | msg/rfc822 | image/png | app/abc | app/oct-str | - * | M |-------------| (always | | | | - * | E | app/oct-str | overrides | | | | - * | T |-------------| | |-----------------------------| - * | Y | text/plain | | | text/plain | - * | P |-------------| |-------------------------------------------| - * | E | any/type | | any/type | - * |---|-----------------------------------------------------------------------| - * </pre> - * - * NOTE: Since mime types on Android are case-*sensitive*, return values are always in - * lower case. - * - * @param fileName The given filename - * @param mimeType The given mime type - * @return A likely mime type for the attachment - */ - public static String inferMimeType(final String fileName, final String mimeType) { - String resultType = null; - String fileExtension = getFilenameExtension(fileName); - boolean isTextPlain = "text/plain".equalsIgnoreCase(mimeType); - - if ("eml".equals(fileExtension)) { - resultType = "message/rfc822"; - } else { - boolean isGenericType = - isTextPlain || "application/octet-stream".equalsIgnoreCase(mimeType); - // If the given mime type is non-empty and non-generic, return it - if (isGenericType || TextUtils.isEmpty(mimeType)) { - if (!TextUtils.isEmpty(fileExtension)) { - // Otherwise, try to find a mime type based upon the file extension - resultType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(fileExtension); - if (TextUtils.isEmpty(resultType)) { - // Finally, if original mimetype is text/plain, use it; otherwise synthesize - resultType = isTextPlain ? mimeType : "application/" + fileExtension; - } - } - } else { - resultType = mimeType; - } - } - - // No good guess could be made; use an appropriate generic type - if (TextUtils.isEmpty(resultType)) { - resultType = isTextPlain ? "text/plain" : "application/octet-stream"; - } - return resultType.toLowerCase(); - } - - /** - * Extract and return filename's extension, converted to lower case, and not including the "." - * - * @return extension, or null if not found (or null/empty filename) - */ - public static String getFilenameExtension(String fileName) { - String extension = null; - if (!TextUtils.isEmpty(fileName)) { - int lastDot = fileName.lastIndexOf('.'); - if ((lastDot > 0) && (lastDot < fileName.length() - 1)) { - extension = fileName.substring(lastDot + 1).toLowerCase(); - } - } - return extension; - } - - /** * Open an attachment file. There are two "modes" - "raw", which returns an actual file, * and "thumbnail", which attempts to generate a thumbnail image. * @@ -275,17 +141,17 @@ public class AttachmentProvider extends ContentProvider { String accountId = segments.get(0); String id = segments.get(1); String format = segments.get(2); - if (FORMAT_THUMBNAIL.equals(format)) { + if (AttachmentUtilities.FORMAT_THUMBNAIL.equals(format)) { int width = Integer.parseInt(segments.get(3)); int height = Integer.parseInt(segments.get(4)); String filename = "thmb_" + accountId + "_" + id; File dir = getContext().getCacheDir(); File file = new File(dir, filename); if (!file.exists()) { - Uri attachmentUri = + Uri attachmentUri = AttachmentUtilities. getAttachmentUri(Long.parseLong(accountId), Long.parseLong(id)); Cursor c = query(attachmentUri, - new String[] { AttachmentProviderColumns.DATA }, null, null, null); + new String[] { Columns.DATA }, null, null, null); if (c != null) { try { if (c.moveToFirst()) { @@ -355,8 +221,8 @@ public class AttachmentProvider extends ContentProvider { if (projection == null) { projection = new String[] { - AttachmentProviderColumns._ID, - AttachmentProviderColumns.DATA, + Columns._ID, + Columns.DATA, }; } @@ -387,16 +253,16 @@ public class AttachmentProvider extends ContentProvider { Object[] values = new Object[projection.length]; for (int i = 0, count = projection.length; i < count; i++) { String column = projection[i]; - if (AttachmentProviderColumns._ID.equals(column)) { + if (Columns._ID.equals(column)) { values[i] = id; } - else if (AttachmentProviderColumns.DATA.equals(column)) { + else if (Columns.DATA.equals(column)) { values[i] = contentUri; } - else if (AttachmentProviderColumns.DISPLAY_NAME.equals(column)) { + else if (Columns.DISPLAY_NAME.equals(column)) { values[i] = name; } - else if (AttachmentProviderColumns.SIZE.equals(column)) { + else if (Columns.SIZE.equals(column)) { values[i] = size; } } @@ -433,101 +299,6 @@ public class AttachmentProvider extends ContentProvider { } /** - * Resolve attachment id to content URI. Returns the resolved content URI (from the attachment - * DB) or, if not found, simply returns the incoming value. - * - * @param attachmentUri - * @return resolved content URI - * - * TODO: Throws an SQLite exception on a missing DB file (e.g. unknown URI) instead of just - * returning the incoming uri, as it should. - */ - public static Uri resolveAttachmentIdToContentUri(ContentResolver resolver, Uri attachmentUri) { - Cursor c = resolver.query(attachmentUri, - new String[] { AttachmentProvider.AttachmentProviderColumns.DATA }, - null, null, null); - if (c != null) { - try { - if (c.moveToFirst()) { - final String strUri = c.getString(0); - if (strUri != null) { - return Uri.parse(strUri); - } else { - Email.log("AttachmentProvider: attachment with null contentUri"); - } - } - } finally { - c.close(); - } - } - return attachmentUri; - } - - /** - * In support of deleting a message, find all attachments and delete associated attachment - * files. - * @param context - * @param accountId the account for the message - * @param messageId the message - */ - public static void deleteAllAttachmentFiles(Context context, long accountId, long messageId) { - Uri uri = ContentUris.withAppendedId(Attachment.MESSAGE_ID_URI, messageId); - Cursor c = context.getContentResolver().query(uri, Attachment.ID_PROJECTION, - null, null, null); - try { - while (c.moveToNext()) { - long attachmentId = c.getLong(Attachment.ID_PROJECTION_COLUMN); - File attachmentFile = getAttachmentFilename(context, accountId, attachmentId); - // Note, delete() throws no exceptions for basic FS errors (e.g. file not found) - // it just returns false, which we ignore, and proceed to the next file. - // This entire loop is best-effort only. - attachmentFile.delete(); - } - } finally { - c.close(); - } - } - - /** - * In support of deleting a mailbox, find all messages and delete their attachments. - * - * @param context - * @param accountId the account for the mailbox - * @param mailboxId the mailbox for the messages - */ - public static void deleteAllMailboxAttachmentFiles(Context context, long accountId, - long mailboxId) { - Cursor c = context.getContentResolver().query(Message.CONTENT_URI, - Message.ID_COLUMN_PROJECTION, MessageColumns.MAILBOX_KEY + "=?", - new String[] { Long.toString(mailboxId) }, null); - try { - while (c.moveToNext()) { - long messageId = c.getLong(Message.ID_PROJECTION_COLUMN); - deleteAllAttachmentFiles(context, accountId, messageId); - } - } finally { - c.close(); - } - } - - /** - * In support of deleting or wiping an account, delete all related attachments. - * - * @param context - * @param accountId the account to scrub - */ - public static void deleteAllAccountAttachmentFiles(Context context, long accountId) { - File[] files = getAttachmentDirectory(context, accountId).listFiles(); - if (files == null) return; - for (File file : files) { - boolean result = file.delete(); - if (!result) { - Log.e(Email.LOG_TAG, "Failed to delete attachment file " + file.getName()); - } - } - } - - /** * Need this to suppress warning in unit tests. */ @Override diff --git a/src/com/android/email/service/AccountService.java b/src/com/android/email/service/AccountService.java new file mode 100644 index 000000000..c4f12d483 --- /dev/null +++ b/src/com/android/email/service/AccountService.java @@ -0,0 +1,74 @@ +/* + * 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.service; + +import com.android.email.AccountBackupRestore; +import com.android.email.NotificationController; +import com.android.email.ResourceHelper; +import com.android.emailcommon.service.IAccountService; + +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.os.IBinder; +import android.os.RemoteException; + +public class AccountService extends Service { + + private Context mContext; + + private final IAccountService.Stub mBinder = new IAccountService.Stub() { + + @Override + public void notifyLoginFailed(long accountId) throws RemoteException { + NotificationController.getInstance(mContext).showLoginFailedNotification(accountId); + } + + @Override + public void notifyLoginSucceeded(long accountId) throws RemoteException { + NotificationController.getInstance(mContext).cancelLoginFailedNotification(accountId); + } + + @Override + public void notifyNewMessages(long accountId) throws RemoteException { + MailService.actionNotifyNewMessages(mContext, accountId); + } + + @Override + public void restoreAccountsIfNeeded() throws RemoteException { + AccountBackupRestore.restoreAccountsIfNeeded(mContext); + } + + @Override + public void accountDeleted() throws RemoteException { + MailService.accountDeleted(mContext); + } + + @Override + public int getAccountColor(long accountId) throws RemoteException { + return ResourceHelper.getInstance(mContext).getAccountColor(accountId); + } + }; + + @Override + public IBinder onBind(Intent intent) { + if (mContext == null) { + mContext = this; + } + return mBinder; + } +}
\ No newline at end of file diff --git a/src/com/android/email/service/AttachmentDownloadService.java b/src/com/android/email/service/AttachmentDownloadService.java index 2f155f04f..6422b4881 100644 --- a/src/com/android/email/service/AttachmentDownloadService.java +++ b/src/com/android/email/service/AttachmentDownloadService.java @@ -23,7 +23,6 @@ import com.android.email.NotificationController; import com.android.email.Utility; import com.android.email.Controller.ControllerService; import com.android.email.ExchangeUtils.NullEmailService; -import com.android.email.provider.AttachmentProvider; import com.android.email.provider.EmailContent; import com.android.email.provider.EmailContent.Account; import com.android.email.provider.EmailContent.Attachment; @@ -31,6 +30,7 @@ import com.android.email.provider.EmailContent.Message; import com.android.emailcommon.service.EmailServiceProxy; import com.android.emailcommon.service.EmailServiceStatus; import com.android.emailcommon.service.IEmailServiceCallback; +import com.android.emailcommon.utility.AttachmentUtilities; import com.android.exchange.ExchangeService; import android.accounts.AccountManager; @@ -427,7 +427,7 @@ public class AttachmentDownloadService extends Service implements Runnable { */ private void startDownload(Class<? extends Service> serviceClass, DownloadRequest req) throws RemoteException { - File file = AttachmentProvider.getAttachmentFilename(mContext, req.accountId, + File file = AttachmentUtilities.getAttachmentFilename(mContext, req.accountId, req.attachmentId); req.startTime = System.currentTimeMillis(); req.inProgress = true; @@ -437,7 +437,7 @@ public class AttachmentDownloadService extends Service implements Runnable { EmailServiceProxy proxy = new EmailServiceProxy(mContext, serviceClass, mServiceCallback); proxy.loadAttachment(req.attachmentId, file.getAbsolutePath(), - AttachmentProvider.getAttachmentUri(req.accountId, req.attachmentId) + AttachmentUtilities.getAttachmentUri(req.accountId, req.attachmentId) .toString(), req.priority != PRIORITY_FOREGROUND); // Lazily initialize our (reusable) pending intent if (mWatchdogPendingIntent == null) { @@ -949,7 +949,7 @@ public class AttachmentDownloadService extends Service implements Runnable { if (att.mMimeType != null) { pw.print(att.mMimeType); } else { - pw.print(AttachmentProvider.inferMimeType(fileName, null)); + pw.print(AttachmentUtilities.inferMimeType(fileName, null)); pw.print(" [inferred]"); } pw.println(" Size: " + att.mSize); diff --git a/src/com/android/email/service/MailService.java b/src/com/android/email/service/MailService.java index 60e6f06b9..daf4a3bde 100644 --- a/src/com/android/email/service/MailService.java +++ b/src/com/android/email/service/MailService.java @@ -1,930 +1,905 @@ -/*
- * 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.service;
-
-import com.android.email.AccountBackupRestore;
-import com.android.email.Controller;
-import com.android.email.Email;
-import com.android.email.NotificationController;
-import com.android.email.Preferences;
-import com.android.email.SecurityPolicy;
-import com.android.email.SingleRunningTask;
-import com.android.email.Utility;
-import com.android.email.mail.MessagingException;
-import com.android.email.provider.EmailContent;
-import com.android.email.provider.EmailContent.Account;
-import com.android.email.provider.EmailContent.AccountColumns;
-import com.android.email.provider.EmailContent.HostAuth;
-import com.android.email.provider.EmailContent.Mailbox;
-import com.android.email.provider.EmailProvider;
-
-import android.accounts.AccountManager;
-import android.accounts.AccountManagerCallback;
-import android.accounts.AccountManagerFuture;
-import android.accounts.AuthenticatorException;
-import android.accounts.OperationCanceledException;
-import android.app.AlarmManager;
-import android.app.PendingIntent;
-import android.app.Service;
-import android.content.ContentResolver;
-import android.content.ContentUris;
-import android.content.Context;
-import android.content.Intent;
-import android.content.SyncStatusObserver;
-import android.database.Cursor;
-import android.net.ConnectivityManager;
-import android.net.Uri;
-import android.os.Bundle;
-import android.os.Handler;
-import android.os.IBinder;
-import android.os.SystemClock;
-import android.text.TextUtils;
-import android.util.Log;
-
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-
-/**
- * Background service for refreshing non-push email accounts.
- *
- * TODO: Convert to IntentService to move *all* work off the UI thread, serialize work, and avoid
- * possible problems with out-of-order startId processing.
- */
-public class MailService extends Service {
- private static final String LOG_TAG = "Email-MailService";
-
- private static final String ACTION_CHECK_MAIL =
- "com.android.email.intent.action.MAIL_SERVICE_WAKEUP";
- private static final String ACTION_RESCHEDULE =
- "com.android.email.intent.action.MAIL_SERVICE_RESCHEDULE";
- private static final String ACTION_CANCEL =
- "com.android.email.intent.action.MAIL_SERVICE_CANCEL";
- private static final String ACTION_NOTIFY_MAIL =
- "com.android.email.intent.action.MAIL_SERVICE_NOTIFY";
- private static final String ACTION_SEND_PENDING_MAIL =
- "com.android.email.intent.action.MAIL_SERVICE_SEND_PENDING";
-
- private static final String EXTRA_ACCOUNT = "com.android.email.intent.extra.ACCOUNT";
- private static final String EXTRA_ACCOUNT_INFO = "com.android.email.intent.extra.ACCOUNT_INFO";
- private static final String EXTRA_DEBUG_WATCHDOG = "com.android.email.intent.extra.WATCHDOG";
-
- private static final int WATCHDOG_DELAY = 10 * 60 * 1000; // 10 minutes
-
- // Sentinel value asking to update mSyncReports if it's currently empty
- /*package*/ static final int SYNC_REPORTS_ALL_ACCOUNTS_IF_EMPTY = -1;
- // Sentinel value asking that mSyncReports be rebuilt
- /*package*/ static final int SYNC_REPORTS_RESET = -2;
-
- private static final String[] NEW_MESSAGE_COUNT_PROJECTION =
- new String[] {AccountColumns.NEW_MESSAGE_COUNT};
-
- private static MailService sMailService;
-
- /*package*/ Controller mController;
- private final Controller.Result mControllerCallback = new ControllerResults();
- private ContentResolver mContentResolver;
- private Context mContext;
- private Handler mHandler = new Handler();
-
- private int mStartId;
-
- /**
- * Access must be synchronized, because there are accesses from the Controller callback
- */
- /*package*/ static HashMap<Long,AccountSyncReport> mSyncReports =
- new HashMap<Long,AccountSyncReport>();
-
- public static void actionReschedule(Context context) {
- Intent i = new Intent();
- i.setClass(context, MailService.class);
- i.setAction(MailService.ACTION_RESCHEDULE);
- context.startService(i);
- }
-
- public static void actionCancel(Context context) {
- Intent i = new Intent();
- i.setClass(context, MailService.class);
- i.setAction(MailService.ACTION_CANCEL);
- context.startService(i);
- }
-
- /**
- * Entry point for AttachmentDownloadService to ask that pending mail be sent
- * @param context the caller's context
- * @param accountId the account whose pending mail should be sent
- */
- public static void actionSendPendingMail(Context context, long accountId) {
- Intent i = new Intent();
- i.setClass(context, MailService.class);
- i.setAction(MailService.ACTION_SEND_PENDING_MAIL);
- i.putExtra(MailService.EXTRA_ACCOUNT, accountId);
- context.startService(i);
- }
-
- /**
- * Reset new message counts for one or all accounts. This clears both our local copy and
- * the values (if any) stored in the account records.
- *
- * @param accountId account to clear, or -1 for all accounts
- */
- public static void resetNewMessageCount(final Context context, final long accountId) {
- synchronized (mSyncReports) {
- for (AccountSyncReport report : mSyncReports.values()) {
- if (accountId == -1 || accountId == report.accountId) {
- report.unseenMessageCount = 0;
- report.lastUnseenMessageCount = 0;
- }
- }
- }
- // Clear notification
- NotificationController.getInstance(context).cancelNewMessageNotification(accountId);
-
- // now do the database - all accounts, or just one of them
- Utility.runAsync(new Runnable() {
- @Override
- public void run() {
- Uri uri = Account.RESET_NEW_MESSAGE_COUNT_URI;
- if (accountId != -1) {
- uri = ContentUris.withAppendedId(uri, accountId);
- }
- context.getContentResolver().update(uri, null, null, null);
- }
- });
- }
-
- /**
- * Entry point for asynchronous message services (e.g. push mode) to post notifications of new
- * messages. This assumes that the push provider has already synced the messages into the
- * appropriate database - this simply triggers the notification mechanism.
- *
- * @param context a context
- * @param accountId the id of the account that is reporting new messages
- */
- public static void actionNotifyNewMessages(Context context, long accountId) {
- Intent i = new Intent(ACTION_NOTIFY_MAIL);
- i.setClass(context, MailService.class);
- i.putExtra(EXTRA_ACCOUNT, accountId);
- context.startService(i);
- }
-
- /*package*/ static MailService getMailServiceForTest() {
- return sMailService;
- }
-
- @Override
- public int onStartCommand(final Intent intent, int flags, final int startId) {
- super.onStartCommand(intent, flags, startId);
-
- // Save the service away (for unit tests)
- sMailService = this;
-
- // Restore accounts, if it has not happened already
- AccountBackupRestore.restoreAccountsIfNeeded(this);
-
- Utility.runAsync(new Runnable() {
- @Override
- public void run() {
- reconcilePopImapAccountsSync(MailService.this);
- }
- });
-
- // TODO this needs to be passed through the controller and back to us
- mStartId = startId;
- String action = intent.getAction();
- final long accountId = intent.getLongExtra(EXTRA_ACCOUNT, -1);
-
- mController = Controller.getInstance(this);
- mController.addResultCallback(mControllerCallback);
- mContentResolver = getContentResolver();
- mContext = this;
-
- final AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
-
- if (ACTION_CHECK_MAIL.equals(action)) {
- // DB access required to satisfy this intent, so offload from UI thread
- Utility.runAsync(new Runnable() {
- @Override
- public void run() {
- // If we have the data, restore the last-sync-times for each account
- // These are cached in the wakeup intent in case the process was killed.
- restoreSyncReports(intent);
-
- // Sync a specific account if given
- if (Email.DEBUG) {
- Log.d(LOG_TAG, "action: check mail for id=" + accountId);
- }
- if (accountId >= 0) {
- setWatchdog(accountId, alarmManager);
- }
-
- // Start sync if account is given && bg data enabled && account has sync enabled
- boolean syncStarted = false;
- if (accountId != -1 && isBackgroundDataEnabled()) {
- synchronized(mSyncReports) {
- for (AccountSyncReport report: mSyncReports.values()) {
- if (report.accountId == accountId) {
- if (report.syncEnabled) {
- syncStarted = syncOneAccount(mController, accountId,
- startId);
- }
- break;
- }
- }
- }
- }
-
- // Reschedule if we didn't start sync.
- if (!syncStarted) {
- // Prevent runaway on the current account by pretending it updated
- if (accountId != -1) {
- updateAccountReport(accountId, 0);
- }
- // Find next account to sync, and reschedule
- reschedule(alarmManager);
- // Stop the service, unless actually syncing (which will stop the service)
- stopSelf(startId);
- }
- }
- });
- }
- else if (ACTION_CANCEL.equals(action)) {
- if (Email.DEBUG) {
- Log.d(LOG_TAG, "action: cancel");
- }
- cancel();
- stopSelf(startId);
- }
- else if (ACTION_SEND_PENDING_MAIL.equals(action)) {
- if (Email.DEBUG) {
- Log.d(LOG_TAG, "action: send pending mail");
- }
- Utility.runAsync(new Runnable() {
- public void run() {
- mController.sendPendingMessages(accountId);
- }
- });
- stopSelf(startId);
- }
- else if (ACTION_RESCHEDULE.equals(action)) {
- if (Email.DEBUG) {
- Log.d(LOG_TAG, "action: reschedule");
- }
- final NotificationController nc = NotificationController.getInstance(this);
- // DB access required to satisfy this intent, so offload from UI thread
- Utility.runAsync(new Runnable() {
- @Override
- public void run() {
- // Clear all notifications, in case account list has changed.
- //
- // TODO Clear notifications for non-existing accounts. Now that we have
- // separate notifications for each account, NotificationController should be
- // able to do that.
- nc.cancelNewMessageNotification(-1);
-
- // When called externally, we refresh the sync reports table to pick up
- // any changes in the account list or account settings
- refreshSyncReports();
- // Finally, scan for the next needing update, and set an alarm for it
- reschedule(alarmManager);
- stopSelf(startId);
- }
- });
- } else if (ACTION_NOTIFY_MAIL.equals(action)) {
- // DB access required to satisfy this intent, so offload from UI thread
- Utility.runAsync(new Runnable() {
- @Override
- public void run() {
- // Get the current new message count
- Cursor c = mContentResolver.query(
- ContentUris.withAppendedId(Account.CONTENT_URI, accountId),
- NEW_MESSAGE_COUNT_PROJECTION, null, null, null);
- int newMessageCount = 0;
- try {
- if (c.moveToFirst()) {
- newMessageCount = c.getInt(0);
- updateAccountReport(accountId, newMessageCount);
- notifyNewMessages(accountId);
- }
- } finally {
- c.close();
- }
- if (Email.DEBUG) {
- Log.d(LOG_TAG, "notify accountId=" + Long.toString(accountId)
- + " count=" + newMessageCount);
- }
- stopSelf(startId);
- }
- });
- }
-
- // Returning START_NOT_STICKY means that if a mail check is killed (e.g. due to memory
- // pressure, there will be no explicit restart. This is OK; Note that we set a watchdog
- // alarm before each mailbox check. If the mailbox check never completes, the watchdog
- // will fire and get things running again.
- return START_NOT_STICKY;
- }
-
- @Override
- public IBinder onBind(Intent intent) {
- return null;
- }
-
- @Override
- public void onDestroy() {
- super.onDestroy();
- Controller.getInstance(getApplication()).removeResultCallback(mControllerCallback);
- }
-
- private void cancel() {
- AlarmManager alarmMgr = (AlarmManager)getSystemService(Context.ALARM_SERVICE);
- PendingIntent pi = createAlarmIntent(-1, null, false);
- alarmMgr.cancel(pi);
- }
-
- /**
- * Refresh the sync reports, to pick up any changes in the account list or account settings.
- */
- /*package*/ void refreshSyncReports() {
- synchronized (mSyncReports) {
- // Make shallow copy of sync reports so we can recover the prev sync times
- HashMap<Long,AccountSyncReport> oldSyncReports =
- new HashMap<Long,AccountSyncReport>(mSyncReports);
-
- // Delete the sync reports to force a refresh from live account db data
- setupSyncReportsLocked(SYNC_REPORTS_RESET, this);
-
- // Restore prev-sync & next-sync times for any reports in the new list
- for (AccountSyncReport newReport : mSyncReports.values()) {
- AccountSyncReport oldReport = oldSyncReports.get(newReport.accountId);
- if (oldReport != null) {
- newReport.prevSyncTime = oldReport.prevSyncTime;
- if (newReport.syncInterval > 0 && newReport.prevSyncTime != 0) {
- newReport.nextSyncTime =
- newReport.prevSyncTime + (newReport.syncInterval * 1000 * 60);
- }
- }
- }
- }
- }
-
- /**
- * Create and send an alarm with the entire list. This also sends a list of known last-sync
- * times with the alarm, so if we are killed between alarms, we don't lose this info.
- *
- * @param alarmMgr passed in so we can mock for testing.
- */
- /* package */ void reschedule(AlarmManager alarmMgr) {
- // restore the reports if lost
- setupSyncReports(SYNC_REPORTS_ALL_ACCOUNTS_IF_EMPTY);
- synchronized (mSyncReports) {
- int numAccounts = mSyncReports.size();
- long[] accountInfo = new long[numAccounts * 2]; // pairs of { accountId, lastSync }
- int accountInfoIndex = 0;
-
- long nextCheckTime = Long.MAX_VALUE;
- AccountSyncReport nextAccount = null;
- long timeNow = SystemClock.elapsedRealtime();
-
- for (AccountSyncReport report : mSyncReports.values()) {
- if (report.syncInterval <= 0) { // no timed checks - skip
- continue;
- }
- long prevSyncTime = report.prevSyncTime;
- long nextSyncTime = report.nextSyncTime;
-
- // select next account to sync
- if ((prevSyncTime == 0) || (nextSyncTime < timeNow)) { // never checked, or overdue
- nextCheckTime = 0;
- nextAccount = report;
- } else if (nextSyncTime < nextCheckTime) { // next to be checked
- nextCheckTime = nextSyncTime;
- nextAccount = report;
- }
- // collect last-sync-times for all accounts
- // this is using pairs of {long,long} to simplify passing in a bundle
- accountInfo[accountInfoIndex++] = report.accountId;
- accountInfo[accountInfoIndex++] = report.prevSyncTime;
- }
-
- // Clear out any unused elements in the array
- while (accountInfoIndex < accountInfo.length) {
- accountInfo[accountInfoIndex++] = -1;
- }
-
- // set/clear alarm as needed
- long idToCheck = (nextAccount == null) ? -1 : nextAccount.accountId;
- PendingIntent pi = createAlarmIntent(idToCheck, accountInfo, false);
-
- if (nextAccount == null) {
- alarmMgr.cancel(pi);
- if (Email.DEBUG) {
- Log.d(LOG_TAG, "reschedule: alarm cancel - no account to check");
- }
- } else {
- alarmMgr.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, nextCheckTime, pi);
- if (Email.DEBUG) {
- Log.d(LOG_TAG, "reschedule: alarm set at " + nextCheckTime
- + " for " + nextAccount);
- }
- }
- }
- }
-
- /**
- * Create a watchdog alarm and set it. This is used in case a mail check fails (e.g. we are
- * killed by the system due to memory pressure.) Normally, a mail check will complete and
- * the watchdog will be replaced by the call to reschedule().
- * @param accountId the account we were trying to check
- * @param alarmMgr system alarm manager
- */
- private void setWatchdog(long accountId, AlarmManager alarmMgr) {
- PendingIntent pi = createAlarmIntent(accountId, null, true);
- long timeNow = SystemClock.elapsedRealtime();
- long nextCheckTime = timeNow + WATCHDOG_DELAY;
- alarmMgr.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, nextCheckTime, pi);
- }
-
- /**
- * Return a pending intent for use by this alarm. Most of the fields must be the same
- * (in order for the intent to be recognized by the alarm manager) but the extras can
- * be different, and are passed in here as parameters.
- */
- /* package */ PendingIntent createAlarmIntent(long checkId, long[] accountInfo,
- boolean isWatchdog) {
- Intent i = new Intent();
- i.setClass(this, MailService.class);
- i.setAction(ACTION_CHECK_MAIL);
- i.putExtra(EXTRA_ACCOUNT, checkId);
- i.putExtra(EXTRA_ACCOUNT_INFO, accountInfo);
- if (isWatchdog) {
- i.putExtra(EXTRA_DEBUG_WATCHDOG, true);
- }
- PendingIntent pi = PendingIntent.getService(this, 0, i, PendingIntent.FLAG_UPDATE_CURRENT);
- return pi;
- }
-
- /**
- * Start a controller sync for a specific account
- *
- * @param controller The controller to do the sync work
- * @param checkAccountId the account Id to try and check
- * @param startId the id of this service launch
- * @return true if mail checking has started, false if it could not (e.g. bad account id)
- */
- private boolean syncOneAccount(Controller controller, long checkAccountId, int startId) {
- long inboxId = Mailbox.findMailboxOfType(this, checkAccountId, Mailbox.TYPE_INBOX);
- if (inboxId == Mailbox.NO_MAILBOX) {
- return false;
- } else {
- controller.serviceCheckMail(checkAccountId, inboxId, startId);
- return true;
- }
- }
-
- /**
- * Note: Times are relative to SystemClock.elapsedRealtime()
- *
- * TODO: Look more closely at syncEnabled and see if we can simply coalesce it into
- * syncInterval (e.g. if !syncEnabled, set syncInterval to -1).
- */
- /*package*/ static class AccountSyncReport {
- long accountId;
- long prevSyncTime; // 0 == unknown
- long nextSyncTime; // 0 == ASAP -1 == don't sync
-
- /** # of "unseen" messages to show in notification */
- int unseenMessageCount;
-
- /**
- * # of unseen, the value shown on the last notification. Used to
- * calculate "the number of messages that have just been fetched".
- *
- * TODO It's a sort of cheating. Should we use the "real" number? The only difference
- * is the first notification after reboot / process restart.
- */
- int lastUnseenMessageCount;
-
- int syncInterval;
- boolean notify;
-
- boolean syncEnabled; // whether auto sync is enabled for this account
-
- /** # of messages that have just been fetched */
- int getJustFetchedMessageCount() {
- return unseenMessageCount - lastUnseenMessageCount;
- }
-
- @Override
- public String toString() {
- return "id=" + accountId
- + " prevSync=" + prevSyncTime + " nextSync=" + nextSyncTime + " numUnseen="
- + unseenMessageCount;
- }
- }
-
- /**
- * scan accounts to create a list of { acct, prev sync, next sync, #new }
- * use this to create a fresh copy. assumes all accounts need sync
- *
- * @param accountId -1 will rebuild the list if empty. other values will force loading
- * of a single account (e.g if it was created after the original list population)
- */
- /* package */ void setupSyncReports(long accountId) {
- synchronized (mSyncReports) {
- setupSyncReportsLocked(accountId, mContext);
- }
- }
-
- /**
- * Handle the work of setupSyncReports. Must be synchronized on mSyncReports.
- */
- /*package*/ void setupSyncReportsLocked(long accountId, Context context) {
- ContentResolver resolver = context.getContentResolver();
- if (accountId == SYNC_REPORTS_RESET) {
- // For test purposes, force refresh of mSyncReports
- mSyncReports.clear();
- accountId = SYNC_REPORTS_ALL_ACCOUNTS_IF_EMPTY;
- } else if (accountId == SYNC_REPORTS_ALL_ACCOUNTS_IF_EMPTY) {
- // -1 == reload the list if empty, otherwise exit immediately
- if (mSyncReports.size() > 0) {
- return;
- }
- } else {
- // load a single account if it doesn't already have a sync record
- if (mSyncReports.containsKey(accountId)) {
- return;
- }
- }
-
- // setup to add a single account or all accounts
- Uri uri;
- if (accountId == SYNC_REPORTS_ALL_ACCOUNTS_IF_EMPTY) {
- uri = Account.CONTENT_URI;
- } else {
- uri = ContentUris.withAppendedId(Account.CONTENT_URI, accountId);
- }
-
- final boolean oneMinuteRefresh
- = Preferences.getPreferences(this).getForceOneMinuteRefresh();
- if (oneMinuteRefresh) {
- Log.w(LOG_TAG, "One-minute refresh enabled.");
- }
-
- // We use a full projection here because we'll restore each account object from it
- Cursor c = resolver.query(uri, Account.CONTENT_PROJECTION, null, null, null);
- try {
- while (c.moveToNext()) {
- Account account = Account.getContent(c, Account.class);
- // The following sanity checks are primarily for the sake of ignoring non-user
- // accounts that may have been left behind e.g. by failed unit tests.
- // Properly-formed accounts will always pass these simple checks.
- if (TextUtils.isEmpty(account.mEmailAddress)
- || account.mHostAuthKeyRecv <= 0
- || account.mHostAuthKeySend <= 0) {
- continue;
- }
-
- // The account is OK, so proceed
- AccountSyncReport report = new AccountSyncReport();
- int syncInterval = account.mSyncInterval;
-
- // If we're not using MessagingController (EAS at this point), don't schedule syncs
- if (!mController.isMessagingController(account.mId)) {
- syncInterval = Account.CHECK_INTERVAL_NEVER;
- } else if (oneMinuteRefresh && syncInterval >= 0) {
- syncInterval = 1;
- }
-
- report.accountId = account.mId;
- report.prevSyncTime = 0;
- report.nextSyncTime = (syncInterval > 0) ? 0 : -1; // 0 == ASAP -1 == no sync
- report.unseenMessageCount = 0;
- report.lastUnseenMessageCount = 0;
-
- report.syncInterval = syncInterval;
- report.notify = (account.mFlags & Account.FLAGS_NOTIFY_NEW_MAIL) != 0;
-
- // See if the account is enabled for sync in AccountManager
- android.accounts.Account accountManagerAccount =
- new android.accounts.Account(account.mEmailAddress,
- Email.POP_IMAP_ACCOUNT_MANAGER_TYPE);
- report.syncEnabled = ContentResolver.getSyncAutomatically(accountManagerAccount,
- EmailProvider.EMAIL_AUTHORITY);
-
- // TODO lookup # new in inbox
- mSyncReports.put(report.accountId, report);
- }
- } finally {
- c.close();
- }
- }
-
- /**
- * Update list with a single account's sync times and unread count
- *
- * @param accountId the account being updated
- * @param newCount the number of new messages, or -1 if not being reported (don't update)
- * @return the report for the updated account, or null if it doesn't exist (e.g. deleted)
- */
- /* package */ AccountSyncReport updateAccountReport(long accountId, int newCount) {
- // restore the reports if lost
- setupSyncReports(accountId);
- synchronized (mSyncReports) {
- AccountSyncReport report = mSyncReports.get(accountId);
- if (report == null) {
- // discard result - there is no longer an account with this id
- Log.d(LOG_TAG, "No account to update for id=" + Long.toString(accountId));
- return null;
- }
-
- // report found - update it (note - editing the report while in-place in the hashmap)
- report.prevSyncTime = SystemClock.elapsedRealtime();
- if (report.syncInterval > 0) {
- report.nextSyncTime = report.prevSyncTime + (report.syncInterval * 1000 * 60);
- }
- if (newCount != -1) {
- report.unseenMessageCount = newCount;
- }
- if (Email.DEBUG) {
- Log.d(LOG_TAG, "update account " + report.toString());
- }
- return report;
- }
- }
-
- /**
- * when we receive an alarm, update the account sync reports list if necessary
- * this will be the case when if we have restarted the process and lost the data
- * in the global.
- *
- * @param restoreIntent the intent with the list
- */
- /* package */ void restoreSyncReports(Intent restoreIntent) {
- // restore the reports if lost
- setupSyncReports(SYNC_REPORTS_ALL_ACCOUNTS_IF_EMPTY);
- synchronized (mSyncReports) {
- long[] accountInfo = restoreIntent.getLongArrayExtra(EXTRA_ACCOUNT_INFO);
- if (accountInfo == null) {
- Log.d(LOG_TAG, "no data in intent to restore");
- return;
- }
- int accountInfoIndex = 0;
- int accountInfoLimit = accountInfo.length;
- while (accountInfoIndex < accountInfoLimit) {
- long accountId = accountInfo[accountInfoIndex++];
- long prevSync = accountInfo[accountInfoIndex++];
- AccountSyncReport report = mSyncReports.get(accountId);
- if (report != null) {
- if (report.prevSyncTime == 0) {
- report.prevSyncTime = prevSync;
- if (report.syncInterval > 0 && report.prevSyncTime != 0) {
- report.nextSyncTime =
- report.prevSyncTime + (report.syncInterval * 1000 * 60);
- }
- }
- }
- }
- }
- }
-
- class ControllerResults extends Controller.Result {
- @Override
- public void updateMailboxCallback(MessagingException result, long accountId,
- long mailboxId, int progress, int numNewMessages) {
- // First, look for authentication failures and notify
- //checkAuthenticationStatus(result, accountId);
- if (result != null || progress == 100) {
- // We only track the inbox here in the service - ignore other mailboxes
- long inboxId = Mailbox.findMailboxOfType(MailService.this,
- accountId, Mailbox.TYPE_INBOX);
- if (mailboxId == inboxId) {
- if (progress == 100) {
- updateAccountReport(accountId, numNewMessages);
- if (numNewMessages > 0) {
- notifyNewMessages(accountId);
- }
- } else {
- updateAccountReport(accountId, -1);
- }
- }
- }
- }
-
- @Override
- public void serviceCheckMailCallback(MessagingException result, long accountId,
- long mailboxId, int progress, long tag) {
- if (result != null || progress == 100) {
- if (result != null) {
- // the checkmail ended in an error. force an update of the refresh
- // time, so we don't just spin on this account
- updateAccountReport(accountId, -1);
- }
- AlarmManager alarmManager = (AlarmManager)getSystemService(Context.ALARM_SERVICE);
- reschedule(alarmManager);
- int serviceId = MailService.this.mStartId;
- if (tag != 0) {
- serviceId = (int) tag;
- }
- stopSelf(serviceId);
- }
- }
- }
-
- /**
- * Show "new message" notification for an account. (Notification is shown per account.)
- */
- private void notifyNewMessages(final long accountId) {
- final int unseenMessageCount;
- final int justFetchedCount;
- synchronized (mSyncReports) {
- AccountSyncReport report = mSyncReports.get(accountId);
- if (report == null || report.unseenMessageCount == 0 || !report.notify) {
- return;
- }
- unseenMessageCount = report.unseenMessageCount;
- justFetchedCount = report.getJustFetchedMessageCount();
- report.lastUnseenMessageCount = report.unseenMessageCount;
- }
-
- NotificationController.getInstance(this).showNewMessageNotification(accountId,
- unseenMessageCount, justFetchedCount);
- }
-
- /**
- * @see ConnectivityManager#getBackgroundDataSetting()
- */
- private boolean isBackgroundDataEnabled() {
- ConnectivityManager cm =
- (ConnectivityManager)getSystemService(Context.CONNECTIVITY_SERVICE);
- return cm.getBackgroundDataSetting();
- }
-
- public class EmailSyncStatusObserver implements SyncStatusObserver {
- public void onStatusChanged(int which) {
- // We ignore the argument (we can only get called in one case - when settings change)
- }
- }
-
- public static ArrayList<Account> getPopImapAccountList(Context context) {
- ArrayList<Account> providerAccounts = new ArrayList<Account>();
- Cursor c = context.getContentResolver().query(Account.CONTENT_URI, Account.ID_PROJECTION,
- null, null, null);
- try {
- while (c.moveToNext()) {
- long accountId = c.getLong(Account.CONTENT_ID_COLUMN);
- String protocol = Account.getProtocol(context, accountId);
- if ((protocol != null) && ("pop3".equals(protocol) || "imap".equals(protocol))) {
- Account account = Account.restoreAccountWithId(context, accountId);
- if (account != null) {
- providerAccounts.add(account);
- }
- }
- }
- } finally {
- c.close();
- }
- return providerAccounts;
- }
-
- private static final SingleRunningTask<Context> sReconcilePopImapAccountsSyncExecutor =
- new SingleRunningTask<Context>("ReconcilePopImapAccountsSync") {
- @Override
- protected void runInternal(Context context) {
- android.accounts.Account[] accountManagerAccounts = AccountManager.get(context)
- .getAccountsByType(Email.POP_IMAP_ACCOUNT_MANAGER_TYPE);
- ArrayList<Account> providerAccounts = getPopImapAccountList(context);
- MailService.reconcileAccountsWithAccountManager(context, providerAccounts,
- accountManagerAccounts, false, context.getContentResolver());
-
- }
- };
-
- /**
- * Reconcile POP/IMAP accounts.
- */
- public static void reconcilePopImapAccountsSync(Context context) {
- sReconcilePopImapAccountsSyncExecutor.run(context);
- }
-
- /**
- * Compare our account list (obtained from EmailProvider) with the account list owned by
- * AccountManager. If there are any orphans (an account in one list without a corresponding
- * account in the other list), delete the orphan, as these must remain in sync.
- *
- * Note that the duplication of account information is caused by the Email application's
- * incomplete integration with AccountManager.
- *
- * This function may not be called from the main/UI thread, because it makes blocking calls
- * into the account manager.
- *
- * @param context The context in which to operate
- * @param emailProviderAccounts the exchange provider accounts to work from
- * @param accountManagerAccounts The account manager accounts to work from
- * @param blockExternalChanges FOR TESTING ONLY - block backups, security changes, etc.
- * @param resolver the content resolver for making provider updates (injected for testability)
- */
- /* package */ public static void reconcileAccountsWithAccountManager(Context context,
- List<Account> emailProviderAccounts, android.accounts.Account[] accountManagerAccounts,
- boolean blockExternalChanges, ContentResolver resolver) {
- // First, look through our EmailProvider accounts to make sure there's a corresponding
- // AccountManager account
- boolean accountsDeleted = false;
- for (Account providerAccount: emailProviderAccounts) {
- String providerAccountName = providerAccount.mEmailAddress;
- boolean found = false;
- for (android.accounts.Account accountManagerAccount: accountManagerAccounts) {
- if (accountManagerAccount.name.equalsIgnoreCase(providerAccountName)) {
- found = true;
- break;
- }
- }
- if (!found) {
- if ((providerAccount.mFlags & Account.FLAGS_INCOMPLETE) != 0) {
- if (Email.DEBUG) {
- Log.d(LOG_TAG, "Account reconciler noticed incomplete account; ignoring");
- }
- continue;
- }
- // This account has been deleted in the AccountManager!
- Log.d(LOG_TAG, "Account deleted in AccountManager; deleting from provider: " +
- providerAccountName);
- // TODO This will orphan downloaded attachments; need to handle this
- resolver.delete(ContentUris.withAppendedId(Account.CONTENT_URI,
- providerAccount.mId), null, null);
- accountsDeleted = true;
- }
- }
- // Now, look through AccountManager accounts to make sure we have a corresponding cached EAS
- // account from EmailProvider
- for (android.accounts.Account accountManagerAccount: accountManagerAccounts) {
- String accountManagerAccountName = accountManagerAccount.name;
- boolean found = false;
- for (Account cachedEasAccount: emailProviderAccounts) {
- if (cachedEasAccount.mEmailAddress.equalsIgnoreCase(accountManagerAccountName)) {
- found = true;
- }
- }
- if (!found) {
- // This account has been deleted from the EmailProvider database
- Log.d(LOG_TAG, "Account deleted from provider; deleting from AccountManager: " +
- accountManagerAccountName);
- // Delete the account
- AccountManagerFuture<Boolean> blockingResult = AccountManager.get(context)
- .removeAccount(accountManagerAccount, null, null);
- try {
- // Note: All of the potential errors from removeAccount() are simply logged
- // here, as there is nothing to actually do about them.
- blockingResult.getResult();
- } catch (OperationCanceledException e) {
- Log.w(Email.LOG_TAG, e.toString());
- } catch (AuthenticatorException e) {
- Log.w(Email.LOG_TAG, e.toString());
- } catch (IOException e) {
- Log.w(Email.LOG_TAG, e.toString());
- }
- accountsDeleted = true;
- }
- }
- // If we changed the list of accounts, refresh the backup & security settings
- if (!blockExternalChanges && accountsDeleted) {
- AccountBackupRestore.backupAccounts(context);
- SecurityPolicy.getInstance(context).reducePolicies();
- Email.setNotifyUiAccountsChanged(true);
- MailService.actionReschedule(context);
- }
- }
-
- public static void setupAccountManagerAccount(Context context, EmailContent.Account account,
- boolean email, boolean calendar, boolean contacts,
- AccountManagerCallback<Bundle> callback) {
- Bundle options = new Bundle();
- HostAuth hostAuthRecv = HostAuth.restoreHostAuthWithId(context, account.mHostAuthKeyRecv);
- // Set up username/password
- options.putString(EasAuthenticatorService.OPTIONS_USERNAME, account.mEmailAddress);
- options.putString(EasAuthenticatorService.OPTIONS_PASSWORD, hostAuthRecv.mPassword);
- options.putBoolean(EasAuthenticatorService.OPTIONS_CONTACTS_SYNC_ENABLED, contacts);
- options.putBoolean(EasAuthenticatorService.OPTIONS_CALENDAR_SYNC_ENABLED, calendar);
- options.putBoolean(EasAuthenticatorService.OPTIONS_EMAIL_SYNC_ENABLED, email);
- String accountType = hostAuthRecv.mProtocol.equals("eas") ?
- Email.EXCHANGE_ACCOUNT_MANAGER_TYPE :
- Email.POP_IMAP_ACCOUNT_MANAGER_TYPE;
- AccountManager.get(context).addAccount(accountType, null, null, options, null, callback,
- null);
- }
-}
+/* + * 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.service; + +import com.android.email.AccountBackupRestore; +import com.android.email.Controller; +import com.android.email.Email; +import com.android.email.NotificationController; +import com.android.email.Preferences; +import com.android.email.SecurityPolicy; +import com.android.email.SingleRunningTask; +import com.android.email.Utility; +import com.android.email.mail.MessagingException; +import com.android.email.provider.EmailContent; +import com.android.email.provider.EmailProvider; +import com.android.email.provider.EmailContent.Account; +import com.android.email.provider.EmailContent.AccountColumns; +import com.android.email.provider.EmailContent.HostAuth; +import com.android.email.provider.EmailContent.Mailbox; +import com.android.emailcommon.utility.AccountReconciler; + +import android.accounts.AccountManager; +import android.accounts.AccountManagerCallback; +import android.app.AlarmManager; +import android.app.PendingIntent; +import android.app.Service; +import android.content.ContentResolver; +import android.content.ContentUris; +import android.content.Context; +import android.content.Intent; +import android.content.SyncStatusObserver; +import android.database.Cursor; +import android.net.ConnectivityManager; +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.os.IBinder; +import android.os.SystemClock; +import android.text.TextUtils; +import android.util.Log; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +/** + * Background service for refreshing non-push email accounts. + * + * TODO: Convert to IntentService to move *all* work off the UI thread, serialize work, and avoid + * possible problems with out-of-order startId processing. + */ +public class MailService extends Service { + private static final String LOG_TAG = "Email-MailService"; + + private static final String ACTION_CHECK_MAIL = + "com.android.email.intent.action.MAIL_SERVICE_WAKEUP"; + private static final String ACTION_RESCHEDULE = + "com.android.email.intent.action.MAIL_SERVICE_RESCHEDULE"; + private static final String ACTION_CANCEL = + "com.android.email.intent.action.MAIL_SERVICE_CANCEL"; + private static final String ACTION_NOTIFY_MAIL = + "com.android.email.intent.action.MAIL_SERVICE_NOTIFY"; + private static final String ACTION_SEND_PENDING_MAIL = + "com.android.email.intent.action.MAIL_SERVICE_SEND_PENDING"; + private static final String ACTION_DELETE_EXCHANGE_ACCOUNTS = + "com.android.email.intent.action.MAIL_SERVICE_DELETE_EXCHANGE_ACCOUNTS"; + + private static final String EXTRA_ACCOUNT = "com.android.email.intent.extra.ACCOUNT"; + private static final String EXTRA_ACCOUNT_INFO = "com.android.email.intent.extra.ACCOUNT_INFO"; + private static final String EXTRA_DEBUG_WATCHDOG = "com.android.email.intent.extra.WATCHDOG"; + + private static final int WATCHDOG_DELAY = 10 * 60 * 1000; // 10 minutes + + // Sentinel value asking to update mSyncReports if it's currently empty + /*package*/ static final int SYNC_REPORTS_ALL_ACCOUNTS_IF_EMPTY = -1; + // Sentinel value asking that mSyncReports be rebuilt + /*package*/ static final int SYNC_REPORTS_RESET = -2; + + private static final String[] NEW_MESSAGE_COUNT_PROJECTION = + new String[] {AccountColumns.NEW_MESSAGE_COUNT}; + + private static MailService sMailService; + + /*package*/ Controller mController; + private final Controller.Result mControllerCallback = new ControllerResults(); + private ContentResolver mContentResolver; + private Context mContext; + private Handler mHandler = new Handler(); + + private int mStartId; + + /** + * Access must be synchronized, because there are accesses from the Controller callback + */ + /*package*/ static HashMap<Long,AccountSyncReport> mSyncReports = + new HashMap<Long,AccountSyncReport>(); + + public static void actionReschedule(Context context) { + Intent i = new Intent(); + i.setClass(context, MailService.class); + i.setAction(MailService.ACTION_RESCHEDULE); + context.startService(i); + } + + public static void actionCancel(Context context) { + Intent i = new Intent(); + i.setClass(context, MailService.class); + i.setAction(MailService.ACTION_CANCEL); + context.startService(i); + } + + public static void actionDeleteExchangeAccounts(Context context) { + Intent i = new Intent(); + i.setClass(context, MailService.class); + i.setAction(MailService.ACTION_DELETE_EXCHANGE_ACCOUNTS); + context.startService(i); + } + + /** + * Entry point for AttachmentDownloadService to ask that pending mail be sent + * @param context the caller's context + * @param accountId the account whose pending mail should be sent + */ + public static void actionSendPendingMail(Context context, long accountId) { + Intent i = new Intent(); + i.setClass(context, MailService.class); + i.setAction(MailService.ACTION_SEND_PENDING_MAIL); + i.putExtra(MailService.EXTRA_ACCOUNT, accountId); + context.startService(i); + } + + /** + * Reset new message counts for one or all accounts. This clears both our local copy and + * the values (if any) stored in the account records. + * + * @param accountId account to clear, or -1 for all accounts + */ + public static void resetNewMessageCount(final Context context, final long accountId) { + synchronized (mSyncReports) { + for (AccountSyncReport report : mSyncReports.values()) { + if (accountId == -1 || accountId == report.accountId) { + report.unseenMessageCount = 0; + report.lastUnseenMessageCount = 0; + } + } + } + // Clear notification + NotificationController.getInstance(context).cancelNewMessageNotification(accountId); + + // now do the database - all accounts, or just one of them + Utility.runAsync(new Runnable() { + @Override + public void run() { + Uri uri = Account.RESET_NEW_MESSAGE_COUNT_URI; + if (accountId != -1) { + uri = ContentUris.withAppendedId(uri, accountId); + } + context.getContentResolver().update(uri, null, null, null); + } + }); + } + + /** + * Entry point for asynchronous message services (e.g. push mode) to post notifications of new + * messages. This assumes that the push provider has already synced the messages into the + * appropriate database - this simply triggers the notification mechanism. + * + * @param context a context + * @param accountId the id of the account that is reporting new messages + */ + public static void actionNotifyNewMessages(Context context, long accountId) { + Intent i = new Intent(ACTION_NOTIFY_MAIL); + i.setClass(context, MailService.class); + i.putExtra(EXTRA_ACCOUNT, accountId); + context.startService(i); + } + + /*package*/ static MailService getMailServiceForTest() { + return sMailService; + } + + @Override + public int onStartCommand(final Intent intent, int flags, final int startId) { + super.onStartCommand(intent, flags, startId); + + // Save the service away (for unit tests) + sMailService = this; + + // Restore accounts, if it has not happened already + AccountBackupRestore.restoreAccountsIfNeeded(this); + + Utility.runAsync(new Runnable() { + @Override + public void run() { + reconcilePopImapAccountsSync(MailService.this); + } + }); + + // TODO this needs to be passed through the controller and back to us + mStartId = startId; + String action = intent.getAction(); + final long accountId = intent.getLongExtra(EXTRA_ACCOUNT, -1); + + mController = Controller.getInstance(this); + mController.addResultCallback(mControllerCallback); + mContentResolver = getContentResolver(); + mContext = this; + + final AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE); + + if (ACTION_CHECK_MAIL.equals(action)) { + // DB access required to satisfy this intent, so offload from UI thread + Utility.runAsync(new Runnable() { + @Override + public void run() { + // If we have the data, restore the last-sync-times for each account + // These are cached in the wakeup intent in case the process was killed. + restoreSyncReports(intent); + + // Sync a specific account if given + if (Email.DEBUG) { + Log.d(LOG_TAG, "action: check mail for id=" + accountId); + } + if (accountId >= 0) { + setWatchdog(accountId, alarmManager); + } + + // Start sync if account is given && bg data enabled && account has sync enabled + boolean syncStarted = false; + if (accountId != -1 && isBackgroundDataEnabled()) { + synchronized(mSyncReports) { + for (AccountSyncReport report: mSyncReports.values()) { + if (report.accountId == accountId) { + if (report.syncEnabled) { + syncStarted = syncOneAccount(mController, accountId, + startId); + } + break; + } + } + } + } + + // Reschedule if we didn't start sync. + if (!syncStarted) { + // Prevent runaway on the current account by pretending it updated + if (accountId != -1) { + updateAccountReport(accountId, 0); + } + // Find next account to sync, and reschedule + reschedule(alarmManager); + // Stop the service, unless actually syncing (which will stop the service) + stopSelf(startId); + } + } + }); + } + else if (ACTION_CANCEL.equals(action)) { + if (Email.DEBUG) { + Log.d(LOG_TAG, "action: cancel"); + } + cancel(); + stopSelf(startId); + } + else if (ACTION_DELETE_EXCHANGE_ACCOUNTS.equals(action)) { + if (Email.DEBUG) { + Log.d(LOG_TAG, "action: delete exchange accounts"); + } + Utility.runAsync(new Runnable() { + public void run() { + Cursor c = mContentResolver.query(Account.CONTENT_URI, Account.ID_PROJECTION, + null, null, null); + try { + while (c.moveToNext()) { + long accountId = c.getLong(Account.ID_PROJECTION_COLUMN); + if ("eas".equals(Account.getProtocol(mContext, accountId))) { + // Always log this + Log.d(LOG_TAG, "Deleting EAS account: " + accountId); + mController.deleteAccountSync(accountId, mContext); + } + } + } finally { + c.close(); + } + } + }); + stopSelf(startId); + } + else if (ACTION_SEND_PENDING_MAIL.equals(action)) { + if (Email.DEBUG) { + Log.d(LOG_TAG, "action: send pending mail"); + } + Utility.runAsync(new Runnable() { + public void run() { + mController.sendPendingMessages(accountId); + } + }); + stopSelf(startId); + } + else if (ACTION_RESCHEDULE.equals(action)) { + if (Email.DEBUG) { + Log.d(LOG_TAG, "action: reschedule"); + } + final NotificationController nc = NotificationController.getInstance(this); + // DB access required to satisfy this intent, so offload from UI thread + Utility.runAsync(new Runnable() { + @Override + public void run() { + // Clear all notifications, in case account list has changed. + // + // TODO Clear notifications for non-existing accounts. Now that we have + // separate notifications for each account, NotificationController should be + // able to do that. + nc.cancelNewMessageNotification(-1); + + // When called externally, we refresh the sync reports table to pick up + // any changes in the account list or account settings + refreshSyncReports(); + // Finally, scan for the next needing update, and set an alarm for it + reschedule(alarmManager); + stopSelf(startId); + } + }); + } else if (ACTION_NOTIFY_MAIL.equals(action)) { + // DB access required to satisfy this intent, so offload from UI thread + Utility.runAsync(new Runnable() { + @Override + public void run() { + // Get the current new message count + Cursor c = mContentResolver.query( + ContentUris.withAppendedId(Account.CONTENT_URI, accountId), + NEW_MESSAGE_COUNT_PROJECTION, null, null, null); + int newMessageCount = 0; + try { + if (c.moveToFirst()) { + newMessageCount = c.getInt(0); + updateAccountReport(accountId, newMessageCount); + notifyNewMessages(accountId); + } + } finally { + c.close(); + } + if (Email.DEBUG) { + Log.d(LOG_TAG, "notify accountId=" + Long.toString(accountId) + + " count=" + newMessageCount); + } + stopSelf(startId); + } + }); + } + + // Returning START_NOT_STICKY means that if a mail check is killed (e.g. due to memory + // pressure, there will be no explicit restart. This is OK; Note that we set a watchdog + // alarm before each mailbox check. If the mailbox check never completes, the watchdog + // will fire and get things running again. + return START_NOT_STICKY; + } + + @Override + public IBinder onBind(Intent intent) { + return null; + } + + @Override + public void onDestroy() { + super.onDestroy(); + Controller.getInstance(getApplication()).removeResultCallback(mControllerCallback); + } + + private void cancel() { + AlarmManager alarmMgr = (AlarmManager)getSystemService(Context.ALARM_SERVICE); + PendingIntent pi = createAlarmIntent(-1, null, false); + alarmMgr.cancel(pi); + } + + /** + * Refresh the sync reports, to pick up any changes in the account list or account settings. + */ + /*package*/ void refreshSyncReports() { + synchronized (mSyncReports) { + // Make shallow copy of sync reports so we can recover the prev sync times + HashMap<Long,AccountSyncReport> oldSyncReports = + new HashMap<Long,AccountSyncReport>(mSyncReports); + + // Delete the sync reports to force a refresh from live account db data + setupSyncReportsLocked(SYNC_REPORTS_RESET, this); + + // Restore prev-sync & next-sync times for any reports in the new list + for (AccountSyncReport newReport : mSyncReports.values()) { + AccountSyncReport oldReport = oldSyncReports.get(newReport.accountId); + if (oldReport != null) { + newReport.prevSyncTime = oldReport.prevSyncTime; + if (newReport.syncInterval > 0 && newReport.prevSyncTime != 0) { + newReport.nextSyncTime = + newReport.prevSyncTime + (newReport.syncInterval * 1000 * 60); + } + } + } + } + } + + /** + * Create and send an alarm with the entire list. This also sends a list of known last-sync + * times with the alarm, so if we are killed between alarms, we don't lose this info. + * + * @param alarmMgr passed in so we can mock for testing. + */ + /* package */ void reschedule(AlarmManager alarmMgr) { + // restore the reports if lost + setupSyncReports(SYNC_REPORTS_ALL_ACCOUNTS_IF_EMPTY); + synchronized (mSyncReports) { + int numAccounts = mSyncReports.size(); + long[] accountInfo = new long[numAccounts * 2]; // pairs of { accountId, lastSync } + int accountInfoIndex = 0; + + long nextCheckTime = Long.MAX_VALUE; + AccountSyncReport nextAccount = null; + long timeNow = SystemClock.elapsedRealtime(); + + for (AccountSyncReport report : mSyncReports.values()) { + if (report.syncInterval <= 0) { // no timed checks - skip + continue; + } + long prevSyncTime = report.prevSyncTime; + long nextSyncTime = report.nextSyncTime; + + // select next account to sync + if ((prevSyncTime == 0) || (nextSyncTime < timeNow)) { // never checked, or overdue + nextCheckTime = 0; + nextAccount = report; + } else if (nextSyncTime < nextCheckTime) { // next to be checked + nextCheckTime = nextSyncTime; + nextAccount = report; + } + // collect last-sync-times for all accounts + // this is using pairs of {long,long} to simplify passing in a bundle + accountInfo[accountInfoIndex++] = report.accountId; + accountInfo[accountInfoIndex++] = report.prevSyncTime; + } + + // Clear out any unused elements in the array + while (accountInfoIndex < accountInfo.length) { + accountInfo[accountInfoIndex++] = -1; + } + + // set/clear alarm as needed + long idToCheck = (nextAccount == null) ? -1 : nextAccount.accountId; + PendingIntent pi = createAlarmIntent(idToCheck, accountInfo, false); + + if (nextAccount == null) { + alarmMgr.cancel(pi); + if (Email.DEBUG) { + Log.d(LOG_TAG, "reschedule: alarm cancel - no account to check"); + } + } else { + alarmMgr.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, nextCheckTime, pi); + if (Email.DEBUG) { + Log.d(LOG_TAG, "reschedule: alarm set at " + nextCheckTime + + " for " + nextAccount); + } + } + } + } + + /** + * Create a watchdog alarm and set it. This is used in case a mail check fails (e.g. we are + * killed by the system due to memory pressure.) Normally, a mail check will complete and + * the watchdog will be replaced by the call to reschedule(). + * @param accountId the account we were trying to check + * @param alarmMgr system alarm manager + */ + private void setWatchdog(long accountId, AlarmManager alarmMgr) { + PendingIntent pi = createAlarmIntent(accountId, null, true); + long timeNow = SystemClock.elapsedRealtime(); + long nextCheckTime = timeNow + WATCHDOG_DELAY; + alarmMgr.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, nextCheckTime, pi); + } + + /** + * Return a pending intent for use by this alarm. Most of the fields must be the same + * (in order for the intent to be recognized by the alarm manager) but the extras can + * be different, and are passed in here as parameters. + */ + /* package */ PendingIntent createAlarmIntent(long checkId, long[] accountInfo, + boolean isWatchdog) { + Intent i = new Intent(); + i.setClass(this, MailService.class); + i.setAction(ACTION_CHECK_MAIL); + i.putExtra(EXTRA_ACCOUNT, checkId); + i.putExtra(EXTRA_ACCOUNT_INFO, accountInfo); + if (isWatchdog) { + i.putExtra(EXTRA_DEBUG_WATCHDOG, true); + } + PendingIntent pi = PendingIntent.getService(this, 0, i, PendingIntent.FLAG_UPDATE_CURRENT); + return pi; + } + + /** + * Start a controller sync for a specific account + * + * @param controller The controller to do the sync work + * @param checkAccountId the account Id to try and check + * @param startId the id of this service launch + * @return true if mail checking has started, false if it could not (e.g. bad account id) + */ + private boolean syncOneAccount(Controller controller, long checkAccountId, int startId) { + long inboxId = Mailbox.findMailboxOfType(this, checkAccountId, Mailbox.TYPE_INBOX); + if (inboxId == Mailbox.NO_MAILBOX) { + return false; + } else { + controller.serviceCheckMail(checkAccountId, inboxId, startId); + return true; + } + } + + /** + * Note: Times are relative to SystemClock.elapsedRealtime() + * + * TODO: Look more closely at syncEnabled and see if we can simply coalesce it into + * syncInterval (e.g. if !syncEnabled, set syncInterval to -1). + */ + /*package*/ static class AccountSyncReport { + long accountId; + long prevSyncTime; // 0 == unknown + long nextSyncTime; // 0 == ASAP -1 == don't sync + + /** # of "unseen" messages to show in notification */ + int unseenMessageCount; + + /** + * # of unseen, the value shown on the last notification. Used to + * calculate "the number of messages that have just been fetched". + * + * TODO It's a sort of cheating. Should we use the "real" number? The only difference + * is the first notification after reboot / process restart. + */ + int lastUnseenMessageCount; + + int syncInterval; + boolean notify; + + boolean syncEnabled; // whether auto sync is enabled for this account + + /** # of messages that have just been fetched */ + int getJustFetchedMessageCount() { + return unseenMessageCount - lastUnseenMessageCount; + } + + @Override + public String toString() { + return "id=" + accountId + + " prevSync=" + prevSyncTime + " nextSync=" + nextSyncTime + " numUnseen=" + + unseenMessageCount; + } + } + + /** + * scan accounts to create a list of { acct, prev sync, next sync, #new } + * use this to create a fresh copy. assumes all accounts need sync + * + * @param accountId -1 will rebuild the list if empty. other values will force loading + * of a single account (e.g if it was created after the original list population) + */ + /* package */ void setupSyncReports(long accountId) { + synchronized (mSyncReports) { + setupSyncReportsLocked(accountId, mContext); + } + } + + /** + * Handle the work of setupSyncReports. Must be synchronized on mSyncReports. + */ + /*package*/ void setupSyncReportsLocked(long accountId, Context context) { + ContentResolver resolver = context.getContentResolver(); + if (accountId == SYNC_REPORTS_RESET) { + // For test purposes, force refresh of mSyncReports + mSyncReports.clear(); + accountId = SYNC_REPORTS_ALL_ACCOUNTS_IF_EMPTY; + } else if (accountId == SYNC_REPORTS_ALL_ACCOUNTS_IF_EMPTY) { + // -1 == reload the list if empty, otherwise exit immediately + if (mSyncReports.size() > 0) { + return; + } + } else { + // load a single account if it doesn't already have a sync record + if (mSyncReports.containsKey(accountId)) { + return; + } + } + + // setup to add a single account or all accounts + Uri uri; + if (accountId == SYNC_REPORTS_ALL_ACCOUNTS_IF_EMPTY) { + uri = Account.CONTENT_URI; + } else { + uri = ContentUris.withAppendedId(Account.CONTENT_URI, accountId); + } + + final boolean oneMinuteRefresh + = Preferences.getPreferences(this).getForceOneMinuteRefresh(); + if (oneMinuteRefresh) { + Log.w(LOG_TAG, "One-minute refresh enabled."); + } + + // We use a full projection here because we'll restore each account object from it + Cursor c = resolver.query(uri, Account.CONTENT_PROJECTION, null, null, null); + try { + while (c.moveToNext()) { + Account account = Account.getContent(c, Account.class); + // The following sanity checks are primarily for the sake of ignoring non-user + // accounts that may have been left behind e.g. by failed unit tests. + // Properly-formed accounts will always pass these simple checks. + if (TextUtils.isEmpty(account.mEmailAddress) + || account.mHostAuthKeyRecv <= 0 + || account.mHostAuthKeySend <= 0) { + continue; + } + + // The account is OK, so proceed + AccountSyncReport report = new AccountSyncReport(); + int syncInterval = account.mSyncInterval; + + // If we're not using MessagingController (EAS at this point), don't schedule syncs + if (!mController.isMessagingController(account.mId)) { + syncInterval = Account.CHECK_INTERVAL_NEVER; + } else if (oneMinuteRefresh && syncInterval >= 0) { + syncInterval = 1; + } + + report.accountId = account.mId; + report.prevSyncTime = 0; + report.nextSyncTime = (syncInterval > 0) ? 0 : -1; // 0 == ASAP -1 == no sync + report.unseenMessageCount = 0; + report.lastUnseenMessageCount = 0; + + report.syncInterval = syncInterval; + report.notify = (account.mFlags & Account.FLAGS_NOTIFY_NEW_MAIL) != 0; + + // See if the account is enabled for sync in AccountManager + android.accounts.Account accountManagerAccount = + new android.accounts.Account(account.mEmailAddress, + Email.POP_IMAP_ACCOUNT_MANAGER_TYPE); + report.syncEnabled = ContentResolver.getSyncAutomatically(accountManagerAccount, + EmailProvider.EMAIL_AUTHORITY); + + // TODO lookup # new in inbox + mSyncReports.put(report.accountId, report); + } + } finally { + c.close(); + } + } + + /** + * Update list with a single account's sync times and unread count + * + * @param accountId the account being updated + * @param newCount the number of new messages, or -1 if not being reported (don't update) + * @return the report for the updated account, or null if it doesn't exist (e.g. deleted) + */ + /* package */ AccountSyncReport updateAccountReport(long accountId, int newCount) { + // restore the reports if lost + setupSyncReports(accountId); + synchronized (mSyncReports) { + AccountSyncReport report = mSyncReports.get(accountId); + if (report == null) { + // discard result - there is no longer an account with this id + Log.d(LOG_TAG, "No account to update for id=" + Long.toString(accountId)); + return null; + } + + // report found - update it (note - editing the report while in-place in the hashmap) + report.prevSyncTime = SystemClock.elapsedRealtime(); + if (report.syncInterval > 0) { + report.nextSyncTime = report.prevSyncTime + (report.syncInterval * 1000 * 60); + } + if (newCount != -1) { + report.unseenMessageCount = newCount; + } + if (Email.DEBUG) { + Log.d(LOG_TAG, "update account " + report.toString()); + } + return report; + } + } + + /** + * when we receive an alarm, update the account sync reports list if necessary + * this will be the case when if we have restarted the process and lost the data + * in the global. + * + * @param restoreIntent the intent with the list + */ + /* package */ void restoreSyncReports(Intent restoreIntent) { + // restore the reports if lost + setupSyncReports(SYNC_REPORTS_ALL_ACCOUNTS_IF_EMPTY); + synchronized (mSyncReports) { + long[] accountInfo = restoreIntent.getLongArrayExtra(EXTRA_ACCOUNT_INFO); + if (accountInfo == null) { + Log.d(LOG_TAG, "no data in intent to restore"); + return; + } + int accountInfoIndex = 0; + int accountInfoLimit = accountInfo.length; + while (accountInfoIndex < accountInfoLimit) { + long accountId = accountInfo[accountInfoIndex++]; + long prevSync = accountInfo[accountInfoIndex++]; + AccountSyncReport report = mSyncReports.get(accountId); + if (report != null) { + if (report.prevSyncTime == 0) { + report.prevSyncTime = prevSync; + if (report.syncInterval > 0 && report.prevSyncTime != 0) { + report.nextSyncTime = + report.prevSyncTime + (report.syncInterval * 1000 * 60); + } + } + } + } + } + } + + class ControllerResults extends Controller.Result { + @Override + public void updateMailboxCallback(MessagingException result, long accountId, + long mailboxId, int progress, int numNewMessages) { + // First, look for authentication failures and notify + //checkAuthenticationStatus(result, accountId); + if (result != null || progress == 100) { + // We only track the inbox here in the service - ignore other mailboxes + long inboxId = Mailbox.findMailboxOfType(MailService.this, + accountId, Mailbox.TYPE_INBOX); + if (mailboxId == inboxId) { + if (progress == 100) { + updateAccountReport(accountId, numNewMessages); + if (numNewMessages > 0) { + notifyNewMessages(accountId); + } + } else { + updateAccountReport(accountId, -1); + } + } + } + } + + @Override + public void serviceCheckMailCallback(MessagingException result, long accountId, + long mailboxId, int progress, long tag) { + if (result != null || progress == 100) { + if (result != null) { + // the checkmail ended in an error. force an update of the refresh + // time, so we don't just spin on this account + updateAccountReport(accountId, -1); + } + AlarmManager alarmManager = (AlarmManager)getSystemService(Context.ALARM_SERVICE); + reschedule(alarmManager); + int serviceId = MailService.this.mStartId; + if (tag != 0) { + serviceId = (int) tag; + } + stopSelf(serviceId); + } + } + } + + /** + * Show "new message" notification for an account. (Notification is shown per account.) + */ + private void notifyNewMessages(final long accountId) { + final int unseenMessageCount; + final int justFetchedCount; + synchronized (mSyncReports) { + AccountSyncReport report = mSyncReports.get(accountId); + if (report == null || report.unseenMessageCount == 0 || !report.notify) { + return; + } + unseenMessageCount = report.unseenMessageCount; + justFetchedCount = report.getJustFetchedMessageCount(); + report.lastUnseenMessageCount = report.unseenMessageCount; + } + + NotificationController.getInstance(this).showNewMessageNotification(accountId, + unseenMessageCount, justFetchedCount); + } + + /** + * @see ConnectivityManager#getBackgroundDataSetting() + */ + private boolean isBackgroundDataEnabled() { + ConnectivityManager cm = + (ConnectivityManager)getSystemService(Context.CONNECTIVITY_SERVICE); + return cm.getBackgroundDataSetting(); + } + + public class EmailSyncStatusObserver implements SyncStatusObserver { + public void onStatusChanged(int which) { + // We ignore the argument (we can only get called in one case - when settings change) + } + } + + public static ArrayList<Account> getPopImapAccountList(Context context) { + ArrayList<Account> providerAccounts = new ArrayList<Account>(); + Cursor c = context.getContentResolver().query(Account.CONTENT_URI, Account.ID_PROJECTION, + null, null, null); + try { + while (c.moveToNext()) { + long accountId = c.getLong(Account.CONTENT_ID_COLUMN); + String protocol = Account.getProtocol(context, accountId); + if ((protocol != null) && ("pop3".equals(protocol) || "imap".equals(protocol))) { + Account account = Account.restoreAccountWithId(context, accountId); + if (account != null) { + providerAccounts.add(account); + } + } + } + } finally { + c.close(); + } + return providerAccounts; + } + + private static final SingleRunningTask<Context> sReconcilePopImapAccountsSyncExecutor = + new SingleRunningTask<Context>("ReconcilePopImapAccountsSync") { + @Override + protected void runInternal(Context context) { + android.accounts.Account[] accountManagerAccounts = AccountManager.get(context) + .getAccountsByType(Email.POP_IMAP_ACCOUNT_MANAGER_TYPE); + ArrayList<Account> providerAccounts = getPopImapAccountList(context); + MailService.reconcileAccountsWithAccountManager(context, providerAccounts, + accountManagerAccounts, false, context.getContentResolver()); + + } + }; + + /** + * Reconcile POP/IMAP accounts. + */ + public static void reconcilePopImapAccountsSync(Context context) { + sReconcilePopImapAccountsSyncExecutor.run(context); + } + + /** + * Handles a variety of cleanup actions that must be performed when an account has been deleted. + * This includes triggering an account backup, ensuring that security policies are properly + * reset, if necessary, notifying the UI of the change, and resetting scheduled syncs and + * notifications. + * @param context the caller's context + */ + public static void accountDeleted(Context context) { + AccountBackupRestore.backupAccounts(context); + SecurityPolicy.getInstance(context).reducePolicies(); + Email.setNotifyUiAccountsChanged(true); + MailService.actionReschedule(context); + } + + /** + * See Utility.reconcileAccounts for details + * @param context The context in which to operate + * @param emailProviderAccounts the exchange provider accounts to work from + * @param accountManagerAccounts The account manager accounts to work from + * @param blockExternalChanges FOR TESTING ONLY - block backups, security changes, etc. + * @param resolver the content resolver for making provider updates (injected for testability) + */ + /* package */ public static void reconcileAccountsWithAccountManager(Context context, + List<Account> emailProviderAccounts, android.accounts.Account[] accountManagerAccounts, + boolean blockExternalChanges, ContentResolver resolver) { + boolean accountsDeleted = AccountReconciler.reconcileAccounts(context, + emailProviderAccounts, accountManagerAccounts, resolver); + // If we changed the list of accounts, refresh the backup & security settings + if (!blockExternalChanges && accountsDeleted) { + accountDeleted(context); + } + } + + public static void setupAccountManagerAccount(Context context, EmailContent.Account account, + boolean email, boolean calendar, boolean contacts, + AccountManagerCallback<Bundle> callback) { + Bundle options = new Bundle(); + HostAuth hostAuthRecv = HostAuth.restoreHostAuthWithId(context, account.mHostAuthKeyRecv); + // Set up username/password + options.putString(EasAuthenticatorService.OPTIONS_USERNAME, account.mEmailAddress); + options.putString(EasAuthenticatorService.OPTIONS_PASSWORD, hostAuthRecv.mPassword); + options.putBoolean(EasAuthenticatorService.OPTIONS_CONTACTS_SYNC_ENABLED, contacts); + options.putBoolean(EasAuthenticatorService.OPTIONS_CALENDAR_SYNC_ENABLED, calendar); + options.putBoolean(EasAuthenticatorService.OPTIONS_EMAIL_SYNC_ENABLED, email); + String accountType = hostAuthRecv.mProtocol.equals("eas") ? + Email.EXCHANGE_ACCOUNT_MANAGER_TYPE : + Email.POP_IMAP_ACCOUNT_MANAGER_TYPE; + AccountManager.get(context).addAccount(accountType, null, null, options, null, callback, + null); + } +} diff --git a/src/com/android/emailcommon/service/AccountServiceProxy.java b/src/com/android/emailcommon/service/AccountServiceProxy.java new file mode 100644 index 000000000..3f6afbe40 --- /dev/null +++ b/src/com/android/emailcommon/service/AccountServiceProxy.java @@ -0,0 +1,105 @@ +/* + * 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.emailcommon.service; + +import android.content.Context; +import android.content.Intent; +import android.os.IBinder; +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)); + } + + @Override + public void onConnected(IBinder binder) { + mService = IAccountService.Stub.asInterface(binder); + } + + public IBinder asBinder() { + return null; + } + + @Override + public void notifyLoginFailed(final long accountId) throws RemoteException { + setTask(new ProxyTask() { + public void run() throws RemoteException { + mService.notifyLoginFailed(accountId); + } + }, "notifyLoginFailed"); + } + + @Override + public void notifyLoginSucceeded(final long accountId) throws RemoteException { + setTask(new ProxyTask() { + public void run() throws RemoteException { + mService.notifyLoginSucceeded(accountId); + } + }, "notifyLoginSucceeded"); + } + + @Override + public void notifyNewMessages(final long accountId) throws RemoteException { + setTask(new ProxyTask() { + public void run() throws RemoteException { + mService.notifyNewMessages(accountId); + } + }, "notifyNewMessages"); + } + + @Override + public void accountDeleted() throws RemoteException { + setTask(new ProxyTask() { + public void run() throws RemoteException { + mService.accountDeleted(); + } + }, "accountDeleted"); + } + + @Override + public void restoreAccountsIfNeeded() throws RemoteException { + setTask(new ProxyTask() { + public void run() throws RemoteException { + mService.restoreAccountsIfNeeded(); + } + }, "restoreAccountsIfNeeded"); + } + + @Override + public int getAccountColor(final long accountId) throws RemoteException { + setTask(new ProxyTask() { + public void run() throws RemoteException{ + mReturn = mService.getAccountColor(accountId); + } + }, "getAccountColor"); + waitForCompletion(); + if (mReturn == null) { + return DEFAULT_ACCOUNT_COLOR; + } else { + return (Integer)mReturn; + } + } +} + diff --git a/src/com/android/emailcommon/service/IAccountService.aidl b/src/com/android/emailcommon/service/IAccountService.aidl new file mode 100644 index 000000000..9f362c82c --- /dev/null +++ b/src/com/android/emailcommon/service/IAccountService.aidl @@ -0,0 +1,28 @@ +/* + * 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.emailcommon.service; + +interface IAccountService { + oneway void notifyLoginFailed(long accountId); + oneway void notifyLoginSucceeded(long accountId); + oneway void notifyNewMessages(long accountId); + + void accountDeleted(); + void restoreAccountsIfNeeded(); + + int getAccountColor(long accountId); +}
\ No newline at end of file diff --git a/src/com/android/emailcommon/service/PolicyServiceProxy.java b/src/com/android/emailcommon/service/PolicyServiceProxy.java index 463d11dc6..b68b5ac0e 100644 --- a/src/com/android/emailcommon/service/PolicyServiceProxy.java +++ b/src/com/android/emailcommon/service/PolicyServiceProxy.java @@ -223,6 +223,5 @@ public class PolicyServiceProxy extends ServiceProxy implements IPolicyService { } throw new IllegalStateException("PolicyService transaction failed"); } - } diff --git a/src/com/android/emailcommon/service/SyncWindow.java b/src/com/android/emailcommon/service/SyncWindow.java new file mode 100644 index 000000000..b81834fea --- /dev/null +++ b/src/com/android/emailcommon/service/SyncWindow.java @@ -0,0 +1,27 @@ +/* + * 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.emailcommon.service; + +public class SyncWindow { + public static final int SYNC_WINDOW_USER = -1; + public static final int SYNC_WINDOW_1_DAY = 1; + public static final int SYNC_WINDOW_3_DAYS = 2; + public static final int SYNC_WINDOW_1_WEEK = 3; + public static final int SYNC_WINDOW_2_WEEKS = 4; + public static final int SYNC_WINDOW_1_MONTH = 5; + public static final int SYNC_WINDOW_ALL = 6; +} diff --git a/src/com/android/emailcommon/utility/AccountReconciler.java b/src/com/android/emailcommon/utility/AccountReconciler.java new file mode 100644 index 000000000..11abcd3b3 --- /dev/null +++ b/src/com/android/emailcommon/utility/AccountReconciler.java @@ -0,0 +1,117 @@ +/* + * 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.emailcommon.utility; + +import com.android.email.Email; +import com.android.email.provider.EmailContent.Account; + +import android.accounts.AccountManager; +import android.accounts.AccountManagerFuture; +import android.accounts.AuthenticatorException; +import android.accounts.OperationCanceledException; +import android.content.ContentResolver; +import android.content.ContentUris; +import android.content.Context; +import android.util.Log; + +import java.io.IOException; +import java.util.List; + +public class AccountReconciler { + /** + * Compare our account list (obtained from EmailProvider) with the account list owned by + * AccountManager. If there are any orphans (an account in one list without a corresponding + * account in the other list), delete the orphan, as these must remain in sync. + * + * Note that the duplication of account information is caused by the Email application's + * incomplete integration with AccountManager. + * + * This function may not be called from the main/UI thread, because it makes blocking calls + * into the account manager. + * + * @param context The context in which to operate + * @param emailProviderAccounts the exchange provider accounts to work from + * @param accountManagerAccounts The account manager accounts to work from + * @param resolver the content resolver for making provider updates (injected for testability) + */ + public static boolean reconcileAccounts(Context context, + List<Account> emailProviderAccounts, android.accounts.Account[] accountManagerAccounts, + ContentResolver resolver) { + // First, look through our EmailProvider accounts to make sure there's a corresponding + // AccountManager account + boolean accountsDeleted = false; + for (Account providerAccount: emailProviderAccounts) { + String providerAccountName = providerAccount.mEmailAddress; + boolean found = false; + for (android.accounts.Account accountManagerAccount: accountManagerAccounts) { + if (accountManagerAccount.name.equalsIgnoreCase(providerAccountName)) { + found = true; + break; + } + } + if (!found) { + if ((providerAccount.mFlags & Account.FLAGS_INCOMPLETE) != 0) { + if (Email.DEBUG) { + Log.d(Email.LOG_TAG, + "Account reconciler noticed incomplete account; ignoring"); + } + continue; + } + // This account has been deleted in the AccountManager! + Log.d(Email.LOG_TAG, "Account deleted in AccountManager; deleting from provider: " + + providerAccountName); + // TODO This will orphan downloaded attachments; need to handle this + resolver.delete(ContentUris.withAppendedId(Account.CONTENT_URI, + providerAccount.mId), null, null); + accountsDeleted = true; + } + } + // Now, look through AccountManager accounts to make sure we have a corresponding cached EAS + // account from EmailProvider + for (android.accounts.Account accountManagerAccount: accountManagerAccounts) { + String accountManagerAccountName = accountManagerAccount.name; + boolean found = false; + for (Account cachedEasAccount: emailProviderAccounts) { + if (cachedEasAccount.mEmailAddress.equalsIgnoreCase(accountManagerAccountName)) { + found = true; + } + } + if (!found) { + // This account has been deleted from the EmailProvider database + Log.d(Email.LOG_TAG, + "Account deleted from provider; deleting from AccountManager: " + + accountManagerAccountName); + // Delete the account + AccountManagerFuture<Boolean> blockingResult = AccountManager.get(context) + .removeAccount(accountManagerAccount, null, null); + try { + // Note: All of the potential errors from removeAccount() are simply logged + // here, as there is nothing to actually do about them. + blockingResult.getResult(); + } catch (OperationCanceledException e) { + Log.w(Email.LOG_TAG, e.toString()); + } catch (AuthenticatorException e) { + Log.w(Email.LOG_TAG, e.toString()); + } catch (IOException e) { + Log.w(Email.LOG_TAG, e.toString()); + } + accountsDeleted = true; + } + } + return accountsDeleted; + } +} diff --git a/src/com/android/emailcommon/utility/AttachmentUtilities.java b/src/com/android/emailcommon/utility/AttachmentUtilities.java new file mode 100644 index 000000000..98d521978 --- /dev/null +++ b/src/com/android/emailcommon/utility/AttachmentUtilities.java @@ -0,0 +1,258 @@ +/* + * 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.emailcommon.utility; + +import com.android.email.Email; +import com.android.email.provider.EmailContent.Attachment; +import com.android.email.provider.EmailContent.Message; +import com.android.email.provider.EmailContent.MessageColumns; + +import android.content.ContentResolver; +import android.content.ContentUris; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.text.TextUtils; +import android.util.Log; +import android.webkit.MimeTypeMap; + +import java.io.File; + +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"; + + public static class Columns { + public static final String _ID = "_id"; + public static final String DATA = "_data"; + public static final String DISPLAY_NAME = "_display_name"; + public static final String SIZE = "_size"; + } + + 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(); + } + + /** + * Return the filename for a given attachment. This should be used by any code that is + * going to *write* attachments. + * + * This does not create or write the file, or even the directories. It simply builds + * the filename that should be used. + */ + public static File getAttachmentFilename(Context context, long accountId, long attachmentId) { + return new File(getAttachmentDirectory(context, accountId), Long.toString(attachmentId)); + } + + /** + * Return the directory for a given attachment. This should be used by any code that is + * going to *write* attachments. + * + * This does not create or write the directory. It simply builds the pathname that should be + * used. + */ + public static File getAttachmentDirectory(Context context, long accountId) { + return context.getDatabasePath(accountId + ".db_att"); + } + + /** + * Helper to convert unknown or unmapped attachments to something useful based on filename + * extensions. The mime type is inferred based upon the table below. It's not perfect, but + * it helps. + * + * <pre> + * |---------------------------------------------------------| + * | E X T E N S I O N | + * |---------------------------------------------------------| + * | .eml | known(.png) | unknown(.abc) | none | + * | M |-----------------------------------------------------------------------| + * | I | none | msg/rfc822 | image/png | app/abc | app/oct-str | + * | M |-------------| (always | | | | + * | E | app/oct-str | overrides | | | | + * | T |-------------| | |-----------------------------| + * | Y | text/plain | | | text/plain | + * | P |-------------| |-------------------------------------------| + * | E | any/type | | any/type | + * |---|-----------------------------------------------------------------------| + * </pre> + * + * NOTE: Since mime types on Android are case-*sensitive*, return values are always in + * lower case. + * + * @param fileName The given filename + * @param mimeType The given mime type + * @return A likely mime type for the attachment + */ + public static String inferMimeType(final String fileName, final String mimeType) { + String resultType = null; + String fileExtension = getFilenameExtension(fileName); + boolean isTextPlain = "text/plain".equalsIgnoreCase(mimeType); + + if ("eml".equals(fileExtension)) { + resultType = "message/rfc822"; + } else { + boolean isGenericType = + isTextPlain || "application/octet-stream".equalsIgnoreCase(mimeType); + // If the given mime type is non-empty and non-generic, return it + if (isGenericType || TextUtils.isEmpty(mimeType)) { + if (!TextUtils.isEmpty(fileExtension)) { + // Otherwise, try to find a mime type based upon the file extension + resultType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(fileExtension); + if (TextUtils.isEmpty(resultType)) { + // Finally, if original mimetype is text/plain, use it; otherwise synthesize + resultType = isTextPlain ? mimeType : "application/" + fileExtension; + } + } + } else { + resultType = mimeType; + } + } + + // No good guess could be made; use an appropriate generic type + if (TextUtils.isEmpty(resultType)) { + resultType = isTextPlain ? "text/plain" : "application/octet-stream"; + } + return resultType.toLowerCase(); + } + + /** + * Extract and return filename's extension, converted to lower case, and not including the "." + * + * @return extension, or null if not found (or null/empty filename) + */ + public static String getFilenameExtension(String fileName) { + String extension = null; + if (!TextUtils.isEmpty(fileName)) { + int lastDot = fileName.lastIndexOf('.'); + if ((lastDot > 0) && (lastDot < fileName.length() - 1)) { + extension = fileName.substring(lastDot + 1).toLowerCase(); + } + } + return extension; + } + + /** + * Resolve attachment id to content URI. Returns the resolved content URI (from the attachment + * DB) or, if not found, simply returns the incoming value. + * + * @param attachmentUri + * @return resolved content URI + * + * TODO: Throws an SQLite exception on a missing DB file (e.g. unknown URI) instead of just + * returning the incoming uri, as it should. + */ + public static Uri resolveAttachmentIdToContentUri(ContentResolver resolver, Uri attachmentUri) { + Cursor c = resolver.query(attachmentUri, + new String[] { Columns.DATA }, + null, null, null); + if (c != null) { + try { + if (c.moveToFirst()) { + final String strUri = c.getString(0); + if (strUri != null) { + return Uri.parse(strUri); + } + } + } finally { + c.close(); + } + } + return attachmentUri; + } + + /** + * In support of deleting a message, find all attachments and delete associated attachment + * files. + * @param context + * @param accountId the account for the message + * @param messageId the message + */ + public static void deleteAllAttachmentFiles(Context context, long accountId, long messageId) { + Uri uri = ContentUris.withAppendedId(Attachment.MESSAGE_ID_URI, messageId); + Cursor c = context.getContentResolver().query(uri, Attachment.ID_PROJECTION, + null, null, null); + try { + while (c.moveToNext()) { + long attachmentId = c.getLong(Attachment.ID_PROJECTION_COLUMN); + File attachmentFile = getAttachmentFilename(context, accountId, attachmentId); + // Note, delete() throws no exceptions for basic FS errors (e.g. file not found) + // it just returns false, which we ignore, and proceed to the next file. + // This entire loop is best-effort only. + attachmentFile.delete(); + } + } finally { + c.close(); + } + } + + /** + * In support of deleting a mailbox, find all messages and delete their attachments. + * + * @param context + * @param accountId the account for the mailbox + * @param mailboxId the mailbox for the messages + */ + public static void deleteAllMailboxAttachmentFiles(Context context, long accountId, + long mailboxId) { + Cursor c = context.getContentResolver().query(Message.CONTENT_URI, + Message.ID_COLUMN_PROJECTION, MessageColumns.MAILBOX_KEY + "=?", + new String[] { Long.toString(mailboxId) }, null); + try { + while (c.moveToNext()) { + long messageId = c.getLong(Message.ID_PROJECTION_COLUMN); + deleteAllAttachmentFiles(context, accountId, messageId); + } + } finally { + c.close(); + } + } + + /** + * In support of deleting or wiping an account, delete all related attachments. + * + * @param context + * @param accountId the account to scrub + */ + public static void deleteAllAccountAttachmentFiles(Context context, long accountId) { + File[] files = getAttachmentDirectory(context, accountId).listFiles(); + if (files == null) return; + for (File file : files) { + boolean result = file.delete(); + if (!result) { + Log.e(Email.LOG_TAG, "Failed to delete attachment file " + file.getName()); + } + } + } +} diff --git a/src/com/android/emailcommon/utility/ConversionUtilities.java b/src/com/android/emailcommon/utility/ConversionUtilities.java new file mode 100644 index 000000000..722175149 --- /dev/null +++ b/src/com/android/emailcommon/utility/ConversionUtilities.java @@ -0,0 +1,139 @@ +/* + * 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.emailcommon.utility; + +import com.android.email.Snippet; +import com.android.email.mail.MessagingException; +import com.android.email.mail.Part; +import com.android.email.mail.internet.MimeHeader; +import com.android.email.mail.internet.MimeUtility; +import com.android.email.provider.EmailContent; + +import android.text.TextUtils; + +import java.util.ArrayList; + +public class ConversionUtilities { + /** + * Values for HEADER_ANDROID_BODY_QUOTED_PART to tag body parts + */ + public static final String BODY_QUOTED_PART_REPLY = "quoted-reply"; + public static final String BODY_QUOTED_PART_FORWARD = "quoted-forward"; + public static final String BODY_QUOTED_PART_INTRO = "quoted-intro"; + + /** + * Helper function to append text to a StringBuffer, creating it if necessary. + * Optimization: The majority of the time we are *not* appending - we should have a path + * that deals with single strings. + */ + private static StringBuffer appendTextPart(StringBuffer sb, String newText) { + if (newText == null) { + return sb; + } + else if (sb == null) { + sb = new StringBuffer(newText); + } else { + if (sb.length() > 0) { + sb.append('\n'); + } + sb.append(newText); + } + return sb; + } + + /** + * Copy body text (plain and/or HTML) from MimeMessage to provider Message + */ + public static boolean updateBodyFields(EmailContent.Body body, + EmailContent.Message localMessage, ArrayList<Part> viewables) + throws MessagingException { + + body.mMessageKey = localMessage.mId; + + StringBuffer sbHtml = null; + StringBuffer sbText = null; + StringBuffer sbHtmlReply = null; + StringBuffer sbTextReply = null; + StringBuffer sbIntroText = null; + + for (Part viewable : viewables) { + String text = MimeUtility.getTextFromPart(viewable); + String[] replyTags = viewable.getHeader(MimeHeader.HEADER_ANDROID_BODY_QUOTED_PART); + String replyTag = null; + if (replyTags != null && replyTags.length > 0) { + replyTag = replyTags[0]; + } + // Deploy text as marked by the various tags + boolean isHtml = "text/html".equalsIgnoreCase(viewable.getMimeType()); + + if (replyTag != null) { + boolean isQuotedReply = BODY_QUOTED_PART_REPLY.equalsIgnoreCase(replyTag); + boolean isQuotedForward = BODY_QUOTED_PART_FORWARD.equalsIgnoreCase(replyTag); + boolean isQuotedIntro = BODY_QUOTED_PART_INTRO.equalsIgnoreCase(replyTag); + + if (isQuotedReply || isQuotedForward) { + if (isHtml) { + sbHtmlReply = appendTextPart(sbHtmlReply, text); + } else { + sbTextReply = appendTextPart(sbTextReply, text); + } + // Set message flags as well + localMessage.mFlags &= ~EmailContent.Message.FLAG_TYPE_MASK; + localMessage.mFlags |= isQuotedReply + ? EmailContent.Message.FLAG_TYPE_REPLY + : EmailContent.Message.FLAG_TYPE_FORWARD; + continue; + } + if (isQuotedIntro) { + sbIntroText = appendTextPart(sbIntroText, text); + continue; + } + } + + // Most of the time, just process regular body parts + if (isHtml) { + sbHtml = appendTextPart(sbHtml, text); + } else { + sbText = appendTextPart(sbText, text); + } + } + + // write the combined data to the body part + if (!TextUtils.isEmpty(sbText)) { + String text = sbText.toString(); + body.mTextContent = text; + localMessage.mSnippet = Snippet.fromPlainText(text); + } + if (!TextUtils.isEmpty(sbHtml)) { + String text = sbHtml.toString(); + body.mHtmlContent = text; + if (localMessage.mSnippet == null) { + localMessage.mSnippet = Snippet.fromHtmlText(text); + } + } + if (sbHtmlReply != null && sbHtmlReply.length() != 0) { + body.mHtmlReply = sbHtmlReply.toString(); + } + if (sbTextReply != null && sbTextReply.length() != 0) { + body.mTextReply = sbTextReply.toString(); + } + if (sbIntroText != null && sbIntroText.length() != 0) { + body.mIntroText = sbIntroText.toString(); + } + return true; + } +} diff --git a/src/com/android/exchange/ExchangeService.java b/src/com/android/exchange/ExchangeService.java index 19bc2264a..6afcbb8bc 100644 --- a/src/com/android/exchange/ExchangeService.java +++ b/src/com/android/exchange/ExchangeService.java @@ -17,9 +17,7 @@ package com.android.exchange; -import com.android.email.AccountBackupRestore; import com.android.email.Email; -import com.android.email.NotificationController; import com.android.email.Utility; import com.android.email.mail.transport.SSLUtils; import com.android.email.provider.EmailContent; @@ -32,11 +30,12 @@ import com.android.email.provider.EmailContent.Mailbox; import com.android.email.provider.EmailContent.MailboxColumns; import com.android.email.provider.EmailContent.Message; import com.android.email.provider.EmailContent.SyncColumns; -import com.android.email.service.MailService; import com.android.emailcommon.Api; +import com.android.emailcommon.service.AccountServiceProxy; import com.android.emailcommon.service.EmailServiceStatus; import com.android.emailcommon.service.IEmailService; import com.android.emailcommon.service.IEmailServiceCallback; +import com.android.emailcommon.utility.AccountReconciler; import com.android.exchange.adapter.CalendarSyncAdapter; import com.android.exchange.adapter.ContactsSyncAdapter; import com.android.exchange.utility.FileLogger; @@ -1046,8 +1045,15 @@ public class ExchangeService extends Service implements Runnable { // list, which would cause the deletion of all of our accounts AccountList accountList = collectEasAccounts(context, new AccountList()); alwaysLog("Reconciling accounts..."); - MailService.reconcileAccountsWithAccountManager(context, accountList, accountMgrList, - false, context.getContentResolver()); + boolean accountsDeleted = AccountReconciler.reconcileAccounts(context, accountList, + accountMgrList, context.getContentResolver()); + if (accountsDeleted) { + try { + new AccountServiceProxy(context).accountDeleted(); + } catch (RemoteException e) { + // ? + } + } } public static void log(String str) { @@ -1749,13 +1755,22 @@ public class ExchangeService extends Service implements Runnable { @Override public void run() { synchronized (sSyncLock) { + // ExchangeService cannot start unless we can connect to AccountService + if (!new AccountServiceProxy(ExchangeService.this).test()) { + log("!!! Email application not found; stopping self"); + stopSelf(); + } // Restore accounts, if it has not happened already - AccountBackupRestore.restoreAccountsIfNeeded(ExchangeService.this); + try { + new AccountServiceProxy(ExchangeService.this).restoreAccountsIfNeeded(); + } catch (RemoteException e) { + // If we can't restore accounts, don't run + return; + } // Run the reconciler and clean up any mismatched accounts - if we weren't // running when accounts were deleted, it won't have been called. runAccountReconcilerSync(ExchangeService.this); // Update other services depending on final account configuration - Email.setServicesEnabledSync(ExchangeService.this); maybeStartExchangeServiceThread(); if (sServiceThread == null) { log("!!! EAS ExchangeService, stopping self"); @@ -2385,8 +2400,12 @@ public class ExchangeService extends Service implements Runnable { if (account == null) return; if (exchangeService.releaseSyncHolds(exchangeService, AbstractSyncService.EXIT_LOGIN_FAILURE, account)) { - NotificationController.getInstance(exchangeService) - .cancelLoginFailedNotification(accountId); + try { + new AccountServiceProxy(exchangeService).notifyLoginSucceeded( + accountId); + } catch (RemoteException e) { + // No harm if the notification fails + } } } @@ -2413,8 +2432,11 @@ public class ExchangeService extends Service implements Runnable { break; // These errors are not retried automatically case AbstractSyncService.EXIT_LOGIN_FAILURE: - NotificationController.getInstance(exchangeService) - .showLoginFailedNotification(m.mAccountKey); + try { + new AccountServiceProxy(exchangeService).notifyLoginFailed(m.mAccountKey); + } catch (RemoteException e) { + // ? Anything to do? + } // Fall through case AbstractSyncService.EXIT_SECURITY_FAILURE: case AbstractSyncService.EXIT_EXCEPTION: diff --git a/src/com/android/exchange/PolicyServiceDelegate.java b/src/com/android/exchange/PolicyServiceDelegate.java new file mode 100644 index 000000000..d712c5591 --- /dev/null +++ b/src/com/android/exchange/PolicyServiceDelegate.java @@ -0,0 +1,91 @@ +/* + * 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.exchange; + +import com.android.email.provider.EmailContent.Account; +import com.android.emailcommon.service.PolicyServiceProxy; +import com.android.emailcommon.service.PolicySet; + +import android.content.Context; +import android.os.RemoteException; + +public class PolicyServiceDelegate { + + public static boolean isActive(Context context, PolicySet policies) { + try { + return new PolicyServiceProxy(context).isActive(policies); + } catch (RemoteException e) { + } + return false; + } + + public static void policiesRequired(Context context, long accountId) { + try { + new PolicyServiceProxy(context).policiesRequired(accountId); + } catch (RemoteException e) { + throw new IllegalStateException("PolicyService transaction failed"); + } + } + + public static void updatePolicies(Context context, long accountId) { + try { + new PolicyServiceProxy(context).updatePolicies(accountId); + } catch (RemoteException e) { + throw new IllegalStateException("PolicyService transaction failed"); + } + } + + public static void setAccountHoldFlag(Context context, Account account, boolean newState) { + try { + new PolicyServiceProxy(context).setAccountHoldFlag(account.mId, newState); + } catch (RemoteException e) { + throw new IllegalStateException("PolicyService transaction failed"); + } + } + + public static boolean isActiveAdmin(Context context) { + try { + return new PolicyServiceProxy(context).isActiveAdmin(); + } catch (RemoteException e) { + } + return false; + } + + public static void remoteWipe(Context context) { + try { + new PolicyServiceProxy(context).remoteWipe(); + } catch (RemoteException e) { + throw new IllegalStateException("PolicyService transaction failed"); + } + } + + public static boolean isSupported(Context context, PolicySet policies) { + try { + return new PolicyServiceProxy(context).isSupported(policies); + } catch (RemoteException e) { + } + return false; + } + + public static PolicySet clearUnsupportedPolicies(Context context, PolicySet policies) { + try { + return new PolicyServiceProxy(context).clearUnsupportedPolicies(policies); + } catch (RemoteException e) { + } + throw new IllegalStateException("PolicyService transaction failed"); + } +} diff --git a/src/com/android/exchange/adapter/EmailSyncAdapter.java b/src/com/android/exchange/adapter/EmailSyncAdapter.java index d4a27ffb8..b9666c1a4 100644 --- a/src/com/android/exchange/adapter/EmailSyncAdapter.java +++ b/src/com/android/exchange/adapter/EmailSyncAdapter.java @@ -17,7 +17,6 @@ package com.android.exchange.adapter; -import com.android.email.LegacyConversions; import com.android.email.Utility; import com.android.email.mail.Address; import com.android.email.mail.MeetingInfo; @@ -26,8 +25,8 @@ import com.android.email.mail.PackedString; import com.android.email.mail.Part; import com.android.email.mail.internet.MimeMessage; import com.android.email.mail.internet.MimeUtility; -import com.android.email.provider.AttachmentProvider; import com.android.email.provider.EmailContent; +import com.android.email.provider.EmailProvider; import com.android.email.provider.EmailContent.Account; import com.android.email.provider.EmailContent.AccountColumns; import com.android.email.provider.EmailContent.Attachment; @@ -36,8 +35,10 @@ import com.android.email.provider.EmailContent.Mailbox; import com.android.email.provider.EmailContent.Message; import com.android.email.provider.EmailContent.MessageColumns; import com.android.email.provider.EmailContent.SyncColumns; -import com.android.email.provider.EmailProvider; -import com.android.email.service.MailService; +import com.android.emailcommon.service.AccountServiceProxy; +import com.android.emailcommon.service.SyncWindow; +import com.android.emailcommon.utility.AttachmentUtilities; +import com.android.emailcommon.utility.ConversionUtilities; import com.android.exchange.Eas; import com.android.exchange.EasSyncService; import com.android.exchange.MessageMoveRequest; @@ -117,22 +118,23 @@ public class EmailSyncAdapter extends AbstractSyncAdapter { mService.clearRequests(); mFetchRequestList.clear(); // Delete attachments... - AttachmentProvider.deleteAllMailboxAttachmentFiles(mContext, mAccount.mId, mMailbox.mId); + AttachmentUtilities.deleteAllMailboxAttachmentFiles(mContext, mAccount.mId, + mMailbox.mId); } private String getEmailFilter() { switch (mAccount.mSyncLookback) { - case com.android.email.Account.SYNC_WINDOW_1_DAY: + case SyncWindow.SYNC_WINDOW_1_DAY: return Eas.FILTER_1_DAY; - case com.android.email.Account.SYNC_WINDOW_3_DAYS: + case SyncWindow.SYNC_WINDOW_3_DAYS: return Eas.FILTER_3_DAYS; - case com.android.email.Account.SYNC_WINDOW_1_WEEK: + case SyncWindow.SYNC_WINDOW_1_WEEK: return Eas.FILTER_1_WEEK; - case com.android.email.Account.SYNC_WINDOW_2_WEEKS: + case SyncWindow.SYNC_WINDOW_2_WEEKS: return Eas.FILTER_2_WEEKS; - case com.android.email.Account.SYNC_WINDOW_1_MONTH: + case SyncWindow.SYNC_WINDOW_1_MONTH: return Eas.FILTER_1_MONTH; - case com.android.email.Account.SYNC_WINDOW_ALL: + case SyncWindow.SYNC_WINDOW_ALL: return Eas.FILTER_ALL; default: return Eas.FILTER_1_WEEK; @@ -496,7 +498,7 @@ public class EmailSyncAdapter extends AbstractSyncAdapter { MimeUtility.collectParts(mimeMessage, viewables, attachments); Body tempBody = new Body(); // updateBodyFields fills in the content fields of the Body - LegacyConversions.updateBodyFields(tempBody, msg, viewables); + ConversionUtilities.updateBodyFields(tempBody, msg, viewables); // But we need them in the message itself for handling during commit() msg.mHtml = tempBody.mHtmlContent; msg.mText = tempBody.mTextContent; @@ -770,7 +772,7 @@ public class EmailSyncAdapter extends AbstractSyncAdapter { for (Long id : deletedEmails) { ops.add(ContentProviderOperation.newDelete( ContentUris.withAppendedId(Message.CONTENT_URI, id)).build()); - AttachmentProvider.deleteAllAttachmentFiles(mContext, mAccount.mId, id); + AttachmentUtilities.deleteAllAttachmentFiles(mContext, mAccount.mId, id); } if (!changedEmails.isEmpty()) { @@ -822,7 +824,11 @@ public class EmailSyncAdapter extends AbstractSyncAdapter { cv.put(EmailContent.ADD_COLUMN_NAME, notifyCount); Uri uri = ContentUris.withAppendedId(Account.ADD_TO_FIELD_URI, mAccount.mId); mContentResolver.update(uri, cv, null, null); - MailService.actionNotifyNewMessages(mContext, mAccount.mId); + try { + new AccountServiceProxy(mService.mContext).notifyNewMessages(mAccount.mId); + } catch (RemoteException e) { + // ? Anything to do here? + } } } } diff --git a/src/com/android/exchange/adapter/FolderSyncParser.java b/src/com/android/exchange/adapter/FolderSyncParser.java index 7d6229b38..5013337c0 100644 --- a/src/com/android/exchange/adapter/FolderSyncParser.java +++ b/src/com/android/exchange/adapter/FolderSyncParser.java @@ -18,13 +18,13 @@ package com.android.exchange.adapter; import com.android.email.Utility; -import com.android.email.provider.AttachmentProvider; import com.android.email.provider.EmailContent; +import com.android.email.provider.EmailProvider; import com.android.email.provider.EmailContent.Account; import com.android.email.provider.EmailContent.AccountColumns; import com.android.email.provider.EmailContent.Mailbox; import com.android.email.provider.EmailContent.MailboxColumns; -import com.android.email.provider.EmailProvider; +import com.android.emailcommon.utility.AttachmentUtilities; import com.android.exchange.Eas; import com.android.exchange.ExchangeService; import com.android.exchange.MockParserStream; @@ -187,7 +187,7 @@ public class FolderSyncParser extends AbstractSyncParser { ops.add(ContentProviderOperation.newDelete( ContentUris.withAppendedId(Mailbox.CONTENT_URI, c.getLong(0))).build()); - AttachmentProvider.deleteAllMailboxAttachmentFiles(mContext, + AttachmentUtilities.deleteAllMailboxAttachmentFiles(mContext, mAccountId, mMailbox.mId); } } finally { diff --git a/src/com/android/exchange/utility/CalendarUtilities.java b/src/com/android/exchange/utility/CalendarUtilities.java index e1879f653..27452ebac 100644 --- a/src/com/android/exchange/utility/CalendarUtilities.java +++ b/src/com/android/exchange/utility/CalendarUtilities.java @@ -18,7 +18,6 @@ package com.android.exchange.utility; import com.android.email.Email; import com.android.email.R; -import com.android.email.ResourceHelper; import com.android.email.Utility; import com.android.email.mail.Address; import com.android.email.provider.EmailContent; @@ -26,6 +25,7 @@ import com.android.email.provider.EmailContent.Account; import com.android.email.provider.EmailContent.Attachment; import com.android.email.provider.EmailContent.Mailbox; import com.android.email.provider.EmailContent.Message; +import com.android.emailcommon.service.AccountServiceProxy; import com.android.exchange.Eas; import com.android.exchange.EasSyncService; import com.android.exchange.ExchangeService; @@ -1215,8 +1215,12 @@ public class CalendarUtilities { cv.put(Calendars.ORGANIZER_CAN_RESPOND, 0); // TODO Coordinate account colors w/ Calendar, if possible - // Make Email account color opaque - int color = ResourceHelper.getInstance(service.mContext).getAccountColor(account.mId); + int color = AccountServiceProxy.DEFAULT_ACCOUNT_COLOR; + try { + color = new AccountServiceProxy(service.mContext).getAccountColor(account.mId); + } catch (RemoteException e) { + // Use the default + } cv.put(Calendars.COLOR, color); cv.put(Calendars.TIMEZONE, Time.getCurrentTimezone()); cv.put(Calendars.ACCESS_LEVEL, Calendars.OWNER_ACCESS); diff --git a/tests/src/com/android/email/DBTestHelper.java b/tests/src/com/android/email/DBTestHelper.java index d67caae34..729224c8b 100644 --- a/tests/src/com/android/email/DBTestHelper.java +++ b/tests/src/com/android/email/DBTestHelper.java @@ -20,6 +20,7 @@ import com.android.email.provider.AttachmentProvider; import com.android.email.provider.ContentCache; import com.android.email.provider.EmailContent; import com.android.email.provider.EmailProvider; +import com.android.emailcommon.utility.AttachmentUtilities; import android.content.ContentProvider; import android.content.ContentResolver; @@ -37,8 +38,6 @@ import android.test.mock.MockCursor; import java.io.File; -import junit.framework.Assert; - /** * Helper classes (and possibly methods) for database related tests. */ @@ -225,7 +224,7 @@ public final class DBTestHelper { final AttachmentProvider ap = new AttachmentProvider(); ap.attachInfo(providerContext, null); - resolver.addProvider(AttachmentProvider.AUTHORITY, ap); + resolver.addProvider(AttachmentUtilities.AUTHORITY, ap); ContentCache.invalidateAllCachesForTest(); diff --git a/tests/src/com/android/email/LegacyConversionsTests.java b/tests/src/com/android/email/LegacyConversionsTests.java index d946d6a27..edccb5b04 100644 --- a/tests/src/com/android/email/LegacyConversionsTests.java +++ b/tests/src/com/android/email/LegacyConversionsTests.java @@ -20,21 +20,22 @@ import com.android.email.mail.Address; import com.android.email.mail.BodyPart; import com.android.email.mail.Flag; import com.android.email.mail.Message; -import com.android.email.mail.Message.RecipientType; import com.android.email.mail.MessageTestUtils; -import com.android.email.mail.MessageTestUtils.MessageBuilder; -import com.android.email.mail.MessageTestUtils.MultipartBuilder; import com.android.email.mail.MessagingException; import com.android.email.mail.Part; +import com.android.email.mail.Message.RecipientType; +import com.android.email.mail.MessageTestUtils.MessageBuilder; +import com.android.email.mail.MessageTestUtils.MultipartBuilder; import com.android.email.mail.internet.MimeBodyPart; import com.android.email.mail.internet.MimeHeader; import com.android.email.mail.internet.MimeMessage; import com.android.email.mail.internet.MimeUtility; import com.android.email.mail.internet.TextBody; import com.android.email.provider.EmailContent; -import com.android.email.provider.EmailContent.Attachment; import com.android.email.provider.EmailProvider; import com.android.email.provider.ProviderTestUtils; +import com.android.email.provider.EmailContent.Attachment; +import com.android.emailcommon.utility.ConversionUtilities; import android.content.ContentUris; import android.content.Context; @@ -191,7 +192,7 @@ public class LegacyConversionsTests extends ProviderTestCase2<EmailProvider> { viewables.add(emptyTextPart); // a "null" body part of type text/plain should result in a null mTextContent - boolean result = LegacyConversions.updateBodyFields(localBody, localMessage, viewables); + boolean result = ConversionUtilities.updateBodyFields(localBody, localMessage, viewables); assertTrue(result); assertNull(localBody.mTextContent); } diff --git a/tests/src/com/android/email/UtilityUnitTests.java b/tests/src/com/android/email/UtilityUnitTests.java index c72e689d1..e8c681d08 100644 --- a/tests/src/com/android/email/UtilityUnitTests.java +++ b/tests/src/com/android/email/UtilityUnitTests.java @@ -17,17 +17,16 @@ package com.android.email; import com.android.email.Utility.NewFileCreator; -import com.android.email.provider.AttachmentProvider; +import com.android.email.provider.ProviderTestUtils; import com.android.email.provider.EmailContent.Account; import com.android.email.provider.EmailContent.Attachment; import com.android.email.provider.EmailContent.Mailbox; -import com.android.email.provider.ProviderTestUtils; +import com.android.emailcommon.utility.AttachmentUtilities; import android.content.Context; import android.database.Cursor; import android.database.CursorWrapper; import android.database.MatrixCursor; -import android.graphics.Typeface; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Bundle; @@ -422,13 +421,13 @@ public class UtilityUnitTests extends AndroidTestCase { Attachment att = ProviderTestUtils.setupAttachment(mailbox.mId, "name", 123, true, providerContext); long attachmentId = att.mId; - Uri uri = AttachmentProvider.getAttachmentUri(account.mId, attachmentId); + Uri uri = AttachmentUtilities.getAttachmentUri(account.mId, attachmentId); // Case 1: exists in the provider. assertEquals("name", Utility.getContentFileName(providerContext, uri)); // Case 2: doesn't exist in the provider - Uri notExistUri = AttachmentProvider.getAttachmentUri(account.mId, 123456789); + Uri notExistUri = AttachmentUtilities.getAttachmentUri(account.mId, 123456789); String lastPathSegment = notExistUri.getLastPathSegment(); assertEquals(lastPathSegment, Utility.getContentFileName(providerContext, notExistUri)); } diff --git a/tests/src/com/android/email/mail/MessageTestUtils.java b/tests/src/com/android/email/mail/MessageTestUtils.java index 487fbfa33..c7185a8b1 100644 --- a/tests/src/com/android/email/mail/MessageTestUtils.java +++ b/tests/src/com/android/email/mail/MessageTestUtils.java @@ -21,8 +21,8 @@ import com.android.email.mail.internet.MimeHeader; import com.android.email.mail.internet.MimeMessage; import com.android.email.mail.internet.MimeMultipart; import com.android.email.mail.internet.TextBody; -import com.android.email.provider.AttachmentProvider; import com.android.email.provider.EmailContent; +import com.android.emailcommon.utility.AttachmentUtilities; import android.net.Uri; @@ -63,7 +63,7 @@ public class MessageTestUtils { * @return AttachmentProvider content URI */ public static Uri contentUri(long attachmentId, EmailContent.Account account) { - return AttachmentProvider.getAttachmentUri(account.mId, attachmentId); + return AttachmentUtilities.getAttachmentUri(account.mId, attachmentId); } /** diff --git a/tests/src/com/android/email/provider/AttachmentProviderTests.java b/tests/src/com/android/email/provider/AttachmentProviderTests.java index c19b6e1dc..a7794aa3f 100644 --- a/tests/src/com/android/email/provider/AttachmentProviderTests.java +++ b/tests/src/com/android/email/provider/AttachmentProviderTests.java @@ -19,11 +19,11 @@ package com.android.email.provider; import com.android.email.AttachmentInfo; import com.android.email.R; import com.android.email.mail.MessagingException; -import com.android.email.provider.AttachmentProvider.AttachmentProviderColumns; import com.android.email.provider.EmailContent.Account; import com.android.email.provider.EmailContent.Attachment; import com.android.email.provider.EmailContent.Mailbox; import com.android.email.provider.EmailContent.Message; +import com.android.emailcommon.utility.AttachmentUtilities; import android.content.ContentResolver; import android.content.Context; @@ -53,7 +53,7 @@ public class AttachmentProviderTests extends ProviderTestCase2<AttachmentProvide ContentResolver mMockResolver; public AttachmentProviderTests() { - super(AttachmentProvider.class, AttachmentProvider.AUTHORITY); + super(AttachmentProvider.class, AttachmentUtilities.AUTHORITY); } @Override @@ -76,9 +76,10 @@ public class AttachmentProviderTests extends ProviderTestCase2<AttachmentProvide * test insert() - should do nothing */ public void testUnimplemented() { - assertEquals(0, mMockResolver.delete(AttachmentProvider.CONTENT_URI, null, null)); - assertEquals(0, mMockResolver.update(AttachmentProvider.CONTENT_URI, null, null, null)); - assertEquals(null, mMockResolver.insert(AttachmentProvider.CONTENT_URI, null)); + assertEquals(0, mMockResolver.delete(AttachmentUtilities.CONTENT_URI, null, null)); + assertEquals(0, mMockResolver.update(AttachmentUtilities.CONTENT_URI, null, null, + null)); + assertEquals(null, mMockResolver.insert(AttachmentUtilities.CONTENT_URI, null)); } /** @@ -100,9 +101,12 @@ public class AttachmentProviderTests extends ProviderTestCase2<AttachmentProvide // attachment we add will be id=1 and the 2nd will have id=2. This could fail on // a legitimate implementation. Asserts below will catch this and fail the test // if necessary. - Uri attachment1Uri = AttachmentProvider.getAttachmentUri(account1.mId, attachment1Id); - Uri attachment2Uri = AttachmentProvider.getAttachmentUri(account1.mId, attachment2Id); - Uri attachment3Uri = AttachmentProvider.getAttachmentUri(account1.mId, attachment3Id); + Uri attachment1Uri = AttachmentUtilities.getAttachmentUri(account1.mId, + attachment1Id); + Uri attachment2Uri = AttachmentUtilities.getAttachmentUri(account1.mId, + attachment2Id); + Uri attachment3Uri = AttachmentUtilities.getAttachmentUri(account1.mId, + attachment3Id); // Test with no attachment found - should return null Cursor c = mMockResolver.query(attachment1Uri, (String[])null, null, (String[])null, null); @@ -113,31 +117,32 @@ public class AttachmentProviderTests extends ProviderTestCase2<AttachmentProvide Attachment newAttachment1 = ProviderTestUtils.setupAttachment(message1Id, "file1", 100, false, mMockContext); newAttachment1.mContentUri = - AttachmentProvider.getAttachmentUri(account1.mId, attachment1Id).toString(); + AttachmentUtilities.getAttachmentUri(account1.mId, attachment1Id).toString(); attachment1Id = addAttachmentToDb(account1, newAttachment1); assertEquals("Broken test: Unexpected id assignment", 1, attachment1Id); Attachment newAttachment2 = ProviderTestUtils.setupAttachment(message1Id, "file2", 200, false, mMockContext); newAttachment2.mContentUri = - AttachmentProvider.getAttachmentUri(account1.mId, attachment2Id).toString(); + AttachmentUtilities.getAttachmentUri(account1.mId, attachment2Id).toString(); attachment2Id = addAttachmentToDb(account1, newAttachment2); assertEquals("Broken test: Unexpected id assignment", 2, attachment2Id); Attachment newAttachment3 = ProviderTestUtils.setupAttachment(message1Id, "file3", 300, false, mMockContext); newAttachment3.mContentUri = - AttachmentProvider.getAttachmentUri(account1.mId, attachment3Id).toString(); + AttachmentUtilities.getAttachmentUri(account1.mId, attachment3Id).toString(); attachment3Id = addAttachmentToDb(account1, newAttachment3); assertEquals("Broken test: Unexpected id assignment", 3, attachment3Id); // Return a row with all columns specified - attachment2Uri = AttachmentProvider.getAttachmentUri(account1.mId, attachment2Id); + attachment2Uri = AttachmentUtilities.getAttachmentUri(account1.mId, attachment2Id); c = mMockResolver.query( attachment2Uri, - new String[] { AttachmentProviderColumns._ID, AttachmentProviderColumns.DATA, - AttachmentProviderColumns.DISPLAY_NAME, - AttachmentProviderColumns.SIZE }, + new String[] { AttachmentUtilities.Columns._ID, + AttachmentUtilities.Columns.DATA, + AttachmentUtilities.Columns.DISPLAY_NAME, + AttachmentUtilities.Columns.SIZE }, null, null, null); assertEquals(1, c.getCount()); assertTrue(c.moveToFirst()); @@ -147,12 +152,13 @@ public class AttachmentProviderTests extends ProviderTestCase2<AttachmentProvide assertEquals(200, c.getInt(3)); // size // Return a row with permuted columns - attachment3Uri = AttachmentProvider.getAttachmentUri(account1.mId, attachment3Id); + attachment3Uri = AttachmentUtilities.getAttachmentUri(account1.mId, attachment3Id); c = mMockResolver.query( attachment3Uri, - new String[] { AttachmentProviderColumns.SIZE, - AttachmentProviderColumns.DISPLAY_NAME, - AttachmentProviderColumns.DATA, AttachmentProviderColumns._ID }, + new String[] { AttachmentUtilities.Columns.SIZE, + AttachmentUtilities.Columns.DISPLAY_NAME, + AttachmentUtilities.Columns.DATA, + AttachmentUtilities.Columns._ID }, null, null, null); assertEquals(1, c.getCount()); assertTrue(c.moveToFirst()); @@ -264,7 +270,8 @@ public class AttachmentProviderTests extends ProviderTestCase2<AttachmentProvide long attachment5Id = 5; long attachment6Id = 6; - Uri attachment1Uri = AttachmentProvider.getAttachmentUri(account1.mId, attachment1Id); + Uri attachment1Uri = AttachmentUtilities.getAttachmentUri(account1.mId, + attachment1Id); // Test with no attachment found - should return null String type = mMockResolver.getType(attachment1Uri); @@ -298,27 +305,29 @@ public class AttachmentProviderTests extends ProviderTestCase2<AttachmentProvide attachment6Id = addAttachmentToDb(account1, newAttachment6); // Check the returned filetypes - Uri uri = AttachmentProvider.getAttachmentUri(account1.mId, attachment2Id); + Uri uri = AttachmentUtilities.getAttachmentUri(account1.mId, attachment2Id); type = mMockResolver.getType(uri); assertEquals("image/jpg", type); - uri = AttachmentProvider.getAttachmentUri(account1.mId, attachment3Id); + uri = AttachmentUtilities.getAttachmentUri(account1.mId, attachment3Id); type = mMockResolver.getType(uri); assertEquals("text/plain", type); - uri = AttachmentProvider.getAttachmentUri(account1.mId, attachment4Id); + uri = AttachmentUtilities.getAttachmentUri(account1.mId, attachment4Id); type = mMockResolver.getType(uri); assertEquals("application/msword", type); - uri = AttachmentProvider.getAttachmentUri(account1.mId, attachment5Id); + uri = AttachmentUtilities.getAttachmentUri(account1.mId, attachment5Id); type = mMockResolver.getType(uri); assertEquals("application/xyz", type); - uri = AttachmentProvider.getAttachmentUri(account1.mId, attachment6Id); + uri = AttachmentUtilities.getAttachmentUri(account1.mId, attachment6Id); type = mMockResolver.getType(uri); assertEquals("application/octet-stream", type); // Check the returned filetypes for the thumbnails - uri = AttachmentProvider.getAttachmentThumbnailUri(account1.mId, attachment2Id, 62, 62); + uri = AttachmentUtilities.getAttachmentThumbnailUri(account1.mId, attachment2Id, 62, + 62); type = mMockResolver.getType(uri); assertEquals("image/png", type); - uri = AttachmentProvider.getAttachmentThumbnailUri(account1.mId, attachment3Id, 62, 62); + uri = AttachmentUtilities.getAttachmentThumbnailUri(account1.mId, attachment3Id, 62, + 62); type = mMockResolver.getType(uri); assertEquals("image/png", type); } @@ -355,42 +364,49 @@ public class AttachmentProviderTests extends ProviderTestCase2<AttachmentProvide final String FILE_NO_EXT = "myfile"; // .eml files always override mime type - assertEquals("message/rfc822", AttachmentProvider.inferMimeType("a.eml", null)); - assertEquals("message/rfc822", AttachmentProvider.inferMimeType("a.eml", "")); - assertEquals("message/rfc822", AttachmentProvider.inferMimeType("a.eml", DEFAULT_LOWER)); - assertEquals("message/rfc822", AttachmentProvider.inferMimeType("a.eMl", TEXT_PLAIN)); - assertEquals("message/rfc822", AttachmentProvider.inferMimeType("a.eml", TYPE_IMG_PNG)); + assertEquals("message/rfc822", AttachmentUtilities.inferMimeType("a.eml", null)); + assertEquals("message/rfc822", AttachmentUtilities.inferMimeType("a.eml", "")); + assertEquals("message/rfc822", + AttachmentUtilities.inferMimeType("a.eml", DEFAULT_LOWER)); + assertEquals("message/rfc822", + AttachmentUtilities.inferMimeType("a.eMl", TEXT_PLAIN)); + assertEquals("message/rfc822", + AttachmentUtilities.inferMimeType("a.eml", TYPE_IMG_PNG)); // Non-generic, non-empty mime type; return it - assertEquals("mime/type", AttachmentProvider.inferMimeType(FILE_PNG, "Mime/TyPe")); - assertEquals("mime/type", AttachmentProvider.inferMimeType(FILE_ABC, "Mime/TyPe")); - assertEquals("mime/type", AttachmentProvider.inferMimeType(FILE_NO_EXT, "Mime/TyPe")); - assertEquals("mime/type", AttachmentProvider.inferMimeType(null, "Mime/TyPe")); - assertEquals("mime/type", AttachmentProvider.inferMimeType("", "Mime/TyPe")); + assertEquals("mime/type", AttachmentUtilities.inferMimeType(FILE_PNG, "Mime/TyPe")); + assertEquals("mime/type", AttachmentUtilities.inferMimeType(FILE_ABC, "Mime/TyPe")); + assertEquals("mime/type", + AttachmentUtilities.inferMimeType(FILE_NO_EXT, "Mime/TyPe")); + assertEquals("mime/type", AttachmentUtilities.inferMimeType(null, "Mime/TyPe")); + assertEquals("mime/type", AttachmentUtilities.inferMimeType("", "Mime/TyPe")); // Recognizable file extension; return known type - assertEquals("image/png", AttachmentProvider.inferMimeType(FILE_PNG, null)); - assertEquals("image/png", AttachmentProvider.inferMimeType(FILE_PNG, "")); - assertEquals("image/png", AttachmentProvider.inferMimeType(FILE_PNG, DEFAULT_MIX)); - assertEquals("image/png", AttachmentProvider.inferMimeType(FILE_PNG, TEXT_PLAIN)); + assertEquals("image/png", AttachmentUtilities.inferMimeType(FILE_PNG, null)); + assertEquals("image/png", AttachmentUtilities.inferMimeType(FILE_PNG, "")); + assertEquals("image/png", AttachmentUtilities.inferMimeType(FILE_PNG, DEFAULT_MIX)); + assertEquals("image/png", AttachmentUtilities.inferMimeType(FILE_PNG, TEXT_PLAIN)); // Unrecognized and non-empty file extension, non-"text/plain" type; generate mime type - assertEquals("application/abc", AttachmentProvider.inferMimeType(FILE_ABC, null)); - assertEquals("application/abc", AttachmentProvider.inferMimeType(FILE_ABC, "")); - assertEquals("application/abc", AttachmentProvider.inferMimeType(FILE_ABC, DEFAULT_MIX)); + assertEquals("application/abc", AttachmentUtilities.inferMimeType(FILE_ABC, null)); + assertEquals("application/abc", AttachmentUtilities.inferMimeType(FILE_ABC, "")); + assertEquals("application/abc", + AttachmentUtilities.inferMimeType(FILE_ABC, DEFAULT_MIX)); // Unrecognized and empty file extension, non-"text/plain" type; return "app/octet-stream" - assertEquals(DEFAULT_LOWER, AttachmentProvider.inferMimeType(FILE_NO_EXT, null)); - assertEquals(DEFAULT_LOWER, AttachmentProvider.inferMimeType(FILE_NO_EXT, "")); - assertEquals(DEFAULT_LOWER, AttachmentProvider.inferMimeType(FILE_NO_EXT, DEFAULT_MIX)); - assertEquals(DEFAULT_LOWER, AttachmentProvider.inferMimeType(null, null)); - assertEquals(DEFAULT_LOWER, AttachmentProvider.inferMimeType("", "")); + assertEquals(DEFAULT_LOWER, AttachmentUtilities.inferMimeType(FILE_NO_EXT, null)); + assertEquals(DEFAULT_LOWER, AttachmentUtilities.inferMimeType(FILE_NO_EXT, "")); + assertEquals(DEFAULT_LOWER, + AttachmentUtilities.inferMimeType(FILE_NO_EXT, DEFAULT_MIX)); + assertEquals(DEFAULT_LOWER, AttachmentUtilities.inferMimeType(null, null)); + assertEquals(DEFAULT_LOWER, AttachmentUtilities.inferMimeType("", "")); // Unrecognized or empty file extension, "text/plain" type; return "text/plain" - assertEquals(TEXT_PLAIN, AttachmentProvider.inferMimeType(FILE_ABC, TEXT_PLAIN)); - assertEquals(TEXT_PLAIN, AttachmentProvider.inferMimeType(FILE_NO_EXT, TEXT_PLAIN)); - assertEquals(TEXT_PLAIN, AttachmentProvider.inferMimeType(null, TEXT_PLAIN)); - assertEquals(TEXT_PLAIN, AttachmentProvider.inferMimeType("", TEXT_PLAIN)); + assertEquals(TEXT_PLAIN, AttachmentUtilities.inferMimeType(FILE_ABC, TEXT_PLAIN)); + assertEquals(TEXT_PLAIN, + AttachmentUtilities.inferMimeType(FILE_NO_EXT, TEXT_PLAIN)); + assertEquals(TEXT_PLAIN, AttachmentUtilities.inferMimeType(null, TEXT_PLAIN)); + assertEquals(TEXT_PLAIN, AttachmentUtilities.inferMimeType("", TEXT_PLAIN)); } /** @@ -401,17 +417,17 @@ public class AttachmentProviderTests extends ProviderTestCase2<AttachmentProvide final String FILE_EXTENSION = "myfile.pDf"; final String FILE_TWO_EXTENSIONS = "myfile.false.AbC"; - assertNull(AttachmentProvider.getFilenameExtension(null)); - assertNull(AttachmentProvider.getFilenameExtension("")); - assertNull(AttachmentProvider.getFilenameExtension(FILE_NO_EXTENSION)); + assertNull(AttachmentUtilities.getFilenameExtension(null)); + assertNull(AttachmentUtilities.getFilenameExtension("")); + assertNull(AttachmentUtilities.getFilenameExtension(FILE_NO_EXTENSION)); - assertEquals("pdf", AttachmentProvider.getFilenameExtension(FILE_EXTENSION)); - assertEquals("abc", AttachmentProvider.getFilenameExtension(FILE_TWO_EXTENSIONS)); + assertEquals("pdf", AttachmentUtilities.getFilenameExtension(FILE_EXTENSION)); + assertEquals("abc", AttachmentUtilities.getFilenameExtension(FILE_TWO_EXTENSIONS)); // The API makes no claim as to how these are handled (it probably should), // but make sure that they don't crash. - AttachmentProvider.getFilenameExtension("filename."); - AttachmentProvider.getFilenameExtension(".extension"); + AttachmentUtilities.getFilenameExtension("filename."); + AttachmentUtilities.getFilenameExtension(".extension"); } /** @@ -431,8 +447,8 @@ public class AttachmentProviderTests extends ProviderTestCase2<AttachmentProvide // attachment we add will be id=1 and the 2nd will have id=2. This could fail on // a legitimate implementation. Asserts below will catch this and fail the test // if necessary. - Uri file1Uri = AttachmentProvider.getAttachmentUri(account1.mId, attachment1Id); - Uri file2Uri = AttachmentProvider.getAttachmentUri(account1.mId, attachment2Id); + Uri file1Uri = AttachmentUtilities.getAttachmentUri(account1.mId, attachment1Id); + Uri file2Uri = AttachmentUtilities.getAttachmentUri(account1.mId, attachment2Id); // Test with no attachment found AssetFileDescriptor afd; @@ -463,7 +479,8 @@ public class AttachmentProviderTests extends ProviderTestCase2<AttachmentProvide false, mMockContext); newAttachment2.mContentId = null; newAttachment2.mContentUri = - AttachmentProvider.getAttachmentUri(account1.mId, attachment2Id).toString(); + AttachmentUtilities.getAttachmentUri(account1.mId, attachment2Id) + .toString(); newAttachment2.mMimeType = "image/png"; attachment2Id = addAttachmentToDb(account1, newAttachment2); assertEquals("Broken test: Unexpected id assignment", 2, attachment2Id); @@ -496,10 +513,10 @@ public class AttachmentProviderTests extends ProviderTestCase2<AttachmentProvide // attachment we add will be id=1 and the 2nd will have id=2. This could fail on // a legitimate implementation. Asserts below will catch this and fail the test // if necessary. - Uri thumb1Uri = AttachmentProvider.getAttachmentThumbnailUri(account1.mId, attachment1Id, - 62, 62); - Uri thumb2Uri = AttachmentProvider.getAttachmentThumbnailUri(account1.mId, attachment2Id, - 62, 62); + Uri thumb1Uri = AttachmentUtilities.getAttachmentThumbnailUri(account1.mId, + attachment1Id, 62, 62); + Uri thumb2Uri = AttachmentUtilities.getAttachmentThumbnailUri(account1.mId, + attachment2Id, 62, 62); // Test with an attached database, but no attachment found AssetFileDescriptor afd = mMockResolver.openAssetFileDescriptor(thumb1Uri, "r"); @@ -521,7 +538,8 @@ public class AttachmentProviderTests extends ProviderTestCase2<AttachmentProvide false, mMockContext); newAttachment2.mContentId = null; newAttachment2.mContentUri = - AttachmentProvider.getAttachmentUri(account1.mId, attachment2Id).toString(); + AttachmentUtilities.getAttachmentUri(account1.mId, attachment2Id) + .toString(); newAttachment2.mMimeType = "image/png"; attachment2Id = addAttachmentToDb(account1, newAttachment2); assertEquals("Broken test: Unexpected id assignment", 2, attachment2Id); @@ -539,7 +557,7 @@ public class AttachmentProviderTests extends ProviderTestCase2<AttachmentProvide false, mMockContext); newAttachment.mContentUri = contentUriStr; long attachmentId = addAttachmentToDb(account, newAttachment); - Uri attachmentUri = AttachmentProvider.getAttachmentUri(account.mId, attachmentId); + Uri attachmentUri = AttachmentUtilities.getAttachmentUri(account.mId, attachmentId); return attachmentUri; } @@ -557,12 +575,13 @@ public class AttachmentProviderTests extends ProviderTestCase2<AttachmentProvide final long message1Id = 1; // We use attachmentId == 1 but any other id would do final long attachment1Id = 1; - final Uri attachment1Uri = AttachmentProvider.getAttachmentUri(account1.mId, attachment1Id); + final Uri attachment1Uri = AttachmentUtilities.getAttachmentUri(account1.mId, + attachment1Id); // Test with no attachment found - should return input // We know that the attachmentId 1 does not exist because there are no attachments // created at this point - Uri result = AttachmentProvider.resolveAttachmentIdToContentUri( + Uri result = AttachmentUtilities.resolveAttachmentIdToContentUri( mMockResolver, attachment1Uri); assertEquals(attachment1Uri, result); @@ -571,8 +590,8 @@ public class AttachmentProviderTests extends ProviderTestCase2<AttachmentProvide // the DB, and does not sample the files, so we won't bother creating the files { Uri attachmentUri = createAttachment(account1, message1Id, "file:///path/to/file"); - Uri contentUri = AttachmentProvider.resolveAttachmentIdToContentUri(mMockResolver, - attachmentUri); + Uri contentUri = AttachmentUtilities.resolveAttachmentIdToContentUri( + mMockResolver, attachmentUri); // When the attachment is found, return the stored content_uri value assertEquals("file:///path/to/file", contentUri.toString()); } @@ -580,8 +599,8 @@ public class AttachmentProviderTests extends ProviderTestCase2<AttachmentProvide // Test with existing attachement and contentUri == null { Uri attachmentUri = createAttachment(account1, message1Id, null); - Uri contentUri = AttachmentProvider.resolveAttachmentIdToContentUri(mMockResolver, - attachmentUri); + Uri contentUri = AttachmentUtilities.resolveAttachmentIdToContentUri( + mMockResolver, attachmentUri); // When contentUri is null should return input assertEquals(attachmentUri, contentUri); } @@ -615,25 +634,30 @@ public class AttachmentProviderTests extends ProviderTestCase2<AttachmentProvide createAttachmentFile(account1, newAttachment3.mId); // Confirm 3 attachment files found - File attachmentsDir = AttachmentProvider.getAttachmentDirectory(mMockContext, account1.mId); + File attachmentsDir = AttachmentUtilities.getAttachmentDirectory(mMockContext, + account1.mId); assertEquals(3, attachmentsDir.listFiles().length); // Command deletion of some files and check for results // Message 4 has no attachments so no files should be deleted - AttachmentProvider.deleteAllAttachmentFiles(mMockContext, account1.mId, message4Id); + AttachmentUtilities.deleteAllAttachmentFiles(mMockContext, account1.mId, + message4Id); assertEquals(3, attachmentsDir.listFiles().length); // Message 3 has no attachment files so no files should be deleted - AttachmentProvider.deleteAllAttachmentFiles(mMockContext, account1.mId, message3Id); + AttachmentUtilities.deleteAllAttachmentFiles(mMockContext, account1.mId, + message3Id); assertEquals(3, attachmentsDir.listFiles().length); // Message 2 has 2 attachment files so this should delete 2 files - AttachmentProvider.deleteAllAttachmentFiles(mMockContext, account1.mId, message2Id); + AttachmentUtilities.deleteAllAttachmentFiles(mMockContext, account1.mId, + message2Id); assertEquals(1, attachmentsDir.listFiles().length); // Message 1 has 1 attachment file so this should delete the last file - AttachmentProvider.deleteAllAttachmentFiles(mMockContext, account1.mId, message1Id); + AttachmentUtilities.deleteAllAttachmentFiles(mMockContext, account1.mId, + message1Id); assertEquals(0, attachmentsDir.listFiles().length); } @@ -655,15 +679,18 @@ public class AttachmentProviderTests extends ProviderTestCase2<AttachmentProvide populateAccountMailbox(account1, mailbox2Id, 1); // Confirm four attachment files found - File attachmentsDir = AttachmentProvider.getAttachmentDirectory(mMockContext, account1.mId); + File attachmentsDir = AttachmentUtilities.getAttachmentDirectory(mMockContext, + account1.mId); assertEquals(4, attachmentsDir.listFiles().length); // Command the deletion of mailbox 1 - we should lose 3 attachment files - AttachmentProvider.deleteAllMailboxAttachmentFiles(mMockContext, account1Id, mailbox1Id); + AttachmentUtilities.deleteAllMailboxAttachmentFiles(mMockContext, account1Id, + mailbox1Id); assertEquals(1, attachmentsDir.listFiles().length); // Command the deletion of mailbox 2 - we should lose 1 attachment file - AttachmentProvider.deleteAllMailboxAttachmentFiles(mMockContext, account1Id, mailbox2Id); + AttachmentUtilities.deleteAllMailboxAttachmentFiles(mMockContext, account1Id, + mailbox2Id); assertEquals(0, attachmentsDir.listFiles().length); } @@ -697,18 +724,20 @@ public class AttachmentProviderTests extends ProviderTestCase2<AttachmentProvide populateAccountMailbox(account2, mailbox4Id, 2); // Confirm eleven attachment files found - File directory1 = AttachmentProvider.getAttachmentDirectory(mMockContext, account1.mId); + File directory1 = AttachmentUtilities.getAttachmentDirectory(mMockContext, + account1.mId); assertEquals(4, directory1.listFiles().length); - File directory2 = AttachmentProvider.getAttachmentDirectory(mMockContext, account2.mId); + File directory2 = AttachmentUtilities.getAttachmentDirectory(mMockContext, + account2.mId); assertEquals(7, directory2.listFiles().length); // Command the deletion of account 1 - we should lose 4 attachment files - AttachmentProvider.deleteAllAccountAttachmentFiles(mMockContext, account1Id); + AttachmentUtilities.deleteAllAccountAttachmentFiles(mMockContext, account1Id); assertEquals(0, directory1.listFiles().length); assertEquals(7, directory2.listFiles().length); // Command the deletion of account 2 - we should lose 7 attachment file - AttachmentProvider.deleteAllAccountAttachmentFiles(mMockContext, account2Id); + AttachmentUtilities.deleteAllAccountAttachmentFiles(mMockContext, account2Id); assertEquals(0, directory1.listFiles().length); assertEquals(0, directory2.listFiles().length); } |
