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