segments = uri.getPathSegments();
String id = segments.get(1);
String format = segments.get(2);
- if (FORMAT_THUMBNAIL.equals(format)) {
+ if (AttachmentUtilities.FORMAT_THUMBNAIL.equals(format)) {
return "image/png";
} else {
uri = ContentUris.withAppendedId(Attachment.CONTENT_URI, Long.parseLong(id));
- Cursor c = getContext().getContentResolver().query(uri, MIME_TYPE_PROJECTION,
- null, null, null);
+ Cursor c = getContext().getContentResolver().query(uri, MIME_TYPE_PROJECTION, null,
+ null, null);
try {
if (c.moveToFirst()) {
String mimeType = c.getString(MIME_TYPE_COLUMN_MIME_TYPE);
String fileName = c.getString(MIME_TYPE_COLUMN_FILENAME);
- mimeType = inferMimeType(fileName, mimeType);
+ mimeType = AttachmentUtilities.inferMimeType(fileName, mimeType);
return mimeType;
}
} finally {
@@ -180,82 +122,6 @@ public class AttachmentProvider extends ContentProvider {
}
}
- /**
- * Helper to convert unknown or unmapped attachments to something useful based on filename
- * extensions. The mime type is inferred based upon the table below. It's not perfect, but
- * it helps.
- *
- *
- * |---------------------------------------------------------|
- * | E X T E N S I O N |
- * |---------------------------------------------------------|
- * | .eml | known(.png) | unknown(.abc) | none |
- * | M |-----------------------------------------------------------------------|
- * | I | none | msg/rfc822 | image/png | app/abc | app/oct-str |
- * | M |-------------| (always | | | |
- * | E | app/oct-str | overrides | | | |
- * | T |-------------| | |-----------------------------|
- * | Y | text/plain | | | text/plain |
- * | P |-------------| |-------------------------------------------|
- * | E | any/type | | any/type |
- * |---|-----------------------------------------------------------------------|
- *
- *
- * NOTE: Since mime types on Android are case-*sensitive*, return values are always in
- * lower case.
- *
- * @param fileName The given filename
- * @param mimeType The given mime type
- * @return A likely mime type for the attachment
- */
- public static String inferMimeType(final String fileName, final String mimeType) {
- String resultType = null;
- String fileExtension = getFilenameExtension(fileName);
- boolean isTextPlain = "text/plain".equalsIgnoreCase(mimeType);
-
- if ("eml".equals(fileExtension)) {
- resultType = "message/rfc822";
- } else {
- boolean isGenericType =
- isTextPlain || "application/octet-stream".equalsIgnoreCase(mimeType);
- // If the given mime type is non-empty and non-generic, return it
- if (isGenericType || TextUtils.isEmpty(mimeType)) {
- if (!TextUtils.isEmpty(fileExtension)) {
- // Otherwise, try to find a mime type based upon the file extension
- resultType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(fileExtension);
- if (TextUtils.isEmpty(resultType)) {
- // Finally, if original mimetype is text/plain, use it; otherwise synthesize
- resultType = isTextPlain ? mimeType : "application/" + fileExtension;
- }
- }
- } else {
- resultType = mimeType;
- }
- }
-
- // No good guess could be made; use an appropriate generic type
- if (TextUtils.isEmpty(resultType)) {
- resultType = isTextPlain ? "text/plain" : "application/octet-stream";
- }
- return resultType.toLowerCase();
- }
-
- /**
- * Extract and return filename's extension, converted to lower case, and not including the "."
- *
- * @return extension, or null if not found (or null/empty filename)
- */
- public static String getFilenameExtension(String fileName) {
- String extension = null;
- if (!TextUtils.isEmpty(fileName)) {
- int lastDot = fileName.lastIndexOf('.');
- if ((lastDot > 0) && (lastDot < fileName.length() - 1)) {
- extension = fileName.substring(lastDot + 1).toLowerCase();
- }
- }
- return extension;
- }
-
/**
* Open an attachment file. There are two "modes" - "raw", which returns an actual file,
* and "thumbnail", which attempts to generate a thumbnail image.
@@ -275,17 +141,17 @@ public class AttachmentProvider extends ContentProvider {
String accountId = segments.get(0);
String id = segments.get(1);
String format = segments.get(2);
- if (FORMAT_THUMBNAIL.equals(format)) {
+ if (AttachmentUtilities.FORMAT_THUMBNAIL.equals(format)) {
int width = Integer.parseInt(segments.get(3));
int height = Integer.parseInt(segments.get(4));
String filename = "thmb_" + accountId + "_" + id;
File dir = getContext().getCacheDir();
File file = new File(dir, filename);
if (!file.exists()) {
- Uri attachmentUri =
+ Uri attachmentUri = AttachmentUtilities.
getAttachmentUri(Long.parseLong(accountId), Long.parseLong(id));
Cursor c = query(attachmentUri,
- new String[] { AttachmentProviderColumns.DATA }, null, null, null);
+ new String[] { Columns.DATA }, null, null, null);
if (c != null) {
try {
if (c.moveToFirst()) {
@@ -355,8 +221,8 @@ public class AttachmentProvider extends ContentProvider {
if (projection == null) {
projection =
new String[] {
- AttachmentProviderColumns._ID,
- AttachmentProviderColumns.DATA,
+ Columns._ID,
+ Columns.DATA,
};
}
@@ -387,16 +253,16 @@ public class AttachmentProvider extends ContentProvider {
Object[] values = new Object[projection.length];
for (int i = 0, count = projection.length; i < count; i++) {
String column = projection[i];
- if (AttachmentProviderColumns._ID.equals(column)) {
+ if (Columns._ID.equals(column)) {
values[i] = id;
}
- else if (AttachmentProviderColumns.DATA.equals(column)) {
+ else if (Columns.DATA.equals(column)) {
values[i] = contentUri;
}
- else if (AttachmentProviderColumns.DISPLAY_NAME.equals(column)) {
+ else if (Columns.DISPLAY_NAME.equals(column)) {
values[i] = name;
}
- else if (AttachmentProviderColumns.SIZE.equals(column)) {
+ else if (Columns.SIZE.equals(column)) {
values[i] = size;
}
}
@@ -432,101 +298,6 @@ public class AttachmentProvider extends ContentProvider {
}
}
- /**
- * Resolve attachment id to content URI. Returns the resolved content URI (from the attachment
- * DB) or, if not found, simply returns the incoming value.
- *
- * @param attachmentUri
- * @return resolved content URI
- *
- * TODO: Throws an SQLite exception on a missing DB file (e.g. unknown URI) instead of just
- * returning the incoming uri, as it should.
- */
- public static Uri resolveAttachmentIdToContentUri(ContentResolver resolver, Uri attachmentUri) {
- Cursor c = resolver.query(attachmentUri,
- new String[] { AttachmentProvider.AttachmentProviderColumns.DATA },
- null, null, null);
- if (c != null) {
- try {
- if (c.moveToFirst()) {
- final String strUri = c.getString(0);
- if (strUri != null) {
- return Uri.parse(strUri);
- } else {
- Email.log("AttachmentProvider: attachment with null contentUri");
- }
- }
- } finally {
- c.close();
- }
- }
- return attachmentUri;
- }
-
- /**
- * In support of deleting a message, find all attachments and delete associated attachment
- * files.
- * @param context
- * @param accountId the account for the message
- * @param messageId the message
- */
- public static void deleteAllAttachmentFiles(Context context, long accountId, long messageId) {
- Uri uri = ContentUris.withAppendedId(Attachment.MESSAGE_ID_URI, messageId);
- Cursor c = context.getContentResolver().query(uri, Attachment.ID_PROJECTION,
- null, null, null);
- try {
- while (c.moveToNext()) {
- long attachmentId = c.getLong(Attachment.ID_PROJECTION_COLUMN);
- File attachmentFile = getAttachmentFilename(context, accountId, attachmentId);
- // Note, delete() throws no exceptions for basic FS errors (e.g. file not found)
- // it just returns false, which we ignore, and proceed to the next file.
- // This entire loop is best-effort only.
- attachmentFile.delete();
- }
- } finally {
- c.close();
- }
- }
-
- /**
- * In support of deleting a mailbox, find all messages and delete their attachments.
- *
- * @param context
- * @param accountId the account for the mailbox
- * @param mailboxId the mailbox for the messages
- */
- public static void deleteAllMailboxAttachmentFiles(Context context, long accountId,
- long mailboxId) {
- Cursor c = context.getContentResolver().query(Message.CONTENT_URI,
- Message.ID_COLUMN_PROJECTION, MessageColumns.MAILBOX_KEY + "=?",
- new String[] { Long.toString(mailboxId) }, null);
- try {
- while (c.moveToNext()) {
- long messageId = c.getLong(Message.ID_PROJECTION_COLUMN);
- deleteAllAttachmentFiles(context, accountId, messageId);
- }
- } finally {
- c.close();
- }
- }
-
- /**
- * In support of deleting or wiping an account, delete all related attachments.
- *
- * @param context
- * @param accountId the account to scrub
- */
- public static void deleteAllAccountAttachmentFiles(Context context, long accountId) {
- File[] files = getAttachmentDirectory(context, accountId).listFiles();
- if (files == null) return;
- for (File file : files) {
- boolean result = file.delete();
- if (!result) {
- Log.e(Email.LOG_TAG, "Failed to delete attachment file " + file.getName());
- }
- }
- }
-
/**
* Need this to suppress warning in unit tests.
*/
diff --git a/src/com/android/email/service/AccountService.java b/src/com/android/email/service/AccountService.java
new file mode 100644
index 000000000..c4f12d483
--- /dev/null
+++ b/src/com/android/email/service/AccountService.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.email.service;
+
+import com.android.email.AccountBackupRestore;
+import com.android.email.NotificationController;
+import com.android.email.ResourceHelper;
+import com.android.emailcommon.service.IAccountService;
+
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.os.IBinder;
+import android.os.RemoteException;
+
+public class AccountService extends Service {
+
+ private Context mContext;
+
+ private final IAccountService.Stub mBinder = new IAccountService.Stub() {
+
+ @Override
+ public void notifyLoginFailed(long accountId) throws RemoteException {
+ NotificationController.getInstance(mContext).showLoginFailedNotification(accountId);
+ }
+
+ @Override
+ public void notifyLoginSucceeded(long accountId) throws RemoteException {
+ NotificationController.getInstance(mContext).cancelLoginFailedNotification(accountId);
+ }
+
+ @Override
+ public void notifyNewMessages(long accountId) throws RemoteException {
+ MailService.actionNotifyNewMessages(mContext, accountId);
+ }
+
+ @Override
+ public void restoreAccountsIfNeeded() throws RemoteException {
+ AccountBackupRestore.restoreAccountsIfNeeded(mContext);
+ }
+
+ @Override
+ public void accountDeleted() throws RemoteException {
+ MailService.accountDeleted(mContext);
+ }
+
+ @Override
+ public int getAccountColor(long accountId) throws RemoteException {
+ return ResourceHelper.getInstance(mContext).getAccountColor(accountId);
+ }
+ };
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ if (mContext == null) {
+ mContext = this;
+ }
+ return mBinder;
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/email/service/AttachmentDownloadService.java b/src/com/android/email/service/AttachmentDownloadService.java
index 2f155f04f..6422b4881 100644
--- a/src/com/android/email/service/AttachmentDownloadService.java
+++ b/src/com/android/email/service/AttachmentDownloadService.java
@@ -23,7 +23,6 @@ import com.android.email.NotificationController;
import com.android.email.Utility;
import com.android.email.Controller.ControllerService;
import com.android.email.ExchangeUtils.NullEmailService;
-import com.android.email.provider.AttachmentProvider;
import com.android.email.provider.EmailContent;
import com.android.email.provider.EmailContent.Account;
import com.android.email.provider.EmailContent.Attachment;
@@ -31,6 +30,7 @@ import com.android.email.provider.EmailContent.Message;
import com.android.emailcommon.service.EmailServiceProxy;
import com.android.emailcommon.service.EmailServiceStatus;
import com.android.emailcommon.service.IEmailServiceCallback;
+import com.android.emailcommon.utility.AttachmentUtilities;
import com.android.exchange.ExchangeService;
import android.accounts.AccountManager;
@@ -427,7 +427,7 @@ public class AttachmentDownloadService extends Service implements Runnable {
*/
private void startDownload(Class 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 mSyncReports =
- new HashMap();
-
- public static void actionReschedule(Context context) {
- Intent i = new Intent();
- i.setClass(context, MailService.class);
- i.setAction(MailService.ACTION_RESCHEDULE);
- context.startService(i);
- }
-
- public static void actionCancel(Context context) {
- Intent i = new Intent();
- i.setClass(context, MailService.class);
- i.setAction(MailService.ACTION_CANCEL);
- context.startService(i);
- }
-
- /**
- * Entry point for AttachmentDownloadService to ask that pending mail be sent
- * @param context the caller's context
- * @param accountId the account whose pending mail should be sent
- */
- public static void actionSendPendingMail(Context context, long accountId) {
- Intent i = new Intent();
- i.setClass(context, MailService.class);
- i.setAction(MailService.ACTION_SEND_PENDING_MAIL);
- i.putExtra(MailService.EXTRA_ACCOUNT, accountId);
- context.startService(i);
- }
-
- /**
- * Reset new message counts for one or all accounts. This clears both our local copy and
- * the values (if any) stored in the account records.
- *
- * @param accountId account to clear, or -1 for all accounts
- */
- public static void resetNewMessageCount(final Context context, final long accountId) {
- synchronized (mSyncReports) {
- for (AccountSyncReport report : mSyncReports.values()) {
- if (accountId == -1 || accountId == report.accountId) {
- report.unseenMessageCount = 0;
- report.lastUnseenMessageCount = 0;
- }
- }
- }
- // Clear notification
- NotificationController.getInstance(context).cancelNewMessageNotification(accountId);
-
- // now do the database - all accounts, or just one of them
- Utility.runAsync(new Runnable() {
- @Override
- public void run() {
- Uri uri = Account.RESET_NEW_MESSAGE_COUNT_URI;
- if (accountId != -1) {
- uri = ContentUris.withAppendedId(uri, accountId);
- }
- context.getContentResolver().update(uri, null, null, null);
- }
- });
- }
-
- /**
- * Entry point for asynchronous message services (e.g. push mode) to post notifications of new
- * messages. This assumes that the push provider has already synced the messages into the
- * appropriate database - this simply triggers the notification mechanism.
- *
- * @param context a context
- * @param accountId the id of the account that is reporting new messages
- */
- public static void actionNotifyNewMessages(Context context, long accountId) {
- Intent i = new Intent(ACTION_NOTIFY_MAIL);
- i.setClass(context, MailService.class);
- i.putExtra(EXTRA_ACCOUNT, accountId);
- context.startService(i);
- }
-
- /*package*/ static MailService getMailServiceForTest() {
- return sMailService;
- }
-
- @Override
- public int onStartCommand(final Intent intent, int flags, final int startId) {
- super.onStartCommand(intent, flags, startId);
-
- // Save the service away (for unit tests)
- sMailService = this;
-
- // Restore accounts, if it has not happened already
- AccountBackupRestore.restoreAccountsIfNeeded(this);
-
- Utility.runAsync(new Runnable() {
- @Override
- public void run() {
- reconcilePopImapAccountsSync(MailService.this);
- }
- });
-
- // TODO this needs to be passed through the controller and back to us
- mStartId = startId;
- String action = intent.getAction();
- final long accountId = intent.getLongExtra(EXTRA_ACCOUNT, -1);
-
- mController = Controller.getInstance(this);
- mController.addResultCallback(mControllerCallback);
- mContentResolver = getContentResolver();
- mContext = this;
-
- final AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
-
- if (ACTION_CHECK_MAIL.equals(action)) {
- // DB access required to satisfy this intent, so offload from UI thread
- Utility.runAsync(new Runnable() {
- @Override
- public void run() {
- // If we have the data, restore the last-sync-times for each account
- // These are cached in the wakeup intent in case the process was killed.
- restoreSyncReports(intent);
-
- // Sync a specific account if given
- if (Email.DEBUG) {
- Log.d(LOG_TAG, "action: check mail for id=" + accountId);
- }
- if (accountId >= 0) {
- setWatchdog(accountId, alarmManager);
- }
-
- // Start sync if account is given && bg data enabled && account has sync enabled
- boolean syncStarted = false;
- if (accountId != -1 && isBackgroundDataEnabled()) {
- synchronized(mSyncReports) {
- for (AccountSyncReport report: mSyncReports.values()) {
- if (report.accountId == accountId) {
- if (report.syncEnabled) {
- syncStarted = syncOneAccount(mController, accountId,
- startId);
- }
- break;
- }
- }
- }
- }
-
- // Reschedule if we didn't start sync.
- if (!syncStarted) {
- // Prevent runaway on the current account by pretending it updated
- if (accountId != -1) {
- updateAccountReport(accountId, 0);
- }
- // Find next account to sync, and reschedule
- reschedule(alarmManager);
- // Stop the service, unless actually syncing (which will stop the service)
- stopSelf(startId);
- }
- }
- });
- }
- else if (ACTION_CANCEL.equals(action)) {
- if (Email.DEBUG) {
- Log.d(LOG_TAG, "action: cancel");
- }
- cancel();
- stopSelf(startId);
- }
- else if (ACTION_SEND_PENDING_MAIL.equals(action)) {
- if (Email.DEBUG) {
- Log.d(LOG_TAG, "action: send pending mail");
- }
- Utility.runAsync(new Runnable() {
- public void run() {
- mController.sendPendingMessages(accountId);
- }
- });
- stopSelf(startId);
- }
- else if (ACTION_RESCHEDULE.equals(action)) {
- if (Email.DEBUG) {
- Log.d(LOG_TAG, "action: reschedule");
- }
- final NotificationController nc = NotificationController.getInstance(this);
- // DB access required to satisfy this intent, so offload from UI thread
- Utility.runAsync(new Runnable() {
- @Override
- public void run() {
- // Clear all notifications, in case account list has changed.
- //
- // TODO Clear notifications for non-existing accounts. Now that we have
- // separate notifications for each account, NotificationController should be
- // able to do that.
- nc.cancelNewMessageNotification(-1);
-
- // When called externally, we refresh the sync reports table to pick up
- // any changes in the account list or account settings
- refreshSyncReports();
- // Finally, scan for the next needing update, and set an alarm for it
- reschedule(alarmManager);
- stopSelf(startId);
- }
- });
- } else if (ACTION_NOTIFY_MAIL.equals(action)) {
- // DB access required to satisfy this intent, so offload from UI thread
- Utility.runAsync(new Runnable() {
- @Override
- public void run() {
- // Get the current new message count
- Cursor c = mContentResolver.query(
- ContentUris.withAppendedId(Account.CONTENT_URI, accountId),
- NEW_MESSAGE_COUNT_PROJECTION, null, null, null);
- int newMessageCount = 0;
- try {
- if (c.moveToFirst()) {
- newMessageCount = c.getInt(0);
- updateAccountReport(accountId, newMessageCount);
- notifyNewMessages(accountId);
- }
- } finally {
- c.close();
- }
- if (Email.DEBUG) {
- Log.d(LOG_TAG, "notify accountId=" + Long.toString(accountId)
- + " count=" + newMessageCount);
- }
- stopSelf(startId);
- }
- });
- }
-
- // Returning START_NOT_STICKY means that if a mail check is killed (e.g. due to memory
- // pressure, there will be no explicit restart. This is OK; Note that we set a watchdog
- // alarm before each mailbox check. If the mailbox check never completes, the watchdog
- // will fire and get things running again.
- return START_NOT_STICKY;
- }
-
- @Override
- public IBinder onBind(Intent intent) {
- return null;
- }
-
- @Override
- public void onDestroy() {
- super.onDestroy();
- Controller.getInstance(getApplication()).removeResultCallback(mControllerCallback);
- }
-
- private void cancel() {
- AlarmManager alarmMgr = (AlarmManager)getSystemService(Context.ALARM_SERVICE);
- PendingIntent pi = createAlarmIntent(-1, null, false);
- alarmMgr.cancel(pi);
- }
-
- /**
- * Refresh the sync reports, to pick up any changes in the account list or account settings.
- */
- /*package*/ void refreshSyncReports() {
- synchronized (mSyncReports) {
- // Make shallow copy of sync reports so we can recover the prev sync times
- HashMap oldSyncReports =
- new HashMap(mSyncReports);
-
- // Delete the sync reports to force a refresh from live account db data
- setupSyncReportsLocked(SYNC_REPORTS_RESET, this);
-
- // Restore prev-sync & next-sync times for any reports in the new list
- for (AccountSyncReport newReport : mSyncReports.values()) {
- AccountSyncReport oldReport = oldSyncReports.get(newReport.accountId);
- if (oldReport != null) {
- newReport.prevSyncTime = oldReport.prevSyncTime;
- if (newReport.syncInterval > 0 && newReport.prevSyncTime != 0) {
- newReport.nextSyncTime =
- newReport.prevSyncTime + (newReport.syncInterval * 1000 * 60);
- }
- }
- }
- }
- }
-
- /**
- * Create and send an alarm with the entire list. This also sends a list of known last-sync
- * times with the alarm, so if we are killed between alarms, we don't lose this info.
- *
- * @param alarmMgr passed in so we can mock for testing.
- */
- /* package */ void reschedule(AlarmManager alarmMgr) {
- // restore the reports if lost
- setupSyncReports(SYNC_REPORTS_ALL_ACCOUNTS_IF_EMPTY);
- synchronized (mSyncReports) {
- int numAccounts = mSyncReports.size();
- long[] accountInfo = new long[numAccounts * 2]; // pairs of { accountId, lastSync }
- int accountInfoIndex = 0;
-
- long nextCheckTime = Long.MAX_VALUE;
- AccountSyncReport nextAccount = null;
- long timeNow = SystemClock.elapsedRealtime();
-
- for (AccountSyncReport report : mSyncReports.values()) {
- if (report.syncInterval <= 0) { // no timed checks - skip
- continue;
- }
- long prevSyncTime = report.prevSyncTime;
- long nextSyncTime = report.nextSyncTime;
-
- // select next account to sync
- if ((prevSyncTime == 0) || (nextSyncTime < timeNow)) { // never checked, or overdue
- nextCheckTime = 0;
- nextAccount = report;
- } else if (nextSyncTime < nextCheckTime) { // next to be checked
- nextCheckTime = nextSyncTime;
- nextAccount = report;
- }
- // collect last-sync-times for all accounts
- // this is using pairs of {long,long} to simplify passing in a bundle
- accountInfo[accountInfoIndex++] = report.accountId;
- accountInfo[accountInfoIndex++] = report.prevSyncTime;
- }
-
- // Clear out any unused elements in the array
- while (accountInfoIndex < accountInfo.length) {
- accountInfo[accountInfoIndex++] = -1;
- }
-
- // set/clear alarm as needed
- long idToCheck = (nextAccount == null) ? -1 : nextAccount.accountId;
- PendingIntent pi = createAlarmIntent(idToCheck, accountInfo, false);
-
- if (nextAccount == null) {
- alarmMgr.cancel(pi);
- if (Email.DEBUG) {
- Log.d(LOG_TAG, "reschedule: alarm cancel - no account to check");
- }
- } else {
- alarmMgr.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, nextCheckTime, pi);
- if (Email.DEBUG) {
- Log.d(LOG_TAG, "reschedule: alarm set at " + nextCheckTime
- + " for " + nextAccount);
- }
- }
- }
- }
-
- /**
- * Create a watchdog alarm and set it. This is used in case a mail check fails (e.g. we are
- * killed by the system due to memory pressure.) Normally, a mail check will complete and
- * the watchdog will be replaced by the call to reschedule().
- * @param accountId the account we were trying to check
- * @param alarmMgr system alarm manager
- */
- private void setWatchdog(long accountId, AlarmManager alarmMgr) {
- PendingIntent pi = createAlarmIntent(accountId, null, true);
- long timeNow = SystemClock.elapsedRealtime();
- long nextCheckTime = timeNow + WATCHDOG_DELAY;
- alarmMgr.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, nextCheckTime, pi);
- }
-
- /**
- * Return a pending intent for use by this alarm. Most of the fields must be the same
- * (in order for the intent to be recognized by the alarm manager) but the extras can
- * be different, and are passed in here as parameters.
- */
- /* package */ PendingIntent createAlarmIntent(long checkId, long[] accountInfo,
- boolean isWatchdog) {
- Intent i = new Intent();
- i.setClass(this, MailService.class);
- i.setAction(ACTION_CHECK_MAIL);
- i.putExtra(EXTRA_ACCOUNT, checkId);
- i.putExtra(EXTRA_ACCOUNT_INFO, accountInfo);
- if (isWatchdog) {
- i.putExtra(EXTRA_DEBUG_WATCHDOG, true);
- }
- PendingIntent pi = PendingIntent.getService(this, 0, i, PendingIntent.FLAG_UPDATE_CURRENT);
- return pi;
- }
-
- /**
- * Start a controller sync for a specific account
- *
- * @param controller The controller to do the sync work
- * @param checkAccountId the account Id to try and check
- * @param startId the id of this service launch
- * @return true if mail checking has started, false if it could not (e.g. bad account id)
- */
- private boolean syncOneAccount(Controller controller, long checkAccountId, int startId) {
- long inboxId = Mailbox.findMailboxOfType(this, checkAccountId, Mailbox.TYPE_INBOX);
- if (inboxId == Mailbox.NO_MAILBOX) {
- return false;
- } else {
- controller.serviceCheckMail(checkAccountId, inboxId, startId);
- return true;
- }
- }
-
- /**
- * Note: Times are relative to SystemClock.elapsedRealtime()
- *
- * TODO: Look more closely at syncEnabled and see if we can simply coalesce it into
- * syncInterval (e.g. if !syncEnabled, set syncInterval to -1).
- */
- /*package*/ static class AccountSyncReport {
- long accountId;
- long prevSyncTime; // 0 == unknown
- long nextSyncTime; // 0 == ASAP -1 == don't sync
-
- /** # of "unseen" messages to show in notification */
- int unseenMessageCount;
-
- /**
- * # of unseen, the value shown on the last notification. Used to
- * calculate "the number of messages that have just been fetched".
- *
- * TODO It's a sort of cheating. Should we use the "real" number? The only difference
- * is the first notification after reboot / process restart.
- */
- int lastUnseenMessageCount;
-
- int syncInterval;
- boolean notify;
-
- boolean syncEnabled; // whether auto sync is enabled for this account
-
- /** # of messages that have just been fetched */
- int getJustFetchedMessageCount() {
- return unseenMessageCount - lastUnseenMessageCount;
- }
-
- @Override
- public String toString() {
- return "id=" + accountId
- + " prevSync=" + prevSyncTime + " nextSync=" + nextSyncTime + " numUnseen="
- + unseenMessageCount;
- }
- }
-
- /**
- * scan accounts to create a list of { acct, prev sync, next sync, #new }
- * use this to create a fresh copy. assumes all accounts need sync
- *
- * @param accountId -1 will rebuild the list if empty. other values will force loading
- * of a single account (e.g if it was created after the original list population)
- */
- /* package */ void setupSyncReports(long accountId) {
- synchronized (mSyncReports) {
- setupSyncReportsLocked(accountId, mContext);
- }
- }
-
- /**
- * Handle the work of setupSyncReports. Must be synchronized on mSyncReports.
- */
- /*package*/ void setupSyncReportsLocked(long accountId, Context context) {
- ContentResolver resolver = context.getContentResolver();
- if (accountId == SYNC_REPORTS_RESET) {
- // For test purposes, force refresh of mSyncReports
- mSyncReports.clear();
- accountId = SYNC_REPORTS_ALL_ACCOUNTS_IF_EMPTY;
- } else if (accountId == SYNC_REPORTS_ALL_ACCOUNTS_IF_EMPTY) {
- // -1 == reload the list if empty, otherwise exit immediately
- if (mSyncReports.size() > 0) {
- return;
- }
- } else {
- // load a single account if it doesn't already have a sync record
- if (mSyncReports.containsKey(accountId)) {
- return;
- }
- }
-
- // setup to add a single account or all accounts
- Uri uri;
- if (accountId == SYNC_REPORTS_ALL_ACCOUNTS_IF_EMPTY) {
- uri = Account.CONTENT_URI;
- } else {
- uri = ContentUris.withAppendedId(Account.CONTENT_URI, accountId);
- }
-
- final boolean oneMinuteRefresh
- = Preferences.getPreferences(this).getForceOneMinuteRefresh();
- if (oneMinuteRefresh) {
- Log.w(LOG_TAG, "One-minute refresh enabled.");
- }
-
- // We use a full projection here because we'll restore each account object from it
- Cursor c = resolver.query(uri, Account.CONTENT_PROJECTION, null, null, null);
- try {
- while (c.moveToNext()) {
- Account account = Account.getContent(c, Account.class);
- // The following sanity checks are primarily for the sake of ignoring non-user
- // accounts that may have been left behind e.g. by failed unit tests.
- // Properly-formed accounts will always pass these simple checks.
- if (TextUtils.isEmpty(account.mEmailAddress)
- || account.mHostAuthKeyRecv <= 0
- || account.mHostAuthKeySend <= 0) {
- continue;
- }
-
- // The account is OK, so proceed
- AccountSyncReport report = new AccountSyncReport();
- int syncInterval = account.mSyncInterval;
-
- // If we're not using MessagingController (EAS at this point), don't schedule syncs
- if (!mController.isMessagingController(account.mId)) {
- syncInterval = Account.CHECK_INTERVAL_NEVER;
- } else if (oneMinuteRefresh && syncInterval >= 0) {
- syncInterval = 1;
- }
-
- report.accountId = account.mId;
- report.prevSyncTime = 0;
- report.nextSyncTime = (syncInterval > 0) ? 0 : -1; // 0 == ASAP -1 == no sync
- report.unseenMessageCount = 0;
- report.lastUnseenMessageCount = 0;
-
- report.syncInterval = syncInterval;
- report.notify = (account.mFlags & Account.FLAGS_NOTIFY_NEW_MAIL) != 0;
-
- // See if the account is enabled for sync in AccountManager
- android.accounts.Account accountManagerAccount =
- new android.accounts.Account(account.mEmailAddress,
- Email.POP_IMAP_ACCOUNT_MANAGER_TYPE);
- report.syncEnabled = ContentResolver.getSyncAutomatically(accountManagerAccount,
- EmailProvider.EMAIL_AUTHORITY);
-
- // TODO lookup # new in inbox
- mSyncReports.put(report.accountId, report);
- }
- } finally {
- c.close();
- }
- }
-
- /**
- * Update list with a single account's sync times and unread count
- *
- * @param accountId the account being updated
- * @param newCount the number of new messages, or -1 if not being reported (don't update)
- * @return the report for the updated account, or null if it doesn't exist (e.g. deleted)
- */
- /* package */ AccountSyncReport updateAccountReport(long accountId, int newCount) {
- // restore the reports if lost
- setupSyncReports(accountId);
- synchronized (mSyncReports) {
- AccountSyncReport report = mSyncReports.get(accountId);
- if (report == null) {
- // discard result - there is no longer an account with this id
- Log.d(LOG_TAG, "No account to update for id=" + Long.toString(accountId));
- return null;
- }
-
- // report found - update it (note - editing the report while in-place in the hashmap)
- report.prevSyncTime = SystemClock.elapsedRealtime();
- if (report.syncInterval > 0) {
- report.nextSyncTime = report.prevSyncTime + (report.syncInterval * 1000 * 60);
- }
- if (newCount != -1) {
- report.unseenMessageCount = newCount;
- }
- if (Email.DEBUG) {
- Log.d(LOG_TAG, "update account " + report.toString());
- }
- return report;
- }
- }
-
- /**
- * when we receive an alarm, update the account sync reports list if necessary
- * this will be the case when if we have restarted the process and lost the data
- * in the global.
- *
- * @param restoreIntent the intent with the list
- */
- /* package */ void restoreSyncReports(Intent restoreIntent) {
- // restore the reports if lost
- setupSyncReports(SYNC_REPORTS_ALL_ACCOUNTS_IF_EMPTY);
- synchronized (mSyncReports) {
- long[] accountInfo = restoreIntent.getLongArrayExtra(EXTRA_ACCOUNT_INFO);
- if (accountInfo == null) {
- Log.d(LOG_TAG, "no data in intent to restore");
- return;
- }
- int accountInfoIndex = 0;
- int accountInfoLimit = accountInfo.length;
- while (accountInfoIndex < accountInfoLimit) {
- long accountId = accountInfo[accountInfoIndex++];
- long prevSync = accountInfo[accountInfoIndex++];
- AccountSyncReport report = mSyncReports.get(accountId);
- if (report != null) {
- if (report.prevSyncTime == 0) {
- report.prevSyncTime = prevSync;
- if (report.syncInterval > 0 && report.prevSyncTime != 0) {
- report.nextSyncTime =
- report.prevSyncTime + (report.syncInterval * 1000 * 60);
- }
- }
- }
- }
- }
- }
-
- class ControllerResults extends Controller.Result {
- @Override
- public void updateMailboxCallback(MessagingException result, long accountId,
- long mailboxId, int progress, int numNewMessages) {
- // First, look for authentication failures and notify
- //checkAuthenticationStatus(result, accountId);
- if (result != null || progress == 100) {
- // We only track the inbox here in the service - ignore other mailboxes
- long inboxId = Mailbox.findMailboxOfType(MailService.this,
- accountId, Mailbox.TYPE_INBOX);
- if (mailboxId == inboxId) {
- if (progress == 100) {
- updateAccountReport(accountId, numNewMessages);
- if (numNewMessages > 0) {
- notifyNewMessages(accountId);
- }
- } else {
- updateAccountReport(accountId, -1);
- }
- }
- }
- }
-
- @Override
- public void serviceCheckMailCallback(MessagingException result, long accountId,
- long mailboxId, int progress, long tag) {
- if (result != null || progress == 100) {
- if (result != null) {
- // the checkmail ended in an error. force an update of the refresh
- // time, so we don't just spin on this account
- updateAccountReport(accountId, -1);
- }
- AlarmManager alarmManager = (AlarmManager)getSystemService(Context.ALARM_SERVICE);
- reschedule(alarmManager);
- int serviceId = MailService.this.mStartId;
- if (tag != 0) {
- serviceId = (int) tag;
- }
- stopSelf(serviceId);
- }
- }
- }
-
- /**
- * Show "new message" notification for an account. (Notification is shown per account.)
- */
- private void notifyNewMessages(final long accountId) {
- final int unseenMessageCount;
- final int justFetchedCount;
- synchronized (mSyncReports) {
- AccountSyncReport report = mSyncReports.get(accountId);
- if (report == null || report.unseenMessageCount == 0 || !report.notify) {
- return;
- }
- unseenMessageCount = report.unseenMessageCount;
- justFetchedCount = report.getJustFetchedMessageCount();
- report.lastUnseenMessageCount = report.unseenMessageCount;
- }
-
- NotificationController.getInstance(this).showNewMessageNotification(accountId,
- unseenMessageCount, justFetchedCount);
- }
-
- /**
- * @see ConnectivityManager#getBackgroundDataSetting()
- */
- private boolean isBackgroundDataEnabled() {
- ConnectivityManager cm =
- (ConnectivityManager)getSystemService(Context.CONNECTIVITY_SERVICE);
- return cm.getBackgroundDataSetting();
- }
-
- public class EmailSyncStatusObserver implements SyncStatusObserver {
- public void onStatusChanged(int which) {
- // We ignore the argument (we can only get called in one case - when settings change)
- }
- }
-
- public static ArrayList getPopImapAccountList(Context context) {
- ArrayList providerAccounts = new ArrayList();
- Cursor c = context.getContentResolver().query(Account.CONTENT_URI, Account.ID_PROJECTION,
- null, null, null);
- try {
- while (c.moveToNext()) {
- long accountId = c.getLong(Account.CONTENT_ID_COLUMN);
- String protocol = Account.getProtocol(context, accountId);
- if ((protocol != null) && ("pop3".equals(protocol) || "imap".equals(protocol))) {
- Account account = Account.restoreAccountWithId(context, accountId);
- if (account != null) {
- providerAccounts.add(account);
- }
- }
- }
- } finally {
- c.close();
- }
- return providerAccounts;
- }
-
- private static final SingleRunningTask sReconcilePopImapAccountsSyncExecutor =
- new SingleRunningTask("ReconcilePopImapAccountsSync") {
- @Override
- protected void runInternal(Context context) {
- android.accounts.Account[] accountManagerAccounts = AccountManager.get(context)
- .getAccountsByType(Email.POP_IMAP_ACCOUNT_MANAGER_TYPE);
- ArrayList providerAccounts = getPopImapAccountList(context);
- MailService.reconcileAccountsWithAccountManager(context, providerAccounts,
- accountManagerAccounts, false, context.getContentResolver());
-
- }
- };
-
- /**
- * Reconcile POP/IMAP accounts.
- */
- public static void reconcilePopImapAccountsSync(Context context) {
- sReconcilePopImapAccountsSyncExecutor.run(context);
- }
-
- /**
- * Compare our account list (obtained from EmailProvider) with the account list owned by
- * AccountManager. If there are any orphans (an account in one list without a corresponding
- * account in the other list), delete the orphan, as these must remain in sync.
- *
- * Note that the duplication of account information is caused by the Email application's
- * incomplete integration with AccountManager.
- *
- * This function may not be called from the main/UI thread, because it makes blocking calls
- * into the account manager.
- *
- * @param context The context in which to operate
- * @param emailProviderAccounts the exchange provider accounts to work from
- * @param accountManagerAccounts The account manager accounts to work from
- * @param blockExternalChanges FOR TESTING ONLY - block backups, security changes, etc.
- * @param resolver the content resolver for making provider updates (injected for testability)
- */
- /* package */ public static void reconcileAccountsWithAccountManager(Context context,
- List emailProviderAccounts, android.accounts.Account[] accountManagerAccounts,
- boolean blockExternalChanges, ContentResolver resolver) {
- // First, look through our EmailProvider accounts to make sure there's a corresponding
- // AccountManager account
- boolean accountsDeleted = false;
- for (Account providerAccount: emailProviderAccounts) {
- String providerAccountName = providerAccount.mEmailAddress;
- boolean found = false;
- for (android.accounts.Account accountManagerAccount: accountManagerAccounts) {
- if (accountManagerAccount.name.equalsIgnoreCase(providerAccountName)) {
- found = true;
- break;
- }
- }
- if (!found) {
- if ((providerAccount.mFlags & Account.FLAGS_INCOMPLETE) != 0) {
- if (Email.DEBUG) {
- Log.d(LOG_TAG, "Account reconciler noticed incomplete account; ignoring");
- }
- continue;
- }
- // This account has been deleted in the AccountManager!
- Log.d(LOG_TAG, "Account deleted in AccountManager; deleting from provider: " +
- providerAccountName);
- // TODO This will orphan downloaded attachments; need to handle this
- resolver.delete(ContentUris.withAppendedId(Account.CONTENT_URI,
- providerAccount.mId), null, null);
- accountsDeleted = true;
- }
- }
- // Now, look through AccountManager accounts to make sure we have a corresponding cached EAS
- // account from EmailProvider
- for (android.accounts.Account accountManagerAccount: accountManagerAccounts) {
- String accountManagerAccountName = accountManagerAccount.name;
- boolean found = false;
- for (Account cachedEasAccount: emailProviderAccounts) {
- if (cachedEasAccount.mEmailAddress.equalsIgnoreCase(accountManagerAccountName)) {
- found = true;
- }
- }
- if (!found) {
- // This account has been deleted from the EmailProvider database
- Log.d(LOG_TAG, "Account deleted from provider; deleting from AccountManager: " +
- accountManagerAccountName);
- // Delete the account
- AccountManagerFuture blockingResult = AccountManager.get(context)
- .removeAccount(accountManagerAccount, null, null);
- try {
- // Note: All of the potential errors from removeAccount() are simply logged
- // here, as there is nothing to actually do about them.
- blockingResult.getResult();
- } catch (OperationCanceledException e) {
- Log.w(Email.LOG_TAG, e.toString());
- } catch (AuthenticatorException e) {
- Log.w(Email.LOG_TAG, e.toString());
- } catch (IOException e) {
- Log.w(Email.LOG_TAG, e.toString());
- }
- accountsDeleted = true;
- }
- }
- // If we changed the list of accounts, refresh the backup & security settings
- if (!blockExternalChanges && accountsDeleted) {
- AccountBackupRestore.backupAccounts(context);
- SecurityPolicy.getInstance(context).reducePolicies();
- Email.setNotifyUiAccountsChanged(true);
- MailService.actionReschedule(context);
- }
- }
-
- public static void setupAccountManagerAccount(Context context, EmailContent.Account account,
- boolean email, boolean calendar, boolean contacts,
- AccountManagerCallback callback) {
- Bundle options = new Bundle();
- HostAuth hostAuthRecv = HostAuth.restoreHostAuthWithId(context, account.mHostAuthKeyRecv);
- // Set up username/password
- options.putString(EasAuthenticatorService.OPTIONS_USERNAME, account.mEmailAddress);
- options.putString(EasAuthenticatorService.OPTIONS_PASSWORD, hostAuthRecv.mPassword);
- options.putBoolean(EasAuthenticatorService.OPTIONS_CONTACTS_SYNC_ENABLED, contacts);
- options.putBoolean(EasAuthenticatorService.OPTIONS_CALENDAR_SYNC_ENABLED, calendar);
- options.putBoolean(EasAuthenticatorService.OPTIONS_EMAIL_SYNC_ENABLED, email);
- String accountType = hostAuthRecv.mProtocol.equals("eas") ?
- Email.EXCHANGE_ACCOUNT_MANAGER_TYPE :
- Email.POP_IMAP_ACCOUNT_MANAGER_TYPE;
- AccountManager.get(context).addAccount(accountType, null, null, options, null, callback,
- null);
- }
-}
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.email.service;
+
+import com.android.email.AccountBackupRestore;
+import com.android.email.Controller;
+import com.android.email.Email;
+import com.android.email.NotificationController;
+import com.android.email.Preferences;
+import com.android.email.SecurityPolicy;
+import com.android.email.SingleRunningTask;
+import com.android.email.Utility;
+import com.android.email.mail.MessagingException;
+import com.android.email.provider.EmailContent;
+import com.android.email.provider.EmailProvider;
+import com.android.email.provider.EmailContent.Account;
+import com.android.email.provider.EmailContent.AccountColumns;
+import com.android.email.provider.EmailContent.HostAuth;
+import com.android.email.provider.EmailContent.Mailbox;
+import com.android.emailcommon.utility.AccountReconciler;
+
+import android.accounts.AccountManager;
+import android.accounts.AccountManagerCallback;
+import android.app.AlarmManager;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SyncStatusObserver;
+import android.database.Cursor;
+import android.net.ConnectivityManager;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.SystemClock;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
+/**
+ * Background service for refreshing non-push email accounts.
+ *
+ * TODO: Convert to IntentService to move *all* work off the UI thread, serialize work, and avoid
+ * possible problems with out-of-order startId processing.
+ */
+public class MailService extends Service {
+ private static final String LOG_TAG = "Email-MailService";
+
+ private static final String ACTION_CHECK_MAIL =
+ "com.android.email.intent.action.MAIL_SERVICE_WAKEUP";
+ private static final String ACTION_RESCHEDULE =
+ "com.android.email.intent.action.MAIL_SERVICE_RESCHEDULE";
+ private static final String ACTION_CANCEL =
+ "com.android.email.intent.action.MAIL_SERVICE_CANCEL";
+ private static final String ACTION_NOTIFY_MAIL =
+ "com.android.email.intent.action.MAIL_SERVICE_NOTIFY";
+ private static final String ACTION_SEND_PENDING_MAIL =
+ "com.android.email.intent.action.MAIL_SERVICE_SEND_PENDING";
+ private static final String ACTION_DELETE_EXCHANGE_ACCOUNTS =
+ "com.android.email.intent.action.MAIL_SERVICE_DELETE_EXCHANGE_ACCOUNTS";
+
+ private static final String EXTRA_ACCOUNT = "com.android.email.intent.extra.ACCOUNT";
+ private static final String EXTRA_ACCOUNT_INFO = "com.android.email.intent.extra.ACCOUNT_INFO";
+ private static final String EXTRA_DEBUG_WATCHDOG = "com.android.email.intent.extra.WATCHDOG";
+
+ private static final int WATCHDOG_DELAY = 10 * 60 * 1000; // 10 minutes
+
+ // Sentinel value asking to update mSyncReports if it's currently empty
+ /*package*/ static final int SYNC_REPORTS_ALL_ACCOUNTS_IF_EMPTY = -1;
+ // Sentinel value asking that mSyncReports be rebuilt
+ /*package*/ static final int SYNC_REPORTS_RESET = -2;
+
+ private static final String[] NEW_MESSAGE_COUNT_PROJECTION =
+ new String[] {AccountColumns.NEW_MESSAGE_COUNT};
+
+ private static MailService sMailService;
+
+ /*package*/ Controller mController;
+ private final Controller.Result mControllerCallback = new ControllerResults();
+ private ContentResolver mContentResolver;
+ private Context mContext;
+ private Handler mHandler = new Handler();
+
+ private int mStartId;
+
+ /**
+ * Access must be synchronized, because there are accesses from the Controller callback
+ */
+ /*package*/ static HashMap mSyncReports =
+ new HashMap();
+
+ public static void actionReschedule(Context context) {
+ Intent i = new Intent();
+ i.setClass(context, MailService.class);
+ i.setAction(MailService.ACTION_RESCHEDULE);
+ context.startService(i);
+ }
+
+ public static void actionCancel(Context context) {
+ Intent i = new Intent();
+ i.setClass(context, MailService.class);
+ i.setAction(MailService.ACTION_CANCEL);
+ context.startService(i);
+ }
+
+ public static void actionDeleteExchangeAccounts(Context context) {
+ Intent i = new Intent();
+ i.setClass(context, MailService.class);
+ i.setAction(MailService.ACTION_DELETE_EXCHANGE_ACCOUNTS);
+ context.startService(i);
+ }
+
+ /**
+ * Entry point for AttachmentDownloadService to ask that pending mail be sent
+ * @param context the caller's context
+ * @param accountId the account whose pending mail should be sent
+ */
+ public static void actionSendPendingMail(Context context, long accountId) {
+ Intent i = new Intent();
+ i.setClass(context, MailService.class);
+ i.setAction(MailService.ACTION_SEND_PENDING_MAIL);
+ i.putExtra(MailService.EXTRA_ACCOUNT, accountId);
+ context.startService(i);
+ }
+
+ /**
+ * Reset new message counts for one or all accounts. This clears both our local copy and
+ * the values (if any) stored in the account records.
+ *
+ * @param accountId account to clear, or -1 for all accounts
+ */
+ public static void resetNewMessageCount(final Context context, final long accountId) {
+ synchronized (mSyncReports) {
+ for (AccountSyncReport report : mSyncReports.values()) {
+ if (accountId == -1 || accountId == report.accountId) {
+ report.unseenMessageCount = 0;
+ report.lastUnseenMessageCount = 0;
+ }
+ }
+ }
+ // Clear notification
+ NotificationController.getInstance(context).cancelNewMessageNotification(accountId);
+
+ // now do the database - all accounts, or just one of them
+ Utility.runAsync(new Runnable() {
+ @Override
+ public void run() {
+ Uri uri = Account.RESET_NEW_MESSAGE_COUNT_URI;
+ if (accountId != -1) {
+ uri = ContentUris.withAppendedId(uri, accountId);
+ }
+ context.getContentResolver().update(uri, null, null, null);
+ }
+ });
+ }
+
+ /**
+ * Entry point for asynchronous message services (e.g. push mode) to post notifications of new
+ * messages. This assumes that the push provider has already synced the messages into the
+ * appropriate database - this simply triggers the notification mechanism.
+ *
+ * @param context a context
+ * @param accountId the id of the account that is reporting new messages
+ */
+ public static void actionNotifyNewMessages(Context context, long accountId) {
+ Intent i = new Intent(ACTION_NOTIFY_MAIL);
+ i.setClass(context, MailService.class);
+ i.putExtra(EXTRA_ACCOUNT, accountId);
+ context.startService(i);
+ }
+
+ /*package*/ static MailService getMailServiceForTest() {
+ return sMailService;
+ }
+
+ @Override
+ public int onStartCommand(final Intent intent, int flags, final int startId) {
+ super.onStartCommand(intent, flags, startId);
+
+ // Save the service away (for unit tests)
+ sMailService = this;
+
+ // Restore accounts, if it has not happened already
+ AccountBackupRestore.restoreAccountsIfNeeded(this);
+
+ Utility.runAsync(new Runnable() {
+ @Override
+ public void run() {
+ reconcilePopImapAccountsSync(MailService.this);
+ }
+ });
+
+ // TODO this needs to be passed through the controller and back to us
+ mStartId = startId;
+ String action = intent.getAction();
+ final long accountId = intent.getLongExtra(EXTRA_ACCOUNT, -1);
+
+ mController = Controller.getInstance(this);
+ mController.addResultCallback(mControllerCallback);
+ mContentResolver = getContentResolver();
+ mContext = this;
+
+ final AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
+
+ if (ACTION_CHECK_MAIL.equals(action)) {
+ // DB access required to satisfy this intent, so offload from UI thread
+ Utility.runAsync(new Runnable() {
+ @Override
+ public void run() {
+ // If we have the data, restore the last-sync-times for each account
+ // These are cached in the wakeup intent in case the process was killed.
+ restoreSyncReports(intent);
+
+ // Sync a specific account if given
+ if (Email.DEBUG) {
+ Log.d(LOG_TAG, "action: check mail for id=" + accountId);
+ }
+ if (accountId >= 0) {
+ setWatchdog(accountId, alarmManager);
+ }
+
+ // Start sync if account is given && bg data enabled && account has sync enabled
+ boolean syncStarted = false;
+ if (accountId != -1 && isBackgroundDataEnabled()) {
+ synchronized(mSyncReports) {
+ for (AccountSyncReport report: mSyncReports.values()) {
+ if (report.accountId == accountId) {
+ if (report.syncEnabled) {
+ syncStarted = syncOneAccount(mController, accountId,
+ startId);
+ }
+ break;
+ }
+ }
+ }
+ }
+
+ // Reschedule if we didn't start sync.
+ if (!syncStarted) {
+ // Prevent runaway on the current account by pretending it updated
+ if (accountId != -1) {
+ updateAccountReport(accountId, 0);
+ }
+ // Find next account to sync, and reschedule
+ reschedule(alarmManager);
+ // Stop the service, unless actually syncing (which will stop the service)
+ stopSelf(startId);
+ }
+ }
+ });
+ }
+ else if (ACTION_CANCEL.equals(action)) {
+ if (Email.DEBUG) {
+ Log.d(LOG_TAG, "action: cancel");
+ }
+ cancel();
+ stopSelf(startId);
+ }
+ else if (ACTION_DELETE_EXCHANGE_ACCOUNTS.equals(action)) {
+ if (Email.DEBUG) {
+ Log.d(LOG_TAG, "action: delete exchange accounts");
+ }
+ Utility.runAsync(new Runnable() {
+ public void run() {
+ Cursor c = mContentResolver.query(Account.CONTENT_URI, Account.ID_PROJECTION,
+ null, null, null);
+ try {
+ while (c.moveToNext()) {
+ long accountId = c.getLong(Account.ID_PROJECTION_COLUMN);
+ if ("eas".equals(Account.getProtocol(mContext, accountId))) {
+ // Always log this
+ Log.d(LOG_TAG, "Deleting EAS account: " + accountId);
+ mController.deleteAccountSync(accountId, mContext);
+ }
+ }
+ } finally {
+ c.close();
+ }
+ }
+ });
+ stopSelf(startId);
+ }
+ else if (ACTION_SEND_PENDING_MAIL.equals(action)) {
+ if (Email.DEBUG) {
+ Log.d(LOG_TAG, "action: send pending mail");
+ }
+ Utility.runAsync(new Runnable() {
+ public void run() {
+ mController.sendPendingMessages(accountId);
+ }
+ });
+ stopSelf(startId);
+ }
+ else if (ACTION_RESCHEDULE.equals(action)) {
+ if (Email.DEBUG) {
+ Log.d(LOG_TAG, "action: reschedule");
+ }
+ final NotificationController nc = NotificationController.getInstance(this);
+ // DB access required to satisfy this intent, so offload from UI thread
+ Utility.runAsync(new Runnable() {
+ @Override
+ public void run() {
+ // Clear all notifications, in case account list has changed.
+ //
+ // TODO Clear notifications for non-existing accounts. Now that we have
+ // separate notifications for each account, NotificationController should be
+ // able to do that.
+ nc.cancelNewMessageNotification(-1);
+
+ // When called externally, we refresh the sync reports table to pick up
+ // any changes in the account list or account settings
+ refreshSyncReports();
+ // Finally, scan for the next needing update, and set an alarm for it
+ reschedule(alarmManager);
+ stopSelf(startId);
+ }
+ });
+ } else if (ACTION_NOTIFY_MAIL.equals(action)) {
+ // DB access required to satisfy this intent, so offload from UI thread
+ Utility.runAsync(new Runnable() {
+ @Override
+ public void run() {
+ // Get the current new message count
+ Cursor c = mContentResolver.query(
+ ContentUris.withAppendedId(Account.CONTENT_URI, accountId),
+ NEW_MESSAGE_COUNT_PROJECTION, null, null, null);
+ int newMessageCount = 0;
+ try {
+ if (c.moveToFirst()) {
+ newMessageCount = c.getInt(0);
+ updateAccountReport(accountId, newMessageCount);
+ notifyNewMessages(accountId);
+ }
+ } finally {
+ c.close();
+ }
+ if (Email.DEBUG) {
+ Log.d(LOG_TAG, "notify accountId=" + Long.toString(accountId)
+ + " count=" + newMessageCount);
+ }
+ stopSelf(startId);
+ }
+ });
+ }
+
+ // Returning START_NOT_STICKY means that if a mail check is killed (e.g. due to memory
+ // pressure, there will be no explicit restart. This is OK; Note that we set a watchdog
+ // alarm before each mailbox check. If the mailbox check never completes, the watchdog
+ // will fire and get things running again.
+ return START_NOT_STICKY;
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return null;
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ Controller.getInstance(getApplication()).removeResultCallback(mControllerCallback);
+ }
+
+ private void cancel() {
+ AlarmManager alarmMgr = (AlarmManager)getSystemService(Context.ALARM_SERVICE);
+ PendingIntent pi = createAlarmIntent(-1, null, false);
+ alarmMgr.cancel(pi);
+ }
+
+ /**
+ * Refresh the sync reports, to pick up any changes in the account list or account settings.
+ */
+ /*package*/ void refreshSyncReports() {
+ synchronized (mSyncReports) {
+ // Make shallow copy of sync reports so we can recover the prev sync times
+ HashMap oldSyncReports =
+ new HashMap(mSyncReports);
+
+ // Delete the sync reports to force a refresh from live account db data
+ setupSyncReportsLocked(SYNC_REPORTS_RESET, this);
+
+ // Restore prev-sync & next-sync times for any reports in the new list
+ for (AccountSyncReport newReport : mSyncReports.values()) {
+ AccountSyncReport oldReport = oldSyncReports.get(newReport.accountId);
+ if (oldReport != null) {
+ newReport.prevSyncTime = oldReport.prevSyncTime;
+ if (newReport.syncInterval > 0 && newReport.prevSyncTime != 0) {
+ newReport.nextSyncTime =
+ newReport.prevSyncTime + (newReport.syncInterval * 1000 * 60);
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Create and send an alarm with the entire list. This also sends a list of known last-sync
+ * times with the alarm, so if we are killed between alarms, we don't lose this info.
+ *
+ * @param alarmMgr passed in so we can mock for testing.
+ */
+ /* package */ void reschedule(AlarmManager alarmMgr) {
+ // restore the reports if lost
+ setupSyncReports(SYNC_REPORTS_ALL_ACCOUNTS_IF_EMPTY);
+ synchronized (mSyncReports) {
+ int numAccounts = mSyncReports.size();
+ long[] accountInfo = new long[numAccounts * 2]; // pairs of { accountId, lastSync }
+ int accountInfoIndex = 0;
+
+ long nextCheckTime = Long.MAX_VALUE;
+ AccountSyncReport nextAccount = null;
+ long timeNow = SystemClock.elapsedRealtime();
+
+ for (AccountSyncReport report : mSyncReports.values()) {
+ if (report.syncInterval <= 0) { // no timed checks - skip
+ continue;
+ }
+ long prevSyncTime = report.prevSyncTime;
+ long nextSyncTime = report.nextSyncTime;
+
+ // select next account to sync
+ if ((prevSyncTime == 0) || (nextSyncTime < timeNow)) { // never checked, or overdue
+ nextCheckTime = 0;
+ nextAccount = report;
+ } else if (nextSyncTime < nextCheckTime) { // next to be checked
+ nextCheckTime = nextSyncTime;
+ nextAccount = report;
+ }
+ // collect last-sync-times for all accounts
+ // this is using pairs of {long,long} to simplify passing in a bundle
+ accountInfo[accountInfoIndex++] = report.accountId;
+ accountInfo[accountInfoIndex++] = report.prevSyncTime;
+ }
+
+ // Clear out any unused elements in the array
+ while (accountInfoIndex < accountInfo.length) {
+ accountInfo[accountInfoIndex++] = -1;
+ }
+
+ // set/clear alarm as needed
+ long idToCheck = (nextAccount == null) ? -1 : nextAccount.accountId;
+ PendingIntent pi = createAlarmIntent(idToCheck, accountInfo, false);
+
+ if (nextAccount == null) {
+ alarmMgr.cancel(pi);
+ if (Email.DEBUG) {
+ Log.d(LOG_TAG, "reschedule: alarm cancel - no account to check");
+ }
+ } else {
+ alarmMgr.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, nextCheckTime, pi);
+ if (Email.DEBUG) {
+ Log.d(LOG_TAG, "reschedule: alarm set at " + nextCheckTime
+ + " for " + nextAccount);
+ }
+ }
+ }
+ }
+
+ /**
+ * Create a watchdog alarm and set it. This is used in case a mail check fails (e.g. we are
+ * killed by the system due to memory pressure.) Normally, a mail check will complete and
+ * the watchdog will be replaced by the call to reschedule().
+ * @param accountId the account we were trying to check
+ * @param alarmMgr system alarm manager
+ */
+ private void setWatchdog(long accountId, AlarmManager alarmMgr) {
+ PendingIntent pi = createAlarmIntent(accountId, null, true);
+ long timeNow = SystemClock.elapsedRealtime();
+ long nextCheckTime = timeNow + WATCHDOG_DELAY;
+ alarmMgr.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, nextCheckTime, pi);
+ }
+
+ /**
+ * Return a pending intent for use by this alarm. Most of the fields must be the same
+ * (in order for the intent to be recognized by the alarm manager) but the extras can
+ * be different, and are passed in here as parameters.
+ */
+ /* package */ PendingIntent createAlarmIntent(long checkId, long[] accountInfo,
+ boolean isWatchdog) {
+ Intent i = new Intent();
+ i.setClass(this, MailService.class);
+ i.setAction(ACTION_CHECK_MAIL);
+ i.putExtra(EXTRA_ACCOUNT, checkId);
+ i.putExtra(EXTRA_ACCOUNT_INFO, accountInfo);
+ if (isWatchdog) {
+ i.putExtra(EXTRA_DEBUG_WATCHDOG, true);
+ }
+ PendingIntent pi = PendingIntent.getService(this, 0, i, PendingIntent.FLAG_UPDATE_CURRENT);
+ return pi;
+ }
+
+ /**
+ * Start a controller sync for a specific account
+ *
+ * @param controller The controller to do the sync work
+ * @param checkAccountId the account Id to try and check
+ * @param startId the id of this service launch
+ * @return true if mail checking has started, false if it could not (e.g. bad account id)
+ */
+ private boolean syncOneAccount(Controller controller, long checkAccountId, int startId) {
+ long inboxId = Mailbox.findMailboxOfType(this, checkAccountId, Mailbox.TYPE_INBOX);
+ if (inboxId == Mailbox.NO_MAILBOX) {
+ return false;
+ } else {
+ controller.serviceCheckMail(checkAccountId, inboxId, startId);
+ return true;
+ }
+ }
+
+ /**
+ * Note: Times are relative to SystemClock.elapsedRealtime()
+ *
+ * TODO: Look more closely at syncEnabled and see if we can simply coalesce it into
+ * syncInterval (e.g. if !syncEnabled, set syncInterval to -1).
+ */
+ /*package*/ static class AccountSyncReport {
+ long accountId;
+ long prevSyncTime; // 0 == unknown
+ long nextSyncTime; // 0 == ASAP -1 == don't sync
+
+ /** # of "unseen" messages to show in notification */
+ int unseenMessageCount;
+
+ /**
+ * # of unseen, the value shown on the last notification. Used to
+ * calculate "the number of messages that have just been fetched".
+ *
+ * TODO It's a sort of cheating. Should we use the "real" number? The only difference
+ * is the first notification after reboot / process restart.
+ */
+ int lastUnseenMessageCount;
+
+ int syncInterval;
+ boolean notify;
+
+ boolean syncEnabled; // whether auto sync is enabled for this account
+
+ /** # of messages that have just been fetched */
+ int getJustFetchedMessageCount() {
+ return unseenMessageCount - lastUnseenMessageCount;
+ }
+
+ @Override
+ public String toString() {
+ return "id=" + accountId
+ + " prevSync=" + prevSyncTime + " nextSync=" + nextSyncTime + " numUnseen="
+ + unseenMessageCount;
+ }
+ }
+
+ /**
+ * scan accounts to create a list of { acct, prev sync, next sync, #new }
+ * use this to create a fresh copy. assumes all accounts need sync
+ *
+ * @param accountId -1 will rebuild the list if empty. other values will force loading
+ * of a single account (e.g if it was created after the original list population)
+ */
+ /* package */ void setupSyncReports(long accountId) {
+ synchronized (mSyncReports) {
+ setupSyncReportsLocked(accountId, mContext);
+ }
+ }
+
+ /**
+ * Handle the work of setupSyncReports. Must be synchronized on mSyncReports.
+ */
+ /*package*/ void setupSyncReportsLocked(long accountId, Context context) {
+ ContentResolver resolver = context.getContentResolver();
+ if (accountId == SYNC_REPORTS_RESET) {
+ // For test purposes, force refresh of mSyncReports
+ mSyncReports.clear();
+ accountId = SYNC_REPORTS_ALL_ACCOUNTS_IF_EMPTY;
+ } else if (accountId == SYNC_REPORTS_ALL_ACCOUNTS_IF_EMPTY) {
+ // -1 == reload the list if empty, otherwise exit immediately
+ if (mSyncReports.size() > 0) {
+ return;
+ }
+ } else {
+ // load a single account if it doesn't already have a sync record
+ if (mSyncReports.containsKey(accountId)) {
+ return;
+ }
+ }
+
+ // setup to add a single account or all accounts
+ Uri uri;
+ if (accountId == SYNC_REPORTS_ALL_ACCOUNTS_IF_EMPTY) {
+ uri = Account.CONTENT_URI;
+ } else {
+ uri = ContentUris.withAppendedId(Account.CONTENT_URI, accountId);
+ }
+
+ final boolean oneMinuteRefresh
+ = Preferences.getPreferences(this).getForceOneMinuteRefresh();
+ if (oneMinuteRefresh) {
+ Log.w(LOG_TAG, "One-minute refresh enabled.");
+ }
+
+ // We use a full projection here because we'll restore each account object from it
+ Cursor c = resolver.query(uri, Account.CONTENT_PROJECTION, null, null, null);
+ try {
+ while (c.moveToNext()) {
+ Account account = Account.getContent(c, Account.class);
+ // The following sanity checks are primarily for the sake of ignoring non-user
+ // accounts that may have been left behind e.g. by failed unit tests.
+ // Properly-formed accounts will always pass these simple checks.
+ if (TextUtils.isEmpty(account.mEmailAddress)
+ || account.mHostAuthKeyRecv <= 0
+ || account.mHostAuthKeySend <= 0) {
+ continue;
+ }
+
+ // The account is OK, so proceed
+ AccountSyncReport report = new AccountSyncReport();
+ int syncInterval = account.mSyncInterval;
+
+ // If we're not using MessagingController (EAS at this point), don't schedule syncs
+ if (!mController.isMessagingController(account.mId)) {
+ syncInterval = Account.CHECK_INTERVAL_NEVER;
+ } else if (oneMinuteRefresh && syncInterval >= 0) {
+ syncInterval = 1;
+ }
+
+ report.accountId = account.mId;
+ report.prevSyncTime = 0;
+ report.nextSyncTime = (syncInterval > 0) ? 0 : -1; // 0 == ASAP -1 == no sync
+ report.unseenMessageCount = 0;
+ report.lastUnseenMessageCount = 0;
+
+ report.syncInterval = syncInterval;
+ report.notify = (account.mFlags & Account.FLAGS_NOTIFY_NEW_MAIL) != 0;
+
+ // See if the account is enabled for sync in AccountManager
+ android.accounts.Account accountManagerAccount =
+ new android.accounts.Account(account.mEmailAddress,
+ Email.POP_IMAP_ACCOUNT_MANAGER_TYPE);
+ report.syncEnabled = ContentResolver.getSyncAutomatically(accountManagerAccount,
+ EmailProvider.EMAIL_AUTHORITY);
+
+ // TODO lookup # new in inbox
+ mSyncReports.put(report.accountId, report);
+ }
+ } finally {
+ c.close();
+ }
+ }
+
+ /**
+ * Update list with a single account's sync times and unread count
+ *
+ * @param accountId the account being updated
+ * @param newCount the number of new messages, or -1 if not being reported (don't update)
+ * @return the report for the updated account, or null if it doesn't exist (e.g. deleted)
+ */
+ /* package */ AccountSyncReport updateAccountReport(long accountId, int newCount) {
+ // restore the reports if lost
+ setupSyncReports(accountId);
+ synchronized (mSyncReports) {
+ AccountSyncReport report = mSyncReports.get(accountId);
+ if (report == null) {
+ // discard result - there is no longer an account with this id
+ Log.d(LOG_TAG, "No account to update for id=" + Long.toString(accountId));
+ return null;
+ }
+
+ // report found - update it (note - editing the report while in-place in the hashmap)
+ report.prevSyncTime = SystemClock.elapsedRealtime();
+ if (report.syncInterval > 0) {
+ report.nextSyncTime = report.prevSyncTime + (report.syncInterval * 1000 * 60);
+ }
+ if (newCount != -1) {
+ report.unseenMessageCount = newCount;
+ }
+ if (Email.DEBUG) {
+ Log.d(LOG_TAG, "update account " + report.toString());
+ }
+ return report;
+ }
+ }
+
+ /**
+ * when we receive an alarm, update the account sync reports list if necessary
+ * this will be the case when if we have restarted the process and lost the data
+ * in the global.
+ *
+ * @param restoreIntent the intent with the list
+ */
+ /* package */ void restoreSyncReports(Intent restoreIntent) {
+ // restore the reports if lost
+ setupSyncReports(SYNC_REPORTS_ALL_ACCOUNTS_IF_EMPTY);
+ synchronized (mSyncReports) {
+ long[] accountInfo = restoreIntent.getLongArrayExtra(EXTRA_ACCOUNT_INFO);
+ if (accountInfo == null) {
+ Log.d(LOG_TAG, "no data in intent to restore");
+ return;
+ }
+ int accountInfoIndex = 0;
+ int accountInfoLimit = accountInfo.length;
+ while (accountInfoIndex < accountInfoLimit) {
+ long accountId = accountInfo[accountInfoIndex++];
+ long prevSync = accountInfo[accountInfoIndex++];
+ AccountSyncReport report = mSyncReports.get(accountId);
+ if (report != null) {
+ if (report.prevSyncTime == 0) {
+ report.prevSyncTime = prevSync;
+ if (report.syncInterval > 0 && report.prevSyncTime != 0) {
+ report.nextSyncTime =
+ report.prevSyncTime + (report.syncInterval * 1000 * 60);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ class ControllerResults extends Controller.Result {
+ @Override
+ public void updateMailboxCallback(MessagingException result, long accountId,
+ long mailboxId, int progress, int numNewMessages) {
+ // First, look for authentication failures and notify
+ //checkAuthenticationStatus(result, accountId);
+ if (result != null || progress == 100) {
+ // We only track the inbox here in the service - ignore other mailboxes
+ long inboxId = Mailbox.findMailboxOfType(MailService.this,
+ accountId, Mailbox.TYPE_INBOX);
+ if (mailboxId == inboxId) {
+ if (progress == 100) {
+ updateAccountReport(accountId, numNewMessages);
+ if (numNewMessages > 0) {
+ notifyNewMessages(accountId);
+ }
+ } else {
+ updateAccountReport(accountId, -1);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void serviceCheckMailCallback(MessagingException result, long accountId,
+ long mailboxId, int progress, long tag) {
+ if (result != null || progress == 100) {
+ if (result != null) {
+ // the checkmail ended in an error. force an update of the refresh
+ // time, so we don't just spin on this account
+ updateAccountReport(accountId, -1);
+ }
+ AlarmManager alarmManager = (AlarmManager)getSystemService(Context.ALARM_SERVICE);
+ reschedule(alarmManager);
+ int serviceId = MailService.this.mStartId;
+ if (tag != 0) {
+ serviceId = (int) tag;
+ }
+ stopSelf(serviceId);
+ }
+ }
+ }
+
+ /**
+ * Show "new message" notification for an account. (Notification is shown per account.)
+ */
+ private void notifyNewMessages(final long accountId) {
+ final int unseenMessageCount;
+ final int justFetchedCount;
+ synchronized (mSyncReports) {
+ AccountSyncReport report = mSyncReports.get(accountId);
+ if (report == null || report.unseenMessageCount == 0 || !report.notify) {
+ return;
+ }
+ unseenMessageCount = report.unseenMessageCount;
+ justFetchedCount = report.getJustFetchedMessageCount();
+ report.lastUnseenMessageCount = report.unseenMessageCount;
+ }
+
+ NotificationController.getInstance(this).showNewMessageNotification(accountId,
+ unseenMessageCount, justFetchedCount);
+ }
+
+ /**
+ * @see ConnectivityManager#getBackgroundDataSetting()
+ */
+ private boolean isBackgroundDataEnabled() {
+ ConnectivityManager cm =
+ (ConnectivityManager)getSystemService(Context.CONNECTIVITY_SERVICE);
+ return cm.getBackgroundDataSetting();
+ }
+
+ public class EmailSyncStatusObserver implements SyncStatusObserver {
+ public void onStatusChanged(int which) {
+ // We ignore the argument (we can only get called in one case - when settings change)
+ }
+ }
+
+ public static ArrayList getPopImapAccountList(Context context) {
+ ArrayList providerAccounts = new ArrayList();
+ Cursor c = context.getContentResolver().query(Account.CONTENT_URI, Account.ID_PROJECTION,
+ null, null, null);
+ try {
+ while (c.moveToNext()) {
+ long accountId = c.getLong(Account.CONTENT_ID_COLUMN);
+ String protocol = Account.getProtocol(context, accountId);
+ if ((protocol != null) && ("pop3".equals(protocol) || "imap".equals(protocol))) {
+ Account account = Account.restoreAccountWithId(context, accountId);
+ if (account != null) {
+ providerAccounts.add(account);
+ }
+ }
+ }
+ } finally {
+ c.close();
+ }
+ return providerAccounts;
+ }
+
+ private static final SingleRunningTask sReconcilePopImapAccountsSyncExecutor =
+ new SingleRunningTask("ReconcilePopImapAccountsSync") {
+ @Override
+ protected void runInternal(Context context) {
+ android.accounts.Account[] accountManagerAccounts = AccountManager.get(context)
+ .getAccountsByType(Email.POP_IMAP_ACCOUNT_MANAGER_TYPE);
+ ArrayList providerAccounts = getPopImapAccountList(context);
+ MailService.reconcileAccountsWithAccountManager(context, providerAccounts,
+ accountManagerAccounts, false, context.getContentResolver());
+
+ }
+ };
+
+ /**
+ * Reconcile POP/IMAP accounts.
+ */
+ public static void reconcilePopImapAccountsSync(Context context) {
+ sReconcilePopImapAccountsSyncExecutor.run(context);
+ }
+
+ /**
+ * Handles a variety of cleanup actions that must be performed when an account has been deleted.
+ * This includes triggering an account backup, ensuring that security policies are properly
+ * reset, if necessary, notifying the UI of the change, and resetting scheduled syncs and
+ * notifications.
+ * @param context the caller's context
+ */
+ public static void accountDeleted(Context context) {
+ AccountBackupRestore.backupAccounts(context);
+ SecurityPolicy.getInstance(context).reducePolicies();
+ Email.setNotifyUiAccountsChanged(true);
+ MailService.actionReschedule(context);
+ }
+
+ /**
+ * See Utility.reconcileAccounts for details
+ * @param context The context in which to operate
+ * @param emailProviderAccounts the exchange provider accounts to work from
+ * @param accountManagerAccounts The account manager accounts to work from
+ * @param blockExternalChanges FOR TESTING ONLY - block backups, security changes, etc.
+ * @param resolver the content resolver for making provider updates (injected for testability)
+ */
+ /* package */ public static void reconcileAccountsWithAccountManager(Context context,
+ List emailProviderAccounts, android.accounts.Account[] accountManagerAccounts,
+ boolean blockExternalChanges, ContentResolver resolver) {
+ boolean accountsDeleted = AccountReconciler.reconcileAccounts(context,
+ emailProviderAccounts, accountManagerAccounts, resolver);
+ // If we changed the list of accounts, refresh the backup & security settings
+ if (!blockExternalChanges && accountsDeleted) {
+ accountDeleted(context);
+ }
+ }
+
+ public static void setupAccountManagerAccount(Context context, EmailContent.Account account,
+ boolean email, boolean calendar, boolean contacts,
+ AccountManagerCallback callback) {
+ Bundle options = new Bundle();
+ HostAuth hostAuthRecv = HostAuth.restoreHostAuthWithId(context, account.mHostAuthKeyRecv);
+ // Set up username/password
+ options.putString(EasAuthenticatorService.OPTIONS_USERNAME, account.mEmailAddress);
+ options.putString(EasAuthenticatorService.OPTIONS_PASSWORD, hostAuthRecv.mPassword);
+ options.putBoolean(EasAuthenticatorService.OPTIONS_CONTACTS_SYNC_ENABLED, contacts);
+ options.putBoolean(EasAuthenticatorService.OPTIONS_CALENDAR_SYNC_ENABLED, calendar);
+ options.putBoolean(EasAuthenticatorService.OPTIONS_EMAIL_SYNC_ENABLED, email);
+ String accountType = hostAuthRecv.mProtocol.equals("eas") ?
+ Email.EXCHANGE_ACCOUNT_MANAGER_TYPE :
+ Email.POP_IMAP_ACCOUNT_MANAGER_TYPE;
+ AccountManager.get(context).addAccount(accountType, null, null, options, null, callback,
+ null);
+ }
+}
diff --git a/src/com/android/emailcommon/service/AccountServiceProxy.java b/src/com/android/emailcommon/service/AccountServiceProxy.java
new file mode 100644
index 000000000..3f6afbe40
--- /dev/null
+++ b/src/com/android/emailcommon/service/AccountServiceProxy.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.emailcommon.service;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.IBinder;
+import android.os.RemoteException;
+
+public class AccountServiceProxy extends ServiceProxy implements IAccountService {
+
+ public static final String ACCOUNT_INTENT = "com.android.email.ACCOUNT_INTENT";
+ public static final int DEFAULT_ACCOUNT_COLOR = 0xFF0000FF;
+
+ private IAccountService mService = null;
+ private Object mReturn;
+
+ public AccountServiceProxy(Context _context) {
+ super(_context, new Intent(ACCOUNT_INTENT));
+ }
+
+ @Override
+ public void onConnected(IBinder binder) {
+ mService = IAccountService.Stub.asInterface(binder);
+ }
+
+ public IBinder asBinder() {
+ return null;
+ }
+
+ @Override
+ public void notifyLoginFailed(final long accountId) throws RemoteException {
+ setTask(new ProxyTask() {
+ public void run() throws RemoteException {
+ mService.notifyLoginFailed(accountId);
+ }
+ }, "notifyLoginFailed");
+ }
+
+ @Override
+ public void notifyLoginSucceeded(final long accountId) throws RemoteException {
+ setTask(new ProxyTask() {
+ public void run() throws RemoteException {
+ mService.notifyLoginSucceeded(accountId);
+ }
+ }, "notifyLoginSucceeded");
+ }
+
+ @Override
+ public void notifyNewMessages(final long accountId) throws RemoteException {
+ setTask(new ProxyTask() {
+ public void run() throws RemoteException {
+ mService.notifyNewMessages(accountId);
+ }
+ }, "notifyNewMessages");
+ }
+
+ @Override
+ public void accountDeleted() throws RemoteException {
+ setTask(new ProxyTask() {
+ public void run() throws RemoteException {
+ mService.accountDeleted();
+ }
+ }, "accountDeleted");
+ }
+
+ @Override
+ public void restoreAccountsIfNeeded() throws RemoteException {
+ setTask(new ProxyTask() {
+ public void run() throws RemoteException {
+ mService.restoreAccountsIfNeeded();
+ }
+ }, "restoreAccountsIfNeeded");
+ }
+
+ @Override
+ public int getAccountColor(final long accountId) throws RemoteException {
+ setTask(new ProxyTask() {
+ public void run() throws RemoteException{
+ mReturn = mService.getAccountColor(accountId);
+ }
+ }, "getAccountColor");
+ waitForCompletion();
+ if (mReturn == null) {
+ return DEFAULT_ACCOUNT_COLOR;
+ } else {
+ return (Integer)mReturn;
+ }
+ }
+}
+
diff --git a/src/com/android/emailcommon/service/IAccountService.aidl b/src/com/android/emailcommon/service/IAccountService.aidl
new file mode 100644
index 000000000..9f362c82c
--- /dev/null
+++ b/src/com/android/emailcommon/service/IAccountService.aidl
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.emailcommon.service;
+
+interface IAccountService {
+ oneway void notifyLoginFailed(long accountId);
+ oneway void notifyLoginSucceeded(long accountId);
+ oneway void notifyNewMessages(long accountId);
+
+ void accountDeleted();
+ void restoreAccountsIfNeeded();
+
+ int getAccountColor(long accountId);
+}
\ No newline at end of file
diff --git a/src/com/android/emailcommon/service/PolicyServiceProxy.java b/src/com/android/emailcommon/service/PolicyServiceProxy.java
index 463d11dc6..b68b5ac0e 100644
--- a/src/com/android/emailcommon/service/PolicyServiceProxy.java
+++ b/src/com/android/emailcommon/service/PolicyServiceProxy.java
@@ -223,6 +223,5 @@ public class PolicyServiceProxy extends ServiceProxy implements IPolicyService {
}
throw new IllegalStateException("PolicyService transaction failed");
}
-
}
diff --git a/src/com/android/emailcommon/service/SyncWindow.java b/src/com/android/emailcommon/service/SyncWindow.java
new file mode 100644
index 000000000..b81834fea
--- /dev/null
+++ b/src/com/android/emailcommon/service/SyncWindow.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.emailcommon.service;
+
+public class SyncWindow {
+ public static final int SYNC_WINDOW_USER = -1;
+ public static final int SYNC_WINDOW_1_DAY = 1;
+ public static final int SYNC_WINDOW_3_DAYS = 2;
+ public static final int SYNC_WINDOW_1_WEEK = 3;
+ public static final int SYNC_WINDOW_2_WEEKS = 4;
+ public static final int SYNC_WINDOW_1_MONTH = 5;
+ public static final int SYNC_WINDOW_ALL = 6;
+}
diff --git a/src/com/android/emailcommon/utility/AccountReconciler.java b/src/com/android/emailcommon/utility/AccountReconciler.java
new file mode 100644
index 000000000..11abcd3b3
--- /dev/null
+++ b/src/com/android/emailcommon/utility/AccountReconciler.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.emailcommon.utility;
+
+import com.android.email.Email;
+import com.android.email.provider.EmailContent.Account;
+
+import android.accounts.AccountManager;
+import android.accounts.AccountManagerFuture;
+import android.accounts.AuthenticatorException;
+import android.accounts.OperationCanceledException;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.Context;
+import android.util.Log;
+
+import java.io.IOException;
+import java.util.List;
+
+public class AccountReconciler {
+ /**
+ * Compare our account list (obtained from EmailProvider) with the account list owned by
+ * AccountManager. If there are any orphans (an account in one list without a corresponding
+ * account in the other list), delete the orphan, as these must remain in sync.
+ *
+ * Note that the duplication of account information is caused by the Email application's
+ * incomplete integration with AccountManager.
+ *
+ * This function may not be called from the main/UI thread, because it makes blocking calls
+ * into the account manager.
+ *
+ * @param context The context in which to operate
+ * @param emailProviderAccounts the exchange provider accounts to work from
+ * @param accountManagerAccounts The account manager accounts to work from
+ * @param resolver the content resolver for making provider updates (injected for testability)
+ */
+ public static boolean reconcileAccounts(Context context,
+ List emailProviderAccounts, android.accounts.Account[] accountManagerAccounts,
+ ContentResolver resolver) {
+ // First, look through our EmailProvider accounts to make sure there's a corresponding
+ // AccountManager account
+ boolean accountsDeleted = false;
+ for (Account providerAccount: emailProviderAccounts) {
+ String providerAccountName = providerAccount.mEmailAddress;
+ boolean found = false;
+ for (android.accounts.Account accountManagerAccount: accountManagerAccounts) {
+ if (accountManagerAccount.name.equalsIgnoreCase(providerAccountName)) {
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ if ((providerAccount.mFlags & Account.FLAGS_INCOMPLETE) != 0) {
+ if (Email.DEBUG) {
+ Log.d(Email.LOG_TAG,
+ "Account reconciler noticed incomplete account; ignoring");
+ }
+ continue;
+ }
+ // This account has been deleted in the AccountManager!
+ Log.d(Email.LOG_TAG, "Account deleted in AccountManager; deleting from provider: " +
+ providerAccountName);
+ // TODO This will orphan downloaded attachments; need to handle this
+ resolver.delete(ContentUris.withAppendedId(Account.CONTENT_URI,
+ providerAccount.mId), null, null);
+ accountsDeleted = true;
+ }
+ }
+ // Now, look through AccountManager accounts to make sure we have a corresponding cached EAS
+ // account from EmailProvider
+ for (android.accounts.Account accountManagerAccount: accountManagerAccounts) {
+ String accountManagerAccountName = accountManagerAccount.name;
+ boolean found = false;
+ for (Account cachedEasAccount: emailProviderAccounts) {
+ if (cachedEasAccount.mEmailAddress.equalsIgnoreCase(accountManagerAccountName)) {
+ found = true;
+ }
+ }
+ if (!found) {
+ // This account has been deleted from the EmailProvider database
+ Log.d(Email.LOG_TAG,
+ "Account deleted from provider; deleting from AccountManager: " +
+ accountManagerAccountName);
+ // Delete the account
+ AccountManagerFuture blockingResult = AccountManager.get(context)
+ .removeAccount(accountManagerAccount, null, null);
+ try {
+ // Note: All of the potential errors from removeAccount() are simply logged
+ // here, as there is nothing to actually do about them.
+ blockingResult.getResult();
+ } catch (OperationCanceledException e) {
+ Log.w(Email.LOG_TAG, e.toString());
+ } catch (AuthenticatorException e) {
+ Log.w(Email.LOG_TAG, e.toString());
+ } catch (IOException e) {
+ Log.w(Email.LOG_TAG, e.toString());
+ }
+ accountsDeleted = true;
+ }
+ }
+ return accountsDeleted;
+ }
+}
diff --git a/src/com/android/emailcommon/utility/AttachmentUtilities.java b/src/com/android/emailcommon/utility/AttachmentUtilities.java
new file mode 100644
index 000000000..98d521978
--- /dev/null
+++ b/src/com/android/emailcommon/utility/AttachmentUtilities.java
@@ -0,0 +1,258 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.emailcommon.utility;
+
+import com.android.email.Email;
+import com.android.email.provider.EmailContent.Attachment;
+import com.android.email.provider.EmailContent.Message;
+import com.android.email.provider.EmailContent.MessageColumns;
+
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.text.TextUtils;
+import android.util.Log;
+import android.webkit.MimeTypeMap;
+
+import java.io.File;
+
+public class AttachmentUtilities {
+ public static final String AUTHORITY = "com.android.email.attachmentprovider";
+ public static final Uri CONTENT_URI = Uri.parse( "content://" + AUTHORITY);
+
+ public static final String FORMAT_RAW = "RAW";
+ public static final String FORMAT_THUMBNAIL = "THUMBNAIL";
+
+ public static class Columns {
+ public static final String _ID = "_id";
+ public static final String DATA = "_data";
+ public static final String DISPLAY_NAME = "_display_name";
+ public static final String SIZE = "_size";
+ }
+
+ public static Uri getAttachmentUri(long accountId, long id) {
+ return CONTENT_URI.buildUpon()
+ .appendPath(Long.toString(accountId))
+ .appendPath(Long.toString(id))
+ .appendPath(FORMAT_RAW)
+ .build();
+ }
+
+ public static Uri getAttachmentThumbnailUri(long accountId, long id,
+ int width, int height) {
+ return CONTENT_URI.buildUpon()
+ .appendPath(Long.toString(accountId))
+ .appendPath(Long.toString(id))
+ .appendPath(FORMAT_THUMBNAIL)
+ .appendPath(Integer.toString(width))
+ .appendPath(Integer.toString(height))
+ .build();
+ }
+
+ /**
+ * Return the filename for a given attachment. This should be used by any code that is
+ * going to *write* attachments.
+ *
+ * This does not create or write the file, or even the directories. It simply builds
+ * the filename that should be used.
+ */
+ public static File getAttachmentFilename(Context context, long accountId, long attachmentId) {
+ return new File(getAttachmentDirectory(context, accountId), Long.toString(attachmentId));
+ }
+
+ /**
+ * Return the directory for a given attachment. This should be used by any code that is
+ * going to *write* attachments.
+ *
+ * This does not create or write the directory. It simply builds the pathname that should be
+ * used.
+ */
+ public static File getAttachmentDirectory(Context context, long accountId) {
+ return context.getDatabasePath(accountId + ".db_att");
+ }
+
+ /**
+ * Helper to convert unknown or unmapped attachments to something useful based on filename
+ * extensions. The mime type is inferred based upon the table below. It's not perfect, but
+ * it helps.
+ *
+ *
+ * |---------------------------------------------------------|
+ * | E X T E N S I O N |
+ * |---------------------------------------------------------|
+ * | .eml | known(.png) | unknown(.abc) | none |
+ * | M |-----------------------------------------------------------------------|
+ * | I | none | msg/rfc822 | image/png | app/abc | app/oct-str |
+ * | M |-------------| (always | | | |
+ * | E | app/oct-str | overrides | | | |
+ * | T |-------------| | |-----------------------------|
+ * | Y | text/plain | | | text/plain |
+ * | P |-------------| |-------------------------------------------|
+ * | E | any/type | | any/type |
+ * |---|-----------------------------------------------------------------------|
+ *
+ *
+ * NOTE: Since mime types on Android are case-*sensitive*, return values are always in
+ * lower case.
+ *
+ * @param fileName The given filename
+ * @param mimeType The given mime type
+ * @return A likely mime type for the attachment
+ */
+ public static String inferMimeType(final String fileName, final String mimeType) {
+ String resultType = null;
+ String fileExtension = getFilenameExtension(fileName);
+ boolean isTextPlain = "text/plain".equalsIgnoreCase(mimeType);
+
+ if ("eml".equals(fileExtension)) {
+ resultType = "message/rfc822";
+ } else {
+ boolean isGenericType =
+ isTextPlain || "application/octet-stream".equalsIgnoreCase(mimeType);
+ // If the given mime type is non-empty and non-generic, return it
+ if (isGenericType || TextUtils.isEmpty(mimeType)) {
+ if (!TextUtils.isEmpty(fileExtension)) {
+ // Otherwise, try to find a mime type based upon the file extension
+ resultType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(fileExtension);
+ if (TextUtils.isEmpty(resultType)) {
+ // Finally, if original mimetype is text/plain, use it; otherwise synthesize
+ resultType = isTextPlain ? mimeType : "application/" + fileExtension;
+ }
+ }
+ } else {
+ resultType = mimeType;
+ }
+ }
+
+ // No good guess could be made; use an appropriate generic type
+ if (TextUtils.isEmpty(resultType)) {
+ resultType = isTextPlain ? "text/plain" : "application/octet-stream";
+ }
+ return resultType.toLowerCase();
+ }
+
+ /**
+ * Extract and return filename's extension, converted to lower case, and not including the "."
+ *
+ * @return extension, or null if not found (or null/empty filename)
+ */
+ public static String getFilenameExtension(String fileName) {
+ String extension = null;
+ if (!TextUtils.isEmpty(fileName)) {
+ int lastDot = fileName.lastIndexOf('.');
+ if ((lastDot > 0) && (lastDot < fileName.length() - 1)) {
+ extension = fileName.substring(lastDot + 1).toLowerCase();
+ }
+ }
+ return extension;
+ }
+
+ /**
+ * Resolve attachment id to content URI. Returns the resolved content URI (from the attachment
+ * DB) or, if not found, simply returns the incoming value.
+ *
+ * @param attachmentUri
+ * @return resolved content URI
+ *
+ * TODO: Throws an SQLite exception on a missing DB file (e.g. unknown URI) instead of just
+ * returning the incoming uri, as it should.
+ */
+ public static Uri resolveAttachmentIdToContentUri(ContentResolver resolver, Uri attachmentUri) {
+ Cursor c = resolver.query(attachmentUri,
+ new String[] { Columns.DATA },
+ null, null, null);
+ if (c != null) {
+ try {
+ if (c.moveToFirst()) {
+ final String strUri = c.getString(0);
+ if (strUri != null) {
+ return Uri.parse(strUri);
+ }
+ }
+ } finally {
+ c.close();
+ }
+ }
+ return attachmentUri;
+ }
+
+ /**
+ * In support of deleting a message, find all attachments and delete associated attachment
+ * files.
+ * @param context
+ * @param accountId the account for the message
+ * @param messageId the message
+ */
+ public static void deleteAllAttachmentFiles(Context context, long accountId, long messageId) {
+ Uri uri = ContentUris.withAppendedId(Attachment.MESSAGE_ID_URI, messageId);
+ Cursor c = context.getContentResolver().query(uri, Attachment.ID_PROJECTION,
+ null, null, null);
+ try {
+ while (c.moveToNext()) {
+ long attachmentId = c.getLong(Attachment.ID_PROJECTION_COLUMN);
+ File attachmentFile = getAttachmentFilename(context, accountId, attachmentId);
+ // Note, delete() throws no exceptions for basic FS errors (e.g. file not found)
+ // it just returns false, which we ignore, and proceed to the next file.
+ // This entire loop is best-effort only.
+ attachmentFile.delete();
+ }
+ } finally {
+ c.close();
+ }
+ }
+
+ /**
+ * In support of deleting a mailbox, find all messages and delete their attachments.
+ *
+ * @param context
+ * @param accountId the account for the mailbox
+ * @param mailboxId the mailbox for the messages
+ */
+ public static void deleteAllMailboxAttachmentFiles(Context context, long accountId,
+ long mailboxId) {
+ Cursor c = context.getContentResolver().query(Message.CONTENT_URI,
+ Message.ID_COLUMN_PROJECTION, MessageColumns.MAILBOX_KEY + "=?",
+ new String[] { Long.toString(mailboxId) }, null);
+ try {
+ while (c.moveToNext()) {
+ long messageId = c.getLong(Message.ID_PROJECTION_COLUMN);
+ deleteAllAttachmentFiles(context, accountId, messageId);
+ }
+ } finally {
+ c.close();
+ }
+ }
+
+ /**
+ * In support of deleting or wiping an account, delete all related attachments.
+ *
+ * @param context
+ * @param accountId the account to scrub
+ */
+ public static void deleteAllAccountAttachmentFiles(Context context, long accountId) {
+ File[] files = getAttachmentDirectory(context, accountId).listFiles();
+ if (files == null) return;
+ for (File file : files) {
+ boolean result = file.delete();
+ if (!result) {
+ Log.e(Email.LOG_TAG, "Failed to delete attachment file " + file.getName());
+ }
+ }
+ }
+}
diff --git a/src/com/android/emailcommon/utility/ConversionUtilities.java b/src/com/android/emailcommon/utility/ConversionUtilities.java
new file mode 100644
index 000000000..722175149
--- /dev/null
+++ b/src/com/android/emailcommon/utility/ConversionUtilities.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.emailcommon.utility;
+
+import com.android.email.Snippet;
+import com.android.email.mail.MessagingException;
+import com.android.email.mail.Part;
+import com.android.email.mail.internet.MimeHeader;
+import com.android.email.mail.internet.MimeUtility;
+import com.android.email.provider.EmailContent;
+
+import android.text.TextUtils;
+
+import java.util.ArrayList;
+
+public class ConversionUtilities {
+ /**
+ * Values for HEADER_ANDROID_BODY_QUOTED_PART to tag body parts
+ */
+ public static final String BODY_QUOTED_PART_REPLY = "quoted-reply";
+ public static final String BODY_QUOTED_PART_FORWARD = "quoted-forward";
+ public static final String BODY_QUOTED_PART_INTRO = "quoted-intro";
+
+ /**
+ * Helper function to append text to a StringBuffer, creating it if necessary.
+ * Optimization: The majority of the time we are *not* appending - we should have a path
+ * that deals with single strings.
+ */
+ private static StringBuffer appendTextPart(StringBuffer sb, String newText) {
+ if (newText == null) {
+ return sb;
+ }
+ else if (sb == null) {
+ sb = new StringBuffer(newText);
+ } else {
+ if (sb.length() > 0) {
+ sb.append('\n');
+ }
+ sb.append(newText);
+ }
+ return sb;
+ }
+
+ /**
+ * Copy body text (plain and/or HTML) from MimeMessage to provider Message
+ */
+ public static boolean updateBodyFields(EmailContent.Body body,
+ EmailContent.Message localMessage, ArrayList viewables)
+ throws MessagingException {
+
+ body.mMessageKey = localMessage.mId;
+
+ StringBuffer sbHtml = null;
+ StringBuffer sbText = null;
+ StringBuffer sbHtmlReply = null;
+ StringBuffer sbTextReply = null;
+ StringBuffer sbIntroText = null;
+
+ for (Part viewable : viewables) {
+ String text = MimeUtility.getTextFromPart(viewable);
+ String[] replyTags = viewable.getHeader(MimeHeader.HEADER_ANDROID_BODY_QUOTED_PART);
+ String replyTag = null;
+ if (replyTags != null && replyTags.length > 0) {
+ replyTag = replyTags[0];
+ }
+ // Deploy text as marked by the various tags
+ boolean isHtml = "text/html".equalsIgnoreCase(viewable.getMimeType());
+
+ if (replyTag != null) {
+ boolean isQuotedReply = BODY_QUOTED_PART_REPLY.equalsIgnoreCase(replyTag);
+ boolean isQuotedForward = BODY_QUOTED_PART_FORWARD.equalsIgnoreCase(replyTag);
+ boolean isQuotedIntro = BODY_QUOTED_PART_INTRO.equalsIgnoreCase(replyTag);
+
+ if (isQuotedReply || isQuotedForward) {
+ if (isHtml) {
+ sbHtmlReply = appendTextPart(sbHtmlReply, text);
+ } else {
+ sbTextReply = appendTextPart(sbTextReply, text);
+ }
+ // Set message flags as well
+ localMessage.mFlags &= ~EmailContent.Message.FLAG_TYPE_MASK;
+ localMessage.mFlags |= isQuotedReply
+ ? EmailContent.Message.FLAG_TYPE_REPLY
+ : EmailContent.Message.FLAG_TYPE_FORWARD;
+ continue;
+ }
+ if (isQuotedIntro) {
+ sbIntroText = appendTextPart(sbIntroText, text);
+ continue;
+ }
+ }
+
+ // Most of the time, just process regular body parts
+ if (isHtml) {
+ sbHtml = appendTextPart(sbHtml, text);
+ } else {
+ sbText = appendTextPart(sbText, text);
+ }
+ }
+
+ // write the combined data to the body part
+ if (!TextUtils.isEmpty(sbText)) {
+ String text = sbText.toString();
+ body.mTextContent = text;
+ localMessage.mSnippet = Snippet.fromPlainText(text);
+ }
+ if (!TextUtils.isEmpty(sbHtml)) {
+ String text = sbHtml.toString();
+ body.mHtmlContent = text;
+ if (localMessage.mSnippet == null) {
+ localMessage.mSnippet = Snippet.fromHtmlText(text);
+ }
+ }
+ if (sbHtmlReply != null && sbHtmlReply.length() != 0) {
+ body.mHtmlReply = sbHtmlReply.toString();
+ }
+ if (sbTextReply != null && sbTextReply.length() != 0) {
+ body.mTextReply = sbTextReply.toString();
+ }
+ if (sbIntroText != null && sbIntroText.length() != 0) {
+ body.mIntroText = sbIntroText.toString();
+ }
+ return true;
+ }
+}
diff --git a/src/com/android/exchange/ExchangeService.java b/src/com/android/exchange/ExchangeService.java
index 19bc2264a..6afcbb8bc 100644
--- a/src/com/android/exchange/ExchangeService.java
+++ b/src/com/android/exchange/ExchangeService.java
@@ -17,9 +17,7 @@
package com.android.exchange;
-import com.android.email.AccountBackupRestore;
import com.android.email.Email;
-import com.android.email.NotificationController;
import com.android.email.Utility;
import com.android.email.mail.transport.SSLUtils;
import com.android.email.provider.EmailContent;
@@ -32,11 +30,12 @@ import com.android.email.provider.EmailContent.Mailbox;
import com.android.email.provider.EmailContent.MailboxColumns;
import com.android.email.provider.EmailContent.Message;
import com.android.email.provider.EmailContent.SyncColumns;
-import com.android.email.service.MailService;
import com.android.emailcommon.Api;
+import com.android.emailcommon.service.AccountServiceProxy;
import com.android.emailcommon.service.EmailServiceStatus;
import com.android.emailcommon.service.IEmailService;
import com.android.emailcommon.service.IEmailServiceCallback;
+import com.android.emailcommon.utility.AccountReconciler;
import com.android.exchange.adapter.CalendarSyncAdapter;
import com.android.exchange.adapter.ContactsSyncAdapter;
import com.android.exchange.utility.FileLogger;
@@ -1046,8 +1045,15 @@ public class ExchangeService extends Service implements Runnable {
// list, which would cause the deletion of all of our accounts
AccountList accountList = collectEasAccounts(context, new AccountList());
alwaysLog("Reconciling accounts...");
- MailService.reconcileAccountsWithAccountManager(context, accountList, accountMgrList,
- false, context.getContentResolver());
+ boolean accountsDeleted = AccountReconciler.reconcileAccounts(context, accountList,
+ accountMgrList, context.getContentResolver());
+ if (accountsDeleted) {
+ try {
+ new AccountServiceProxy(context).accountDeleted();
+ } catch (RemoteException e) {
+ // ?
+ }
+ }
}
public static void log(String str) {
@@ -1749,13 +1755,22 @@ public class ExchangeService extends Service implements Runnable {
@Override
public void run() {
synchronized (sSyncLock) {
+ // ExchangeService cannot start unless we can connect to AccountService
+ if (!new AccountServiceProxy(ExchangeService.this).test()) {
+ log("!!! Email application not found; stopping self");
+ stopSelf();
+ }
// Restore accounts, if it has not happened already
- AccountBackupRestore.restoreAccountsIfNeeded(ExchangeService.this);
+ try {
+ new AccountServiceProxy(ExchangeService.this).restoreAccountsIfNeeded();
+ } catch (RemoteException e) {
+ // If we can't restore accounts, don't run
+ return;
+ }
// Run the reconciler and clean up any mismatched accounts - if we weren't
// running when accounts were deleted, it won't have been called.
runAccountReconcilerSync(ExchangeService.this);
// Update other services depending on final account configuration
- Email.setServicesEnabledSync(ExchangeService.this);
maybeStartExchangeServiceThread();
if (sServiceThread == null) {
log("!!! EAS ExchangeService, stopping self");
@@ -2385,8 +2400,12 @@ public class ExchangeService extends Service implements Runnable {
if (account == null) return;
if (exchangeService.releaseSyncHolds(exchangeService,
AbstractSyncService.EXIT_LOGIN_FAILURE, account)) {
- NotificationController.getInstance(exchangeService)
- .cancelLoginFailedNotification(accountId);
+ try {
+ new AccountServiceProxy(exchangeService).notifyLoginSucceeded(
+ accountId);
+ } catch (RemoteException e) {
+ // No harm if the notification fails
+ }
}
}
@@ -2413,8 +2432,11 @@ public class ExchangeService extends Service implements Runnable {
break;
// These errors are not retried automatically
case AbstractSyncService.EXIT_LOGIN_FAILURE:
- NotificationController.getInstance(exchangeService)
- .showLoginFailedNotification(m.mAccountKey);
+ try {
+ new AccountServiceProxy(exchangeService).notifyLoginFailed(m.mAccountKey);
+ } catch (RemoteException e) {
+ // ? Anything to do?
+ }
// Fall through
case AbstractSyncService.EXIT_SECURITY_FAILURE:
case AbstractSyncService.EXIT_EXCEPTION:
diff --git a/src/com/android/exchange/PolicyServiceDelegate.java b/src/com/android/exchange/PolicyServiceDelegate.java
new file mode 100644
index 000000000..d712c5591
--- /dev/null
+++ b/src/com/android/exchange/PolicyServiceDelegate.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.exchange;
+
+import com.android.email.provider.EmailContent.Account;
+import com.android.emailcommon.service.PolicyServiceProxy;
+import com.android.emailcommon.service.PolicySet;
+
+import android.content.Context;
+import android.os.RemoteException;
+
+public class PolicyServiceDelegate {
+
+ public static boolean isActive(Context context, PolicySet policies) {
+ try {
+ return new PolicyServiceProxy(context).isActive(policies);
+ } catch (RemoteException e) {
+ }
+ return false;
+ }
+
+ public static void policiesRequired(Context context, long accountId) {
+ try {
+ new PolicyServiceProxy(context).policiesRequired(accountId);
+ } catch (RemoteException e) {
+ throw new IllegalStateException("PolicyService transaction failed");
+ }
+ }
+
+ public static void updatePolicies(Context context, long accountId) {
+ try {
+ new PolicyServiceProxy(context).updatePolicies(accountId);
+ } catch (RemoteException e) {
+ throw new IllegalStateException("PolicyService transaction failed");
+ }
+ }
+
+ public static void setAccountHoldFlag(Context context, Account account, boolean newState) {
+ try {
+ new PolicyServiceProxy(context).setAccountHoldFlag(account.mId, newState);
+ } catch (RemoteException e) {
+ throw new IllegalStateException("PolicyService transaction failed");
+ }
+ }
+
+ public static boolean isActiveAdmin(Context context) {
+ try {
+ return new PolicyServiceProxy(context).isActiveAdmin();
+ } catch (RemoteException e) {
+ }
+ return false;
+ }
+
+ public static void remoteWipe(Context context) {
+ try {
+ new PolicyServiceProxy(context).remoteWipe();
+ } catch (RemoteException e) {
+ throw new IllegalStateException("PolicyService transaction failed");
+ }
+ }
+
+ public static boolean isSupported(Context context, PolicySet policies) {
+ try {
+ return new PolicyServiceProxy(context).isSupported(policies);
+ } catch (RemoteException e) {
+ }
+ return false;
+ }
+
+ public static PolicySet clearUnsupportedPolicies(Context context, PolicySet policies) {
+ try {
+ return new PolicyServiceProxy(context).clearUnsupportedPolicies(policies);
+ } catch (RemoteException e) {
+ }
+ throw new IllegalStateException("PolicyService transaction failed");
+ }
+}
diff --git a/src/com/android/exchange/adapter/EmailSyncAdapter.java b/src/com/android/exchange/adapter/EmailSyncAdapter.java
index d4a27ffb8..b9666c1a4 100644
--- a/src/com/android/exchange/adapter/EmailSyncAdapter.java
+++ b/src/com/android/exchange/adapter/EmailSyncAdapter.java
@@ -17,7 +17,6 @@
package com.android.exchange.adapter;
-import com.android.email.LegacyConversions;
import com.android.email.Utility;
import com.android.email.mail.Address;
import com.android.email.mail.MeetingInfo;
@@ -26,8 +25,8 @@ import com.android.email.mail.PackedString;
import com.android.email.mail.Part;
import com.android.email.mail.internet.MimeMessage;
import com.android.email.mail.internet.MimeUtility;
-import com.android.email.provider.AttachmentProvider;
import com.android.email.provider.EmailContent;
+import com.android.email.provider.EmailProvider;
import com.android.email.provider.EmailContent.Account;
import com.android.email.provider.EmailContent.AccountColumns;
import com.android.email.provider.EmailContent.Attachment;
@@ -36,8 +35,10 @@ import com.android.email.provider.EmailContent.Mailbox;
import com.android.email.provider.EmailContent.Message;
import com.android.email.provider.EmailContent.MessageColumns;
import com.android.email.provider.EmailContent.SyncColumns;
-import com.android.email.provider.EmailProvider;
-import com.android.email.service.MailService;
+import com.android.emailcommon.service.AccountServiceProxy;
+import com.android.emailcommon.service.SyncWindow;
+import com.android.emailcommon.utility.AttachmentUtilities;
+import com.android.emailcommon.utility.ConversionUtilities;
import com.android.exchange.Eas;
import com.android.exchange.EasSyncService;
import com.android.exchange.MessageMoveRequest;
@@ -117,22 +118,23 @@ public class EmailSyncAdapter extends AbstractSyncAdapter {
mService.clearRequests();
mFetchRequestList.clear();
// Delete attachments...
- AttachmentProvider.deleteAllMailboxAttachmentFiles(mContext, mAccount.mId, mMailbox.mId);
+ AttachmentUtilities.deleteAllMailboxAttachmentFiles(mContext, mAccount.mId,
+ mMailbox.mId);
}
private String getEmailFilter() {
switch (mAccount.mSyncLookback) {
- case com.android.email.Account.SYNC_WINDOW_1_DAY:
+ case SyncWindow.SYNC_WINDOW_1_DAY:
return Eas.FILTER_1_DAY;
- case com.android.email.Account.SYNC_WINDOW_3_DAYS:
+ case SyncWindow.SYNC_WINDOW_3_DAYS:
return Eas.FILTER_3_DAYS;
- case com.android.email.Account.SYNC_WINDOW_1_WEEK:
+ case SyncWindow.SYNC_WINDOW_1_WEEK:
return Eas.FILTER_1_WEEK;
- case com.android.email.Account.SYNC_WINDOW_2_WEEKS:
+ case SyncWindow.SYNC_WINDOW_2_WEEKS:
return Eas.FILTER_2_WEEKS;
- case com.android.email.Account.SYNC_WINDOW_1_MONTH:
+ case SyncWindow.SYNC_WINDOW_1_MONTH:
return Eas.FILTER_1_MONTH;
- case com.android.email.Account.SYNC_WINDOW_ALL:
+ case SyncWindow.SYNC_WINDOW_ALL:
return Eas.FILTER_ALL;
default:
return Eas.FILTER_1_WEEK;
@@ -496,7 +498,7 @@ public class EmailSyncAdapter extends AbstractSyncAdapter {
MimeUtility.collectParts(mimeMessage, viewables, attachments);
Body tempBody = new Body();
// updateBodyFields fills in the content fields of the Body
- LegacyConversions.updateBodyFields(tempBody, msg, viewables);
+ ConversionUtilities.updateBodyFields(tempBody, msg, viewables);
// But we need them in the message itself for handling during commit()
msg.mHtml = tempBody.mHtmlContent;
msg.mText = tempBody.mTextContent;
@@ -770,7 +772,7 @@ public class EmailSyncAdapter extends AbstractSyncAdapter {
for (Long id : deletedEmails) {
ops.add(ContentProviderOperation.newDelete(
ContentUris.withAppendedId(Message.CONTENT_URI, id)).build());
- AttachmentProvider.deleteAllAttachmentFiles(mContext, mAccount.mId, id);
+ AttachmentUtilities.deleteAllAttachmentFiles(mContext, mAccount.mId, id);
}
if (!changedEmails.isEmpty()) {
@@ -822,7 +824,11 @@ public class EmailSyncAdapter extends AbstractSyncAdapter {
cv.put(EmailContent.ADD_COLUMN_NAME, notifyCount);
Uri uri = ContentUris.withAppendedId(Account.ADD_TO_FIELD_URI, mAccount.mId);
mContentResolver.update(uri, cv, null, null);
- MailService.actionNotifyNewMessages(mContext, mAccount.mId);
+ try {
+ new AccountServiceProxy(mService.mContext).notifyNewMessages(mAccount.mId);
+ } catch (RemoteException e) {
+ // ? Anything to do here?
+ }
}
}
}
diff --git a/src/com/android/exchange/adapter/FolderSyncParser.java b/src/com/android/exchange/adapter/FolderSyncParser.java
index 7d6229b38..5013337c0 100644
--- a/src/com/android/exchange/adapter/FolderSyncParser.java
+++ b/src/com/android/exchange/adapter/FolderSyncParser.java
@@ -18,13 +18,13 @@
package com.android.exchange.adapter;
import com.android.email.Utility;
-import com.android.email.provider.AttachmentProvider;
import com.android.email.provider.EmailContent;
+import com.android.email.provider.EmailProvider;
import com.android.email.provider.EmailContent.Account;
import com.android.email.provider.EmailContent.AccountColumns;
import com.android.email.provider.EmailContent.Mailbox;
import com.android.email.provider.EmailContent.MailboxColumns;
-import com.android.email.provider.EmailProvider;
+import com.android.emailcommon.utility.AttachmentUtilities;
import com.android.exchange.Eas;
import com.android.exchange.ExchangeService;
import com.android.exchange.MockParserStream;
@@ -187,7 +187,7 @@ public class FolderSyncParser extends AbstractSyncParser {
ops.add(ContentProviderOperation.newDelete(
ContentUris.withAppendedId(Mailbox.CONTENT_URI,
c.getLong(0))).build());
- AttachmentProvider.deleteAllMailboxAttachmentFiles(mContext,
+ AttachmentUtilities.deleteAllMailboxAttachmentFiles(mContext,
mAccountId, mMailbox.mId);
}
} finally {
diff --git a/src/com/android/exchange/utility/CalendarUtilities.java b/src/com/android/exchange/utility/CalendarUtilities.java
index e1879f653..27452ebac 100644
--- a/src/com/android/exchange/utility/CalendarUtilities.java
+++ b/src/com/android/exchange/utility/CalendarUtilities.java
@@ -18,7 +18,6 @@ package com.android.exchange.utility;
import com.android.email.Email;
import com.android.email.R;
-import com.android.email.ResourceHelper;
import com.android.email.Utility;
import com.android.email.mail.Address;
import com.android.email.provider.EmailContent;
@@ -26,6 +25,7 @@ import com.android.email.provider.EmailContent.Account;
import com.android.email.provider.EmailContent.Attachment;
import com.android.email.provider.EmailContent.Mailbox;
import com.android.email.provider.EmailContent.Message;
+import com.android.emailcommon.service.AccountServiceProxy;
import com.android.exchange.Eas;
import com.android.exchange.EasSyncService;
import com.android.exchange.ExchangeService;
@@ -1215,8 +1215,12 @@ public class CalendarUtilities {
cv.put(Calendars.ORGANIZER_CAN_RESPOND, 0);
// TODO Coordinate account colors w/ Calendar, if possible
- // Make Email account color opaque
- int color = ResourceHelper.getInstance(service.mContext).getAccountColor(account.mId);
+ int color = AccountServiceProxy.DEFAULT_ACCOUNT_COLOR;
+ try {
+ color = new AccountServiceProxy(service.mContext).getAccountColor(account.mId);
+ } catch (RemoteException e) {
+ // Use the default
+ }
cv.put(Calendars.COLOR, color);
cv.put(Calendars.TIMEZONE, Time.getCurrentTimezone());
cv.put(Calendars.ACCESS_LEVEL, Calendars.OWNER_ACCESS);
diff --git a/tests/src/com/android/email/DBTestHelper.java b/tests/src/com/android/email/DBTestHelper.java
index d67caae34..729224c8b 100644
--- a/tests/src/com/android/email/DBTestHelper.java
+++ b/tests/src/com/android/email/DBTestHelper.java
@@ -20,6 +20,7 @@ import com.android.email.provider.AttachmentProvider;
import com.android.email.provider.ContentCache;
import com.android.email.provider.EmailContent;
import com.android.email.provider.EmailProvider;
+import com.android.emailcommon.utility.AttachmentUtilities;
import android.content.ContentProvider;
import android.content.ContentResolver;
@@ -37,8 +38,6 @@ import android.test.mock.MockCursor;
import java.io.File;
-import junit.framework.Assert;
-
/**
* Helper classes (and possibly methods) for database related tests.
*/
@@ -225,7 +224,7 @@ public final class DBTestHelper {
final AttachmentProvider ap = new AttachmentProvider();
ap.attachInfo(providerContext, null);
- resolver.addProvider(AttachmentProvider.AUTHORITY, ap);
+ resolver.addProvider(AttachmentUtilities.AUTHORITY, ap);
ContentCache.invalidateAllCachesForTest();
diff --git a/tests/src/com/android/email/LegacyConversionsTests.java b/tests/src/com/android/email/LegacyConversionsTests.java
index d946d6a27..edccb5b04 100644
--- a/tests/src/com/android/email/LegacyConversionsTests.java
+++ b/tests/src/com/android/email/LegacyConversionsTests.java
@@ -20,21 +20,22 @@ import com.android.email.mail.Address;
import com.android.email.mail.BodyPart;
import com.android.email.mail.Flag;
import com.android.email.mail.Message;
-import com.android.email.mail.Message.RecipientType;
import com.android.email.mail.MessageTestUtils;
-import com.android.email.mail.MessageTestUtils.MessageBuilder;
-import com.android.email.mail.MessageTestUtils.MultipartBuilder;
import com.android.email.mail.MessagingException;
import com.android.email.mail.Part;
+import com.android.email.mail.Message.RecipientType;
+import com.android.email.mail.MessageTestUtils.MessageBuilder;
+import com.android.email.mail.MessageTestUtils.MultipartBuilder;
import com.android.email.mail.internet.MimeBodyPart;
import com.android.email.mail.internet.MimeHeader;
import com.android.email.mail.internet.MimeMessage;
import com.android.email.mail.internet.MimeUtility;
import com.android.email.mail.internet.TextBody;
import com.android.email.provider.EmailContent;
-import com.android.email.provider.EmailContent.Attachment;
import com.android.email.provider.EmailProvider;
import com.android.email.provider.ProviderTestUtils;
+import com.android.email.provider.EmailContent.Attachment;
+import com.android.emailcommon.utility.ConversionUtilities;
import android.content.ContentUris;
import android.content.Context;
@@ -191,7 +192,7 @@ public class LegacyConversionsTests extends ProviderTestCase2 {
viewables.add(emptyTextPart);
// a "null" body part of type text/plain should result in a null mTextContent
- boolean result = LegacyConversions.updateBodyFields(localBody, localMessage, viewables);
+ boolean result = ConversionUtilities.updateBodyFields(localBody, localMessage, viewables);
assertTrue(result);
assertNull(localBody.mTextContent);
}
diff --git a/tests/src/com/android/email/UtilityUnitTests.java b/tests/src/com/android/email/UtilityUnitTests.java
index c72e689d1..e8c681d08 100644
--- a/tests/src/com/android/email/UtilityUnitTests.java
+++ b/tests/src/com/android/email/UtilityUnitTests.java
@@ -17,17 +17,16 @@
package com.android.email;
import com.android.email.Utility.NewFileCreator;
-import com.android.email.provider.AttachmentProvider;
+import com.android.email.provider.ProviderTestUtils;
import com.android.email.provider.EmailContent.Account;
import com.android.email.provider.EmailContent.Attachment;
import com.android.email.provider.EmailContent.Mailbox;
-import com.android.email.provider.ProviderTestUtils;
+import com.android.emailcommon.utility.AttachmentUtilities;
import android.content.Context;
import android.database.Cursor;
import android.database.CursorWrapper;
import android.database.MatrixCursor;
-import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Bundle;
@@ -422,13 +421,13 @@ public class UtilityUnitTests extends AndroidTestCase {
Attachment att = ProviderTestUtils.setupAttachment(mailbox.mId, "name", 123, true,
providerContext);
long attachmentId = att.mId;
- Uri uri = AttachmentProvider.getAttachmentUri(account.mId, attachmentId);
+ Uri uri = AttachmentUtilities.getAttachmentUri(account.mId, attachmentId);
// Case 1: exists in the provider.
assertEquals("name", Utility.getContentFileName(providerContext, uri));
// Case 2: doesn't exist in the provider
- Uri notExistUri = AttachmentProvider.getAttachmentUri(account.mId, 123456789);
+ Uri notExistUri = AttachmentUtilities.getAttachmentUri(account.mId, 123456789);
String lastPathSegment = notExistUri.getLastPathSegment();
assertEquals(lastPathSegment, Utility.getContentFileName(providerContext, notExistUri));
}
diff --git a/tests/src/com/android/email/mail/MessageTestUtils.java b/tests/src/com/android/email/mail/MessageTestUtils.java
index 487fbfa33..c7185a8b1 100644
--- a/tests/src/com/android/email/mail/MessageTestUtils.java
+++ b/tests/src/com/android/email/mail/MessageTestUtils.java
@@ -21,8 +21,8 @@ import com.android.email.mail.internet.MimeHeader;
import com.android.email.mail.internet.MimeMessage;
import com.android.email.mail.internet.MimeMultipart;
import com.android.email.mail.internet.TextBody;
-import com.android.email.provider.AttachmentProvider;
import com.android.email.provider.EmailContent;
+import com.android.emailcommon.utility.AttachmentUtilities;
import android.net.Uri;
@@ -63,7 +63,7 @@ public class MessageTestUtils {
* @return AttachmentProvider content URI
*/
public static Uri contentUri(long attachmentId, EmailContent.Account account) {
- return AttachmentProvider.getAttachmentUri(account.mId, attachmentId);
+ return AttachmentUtilities.getAttachmentUri(account.mId, attachmentId);
}
/**
diff --git a/tests/src/com/android/email/provider/AttachmentProviderTests.java b/tests/src/com/android/email/provider/AttachmentProviderTests.java
index c19b6e1dc..a7794aa3f 100644
--- a/tests/src/com/android/email/provider/AttachmentProviderTests.java
+++ b/tests/src/com/android/email/provider/AttachmentProviderTests.java
@@ -19,11 +19,11 @@ package com.android.email.provider;
import com.android.email.AttachmentInfo;
import com.android.email.R;
import com.android.email.mail.MessagingException;
-import com.android.email.provider.AttachmentProvider.AttachmentProviderColumns;
import com.android.email.provider.EmailContent.Account;
import com.android.email.provider.EmailContent.Attachment;
import com.android.email.provider.EmailContent.Mailbox;
import com.android.email.provider.EmailContent.Message;
+import com.android.emailcommon.utility.AttachmentUtilities;
import android.content.ContentResolver;
import android.content.Context;
@@ -53,7 +53,7 @@ public class AttachmentProviderTests extends ProviderTestCase2