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 @@ + + + + + + 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; @@ -147,108 +146,6 @@ public class LegacyConversions { return true; } - /** - * Copy body text (plain and/or HTML) from MimeMessage to provider Message - */ - public static boolean updateBodyFields(EmailContent.Body body, - EmailContent.Message localMessage, ArrayList 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. * @@ -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 attachments = new ArrayList(); 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 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 { @@ -180,82 +122,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. - * - *
-     *                   |---------------------------------------------------------|
-     *                   |                  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                                  |
-     * |---|-----------------------------------------------------------------------|
-     * 
- * - * 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; } } @@ -432,101 +298,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. */ 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 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 mSyncReports = - new HashMap(); - - 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 oldSyncReports = - new HashMap(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 getPopImapAccountList(Context context) { - ArrayList providerAccounts = new ArrayList(); - 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 sReconcilePopImapAccountsSyncExecutor = - new SingleRunningTask("ReconcilePopImapAccountsSync") { - @Override - protected void runInternal(Context context) { - android.accounts.Account[] accountManagerAccounts = AccountManager.get(context) - .getAccountsByType(Email.POP_IMAP_ACCOUNT_MANAGER_TYPE); - ArrayList 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 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 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 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 mSyncReports = + new HashMap(); + + 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 oldSyncReports = + new HashMap(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 getPopImapAccountList(Context context) { + ArrayList providerAccounts = new ArrayList(); + 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 sReconcilePopImapAccountsSyncExecutor = + new SingleRunningTask("ReconcilePopImapAccountsSync") { + @Override + protected void runInternal(Context context) { + android.accounts.Account[] accountManagerAccounts = AccountManager.get(context) + .getAccountsByType(Email.POP_IMAP_ACCOUNT_MANAGER_TYPE); + ArrayList 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 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 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 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 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. + * + *
+     *                   |---------------------------------------------------------|
+     *                   |                  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                                  |
+     * |---|-----------------------------------------------------------------------|
+     * 
+ * + * 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 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 { 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