From 959404cc68cea89e5fee4446a5717c2566e8bd08 Mon Sep 17 00:00:00 2001 From: Daniel Applebaum Date: Thu, 22 Oct 2009 00:41:06 +0000 Subject: [PATCH] Issue 4: Implements Push Mail for IMAP accounts using IMAP IDLE This commit contains the entirety of the changes performed in the issue4-1.X branch from revision 718 through revision 851. Because the issue4-1.X branch was up-to-date with trunk revision 847 at revision 849, the source of this commit was not an "svn merge". Instead, it is merely a copy of all changed files from the issue4-1.X branch to my trunk working copy and a straight commit. Also: Issue 551 Issue 628 Issue 650 Issue 654 Issue 656 Issue 682 Issue 696 --- AndroidManifest.xml | 7 +- res/menu/folder_list_option.xml | 5 + res/menu/message_list_option.xml | 11 + res/values-de-rDE/strings.xml | 3 +- res/values/arrays.xml | 40 +- res/values/strings.xml | 25 +- res/xml/account_settings_preferences.xml | 9 +- res/xml/folder_settings_preferences.xml | 7 + src/com/android/email/Account.java | 31 +- src/com/android/email/Email.java | 17 +- .../android/email/EmailReceivedIntent.java | 14 + .../android/email/MessagingController.java | 1923 ++++++++++++----- src/com/android/email/MessagingListener.java | 17 +- src/com/android/email/activity/Accounts.java | 11 +- .../android/email/activity/ChooseFolder.java | 8 +- .../android/email/activity/FolderList.java | 306 ++- .../email/activity/MessageCompose.java | 64 +- .../android/email/activity/MessageList.java | 324 +-- .../android/email/activity/MessageView.java | 68 +- .../email/activity/setup/AccountSettings.java | 31 + .../setup/AccountSetupCheckSettings.java | 13 +- .../email/activity/setup/FolderSettings.java | 54 +- src/com/android/email/mail/Folder.java | 40 +- .../email/mail/MessageRetrievalListener.java | 2 + .../email/mail/MessagingException.java | 10 + src/com/android/email/mail/PushReceiver.java | 14 + src/com/android/email/mail/Pusher.java | 14 + src/com/android/email/mail/Store.java | 9 + .../email/mail/store/ImapResponseParser.java | 28 +- .../android/email/mail/store/ImapStore.java | 901 +++++++- .../android/email/mail/store/LocalStore.java | 161 +- .../android/email/service/BootReceiver.java | 7 +- .../android/email/service/MailService.java | 357 ++- 33 files changed, 3328 insertions(+), 1203 deletions(-) create mode 100644 src/com/android/email/EmailReceivedIntent.java create mode 100644 src/com/android/email/mail/PushReceiver.java create mode 100644 src/com/android/email/mail/Pusher.java diff --git a/AndroidManifest.xml b/AndroidManifest.xml index dc056f3a6..d3782521b 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -1,7 +1,7 @@ + android:versionCode="1105" + android:versionName="1.105" package="com.fsck.k9"> @@ -204,6 +204,9 @@ + + + + + + Nebenordner Synchronisationsklasse - Wie Anzeigeklasse + Keine Hauptordner Nebenordner + Wie Anzeigeklasse Einstellungen Posteingang Einstellungen für Posteingangsserver bearbeiten. diff --git a/res/values/arrays.xml b/res/values/arrays.xml index 7bb5048c2..c02169202 100644 --- a/res/values/arrays.xml +++ b/res/values/arrays.xml @@ -81,6 +81,7 @@ @string/account_settings_folder_sync_mode_first_class @string/account_settings_folder_sync_mode_first_and_second_class @string/account_settings_folder_sync_mode_not_second_class + @string/account_settings_folder_sync_mode_none @@ -88,6 +89,23 @@ FIRST_CLASS FIRST_AND_SECOND_CLASS NOT_SECOND_CLASS + NONE + + + + @string/account_settings_folder_push_mode_all + @string/account_settings_folder_push_mode_first_class + @string/account_settings_folder_push_mode_first_and_second_class + @string/account_settings_folder_push_mode_not_second_class + @string/account_settings_folder_push_mode_none + + + + ALL + FIRST_CLASS + FIRST_AND_SECOND_CLASS + NOT_SECOND_CLASS + NONE @@ -111,7 +129,7 @@ - NONE + NO_CLASS FIRST_CLASS SECOND_CLASS @@ -120,12 +138,28 @@ @string/folder_settings_folder_sync_mode_normal @string/folder_settings_folder_sync_mode_first_class @string/folder_settings_folder_sync_mode_second_class + @string/folder_settings_folder_sync_mode_inherited - NONE + NO_CLASS FIRST_CLASS - SECOND_CLASS + SECOND_CLASS + INHERITED + + + + @string/folder_settings_folder_push_mode_normal + @string/folder_settings_folder_push_mode_first_class + @string/folder_settings_folder_push_mode_second_class + @string/folder_settings_folder_push_mode_inherited + + + + NO_CLASS + FIRST_CLASS + SECOND_CLASS + INHERITED diff --git a/res/values/strings.xml b/res/values/strings.xml index dc84350a5..bf2114b17 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -146,6 +146,9 @@ Long-press the Outbox to find the "Send messages" action in order to initiate another sending attempt.\u000A\u000a The %s folder may contain error messages regarding the failures. + K-9 alert + Syncing and sending suspended due to lack of network. + No more messages @@ -210,11 +213,14 @@ Welcome to K-9 Mail setup. K-9 is an open source email client for Android based Select \"Show pictures\" to display embedded pictures. Show pictures Fetching attachment. + Unable to find viewer for %s. Folders New folder New folder name + + (Push) Message copied Message moved @@ -376,12 +382,20 @@ Welcome to K-9 Mail setup. K-9 is an open source email client for Android based Only 1st Class folders 1st and 2nd Class folders All except 2nd Class folders - + Folder sync mode All Only 1st Class folders 1st and 2nd Class folders All except 2nd Class folders + None + + Folder push mode + All + Only 1st Class folders + 1st and 2nd Class folders + All except 2nd Class folders + None Move/copy destination folders All @@ -396,9 +410,16 @@ Welcome to K-9 Mail setup. K-9 is an open source email client for Android based 2nd Class Folder sync class - Same as display class + None 1st Class 2nd Class + Same as display class + + Folder push class + None + 1st Class + 2nd Class + Same as sync class Incoming settings Configure the incoming email server diff --git a/res/xml/account_settings_preferences.xml b/res/xml/account_settings_preferences.xml index 812911331..6c79e4dc2 100644 --- a/res/xml/account_settings_preferences.xml +++ b/res/xml/account_settings_preferences.xml @@ -76,7 +76,14 @@ android:title="@string/account_settings_folder_sync_mode_label" android:entries="@array/account_settings_folder_sync_mode_entries" android:entryValues="@array/account_settings_folder_sync_mode_values" - android:dialogTitle="@string/account_settings_folder_sync_mode_label" /> + android:dialogTitle="@string/account_settings_folder_sync_mode_label" /> + + + + diff --git a/src/com/android/email/Account.java b/src/com/android/email/Account.java index 364359336..68b6d1dee 100644 --- a/src/com/android/email/Account.java +++ b/src/com/android/email/Account.java @@ -49,6 +49,7 @@ public class Account implements Serializable { String mAutoExpandFolderName; FolderMode mFolderDisplayMode; FolderMode mFolderSyncMode; + FolderMode mFolderPushMode; FolderMode mFolderTargetMode; int mAccountNumber; boolean mVibrate; @@ -59,7 +60,7 @@ public class Account implements Serializable { List identities; public enum FolderMode { - ALL, FIRST_CLASS, FIRST_AND_SECOND_CLASS, NOT_SECOND_CLASS; + NONE, ALL, FIRST_CLASS, FIRST_AND_SECOND_CLASS, NOT_SECOND_CLASS; } public enum HideButtons { @@ -87,6 +88,7 @@ public class Account implements Serializable { mVibrate = false; mFolderDisplayMode = FolderMode.NOT_SECOND_CLASS; mFolderSyncMode = FolderMode.FIRST_CLASS; + mFolderPushMode = FolderMode.FIRST_CLASS; mFolderTargetMode = FolderMode.NOT_SECOND_CLASS; mHideMessageViewButtons = HideButtons.NEVER; mRingtoneUri = "content://settings/system/notification_sound"; @@ -238,6 +240,17 @@ public class Account implements Serializable { mFolderSyncMode = FolderMode.FIRST_CLASS; } + try + { + mFolderPushMode = FolderMode.valueOf(preferences.getPreferences().getString(mUuid + ".folderPushMode", + FolderMode.FIRST_CLASS.name())); + } + catch (Exception e) + { + mFolderPushMode = FolderMode.FIRST_CLASS; + } + + try { mFolderTargetMode = FolderMode.valueOf(preferences.getPreferences().getString(mUuid + ".folderTargetMode", @@ -460,6 +473,7 @@ public class Account implements Serializable { editor.remove(mUuid + ".lastFullSync"); editor.remove(mUuid + ".folderDisplayMode"); editor.remove(mUuid + ".folderSyncMode"); + editor.remove(mUuid + ".folderPushMode"); editor.remove(mUuid + ".folderTargetMode"); editor.remove(mUuid + ".hideButtonsEnum"); editor.remove(mUuid + ".signatureBeforeQuotedText"); @@ -523,6 +537,7 @@ public class Account implements Serializable { editor.putString(mUuid + ".ringtone", mRingtoneUri); editor.putString(mUuid + ".folderDisplayMode", mFolderDisplayMode.name()); editor.putString(mUuid + ".folderSyncMode", mFolderSyncMode.name()); + editor.putString(mUuid + ".folderPushMode", mFolderPushMode.name()); editor.putString(mUuid + ".folderTargetMode", mFolderTargetMode.name()); editor.putBoolean(mUuid + ".signatureBeforeQuotedText", this.mIsSignatureBeforeQuotedText); @@ -573,6 +588,10 @@ public class Account implements Serializable { folder.getName().equals(getSentFolderName()) == false && folder.getName().equals(getErrorFolderName()) == false) { + if (aMode == Account.FolderMode.NONE) + { + continue; + } if (aMode == Account.FolderMode.FIRST_CLASS && fMode != Folder.FolderClass.FIRST_CLASS) { @@ -740,6 +759,16 @@ public class Account implements Serializable { { mFolderSyncMode = syncMode; } + + public FolderMode getFolderPushMode() + { + return mFolderPushMode; + } + + public void setFolderPushMode(FolderMode syncMode) + { + mFolderPushMode = syncMode; + } public boolean isShowOngoing() { diff --git a/src/com/android/email/Email.java b/src/com/android/email/Email.java index 88df3da9a..6950a6505 100644 --- a/src/com/android/email/Email.java +++ b/src/com/android/email/Email.java @@ -35,6 +35,7 @@ public class Email extends Application { /** * If this is enabled there will be additional logging information sent to * Log.d, including protocol dumps. + * Controlled by Preferences at run-time */ public static boolean DEBUG = false; @@ -65,9 +66,7 @@ public class Email extends Application { * The MIME type(s) of attachments we're willing to view. */ public static final String[] ACCEPTABLE_ATTACHMENT_VIEW_TYPES = new String[] { - "image/*", - "audio/*", - "text/*", + "*/*", }; /** @@ -134,6 +133,11 @@ public class Email extends Application { public static final int WAKE_LOCK_TIMEOUT = 600000; public static final int MANUAL_WAKE_LOCK_TIMEOUT = 120000; + + public static final int PUSH_WAKE_LOCK_TIMEOUT = 30000; + + public static final int MAIL_SERVICE_WAKE_LOCK_TIMEOUT = 30000; + /** * LED color used for the new email notitication @@ -161,6 +165,7 @@ public class Email extends Application { public static final int FETCHING_EMAIL_NOTIFICATION_ID = -4; public static final int FETCHING_EMAIL_NOTIFICATION_MULTI_ACCOUNT_ID = -1; public static final int FETCHING_EMAIL_NOTIFICATION_NO_ACCOUNT = -2; + public static final int CONNECTIVITY_ID = -3; // Backup formats in case they can't be fetched from the system public static final String BACKUP_DATE_FORMAT = "MM-dd-yyyy"; @@ -237,7 +242,7 @@ public class Email extends Application { DEBUG = prefs.getEnableDebugLogging(); DEBUG_SENSITIVE = prefs.getEnableSensitiveLogging(); MessagingController.getInstance(this).resetVisibleLimits(prefs.getAccounts()); - + /* * We have to give MimeMessage a temp directory because File.createTempFile(String, String) * doesn't work in Android and MimeMessage does not have access to a Context. @@ -249,7 +254,7 @@ public class Email extends Application { */ setServicesEnabled(this); - + MessagingController.getInstance(this).addListener(new MessagingListener() { @Override public void synchronizeMailboxNewMessage(Account account, String folder, Message message) { @@ -272,6 +277,8 @@ public class Email extends Application { } } }); + + MailService.appStarted(this); } } diff --git a/src/com/android/email/EmailReceivedIntent.java b/src/com/android/email/EmailReceivedIntent.java new file mode 100644 index 000000000..1924184d9 --- /dev/null +++ b/src/com/android/email/EmailReceivedIntent.java @@ -0,0 +1,14 @@ +package com.android.email; + +public class EmailReceivedIntent { + + public static final String ACTION_EMAIL_RECEIVED = "com.android.email.intent.action.EMAIL_RECEIVED"; + public static final String EXTRA_ACCOUNT = "com.android.email.intent.extra.ACCOUNT"; + public static final String EXTRA_FOLDER = "com.android.email.intent.extra.FOLDER"; + public static final String EXTRA_SENT_DATE = "com.android.email.intent.extra.SENT_DATE"; + public static final String EXTRA_FROM = "com.android.email.intent.extra.FROM"; + public static final String EXTRA_TO = "com.android.email.intent.extra.TO"; + public static final String EXTRA_CC = "com.android.email.intent.extra.CC"; + public static final String EXTRA_BCC = "com.android.email.intent.extra.BCC"; + public static final String EXTRA_SUBJECT = "com.android.email.intent.extra.SUBJECT"; +} diff --git a/src/com/android/email/MessagingController.java b/src/com/android/email/MessagingController.java index f1b294af8..d026d0525 100644 --- a/src/com/android/email/MessagingController.java +++ b/src/com/android/email/MessagingController.java @@ -3,8 +3,12 @@ package com.android.email; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; import java.io.PrintStream; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.HashMap; @@ -15,12 +19,17 @@ import java.util.Set; import java.util.concurrent.BlockingQueue; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArraySet; +import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.PriorityBlockingQueue; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; + +import org.apache.commons.io.output.NullWriter; import android.app.Application; import android.app.Notification; @@ -28,14 +37,17 @@ import android.app.NotificationManager; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; +import android.net.Uri; import android.os.PowerManager; import android.os.Process; import android.os.PowerManager.WakeLock; +import android.text.TextUtils; import android.util.Config; import android.util.Log; import com.android.email.activity.FolderList; import com.android.email.mail.Address; +import com.android.email.mail.Body; import com.android.email.mail.FetchProfile; import com.android.email.mail.Flag; import com.android.email.mail.Folder; @@ -43,6 +55,8 @@ import com.android.email.mail.Message; import com.android.email.mail.MessageRetrievalListener; import com.android.email.mail.MessagingException; import com.android.email.mail.Part; +import com.android.email.mail.PushReceiver; +import com.android.email.mail.Pusher; import com.android.email.mail.Store; import com.android.email.mail.Transport; import com.android.email.mail.Folder.FolderType; @@ -93,22 +107,20 @@ public class MessagingController implements Runnable { private static final String PENDING_COMMAND_SET_FLAG = "com.android.email.MessagingController.setFlag"; private static final String PENDING_COMMAND_APPEND = "com.android.email.MessagingController.append"; private static final String PENDING_COMMAND_MARK_ALL_AS_READ = "com.android.email.MessagingController.markAllAsRead"; - private static final String PENDING_COMMAND_CLEAR_ALL_PENDING = "com.android.email.MessagingController.clearAllPending"; - private static MessagingController inst = null; - private BlockingQueue mCommands = new LinkedBlockingQueue(); - private BlockingQueue backCommands = new LinkedBlockingQueue(); + private BlockingQueue mCommands = new PriorityBlockingQueue(); private Thread mThread; - //private Set mListeners = Collections.synchronizedSet(new HashSet()); private Set mListeners = new CopyOnWriteArraySet(); private HashMap sortAscending = new HashMap(); private ConcurrentHashMap sendCount = new ConcurrentHashMap(); - - private final ExecutorService threadPool = Executors.newFixedThreadPool(3); + + ConcurrentHashMap pushers = new ConcurrentHashMap(); + + private final ExecutorService threadPool = Executors.newFixedThreadPool(5); public enum SORT_TYPE { SORT_DATE(R.string.sort_earliest_first, R.string.sort_latest_first, false), @@ -144,6 +156,8 @@ public class MessagingController implements Runnable { private MessagingListener checkMailListener = null; + private MemorizingListener memorizingListener = new MemorizingListener(); + private boolean mBusy; private Application mApplication; @@ -227,10 +241,17 @@ public class MessagingController implements Runnable { mApplication = application; mThread = new Thread(this); mThread.start(); + if (memorizingListener != null) + { + addListener(memorizingListener); + } } public void log(String logmess) { - Log.d(Email.LOG_TAG, logmess); + if (Email.DEBUG) + { + Log.d(Email.LOG_TAG, logmess); + } if (Email.logFile != null) { FileOutputStream fos = null; try { @@ -281,24 +302,19 @@ public class MessagingController implements Runnable { while (true) { String commandDescription = null; try { - Command command = mCommands.poll(); - if (command == null) { - command = backCommands.poll(); - } - if (command == null) { - command = mCommands.poll(1, TimeUnit.SECONDS); - } + Command command = mCommands.take(); if (command != null) { commandDescription = command.description; - Log.i(Email.LOG_TAG, "Running background command '" + command.description + "'"); + String ground = (command.isForeground ? "Foreground" : "Background" ); + Log.i(Email.LOG_TAG, "Running " + ground + " command '" + command.description + "', seq = " + command.sequence); mBusy = true; command.runnable.run(); - Log.i(Email.LOG_TAG, "Background command '" + command.description + "' completed"); + Log.i(Email.LOG_TAG, ground + " Command '" + command.description + "' completed"); for (MessagingListener l : getListeners()) { l.controllerCommandCompleted(mCommands.size() > 0); } - if (command.listener != null && !mListeners.contains(command.listener)) { + if (command.listener != null && !getListeners().contains(command.listener)) { command.listener.controllerCommandCompleted(mCommands.size() > 0); } } @@ -311,29 +327,53 @@ public class MessagingController implements Runnable { } private void put(String description, MessagingListener listener, Runnable runnable) { - putCommand(mCommands, description, listener, runnable); - } + putCommand(mCommands, description, listener, runnable, true); + } private void putBackground(String description, MessagingListener listener, Runnable runnable) { - putCommand(backCommands, description, listener, runnable); - } - - private void putCommand(BlockingQueue queue, String description, MessagingListener listener, Runnable runnable) { - try { - Command command = new Command(); - command.listener = listener; - command.runnable = runnable; - command.description = description; + putCommand(mCommands, description, listener, runnable, false); + } + + private void putCommand(BlockingQueue queue, String description, MessagingListener listener, Runnable runnable, boolean isForeground) { + int retries = 10; + Exception e = null; + while (retries-- > 0) + { + try { + Command command = new Command(); + command.listener = listener; + command.runnable = runnable; + command.description = description; + command.isForeground = isForeground; queue.put(command); - } - catch (InterruptedException ie) { - throw new Error(ie); - } + return; + } + catch (InterruptedException ie) { + try + { + Thread.sleep(200); + } + catch (InterruptedException ne) + { + } + e = ie; + } + } + throw new Error(e); } public void addListener(MessagingListener listener) { mListeners.add(listener); + refreshListener(listener); + } + + public void refreshListener(MessagingListener listener) + { + if (memorizingListener != null && listener != null) + { + memorizingListener.refreshOther(listener); + } } public void removeListener(MessagingListener listener) { @@ -357,45 +397,48 @@ public class MessagingController implements Runnable { * @param listener * @throws MessagingException */ - public void listFolders( final Account account, boolean refreshRemote, MessagingListener listener) { - for (MessagingListener l : getListeners()) { - l.listFoldersStarted(account); - } - if (listener != null) { - listener.listFoldersStarted(account); - } - try { - Store localStore = Store.getInstance(account.getLocalStoreUri(), mApplication); - Folder[] localFolders = localStore.getPersonalNamespaces(); - - if ( refreshRemote || localFolders == null || localFolders.length == 0) { - doRefreshRemote(account, listener); - return; - } - for (MessagingListener l : getListeners()) { - l.listFolders(account, localFolders); - } - if (listener != null) { - listener.listFolders(account, localFolders); - } - } - catch (Exception e) { - for (MessagingListener l : getListeners()) { - l.listFoldersFailed(account, e.getMessage()); - } - if (listener != null) { - listener.listFoldersFailed(account, e.getMessage()); - } - addErrorMessage(account, e); - return; - } - for (MessagingListener l : getListeners()) { - l.listFoldersFinished(account); - } - if (listener != null) { - listener.listFoldersFinished(account); - } - + public void listFolders( final Account account, final boolean refreshRemote, final MessagingListener listener) { + threadPool.execute(new Runnable() { + public void run() + { + for (MessagingListener l : getListeners()) { + l.listFoldersStarted(account); + } + if (listener != null) { + listener.listFoldersStarted(account); + } + try { + Store localStore = Store.getInstance(account.getLocalStoreUri(), mApplication); + Folder[] localFolders = localStore.getPersonalNamespaces(); + + if ( refreshRemote || localFolders == null || localFolders.length == 0) { + doRefreshRemote(account, listener); + return; + } + for (MessagingListener l : getListeners()) { + l.listFolders(account, localFolders); + } + if (listener != null) { + listener.listFolders(account, localFolders); + } + } + catch (Exception e) { + for (MessagingListener l : getListeners()) { + l.listFoldersFailed(account, e.getMessage()); + } + if (listener != null) { + listener.listFoldersFailed(account, e.getMessage()); + } + addErrorMessage(account, e); + return; + } + for (MessagingListener l : getListeners()) { + l.listFoldersFinished(account); + } + if (listener != null) { + listener.listFoldersFinished(account); + } + }}); } private void doRefreshRemote (final Account account, MessagingListener listener) { @@ -466,49 +509,170 @@ public class MessagingController implements Runnable { * @param listener * @throws MessagingException */ - public void listLocalMessages(final Account account, final String folder, MessagingListener listener) { + public void listLocalMessages(final Account account, final String folder, final MessagingListener listener) { for (MessagingListener l : getListeners()) { l.listLocalMessagesStarted(account, folder); } - try { - Store localStore = Store.getInstance(account.getLocalStoreUri(), mApplication); - Folder localFolder = localStore.getFolder(folder); - localFolder.open(OpenMode.READ_WRITE); - Message[] localMessages = localFolder.getMessages( - new MessageRetrievalListener() { - public void messageStarted(String message, int number, int ofTotal) {} - public void messageFinished(Message message, int number, int ofTotal) { - - if (!message.isSet(Flag.DELETED) && isMessageSuppressed(account, folder, message) == false) { - //messages.add(message); - for (MessagingListener l : getListeners()) { - l.listLocalMessagesAddMessage(account, folder, message); - } - } else { - for (MessagingListener l : getListeners()) { - l.listLocalMessagesRemoveMessage(account, folder, message); - } - - } + if (listener != null && getListeners().contains(listener) == false) + { + listener.listLocalMessagesStarted(account, folder); + } + + threadPool.execute(new Runnable() { + public void run() { + Folder localFolder = null; + final LinkedBlockingQueue queue = new LinkedBlockingQueue(); + final CountDownLatch latch = new CountDownLatch(1); + + Runnable callbackRunner = new Runnable() + { + List pendingMessages = new ArrayList(); + boolean stop = false; + public void run() + { + if (Email.DEBUG) + { + Log.v(Email.LOG_TAG, "callbackRunner started"); } - } - ); - ArrayList messages = new ArrayList(); - //for (Message message : localMessages) { } - //for (MessagingListener l : getListeners()) { - // l.listLocalMessages(account, folder, messages.toArray(new Message[0])); - //} - for (MessagingListener l : getListeners()) { - l.listLocalMessagesFinished(account, folder); + while (stop == false) + { + MessageContainer messCont = null; + do + { + if (pendingMessages.isEmpty()) + { + try + { + messCont = queue.take(); + } + catch (InterruptedException ie) + { + Log.i(Email.LOG_TAG, "callbackRunner interrupted"); + } + } + else + { + messCont = queue.poll(); + } + if (messCont != null) + { + if (messCont.last == true) + { + stop = true; + messCont = null; + } + else if (messCont.message != null) + { + pendingMessages.add(messCont.message); + + } + } + } while (messCont != null); + callbackPending(); + } + latch.countDown(); + if (Email.DEBUG) + { + Log.v(Email.LOG_TAG, "callbackRunner finished"); + } + } + private void callbackPending() + { + for (MessagingListener l : getListeners()) { + l.listLocalMessagesAddMessages(account, folder, pendingMessages); + } + if (listener != null && getListeners().contains(listener) == false) { + listener.listLocalMessagesAddMessages(account, folder, pendingMessages); + } + pendingMessages.clear(); + } + + }; + + Thread callbackThread = new Thread(callbackRunner); + callbackThread.start(); + + try { + Store localStore = Store.getInstance(account.getLocalStoreUri(), mApplication); + localFolder = localStore.getFolder(folder); + localFolder.open(OpenMode.READ_WRITE); + + localFolder.getMessages( + new MessageRetrievalListener() { + int totalDone = 0; + public void messageStarted(String message, int number, int ofTotal) {} + public void messageFinished(Message message, int number, int ofTotal) { + + if (!message.isSet(Flag.DELETED) && isMessageSuppressed(account, folder, message) == false) { + MessageContainer messCont = new MessageContainer(); + messCont.message = message; + queue.add(messCont); + totalDone++; + + } else { + for (MessagingListener l : getListeners()) { + l.listLocalMessagesRemoveMessage(account, folder, message); + } + if (listener != null && getListeners().contains(listener) == false) { + listener.listLocalMessagesRemoveMessage(account, folder, message); + } + + } + } + public void messagesFinished(int number) { + } + + } + ); + MessageContainer messCont = new MessageContainer(); + messCont.last = true; + queue.add(messCont); + + latch.await(1000, TimeUnit.MILLISECONDS); + if (Email.DEBUG) + { + Log.v(Email.LOG_TAG, "Got ack that callbackRunner finished"); + } + for (MessagingListener l : getListeners()) { + l.listLocalMessagesFinished(account, folder); + } + if (listener != null && getListeners().contains(listener) == false) + { + listener.listLocalMessagesFinished(account, folder); + } } - } - catch (Exception e) { - for (MessagingListener l : getListeners()) { - l.listLocalMessagesFailed(account, folder, e.getMessage()); + catch (Exception e) { + for (MessagingListener l : getListeners()) { + l.listLocalMessagesFailed(account, folder, e.getMessage()); + } + if (listener != null && getListeners().contains(listener) == false) + { + listener.listLocalMessagesFailed(account, folder, e.getMessage()); + } + addErrorMessage(account, e); } - addErrorMessage(account, e); - } + finally + { + if (latch.getCount() != 0) + { + MessageContainer messCont = new MessageContainer(); + messCont.last = true; + queue.add(messCont); + } + if (localFolder != null) + { + try + { + localFolder.close(false); + } + catch (Exception e) + { + Log.e(Email.LOG_TAG, "Exception while closing folder", e); + } + } + } + }}); } public void loadMoreMessages(Account account, String folder, MessagingListener listener) { @@ -546,10 +710,10 @@ public class MessagingController implements Runnable { * @param folder * @param listener */ - public void synchronizeMailbox(final Account account, final String folder, MessagingListener listener) { - put("synchronizeMailbox", listener, new Runnable() { + public void synchronizeMailbox(final Account account, final String folder, final MessagingListener listener) { + putBackground("synchronizeMailbox", listener, new Runnable() { public void run() { - synchronizeMailboxSynchronous(account, folder); + synchronizeMailboxSynchronous(account, folder, listener); } }); } @@ -562,7 +726,7 @@ public class MessagingController implements Runnable { * * TODO Break this method up into smaller chunks. */ - public void synchronizeMailboxSynchronous(final Account account, final String folder) { + public void synchronizeMailboxSynchronous(final Account account, final String folder, final MessagingListener listener) { /* * We don't ever sync the Outbox. */ @@ -573,19 +737,20 @@ public class MessagingController implements Runnable { return; } String debugLine = "Synchronizing folder " + account.getDescription() + ":" + folder; - if (Config.LOGV) { - Log.v(Email.LOG_TAG, debugLine); - } + Log.i(Email.LOG_TAG, debugLine); log(debugLine); for (MessagingListener l : getListeners()) { l.synchronizeMailboxStarted(account, folder); } + if (listener != null && getListeners().contains(listener) == false) { + listener.synchronizeMailboxStarted(account, folder); + } LocalFolder tLocalFolder = null; Exception commandException = null; try { - if (Config.LOGV) { - Log.v(Email.LOG_TAG, "SYNC: About to process pending commands for folder " + + if (Email.DEBUG) { + Log.d(Email.LOG_TAG, "SYNC: About to process pending commands for folder " + account.getDescription() + ":" + folder); } try { @@ -602,7 +767,7 @@ public class MessagingController implements Runnable { * Get the message list from the local store and create an index of * the uids within the list. */ - if (Config.LOGV) { + if (Email.DEBUG) { Log.v(Email.LOG_TAG, "SYNC: About to get local folder " + folder); } final LocalStore localStore = (LocalStore) Store.getInstance(account.getLocalStoreUri(), mApplication); @@ -615,12 +780,12 @@ public class MessagingController implements Runnable { localUidMap.put(message.getUid(), message); } - if (Config.LOGV) { + if (Email.DEBUG) { Log.v(Email.LOG_TAG, "SYNC: About to get remote store for " + folder); } Store remoteStore = Store.getInstance(account.getStoreUri(), mApplication); - if (Config.LOGV) { + if (Email.DEBUG) { Log.v(Email.LOG_TAG, "SYNC: About to get remote folder " + folder); } @@ -641,6 +806,9 @@ public class MessagingController implements Runnable { for (MessagingListener l : getListeners()) { l.synchronizeMailboxFinished(account, folder, 0, 0); } + if (listener != null && getListeners().contains(listener) == false) { + listener.synchronizeMailboxFinished(account, folder, 0, 0); + } Log.i(Email.LOG_TAG, "Done synchronizing folder " + folder); return; } @@ -669,7 +837,7 @@ public class MessagingController implements Runnable { /* * Open the remote folder. This pre-loads certain metadata like message count. */ - if (Config.LOGV) { + if (Email.DEBUG) { Log.v(Email.LOG_TAG, "SYNC: About to open remote folder " + folder); } remoteFolder.open(OpenMode.READ_WRITE); @@ -684,10 +852,10 @@ public class MessagingController implements Runnable { Message[] remoteMessageArray = new Message[0]; final ArrayList remoteMessages = new ArrayList(); - final ArrayList unsyncedMessages = new ArrayList(); + // final ArrayList unsyncedMessages = new ArrayList(); HashMap remoteUidMap = new HashMap(); - if (Config.LOGV) { + if (Email.DEBUG) { Log.v(Email.LOG_TAG, "SYNC: Remote message count for folder " + folder + " is " + remoteMessageCount); } @@ -699,7 +867,7 @@ public class MessagingController implements Runnable { int remoteStart = Math.max(0, remoteMessageCount - visibleLimit) + 1; int remoteEnd = remoteMessageCount; - if (Config.LOGV) { + if (Email.DEBUG) { Log.v(Email.LOG_TAG, "SYNC: About to get messages " + remoteStart + " through " + remoteEnd + " for folder " + folder); } @@ -708,7 +876,7 @@ public class MessagingController implements Runnable { remoteMessages.add(thisMess); remoteUidMap.put(thisMess.getUid(), thisMess); } - if (Config.LOGV) { + if (Email.DEBUG) { Log.v(Email.LOG_TAG, "SYNC: Got " + remoteUidMap.size() + " messages for folder " + folder); } remoteMessageArray = null; @@ -718,147 +886,23 @@ public class MessagingController implements Runnable { * local store, or messages that are in the local store but failed to download * on the last sync. These are the new messages that we will download. */ - Iterator iter = remoteMessages.iterator(); - while (iter.hasNext()) { - Message message = iter.next(); - Message localMessage = localUidMap.get(message.getUid()); - if (localMessage == null || - (!localMessage.isSet(Flag.DELETED) && - !localMessage.isSet(Flag.X_DOWNLOADED_FULL) && - !localMessage.isSet(Flag.X_DOWNLOADED_PARTIAL))) { - unsyncedMessages.add(message); - iter.remove(); - } - } +// Iterator iter = remoteMessages.iterator(); +// while (iter.hasNext()) { +// Message message = iter.next(); +// Message localMessage = localUidMap.get(message.getUid()); +// if (localMessage == null || +// (!localMessage.isSet(Flag.DELETED) && +// !localMessage.isSet(Flag.X_DOWNLOADED_FULL) && +// !localMessage.isSet(Flag.X_DOWNLOADED_PARTIAL))) { +// unsyncedMessages.add(message); +// iter.remove(); +// } +// } } else if (remoteMessageCount < 0) { throw new Exception("Message count " + remoteMessageCount + " for folder " + folder); } - /* - * A list of messages that were downloaded and which did not have the Seen flag set. - * This will serve to indicate the true "new" message count that will be reported to - * the user via notification. - */ - final ArrayList newMessageUidList = new ArrayList(); - - /* - * Fetch the flags and envelope only of the new messages. This is intended to get us - * critical data as fast as possible, and then we'll fill in the details. - */ - if (unsyncedMessages.size() > 0) { - - /* - * Reverse the order of the messages. Depending on the server this may get us - * fetch results for newest to oldest. If not, no harm done. - */ - Collections.reverse(unsyncedMessages); - - FetchProfile fp = new FetchProfile(); - if (remoteFolder.supportsFetchingFlags()) { - fp.add(FetchProfile.Item.FLAGS); - } - fp.add(FetchProfile.Item.ENVELOPE); - - if (Config.LOGV) { - Log.v(Email.LOG_TAG, "SYNC: About to sync unsynced messages for folder " + folder); - } - - remoteFolder.fetch(unsyncedMessages.toArray(new Message[0]), fp, - new MessageRetrievalListener() { - public void messageFinished(Message message, int number, int ofTotal) { - try { - if (!message.isSet(Flag.SEEN)) { - newMessageUidList.add(message.getUid()); - } - - // Store the new message locally - localFolder.appendMessages(new Message[] { - message - }); - - // And include it in the view - if (message.getSubject() != null && - message.getFrom() != null) { - /* - * We check to make sure that we got something worth - * showing (subject and from) because some protocols - * (POP) may not be able to give us headers for - * ENVELOPE, only size. - */ - if (isMessageSuppressed(account, folder, message) == false) { - Log.i(Email.LOG_TAG, "place 2 About to notify listeners that we got a new message "+ account + folder + message.getUid()); - for (MessagingListener l : getListeners()) { - l.synchronizeMailboxAddOrUpdateMessage(account, folder, localFolder.getMessage(message.getUid())); - } - } - } - - } - catch (Exception e) { - Log.e(Email.LOG_TAG, "Error while storing downloaded message.", e); - addErrorMessage(account, e); - - } - } - - public void messageStarted(String uid, int number, int ofTotal) { - } - }); - if (Config.LOGV) { - Log.v(Email.LOG_TAG, "SYNC: Synced unsynced messages for folder " + folder); - } - - } - - /* - * Refresh the flags for any messages in the local store that we didn't just - * download. - */ - if (remoteFolder.supportsFetchingFlags()) { - - if (Config.LOGV) { - Log.v(Email.LOG_TAG, "SYNC: About to sync remote messages for folder " + folder); - } - - FetchProfile fp = new FetchProfile(); - fp.add(FetchProfile.Item.FLAGS); - remoteFolder.fetch(remoteMessages.toArray(new Message[0]), fp, null); - for (Message remoteMessage : remoteMessages) { - boolean messageChanged = false; - Message localMessage = localFolder.getMessage(remoteMessage.getUid()); - if (localMessage == null || localMessage.isSet(Flag.DELETED)) { - continue; - } - for (Flag flag : new Flag[] { Flag.SEEN, Flag.FLAGGED, Flag.ANSWERED } ) { - if (remoteMessage.isSet(flag) != localMessage.isSet(flag)) { - localMessage.setFlag(flag, remoteMessage.isSet(flag)); - messageChanged = true; - } - } - if (messageChanged && isMessageSuppressed(account, folder, localMessage) == false) { - for (MessagingListener l : getListeners()) { - l.synchronizeMailboxAddOrUpdateMessage(account, folder, localMessage); - } - } - } - } - if (Config.LOGV) { - Log.v(Email.LOG_TAG, "SYNC: Synced remote messages for folder " + folder); - } - - - /* - * Get and store the unread message count. - */ - int remoteUnreadMessageCount = remoteFolder.getUnreadMessageCount(); - if (remoteUnreadMessageCount == -1) { - localFolder.setUnreadMessageCount(localFolder.getUnreadMessageCount() + newMessageUidList.size()); - } - else { - localFolder.setUnreadMessageCount(remoteUnreadMessageCount); - } - /* * Remove any messages that are in the local store but no longer on the remote store. */ @@ -869,178 +913,19 @@ public class MessagingController implements Runnable { for (MessagingListener l : getListeners()) { l.synchronizeMailboxRemovedMessage(account, folder, localMessage); } + if (listener != null && getListeners().contains(listener) == false) { + listener.synchronizeMailboxRemovedMessage(account, folder, localMessage); + } } } localMessages = null; - + /* * Now we download the actual content of messages. */ - ArrayList largeMessages = new ArrayList(); - ArrayList smallMessages = new ArrayList(); - for (Message message : unsyncedMessages) { - if (message.getSize() > (MAX_SMALL_MESSAGE_SIZE)) { - largeMessages.add(message); - } else { - smallMessages.add(message); - } - } - - if (Config.LOGV) { - Log.v(Email.LOG_TAG, "SYNC: Have " - + largeMessages.size() + " large messages and " - + smallMessages.size() + " small messages out of " - + unsyncedMessages.size() + " un synced messages " - + " to fetch for folder " + folder); - } - unsyncedMessages.clear(); - - - /* - * Grab the content of the small messages first. This is going to - * be very fast and at very worst will be a single up of a few bytes and a single - * download of 625k. - */ - FetchProfile fp = new FetchProfile(); - fp.add(FetchProfile.Item.BODY); - if (Config.LOGV) { - Log.v(Email.LOG_TAG, "SYNC: Fetching small messages for folder " + folder); - } - - remoteFolder.fetch(smallMessages.toArray(new Message[smallMessages.size()]), - fp, new MessageRetrievalListener() { - public void messageFinished(Message message, int number, int ofTotal) { - try { - // Store the updated message locally - localFolder.appendMessages(new Message[] { message }); - - Message localMessage = localFolder.getMessage(message.getUid()); - - // Set a flag indicating this message has now be fully downloaded - localMessage.setFlag(Flag.X_DOWNLOADED_FULL, true); - if (isMessageSuppressed(account, folder, localMessage) == false) - { - // Update the listener with what we've found - for (MessagingListener l : getListeners()) { - l.synchronizeMailboxAddOrUpdateMessage( account, folder, localMessage); - } - } - if (newMessageUidList.contains(message.getUid())) { - for (MessagingListener l : getListeners()) { - l.synchronizeMailboxNewMessage(account, folder, localMessage); - } - } - } - catch (MessagingException me) { - addErrorMessage(account, me); - - Log.e(Email.LOG_TAG, "SYNC: fetch small messages", me); - } - } - - public void messageStarted(String uid, int number, int ofTotal) { - } - }); - if (Config.LOGV) { - Log.v(Email.LOG_TAG, "SYNC: Done fetching small messages for folder " + folder); - } - smallMessages = null; - - /* - * Now do the large messages that require more round trips. - */ - fp.clear(); - fp.add(FetchProfile.Item.STRUCTURE); - if (Config.LOGV) { - Log.v(Email.LOG_TAG, "SYNC: Fetching large messages for folder " + folder); - } - - remoteFolder.fetch(largeMessages.toArray(new Message[largeMessages.size()]), fp, null); - for (Message message : largeMessages) { - Message localMessage = null; - if (message.getBody() == null) { - /* - * The provider was unable to get the structure of the message, so - * we'll download a reasonable portion of the messge and mark it as - * incomplete so the entire thing can be downloaded later if the user - * wishes to download it. - */ - fp.clear(); - fp.add(FetchProfile.Item.BODY_SANE); - /* - * TODO a good optimization here would be to make sure that all Stores set - * the proper size after this fetch and compare the before and after size. If - * they equal we can mark this SYNCHRONIZED instead of PARTIALLY_SYNCHRONIZED - */ - - remoteFolder.fetch(new Message[] { message }, fp, null); - // Store the updated message locally - localFolder.appendMessages(new Message[] { message }); - - localMessage = localFolder.getMessage(message.getUid()); - - /* - * Mark the message as fully downloaded if the message size is smaller than - * the FETCH_BODY_SANE_SUGGESTED_SIZE, otherwise mark as only a partial - * download. This will prevent the system from downloading the same message - * twice. - */ - if (message.getSize() < Store.FETCH_BODY_SANE_SUGGESTED_SIZE) { - localMessage.setFlag(Flag.X_DOWNLOADED_FULL, true); - } else { - // Set a flag indicating that the message has been partially downloaded and - // is ready for view. - localMessage.setFlag(Flag.X_DOWNLOADED_PARTIAL, true); - } - } else { - /* - * We have a structure to deal with, from which - * we can pull down the parts we want to actually store. - * Build a list of parts we are interested in. Text parts will be downloaded - * right now, attachments will be left for later. - */ - - ArrayList viewables = new ArrayList(); - ArrayList attachments = new ArrayList(); - MimeUtility.collectParts(message, viewables, attachments); - - /* - * Now download the parts we're interested in storing. - */ - for (Part part : viewables) { - fp.clear(); - fp.add(part); - // TODO what happens if the network connection dies? We've got partial - // messages with incorrect status stored. - remoteFolder.fetch(new Message[] { message }, fp, null); - } - // Store the updated message locally - localFolder.appendMessages(new Message[] { message }); - - localMessage = localFolder.getMessage(message.getUid()); - - // Set a flag indicating this message has been fully downloaded and can be - // viewed. - localMessage.setFlag(Flag.X_DOWNLOADED_PARTIAL, true); - } - - if (isMessageSuppressed(account, folder, message) == false) { - Log.i(Email.LOG_TAG, "About to notify listeners that we got a new message "+ account + folder + message.getUid()); - // Update the listener with what we've found - for (MessagingListener l : getListeners()) { - l.synchronizeMailboxAddOrUpdateMessage( account, folder, localMessage); - } - } - if (newMessageUidList.contains(message.getUid())) { - for (MessagingListener l : getListeners()) { - l.synchronizeMailboxNewMessage(account, folder, localMessage); - } - } - }//for large messsages - if (Config.LOGV) { - Log.v(Email.LOG_TAG, "SYNC: Done fetching large messages for folder " + folder); - } - largeMessages = null; + int newMessages = downloadMessages(account, remoteFolder, localFolder, remoteMessages); + + setLocalUnreadCountToRemote(localFolder, remoteFolder, newMessages); /* @@ -1052,22 +937,31 @@ public class MessagingController implements Runnable { remoteFolder.close(false); localFolder.close(false); - if (Config.LOGD) { + if (Email.DEBUG) { log( "Done synchronizing folder " + account.getDescription() + ":" + folder + " @ " + new Date() + - " with " + newMessageUidList.size() + " new messages"); + " with " + newMessages + " new messages"); } for (MessagingListener l : getListeners()) { - l.synchronizeMailboxFinished( account, folder, remoteMessageCount, newMessageUidList.size()); + l.synchronizeMailboxFinished( account, folder, remoteMessageCount, newMessages); } + if (listener != null && getListeners().contains(listener) == false) { + listener.synchronizeMailboxFinished( account, folder, remoteMessageCount, newMessages); + } + if (commandException != null) { String rootMessage = getRootCauseMessage(commandException); + Log.e(Email.LOG_TAG, "Root cause failure in " + account.getDescription() + ":" + + tLocalFolder.getName() + " was '" + rootMessage + "'"); localFolder.setStatus(rootMessage); for (MessagingListener l : getListeners()) { l.synchronizeMailboxFailed( account, folder, rootMessage); } + if (listener != null && getListeners().contains(listener) == false) { + listener.synchronizeMailboxFailed( account, folder, rootMessage); + } } @@ -1098,6 +992,12 @@ public class MessagingController implements Runnable { folder, rootMessage); } + if (listener != null && getListeners().contains(listener) == false) { + listener.synchronizeMailboxFailed( + account, + folder, + rootMessage); + } addErrorMessage(account, e); log("Failed synchronizing folder " + account.getDescription() + ":" + folder + " @ " + new Date()); @@ -1106,6 +1006,390 @@ public class MessagingController implements Runnable { } + private void setLocalUnreadCountToRemote(LocalFolder localFolder, Folder remoteFolder, int newMessageCount) throws MessagingException + { + int remoteUnreadMessageCount = remoteFolder.getUnreadMessageCount(); + if (remoteUnreadMessageCount == -1) { + localFolder.setUnreadMessageCount(localFolder.getUnreadMessageCount() + newMessageCount); + } + else { + localFolder.setUnreadMessageCount(remoteUnreadMessageCount); + } + } + + private int downloadMessages(final Account account, final Folder remoteFolder, + final LocalFolder localFolder, List inputMessages) throws MessagingException + { + final String folder = remoteFolder.getName(); + + ArrayList syncFlagMessages = new ArrayList(); + ArrayList unsyncedMessages = new ArrayList(); + final AtomicInteger newMessages = new AtomicInteger(0); + + int visibleLimit = localFolder.getVisibleLimit(); + int listSize = inputMessages.size(); + + if (listSize > visibleLimit) + { + inputMessages = inputMessages.subList(listSize - visibleLimit, listSize); + } + + List messages = new ArrayList(inputMessages); + + for (Message message : messages) + { + if (isMessageSuppressed(account, folder, message) == false) + { + Message localMessage = localFolder.getMessage(message.getUid()); + + if (localMessage == null) + { + if (!message.isSet(Flag.X_DOWNLOADED_FULL) && !message.isSet(Flag.X_DOWNLOADED_PARTIAL)) + { + if (Email.DEBUG) + { + Log.v(Email.LOG_TAG, "Message with uid " + message.getUid() + " is not downloaded at all"); + } + + unsyncedMessages.add(message); + } + else + { + if (Email.DEBUG) + { + Log.v(Email.LOG_TAG, "Message with uid " + message.getUid() + " is partially or fully downloaded"); + } + // Store the updated message locally + localFolder.appendMessages(new Message[] { message }); + + localMessage = localFolder.getMessage(message.getUid()); + + localMessage.setFlag(Flag.X_DOWNLOADED_FULL, message.isSet(Flag.X_DOWNLOADED_FULL)); + localMessage.setFlag(Flag.X_DOWNLOADED_PARTIAL, message.isSet(Flag.X_DOWNLOADED_PARTIAL)); + + for (MessagingListener l : getListeners()) { + l.synchronizeMailboxAddOrUpdateMessage( account, folder, localMessage); + l.synchronizeMailboxNewMessage( account, folder, localMessage); + } + } + } + else + { + if (Email.DEBUG) + { + Log.v(Email.LOG_TAG, "Message with uid " + message.getUid() + " is already downloaded"); + } + String newPushState = remoteFolder.getNewPushState(localFolder.getPushState(), message); + if (newPushState != null) + { + localFolder.setPushState(newPushState); + } + syncFlagMessages.add(message); + } + } + } + + Log.i(Email.LOG_TAG, "SYNC: Have " + unsyncedMessages.size() + " unsynced messages"); + + messages.clear(); + final ArrayList largeMessages = new ArrayList(); + final ArrayList smallMessages = new ArrayList(); + if (unsyncedMessages.size() > 0) { + + /* + * Reverse the order of the messages. Depending on the server this may get us + * fetch results for newest to oldest. If not, no harm done. + */ + Collections.reverse(unsyncedMessages); + + FetchProfile fp = new FetchProfile(); + if (remoteFolder.supportsFetchingFlags()) { + fp.add(FetchProfile.Item.FLAGS); + } + fp.add(FetchProfile.Item.ENVELOPE); + + if (Email.DEBUG) + { + Log.d(Email.LOG_TAG, "SYNC: About to sync " + unsyncedMessages.size() + " unsynced messages for folder " + folder); + } + + remoteFolder.fetch(unsyncedMessages.toArray(new Message[0]), fp, + new MessageRetrievalListener() { + public void messageFinished(Message message, int number, int ofTotal) { + try { + if (!message.isSet(Flag.SEEN)) { + newMessages.incrementAndGet(); + } + + // Store the new message locally + localFolder.appendMessages(new Message[] { + message + }); + + if (message.getSize() > (MAX_SMALL_MESSAGE_SIZE)) { + largeMessages.add(message); + } + else + { + smallMessages.add(message); + } + String newPushState = remoteFolder.getNewPushState(localFolder.getPushState(), message); + if (newPushState != null) + { + localFolder.setPushState(newPushState); + } + // And include it in the view + if (message.getSubject() != null && + message.getFrom() != null) { + /* + * We check to make sure that we got something worth + * showing (subject and from) because some protocols + * (POP) may not be able to give us headers for + * ENVELOPE, only size. + */ + if (isMessageSuppressed(account, folder, message) == false) { + Message localMessage = localFolder.getMessage(message.getUid()); + syncFlags(localMessage, message); + if (Email.DEBUG) + { + Log.v(Email.LOG_TAG, "About to notify listeners that we got a new unsynced message " + + account + ":" + folder + ":" + message.getUid()); + } + for (MessagingListener l : getListeners()) { + l.synchronizeMailboxAddOrUpdateMessage(account, folder, localMessage); + } + } + } + + } + catch (Exception e) { + Log.e(Email.LOG_TAG, "Error while storing downloaded message.", e); + addErrorMessage(account, e); + + } + } + + public void messageStarted(String uid, int number, int ofTotal) { + } + + public void messagesFinished(int total) {} + }); + // If a message didn't exist, messageFinished won't be called, but we shouldn't try again + // If we got here, nothing failed + for (Message message : unsyncedMessages) + { + String newPushState = remoteFolder.getNewPushState(localFolder.getPushState(), message); + if (newPushState != null) + { + localFolder.setPushState(newPushState); + } + } + Log.i(Email.LOG_TAG, "SYNC: Synced unsynced messages for folder " + folder); + + + } + + + Log.i(Email.LOG_TAG, "SYNC: Have " + + largeMessages.size() + " large messages and " + + smallMessages.size() + " small messages out of " + + unsyncedMessages.size() + " unsynced messages"); + + unsyncedMessages.clear(); + + /* + * Grab the content of the small messages first. This is going to + * be very fast and at very worst will be a single up of a few bytes and a single + * download of 625k. + */ + FetchProfile fp = new FetchProfile(); + fp.add(FetchProfile.Item.BODY); + // fp.add(FetchProfile.Item.FLAGS); + // fp.add(FetchProfile.Item.ENVELOPE); + + Log.i(Email.LOG_TAG, "SYNC: Fetching small messages for folder " + folder); + + remoteFolder.fetch(smallMessages.toArray(new Message[smallMessages.size()]), + fp, new MessageRetrievalListener() { + public void messageFinished(Message message, int number, int ofTotal) { + try { + // Store the updated message locally + localFolder.appendMessages(new Message[] { message }); + + Message localMessage = localFolder.getMessage(message.getUid()); + + // Set a flag indicating this message has now be fully downloaded + localMessage.setFlag(Flag.X_DOWNLOADED_FULL, true); + if (Email.DEBUG) + { + Log.v(Email.LOG_TAG, "About to notify listeners that we got a new small message " + + account + ":" + folder + ":" + message.getUid()); + } + // Update the listener with what we've found + for (MessagingListener l : getListeners()) { + l.synchronizeMailboxAddOrUpdateMessage( account, folder, localMessage); + l.synchronizeMailboxNewMessage(account, folder, localMessage); + } + + } + catch (MessagingException me) { + addErrorMessage(account, me); + + Log.e(Email.LOG_TAG, "SYNC: fetch small messages", me); + } + } + + public void messageStarted(String uid, int number, int ofTotal) { + } + + public void messagesFinished(int total) {} + }); + + Log.i(Email.LOG_TAG, "SYNC: Done fetching small messages for folder " + folder); + smallMessages.clear(); + + /* + * Now do the large messages that require more round trips. + */ + fp.clear(); + fp.add(FetchProfile.Item.STRUCTURE); + + Log.i(Email.LOG_TAG, "SYNC: Fetching large messages for folder " + folder); + + remoteFolder.fetch(largeMessages.toArray(new Message[largeMessages.size()]), fp, null); + for (Message message : largeMessages) { + if (message.getBody() == null) { + /* + * The provider was unable to get the structure of the message, so + * we'll download a reasonable portion of the messge and mark it as + * incomplete so the entire thing can be downloaded later if the user + * wishes to download it. + */ + fp.clear(); + fp.add(FetchProfile.Item.BODY_SANE); + /* + * TODO a good optimization here would be to make sure that all Stores set + * the proper size after this fetch and compare the before and after size. If + * they equal we can mark this SYNCHRONIZED instead of PARTIALLY_SYNCHRONIZED + */ + + remoteFolder.fetch(new Message[] { message }, fp, null); + // Store the updated message locally + localFolder.appendMessages(new Message[] { message }); + + Message localMessage = localFolder.getMessage(message.getUid()); + + /* + * Mark the message as fully downloaded if the message size is smaller than + * the FETCH_BODY_SANE_SUGGESTED_SIZE, otherwise mark as only a partial + * download. This will prevent the system from downloading the same message + * twice. + */ + if (message.getSize() < Store.FETCH_BODY_SANE_SUGGESTED_SIZE) { + localMessage.setFlag(Flag.X_DOWNLOADED_FULL, true); + } else { + // Set a flag indicating that the message has been partially downloaded and + // is ready for view. + localMessage.setFlag(Flag.X_DOWNLOADED_PARTIAL, true); + } + } else { + /* + * We have a structure to deal with, from which + * we can pull down the parts we want to actually store. + * Build a list of parts we are interested in. Text parts will be downloaded + * right now, attachments will be left for later. + */ + + ArrayList viewables = new ArrayList(); + ArrayList attachments = new ArrayList(); + MimeUtility.collectParts(message, viewables, attachments); + + /* + * Now download the parts we're interested in storing. + */ + for (Part part : viewables) { + fp.clear(); + fp.add(part); + // TODO what happens if the network connection dies? We've got partial + // messages with incorrect status stored. + remoteFolder.fetch(new Message[] { message }, fp, null); + } + // Store the updated message locally + localFolder.appendMessages(new Message[] { message }); + + Message localMessage = localFolder.getMessage(message.getUid()); + + // Set a flag indicating this message has been fully downloaded and can be + // viewed. + localMessage.setFlag(Flag.X_DOWNLOADED_PARTIAL, true); + } + if (Email.DEBUG) + { + Log.v(Email.LOG_TAG, "About to notify listeners that we got a new large message " + + account + ":" + folder + ":" + message.getUid()); + } + // Update the listener with what we've found + for (MessagingListener l : getListeners()) { + Message localMessage = localFolder.getMessage(message.getUid()); + l.synchronizeMailboxAddOrUpdateMessage( account, folder, localMessage); + l.synchronizeMailboxNewMessage(account, folder, localMessage); + + } + }//for large messsages + Log.i(Email.LOG_TAG, "SYNC: Done fetching large messages for folder " + folder); + + largeMessages.clear(); + + /* + * Refresh the flags for any messages in the local store that we didn't just + * download. + */ + if (remoteFolder.supportsFetchingFlags()) { + + + Log.i(Email.LOG_TAG, "SYNC: About to sync flags for " + + syncFlagMessages.size() + " remote messages for folder " + folder); + + + fp.clear(); + fp.add(FetchProfile.Item.FLAGS); + remoteFolder.fetch(syncFlagMessages.toArray(new Message[0]), fp, null); + for (Message remoteMessage : syncFlagMessages) { + Message localMessage = localFolder.getMessage(remoteMessage.getUid()); + boolean messageChanged = syncFlags(localMessage, remoteMessage); + + if (messageChanged && isMessageSuppressed(account, folder, localMessage) == false) { + for (MessagingListener l : getListeners()) { + l.synchronizeMailboxAddOrUpdateMessage(account, folder, localMessage); + } + } + } + } + Log.i(Email.LOG_TAG, "SYNC: Synced remote messages for folder " + folder + ", " + newMessages.get() + " new messages"); + + return newMessages.get(); + } + + + private boolean syncFlags(Message localMessage, Message remoteMessage) throws MessagingException + { + boolean messageChanged = false; + if (localMessage == null || localMessage.isSet(Flag.DELETED)) { + return false; + } + if (remoteMessage.isSet(Flag.DELETED)) + { + localMessage.setFlag(Flag.DELETED, true); + messageChanged = true; + } + for (Flag flag : new Flag[] { Flag.SEEN, Flag.FLAGGED, Flag.ANSWERED } ) { + if (remoteMessage.isSet(flag) != localMessage.isSet(flag)) { + localMessage.setFlag(flag, remoteMessage.isSet(flag)); + messageChanged = true; + } + } + return messageChanged; + } private String getRootCauseMessage(Throwable t) { Throwable rootCause = t; @@ -1142,9 +1426,8 @@ public class MessagingController implements Runnable { processPendingCommandsSynchronous(account); } catch (MessagingException me) { - if (Config.LOGV) { - Log.v(Email.LOG_TAG, "processPendingCommands", me); - } + Log.e(Email.LOG_TAG, "processPendingCommands", me); + addErrorMessage(account, me); /* @@ -1166,7 +1449,10 @@ public class MessagingController implements Runnable { { for (PendingCommand command : commands) { processingCommand = command; - Log.d(Email.LOG_TAG, "Processing pending command '" + command + "'"); + if (Email.DEBUG) + { + Log.d(Email.LOG_TAG, "Processing pending command '" + command + "'"); + } /* * We specifically do not catch any exceptions here. If a command fails it is * most likely due to a server or IO error and it must be retried before any @@ -1190,7 +1476,10 @@ public class MessagingController implements Runnable { processPendingEmptyTrash(command, account); } localStore.removePendingCommand(command); - Log.d(Email.LOG_TAG, "Done processing pending command '" + command + "'"); + if (Email.DEBUG) + { + Log.d(Email.LOG_TAG, "Done processing pending command '" + command + "'"); + } } catch (MessagingException me) { @@ -1380,13 +1669,11 @@ public class MessagingController implements Runnable { Folder remoteDestFolder = remoteStore.getFolder(destFolder); if (!remoteSrcFolder.exists()) { - Log.w(Email.LOG_TAG, "processingPendingMoveOrCopy: remoteFolder " + srcFolder + " does not exist"); - return; + throw new MessagingException("processingPendingMoveOrCopy: remoteFolder " + srcFolder + " does not exist", true); } remoteSrcFolder.open(OpenMode.READ_WRITE); if (remoteSrcFolder.getMode() != OpenMode.READ_WRITE) { - Log.w(Email.LOG_TAG, "processingPendingMoveOrCopy: could not open remoteSrcFolder " + srcFolder + " read/write"); - return; + throw new MessagingException("processingPendingMoveOrCopy: could not open remoteSrcFolder " + srcFolder + " read/write", true); } Message remoteMessage = null; @@ -1394,18 +1681,20 @@ public class MessagingController implements Runnable { remoteMessage = remoteSrcFolder.getMessage(uid); } if (remoteMessage == null) { - Log.w(Email.LOG_TAG, "processingPendingMoveOrCopy: remoteMessage " + uid + " does not exist"); - return; + throw new MessagingException("processingPendingMoveOrCopy: remoteMessage " + uid + " does not exist", true); } - if (Config.LOGD) + if (Email.DEBUG) { Log.d(Email.LOG_TAG, "processingPendingMoveOrCopy: source folder = " + srcFolder + ", uid = " + uid + ", destination folder = " + destFolder + ", isCopy = " + isCopy); } if (isCopy == false && destFolder.equals(account.getTrashFolderName())) { - Log.d(Email.LOG_TAG, "processingPendingMoveOrCopy doing special case for deleting message"); + if (Email.DEBUG) + { + Log.d(Email.LOG_TAG, "processingPendingMoveOrCopy doing special case for deleting message"); + } remoteMessage.delete(account.getTrashFolderName()); remoteSrcFolder.close(true); return; @@ -1413,8 +1702,7 @@ public class MessagingController implements Runnable { remoteDestFolder.open(OpenMode.READ_WRITE); if (remoteDestFolder.getMode() != OpenMode.READ_WRITE) { - Log.w(Email.LOG_TAG, "processingPendingMoveOrCopy: could not open remoteDestFolder " + srcFolder + " read/write"); - return; + throw new MessagingException("processingPendingMoveOrCopy: could not open remoteDestFolder " + srcFolder + " read/write", true); } if (isCopy) { @@ -1534,7 +1822,6 @@ public class MessagingController implements Runnable { } String rootCauseMessage = getRootCauseMessage(t); - Log.e(Email.LOG_TAG, rootCauseMessage, t); log("Error" + "'" + rootCauseMessage + "'"); Store localStore = Store.getInstance(account.getLocalStoreUri(), mApplication); @@ -1560,6 +1847,10 @@ public class MessagingController implements Runnable { localFolder.deleteMessagesOlderThan(nowTime - (15 * 60 * 1000)); + for (MessagingListener l : getListeners()) { + l.folderStatusChanged(account, localFolder.getName()); + } + } catch (Throwable it) { @@ -1624,7 +1915,8 @@ public class MessagingController implements Runnable { public void markAllMessagesRead(final Account account, final String folder) { - Log.v(Email.LOG_TAG, "Marking all messages in " + account.getDescription() + ":" + folder + " as read"); + + Log.i(Email.LOG_TAG, "Marking all messages in " + account.getDescription() + ":" + folder + " as read"); List args = new ArrayList(); args.add(folder); PendingCommand command = new PendingCommand(); @@ -1805,7 +2097,7 @@ public class MessagingController implements Runnable { if (!message.isSet(Flag.SEEN)) { markMessageRead(account, localFolder, message, true); } - + if (listener != null && !getListeners().contains(listener)) { listener.loadMessageForViewBodyAvailable(account, folder, uid, message); @@ -1866,16 +2158,13 @@ public class MessagingController implements Runnable { } threadPool.execute(new Runnable() { public void run() { + try { Store localStore = Store.getInstance(account.getLocalStoreUri(), mApplication); LocalFolder localFolder = (LocalFolder) localStore.getFolder(folder); localFolder.open(OpenMode.READ_WRITE); - - LocalMessage message = (LocalMessage)localFolder.getMessage(uid); - if (message==null - || message.getId()==0) { - throw new IllegalArgumentException("Message not found: folder=" + folder + ", uid=" + uid); - } + + Message message = localFolder.getMessage(uid); for (MessagingListener l : getListeners()) { l.loadMessageForViewHeadersAvailable(account, folder, uid, message); @@ -1886,12 +2175,11 @@ public class MessagingController implements Runnable { } if (!message.isSet(Flag.X_DOWNLOADED_FULL)) { - Log.v(Email.LOG_TAG, "Message not fully downloaded --> starting bg remote download"); loadMessageForViewRemote(account, folder, uid, listener); localFolder.close(false); return; } - + FetchProfile fp = new FetchProfile(); fp.add(FetchProfile.Item.ENVELOPE); fp.add(FetchProfile.Item.BODY); @@ -1902,23 +2190,23 @@ public class MessagingController implements Runnable { if (!message.isSet(Flag.SEEN)) { markMessageRead(account, localFolder, message, true); } - + for (MessagingListener l : getListeners()) { l.loadMessageForViewBodyAvailable(account, folder, uid, message); } - if (listener != null && !getListeners().contains(listener)) + if (listener != null && !getListeners().contains(listener)) { - listener.loadMessageForViewBodyAvailable(account, folder, uid, message); + listener.loadMessageForViewBodyAvailable(account, folder, uid, message); } - + for (MessagingListener l : getListeners()) { l.loadMessageForViewFinished(account, folder, uid, message); } - if (listener != null && !getListeners().contains(listener)) + if (listener != null && !getListeners().contains(listener)) { - listener.loadMessageForViewFinished(account, folder, uid, message); + listener.loadMessageForViewFinished(account, folder, uid, message); } - + } catch (Exception e) { for (MessagingListener l : getListeners()) { @@ -1929,11 +2217,10 @@ public class MessagingController implements Runnable { listener.loadMessageForViewFailed(account, folder, uid, e); } addErrorMessage(account, e); - + } } }); - Log.v(Email.LOG_TAG, "loadMessageForView started in bg thread"); } // public void loadMessageForViewSynchronous(final Account account, final String folder, final String uid, @@ -2113,10 +2400,10 @@ public class MessagingController implements Runnable { } if (listener != null) { listener.loadAttachmentFinished(account, message, part, tag); - } + } } catch (MessagingException me) { - if (Config.LOGV) { + if (Email.DEBUG) { Log.v(Email.LOG_TAG, "", me); } for (MessagingListener l : getListeners()) { @@ -2171,7 +2458,7 @@ public class MessagingController implements Runnable { */ public void sendPendingMessages(final Account account, MessagingListener listener) { - put("sendPendingMessages", listener, new Runnable() { + putBackground("sendPendingMessages", listener, new Runnable() { public void run() { sendPendingMessagesSynchronous(account); } @@ -2184,11 +2471,12 @@ public class MessagingController implements Runnable { * @param listener */ public void sendPendingMessagesSynchronous(final Account account) { + Folder localFolder = null; try { Store localStore = Store.getInstance( account.getLocalStoreUri(), mApplication); - Folder localFolder = localStore.getFolder( + localFolder = localStore.getFolder( account.getOutboxFolderName()); if (!localFolder.exists()) { return; @@ -2341,6 +2629,20 @@ public class MessagingController implements Runnable { addErrorMessage(account, e); } + finally + { + if (localFolder != null) + { + try + { + localFolder.close(false); + } + catch (Exception e) + { + Log.e(Email.LOG_TAG, "Exception while closing folder", e); + } + } + } } public void getAccountUnreadCount(final Context context, final Account account, @@ -2369,6 +2671,7 @@ public class MessagingController implements Runnable { final MessagingListener listener) { if (!message.getUid().startsWith(Email.LOCAL_UID_PREFIX)) { + suppressMessage(account, srcFolder, message); put("moveMessage", null, new Runnable() { public void run() { moveOrCopyMessageSynchronous(account, srcFolder, message, destFolder, false, listener); @@ -2458,11 +2761,9 @@ public class MessagingController implements Runnable { String origUid = message.getUid(); if (lMessage != null) { - if (Config.LOGD) - { - Log.d(Email.LOG_TAG, "moveOrCopyMessageSynchronous: source folder = " + srcFolder + Log.i(Email.LOG_TAG, "moveOrCopyMessageSynchronous: source folder = " + srcFolder + ", uid = " + origUid + ", destination folder = " + destFolder + ", isCopy = " + isCopy); - } + if (isCopy) { FetchProfile fp = new FetchProfile(); fp.add(FetchProfile.Item.ENVELOPE); @@ -2514,7 +2815,7 @@ public class MessagingController implements Runnable { { if (folder.equals(account.getTrashFolderName())) { - if (Config.LOGD) + if (Email.DEBUG) { Log.d(Email.LOG_TAG, "Deleting message in trash folder, not copying"); } @@ -2529,7 +2830,7 @@ public class MessagingController implements Runnable { } if (localTrashFolder.exists() == true) { - if (Config.LOGD) + if (Email.DEBUG) { Log.d(Email.LOG_TAG, "Deleting message in normal folder, moving"); } @@ -2548,7 +2849,7 @@ public class MessagingController implements Runnable { l.folderStatusChanged(account, account.getTrashFolderName()); } - if (Config.LOGD) + if (Email.DEBUG) { Log.d(Email.LOG_TAG, "Delete policy for account " + account.getDescription() + " is " + account.getDeletePolicy()); } @@ -2590,7 +2891,7 @@ public class MessagingController implements Runnable { } else { - if (Config.LOGD) + if (Email.DEBUG) { Log.d(Email.LOG_TAG, "Delete policy " + account.getDeletePolicy() + " prevents delete from server"); } @@ -2646,7 +2947,7 @@ public class MessagingController implements Runnable { public void sendAlternate(final Context context, Account account, Message message) { - if (Config.LOGD) + if (Email.DEBUG) { Log.d(Email.LOG_TAG, "About to load message " + account.getDescription() + ":" + message.getFolder().getName() + ":" + message.getUid() + " for sendAlternate"); @@ -2658,7 +2959,7 @@ public class MessagingController implements Runnable { public void loadMessageForViewBodyAvailable(Account account, String folder, String uid, Message message) { - if (Config.LOGD) + if (Email.DEBUG) { Log.d(Email.LOG_TAG, "Got message " + account.getDescription() + ":" + folder + ":" + message.getUid() + " for sendAlternate"); @@ -2718,7 +3019,7 @@ public class MessagingController implements Runnable { for (MessagingListener l : getListeners()) { l.checkMailStarted(context, account); } - put("checkMail", listener, new Runnable() { + putBackground("checkMail", listener, new Runnable() { public void run() { final NotificationManager notifMgr = (NotificationManager)context @@ -2741,18 +3042,14 @@ public class MessagingController implements Runnable { final long accountInterval = account.getAutomaticCheckIntervalMinutes() * 60 * 1000; if (ignoreLastCheckedTime == false && accountInterval <= 0) { - if (Config.LOGV || true) - { - Log.v(Email.LOG_TAG, "Skipping synchronizing account " + account.getDescription()); - } + Log.i(Email.LOG_TAG, "Skipping synchronizing account " + account.getDescription()); + continue; } - if (Config.LOGV || true) - { - Log.v(Email.LOG_TAG, "Synchronizing account " + account.getDescription()); - } + Log.i(Email.LOG_TAG, "Synchronizing account " + account.getDescription()); + putBackground("sendPending " + account.getDescription(), null, new Runnable() { public void run() { if (account.isShowOngoing()) { @@ -2795,49 +3092,37 @@ public class MessagingController implements Runnable { for (final Folder folder : localStore.getPersonalNamespaces()) { - folder.open(Folder.OpenMode.READ_WRITE); - folder.refresh(prefs); - - Folder.FolderClass fDisplayMode = folder.getDisplayClass(); - Folder.FolderClass fSyncMode = folder.getSyncClass(); + folder.open(Folder.OpenMode.READ_WRITE); + folder.refresh(prefs); - if ((aDisplayMode == Account.FolderMode.FIRST_CLASS && - fDisplayMode != Folder.FolderClass.FIRST_CLASS) - || (aDisplayMode == Account.FolderMode.FIRST_AND_SECOND_CLASS && - fDisplayMode != Folder.FolderClass.FIRST_CLASS && - fDisplayMode != Folder.FolderClass.SECOND_CLASS) - || (aDisplayMode == Account.FolderMode.NOT_SECOND_CLASS && - fDisplayMode == Folder.FolderClass.SECOND_CLASS)) - { - // Never sync a folder that isn't displayed - if (Config.LOGV) { - Log.v(Email.LOG_TAG, "Not syncing folder " + folder.getName() + - " which is in display mode " + fDisplayMode + " while account is in display mode " + aDisplayMode); - } + Folder.FolderClass fDisplayClass = folder.getDisplayClass(); + Folder.FolderClass fSyncClass = folder.getSyncClass(); - continue; - } + if (modeMismatch(aDisplayMode, fDisplayClass)) + { + // Never sync a folder that isn't displayed + if (Email.DEBUG) { + Log.v(Email.LOG_TAG, "Not syncing folder " + folder.getName() + + " which is in display mode " + fDisplayClass + " while account is in display mode " + aDisplayMode); + } - if ((aSyncMode == Account.FolderMode.FIRST_CLASS && - fSyncMode != Folder.FolderClass.FIRST_CLASS) - || (aSyncMode == Account.FolderMode.FIRST_AND_SECOND_CLASS && - fSyncMode != Folder.FolderClass.FIRST_CLASS && - fSyncMode != Folder.FolderClass.SECOND_CLASS) - || (aSyncMode == Account.FolderMode.NOT_SECOND_CLASS && - fSyncMode == Folder.FolderClass.SECOND_CLASS)) - { - // Do not sync folders in the wrong class - if (Config.LOGV) { - Log.v(Email.LOG_TAG, "Not syncing folder " + folder.getName() + - " which is in sync mode " + fSyncMode + " while account is in sync mode " + aSyncMode); - } + continue; + } + + if (modeMismatch(aSyncMode, fSyncClass)) + { + // Do not sync folders in the wrong class + if (Email.DEBUG) { + Log.v(Email.LOG_TAG, "Not syncing folder " + folder.getName() + + " which is in sync mode " + fSyncClass + " while account is in sync mode " + aSyncMode); + } + + continue; + } - continue; - } - - if (Config.LOGV) { + if (Email.DEBUG) { Log.v(Email.LOG_TAG, "Folder " + folder.getName() + " was last synced @ " + new Date(folder.getLastChecked())); } @@ -2845,7 +3130,7 @@ public class MessagingController implements Runnable { if (ignoreLastCheckedTime == false && folder.getLastChecked() > (System.currentTimeMillis() - accountInterval)) { - if (Config.LOGV) { + if (Email.DEBUG) { Log.v(Email.LOG_TAG, "Not syncing folder " + folder.getName() + ", previously synced @ " + new Date(folder.getLastChecked()) + " which would be too recent for the account period"); @@ -2866,7 +3151,7 @@ public class MessagingController implements Runnable { if (ignoreLastCheckedTime == false && tLocalFolder.getLastChecked() > (System.currentTimeMillis() - accountInterval)) { - if (Config.LOGV) { + if (Email.DEBUG) { Log.v(Email.LOG_TAG, "Not running Command for folder " + folder.getName() + ", previously synced @ " + new Date(folder.getLastChecked()) + " which would be too recent for the account period"); @@ -2894,7 +3179,7 @@ public class MessagingController implements Runnable { } try { - synchronizeMailboxSynchronous(account, folder.getName()); + synchronizeMailboxSynchronous(account, folder.getName(), listener); } finally { @@ -2963,7 +3248,6 @@ public class MessagingController implements Runnable { } for (MessagingListener l : getListeners()) { l.accountSizeChanged(account, oldSize, newSize); - l.accountReset(account); } } catch (Exception e) @@ -2993,7 +3277,6 @@ public class MessagingController implements Runnable { } for (MessagingListener l : getListeners()) { l.accountSizeChanged(account, oldSize, newSize); - l.accountReset(account); } } catch (Exception e) @@ -3003,6 +3286,40 @@ public class MessagingController implements Runnable { } }); } + + public void notifyAccount(Context context, Account thisAccount, int unreadMessageCount) + { + String notice = context.getString(R.string.notification_new_one_account_fmt, unreadMessageCount, + thisAccount.getDescription()); + Notification notif = new Notification(R.drawable.stat_notify_email_generic, + context.getString(R.string.notification_new_title), System.currentTimeMillis()); + + notif.number = unreadMessageCount; + + Intent i = FolderList.actionHandleAccountIntent(context, thisAccount); + + PendingIntent pi = PendingIntent.getActivity(context, 0, i, 0); + + notif.setLatestEventInfo(context, context.getString(R.string.notification_new_title), notice, pi); + + String ringtone = thisAccount.getRingtone(); + notif.sound = TextUtils.isEmpty(ringtone) ? null : Uri.parse(ringtone); + + if (thisAccount.isVibrate()) { + notif.defaults |= Notification.DEFAULT_VIBRATE; + } + + notif.flags |= Notification.FLAG_SHOW_LIGHTS; + notif.ledARGB = Email.NOTIFICATION_LED_COLOR; + notif.ledOnMS = Email.NOTIFICATION_LED_ON_TIME; + notif.ledOffMS = Email.NOTIFICATION_LED_OFF_TIME; + + NotificationManager notifMgr = + (NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE); + notifMgr.notify(thisAccount.getAccountNumber(), notif); + } + + public void saveDraft(final Account account, final Message message) { try { Store localStore = Store.getInstance(account.getLocalStoreUri(), mApplication); @@ -3028,13 +3345,58 @@ public class MessagingController implements Runnable { addErrorMessage(account, e); } } + + private boolean modeMismatch(Account.FolderMode aMode, Folder.FolderClass fMode) + { + if (aMode == Account.FolderMode.NONE + || (aMode == Account.FolderMode.FIRST_CLASS && + fMode != Folder.FolderClass.FIRST_CLASS) + || (aMode == Account.FolderMode.FIRST_AND_SECOND_CLASS && + fMode != Folder.FolderClass.FIRST_CLASS && + fMode != Folder.FolderClass.SECOND_CLASS) + || (aMode == Account.FolderMode.NOT_SECOND_CLASS && + fMode == Folder.FolderClass.SECOND_CLASS)) + { + return true; + } + else + { + return false; + } + } - class Command { + static AtomicInteger sequencing = new AtomicInteger(0); + class Command implements Comparable { public Runnable runnable; public MessagingListener listener; public String description; + + boolean isForeground; + + int sequence = sequencing.getAndIncrement(); + + public int compareTo(Object arg0) + { + if (arg0 instanceof Command) + { + Command other = (Command)arg0; + if (other.isForeground == true && isForeground == false) + { + return 1; + } + else if (other.isForeground == false && isForeground == true) + { + return -1; + } + else + { + return (sequence - other.sequence); + } + } + return 0; + } } public MessagingListener getCheckMailListener() @@ -3079,4 +3441,503 @@ public class MessagingController implements Runnable { { sortAscending.put(sortType, nsortAscending); } + + public Collection getPushers() + { + return pushers.values(); + } + + public Pusher setupPushing(final Account account) + { + + Pusher pusher = pushers.get(account); + if (pusher != null) + { + return pusher; + } + Store store = null; + try + { + store = Store.getInstance(account.getStoreUri(), mApplication); + if (store.isPushCapable() == false) + { + Log.i(Email.LOG_TAG, "Account " + account.getDescription() + " is not push capable, skipping"); + return null; + } + } + catch (Exception e) + { + Log.e(Email.LOG_TAG, "Could not get remote store", e); + return null; + } + final MessagingController controller = this; + PushReceiver receiver = new PushReceiver() + { + WakeLock wakeLock = null; + int refCount = 0; + public synchronized void pushInProgress() + { + if (wakeLock == null) + { + PowerManager pm = (PowerManager) mApplication.getSystemService(Context.POWER_SERVICE); + wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "Email"); + wakeLock.setReferenceCounted(false); + } + wakeLock.acquire(Email.PUSH_WAKE_LOCK_TIMEOUT); + refCount++; + if (Email.DEBUG) + { + Log.d(Email.LOG_TAG, "Acquired WakeLock for Pushing"); + } + } + + public synchronized void pushComplete() + { + if (Email.DEBUG) + { + Log.d(Email.LOG_TAG, "Considering releasing WakeLock for Pushing"); + } + if (wakeLock != null) + { + if (refCount > 0) + { + refCount--; + } + if (refCount == 0) + { + try + { + if (Email.DEBUG) + { + Log.d(Email.LOG_TAG, "Releasing WakeLock for Pushing"); + } + wakeLock.release(); + } + catch (Exception e) + { + Log.e(Email.LOG_TAG, "Failed to release WakeLock", e); + } + } + } + } + + public void messagesFlagsChanged(String folderName, + List messages) + { + controller.messagesArrived(account, folderName, messages, false); + + } + public void messagesArrived(String folderName, List messages) + { + controller.messagesArrived(account, folderName, messages, true); + } + + public void pushError(String errorMessage, Exception e) + { + String errMess = errorMessage; + String body = null; + + if (errMess == null && e != null) + { + errMess = e.getMessage(); + } + body = errMess; + if (e != null) + { + body = e.toString(); + } + controller.addErrorMessage(account, errMess, body); + } + + public String getPushState(String folderName) + { + LocalFolder localFolder = null; + try + { + LocalStore localStore = (LocalStore) Store.getInstance(account.getLocalStoreUri(), mApplication); + localFolder= (LocalFolder) localStore.getFolder(folderName); + localFolder.open(OpenMode.READ_WRITE); + return localFolder.getPushState(); + } + catch (Exception e) + { + Log.e(Email.LOG_TAG, "Unable to get push state from account " + account.getDescription() + + ", folder " + folderName, e); + return null; + } + finally + { + if (localFolder != null) + { + try + { + localFolder.close(false); + } + catch (Exception e) + { + Log.e(Email.LOG_TAG, "Unable to close folder '" + folderName + "' in account " + account.getDescription(), e); + } + } + } + } + + public void setPushActive(String folderName, boolean enabled) + { + for (MessagingListener l : getListeners()) { + l.setPushActive(account, folderName, enabled); + } + } + + }; + try + { + Preferences prefs = Preferences.getPreferences(mApplication); + + Account.FolderMode aDisplayMode = account.getFolderDisplayMode(); + Account.FolderMode aPushMode = account.getFolderPushMode(); + + List names = new ArrayList(); + + Store localStore = Store.getInstance(account.getLocalStoreUri(), mApplication); + for (final Folder folder : localStore.getPersonalNamespaces()) + { + folder.open(Folder.OpenMode.READ_WRITE); + folder.refresh(prefs); + + Folder.FolderClass fDisplayClass = folder.getDisplayClass(); + Folder.FolderClass fPushClass = folder.getPushClass(); + + if (modeMismatch(aDisplayMode, fDisplayClass)) + { + // Never push a folder that isn't displayed + if (Email.DEBUG) { + Log.v(Email.LOG_TAG, "Not pushing folder " + folder.getName() + + " which is in display class " + fDisplayClass + " while account is in display mode " + aDisplayMode); + } + + continue; + } + + if (modeMismatch(aPushMode, fPushClass)) + { + // Do not push folders in the wrong class + if (Email.DEBUG) { + Log.v(Email.LOG_TAG, "Not pushing folder " + folder.getName() + + " which is in push mode " + fPushClass + " while account is in push mode " + aPushMode); + } + + continue; + } + Log.i(Email.LOG_TAG, "Starting pusher for " + account.getDescription() + ":" + folder.getName()); + names.add(folder.getName()); + } + if (names.size() > 0) + { + pusher = store.getPusher(receiver, names); + if (pusher != null) + { + pushers.put(account, pusher); + } + return pusher; + } + else + { + Log.i(Email.LOG_TAG, "No folders are configured for pushing in account " + account.getDescription()); + return null; + } + + } + catch (Exception e) + { + Log.e(Email.LOG_TAG, "Got exception while setting up pushing", e); + } + return null; + } + + public void stopPushing(Account account) + { + Pusher pusher = pushers.remove(account); + if (pusher != null) + { + pusher.stop(); + } + } + + public void stopAllPushing() + { + Log.i(Email.LOG_TAG, "Stopping all pushers"); + Iterator iter = pushers.values().iterator(); + while (iter.hasNext()) + { + Pusher pusher = iter.next(); + iter.remove(); + pusher.stop(); + } + + } + + public void messagesArrived(final Account account, final String folderName, final List messages, final boolean doNotify) + { + Log.i(Email.LOG_TAG, "Got new pushed email messages for account " + account.getDescription() + + ", folder " + folderName); + final CountDownLatch latch = new CountDownLatch(1); + putBackground("Push messageArrived of account " + account.getDescription() + + ", folder " + folderName, null, new Runnable() + { + public void run() + { + LocalFolder localFolder = null; + Folder remoteFolder = null; + try + { + LocalStore localStore = (LocalStore) Store.getInstance(account.getLocalStoreUri(), mApplication); + Store remoteStore = Store.getInstance(account.getStoreUri(), mApplication); + remoteFolder = remoteStore.getFolder(folderName); + localFolder= (LocalFolder) localStore.getFolder(folderName); + localFolder.open(OpenMode.READ_WRITE); + remoteFolder.open(OpenMode.READ_WRITE); + + downloadMessages(account, remoteFolder, localFolder, messages); + int unreadCount = 0; + for (Message message : messages) + { + if (message.isSet(Flag.SEEN) == false) + { + unreadCount++; + } + } + localFolder.setLastPush(System.currentTimeMillis()); + localFolder.setStatus(null); + + int unreadMessageCount = account.getUnreadMessageCount(mApplication, mApplication); + if (doNotify && unreadCount > 0) + { + notifyAccount(mApplication, account, unreadMessageCount); + } + + for (MessagingListener l : getListeners()) + { + l.folderStatusChanged(account, folderName); + l.accountStatusChanged(account, unreadMessageCount); + } + + } + catch (Exception e) + { + String rootMessage = getRootCauseMessage(e); + String errorMessage = "Push failed: " + rootMessage; + try + { + localFolder.setStatus(errorMessage); + } + catch (Exception se) + { + Log.e(Email.LOG_TAG, "Unable to set failed status on localFolder", se); + } + for (MessagingListener l : getListeners()) { + l.synchronizeMailboxFailed( account, folderName, errorMessage); + } + addErrorMessage(account, e); + } + finally + { + if (localFolder != null) + { + try + { + localFolder.close(false); + } + catch (Exception e) + { + Log.e(Email.LOG_TAG, "Unable to close localFolder", e); + } + } + if (remoteFolder != null) + { + try + { + remoteFolder.close(false); + } + catch (Exception e) + { + Log.e(Email.LOG_TAG, "Unable to close remoteFolder", e); + } + } + latch.countDown(); + } + + } + }); + try + { + latch.await(); + } + catch (Exception e) + { + Log.e(Email.LOG_TAG, "Interrupted while awaiting latch release", e); + } + Log.i(Email.LOG_TAG, "Latch released"); + } + enum MemorizingState { STARTED, FINISHED, FAILED }; + + class Memory + { + Account account; + String folderName; + MemorizingState syncingState = null; + MemorizingState sendingState = null; + MemorizingState pushingState = null; + String failureMessage = null; + + int syncingTotalMessagesInMailbox; + int syncingNumNewMessages; + + Memory(Account nAccount, String nFolderName) + { + account = nAccount; + folderName = nFolderName; + } + + String getKey() + { + return getMemoryKey(account, folderName); + } + + + } + static String getMemoryKey(Account taccount, String tfolderName) + { + return taccount.getDescription() + ":" + tfolderName; + } + class MemorizingListener extends MessagingListener + { + HashMap memories = new HashMap(31); + + Memory getMemory(Account account, String folderName) + { + Memory memory = memories.get(getMemoryKey(account, folderName)); + if (memory == null) + { + memory = new Memory(account, folderName); + memories.put(memory.getKey(), memory); + } + return memory; + } + + public synchronized void synchronizeMailboxStarted(Account account, String folder) { + Memory memory = getMemory(account, folder); + memory.syncingState = MemorizingState.STARTED; + } + + public synchronized void synchronizeMailboxFinished(Account account, String folder, + int totalMessagesInMailbox, int numNewMessages) { + Memory memory = getMemory(account, folder); + memory.syncingState = MemorizingState.FINISHED; + memory.syncingTotalMessagesInMailbox = totalMessagesInMailbox; + memory.syncingNumNewMessages = numNewMessages; + } + + public synchronized void synchronizeMailboxFailed(Account account, String folder, + String message) { + + Memory memory = getMemory(account, folder); + memory.syncingState = MemorizingState.FAILED; + memory.failureMessage = message; + } + synchronized void refreshOther(MessagingListener other) + { + if (other != null) + { + + Memory syncStarted = null; + Memory sendStarted = null; + + for (Memory memory : memories.values()) + { + + if (memory.syncingState != null) + { + switch (memory.syncingState) + { + case STARTED: + syncStarted = memory; + break; + case FINISHED: + other.synchronizeMailboxFinished(memory.account, memory.folderName, + memory.syncingTotalMessagesInMailbox, memory.syncingNumNewMessages); + break; + case FAILED: + other.synchronizeMailboxFailed(memory.account, memory.folderName, + memory.failureMessage); + break; + } + } + + if (memory.sendingState != null) + { + switch (memory.sendingState) + { + case STARTED: + sendStarted = memory; + break; + case FINISHED: + other.sendPendingMessagesCompleted(memory.account); + break; + case FAILED: + other.sendPendingMessagesFailed(memory.account); + break; + } + } + if (memory.pushingState != null) + { + switch (memory.pushingState) + { + case STARTED: + other.setPushActive(memory.account, memory.folderName, true); + break; + case FINISHED: + other.setPushActive(memory.account, memory.folderName, false); + break; + } + } + } + + if (syncStarted != null) + { + other.synchronizeMailboxStarted(syncStarted.account, syncStarted.folderName); + } + if (sendStarted != null) + { + other.sendPendingMessagesStarted(sendStarted.account); + } + + } + } + @Override + public synchronized void setPushActive(Account account, String folderName, boolean active) { + Memory memory = getMemory(account, folderName); + memory.pushingState = (active ? MemorizingState.STARTED : MemorizingState.FINISHED); + } + + public synchronized void sendPendingMessagesStarted(Account account) { + Memory memory = getMemory(account, null); + memory.sendingState = MemorizingState.STARTED; + } + + public synchronized void sendPendingMessagesCompleted(Account account) { + Memory memory = getMemory(account, null); + memory.sendingState = MemorizingState.FINISHED; + } + + public synchronized void sendPendingMessagesFailed(Account account) { + Memory memory = getMemory(account, null); + memory.sendingState = MemorizingState.FAILED; + } + } + + class MessageContainer + { + Message message; + boolean last; + } } diff --git a/src/com/android/email/MessagingListener.java b/src/com/android/email/MessagingListener.java index 63e837449..9075e4e45 100644 --- a/src/com/android/email/MessagingListener.java +++ b/src/com/android/email/MessagingListener.java @@ -1,6 +1,8 @@ package com.android.email; +import java.util.List; + import android.content.Context; import com.android.email.mail.Folder; @@ -15,18 +17,14 @@ import com.android.email.mail.Part; * changes in this class. */ public class MessagingListener { - + public void accountStatusChanged(Account account, int unreadMessageCount) { } public void accountSizeChanged(Account account, long oldSize, long newSize) { } - - public void accountReset(Account account) { - - } - + public void listFoldersStarted(Account account) { } @@ -45,7 +43,7 @@ public class MessagingListener { public void listLocalMessages(Account account, String folder, Message[] messages) { } - public void listLocalMessagesAddMessage(Account account, String folder, Message message) { + public void listLocalMessagesAddMessages(Account account, String folder, List messages) { } public void listLocalMessagesUpdateMessage(Account account, String folder, Message message) { @@ -128,6 +126,11 @@ public class MessagingListener { public void messageUidChanged(Account account, String folder, String oldUid, String newUid) { + } + + public void setPushActive(Account account, String folderName, boolean enabled) + { + } public void loadAttachmentStarted( diff --git a/src/com/android/email/activity/Accounts.java b/src/com/android/email/activity/Accounts.java index 394213455..3fd086417 100644 --- a/src/com/android/email/activity/Accounts.java +++ b/src/com/android/email/activity/Accounts.java @@ -264,6 +264,12 @@ public class Accounts extends K9ListActivity implements OnItemClickListener, OnC intent.putExtra(EXTRA_STARTUP, true); context.startActivity(intent); } + + public static void listAccounts(Context context) { + Intent intent = new Intent(context, Accounts.class); + intent.putExtra(EXTRA_STARTUP, false); + context.startActivity(intent); + } @Override @@ -332,10 +338,11 @@ public class Accounts extends K9ListActivity implements OnItemClickListener, OnC if (accounts.length > 0) { mHandler.progress(Window.PROGRESS_START); } - + pendingWork.clear(); for (Account account : accounts) { - MessagingController.getInstance(getApplication()).getAccountUnreadCount(Accounts.this, account, mListener); pendingWork.put(account, "true"); + MessagingController.getInstance(getApplication()).getAccountUnreadCount(Accounts.this, account, mListener); + } } diff --git a/src/com/android/email/activity/ChooseFolder.java b/src/com/android/email/activity/ChooseFolder.java index cefbc876d..0f5cc67dc 100644 --- a/src/com/android/email/activity/ChooseFolder.java +++ b/src/com/android/email/activity/ChooseFolder.java @@ -77,16 +77,10 @@ public class ChooseFolder extends K9ListActivity setListAdapter(adapter); - new Thread() - { - public void run() - { - Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); + MessagingController.getInstance(getApplication()).listFolders(mAccount, false, mListener); - } - }.start(); this.getListView().setOnItemClickListener(new AdapterView.OnItemClickListener() { public void onItemClick(AdapterView adapterview, View view, int i, long l) diff --git a/src/com/android/email/activity/FolderList.java b/src/com/android/email/activity/FolderList.java index 6d5457bf4..6cc344d5c 100644 --- a/src/com/android/email/activity/FolderList.java +++ b/src/com/android/email/activity/FolderList.java @@ -16,7 +16,6 @@ import android.content.Intent; import android.net.Uri; import android.os.Bundle; import android.os.Handler; -import android.os.Process; import android.util.Config; import android.util.Log; import android.view.ContextMenu; @@ -42,22 +41,15 @@ import com.android.email.MessagingController; import com.android.email.MessagingListener; import com.android.email.Preferences; import com.android.email.R; -import com.android.email.MessagingController.SORT_TYPE; -import com.android.email.activity.FolderList.FolderInfoHolder; -import com.android.email.activity.MessageList.MessageInfoHolder; import com.android.email.activity.setup.AccountSettings; import com.android.email.activity.setup.FolderSettings; import com.android.email.mail.Folder; import com.android.email.mail.Message; import com.android.email.mail.MessagingException; import com.android.email.mail.Store; -import com.android.email.mail.store.LocalStore.LocalFolder; import android.os.PowerManager; import android.os.PowerManager.WakeLock; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; /** * FolderList is the primary user interface for the program. This @@ -88,26 +80,13 @@ public class FolderList extends K9ListActivity { private Account mAccount; - private String mInitialFolder; - - private boolean mRestoringState; - - private boolean mRefreshRemote; - private FolderListHandler mHandler = new FolderListHandler(); private DateFormat dateFormat = null; private DateFormat timeFormat = null; - private boolean sortAscending = true; - - private boolean sortDateAscending = false; - private boolean mStartup = false; - - - private static final ThreadPoolExecutor threadPool = new ThreadPoolExecutor(1, 1, 120000L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue()); private DateFormat getDateFormat() { if (dateFormat == null) { @@ -145,17 +124,24 @@ public class FolderList extends K9ListActivity { private static final int MSG_PROGRESS = 2; private static final int MSG_DATA_CHANGED = 3; - private static final int MSG_EXPAND_GROUP = 5; private static final int MSG_FOLDER_LOADING = 7; - private static final int MSG_SYNC_MESSAGES = 13; private static final int MSG_FOLDER_SYNCING = 18; private static final int MSG_SENDING_OUTBOX = 19; private static final int MSG_ACCOUNT_SIZE_CHANGED = 20; private static final int MSG_WORKING_ACCOUNT = 21; + private static final int MSG_NEW_FOLDERS = 22; @Override public void handleMessage(android.os.Message msg) { switch (msg.what) { + case MSG_NEW_FOLDERS: + ArrayList newFolders = (ArrayList)msg.obj; + mAdapter.mFolders.clear(); + + mAdapter.mFolders.addAll(newFolders); + + mHandler.dataChanged(); + break; case MSG_PROGRESS: setProgressBarIndeterminateVisibility(msg.arg1 != 0); break; @@ -224,10 +210,10 @@ public class FolderList extends K9ListActivity { } } - public void synchronizeMessages(FolderInfoHolder folder, Message[] messages) { + public void newFolders(ArrayList newFolders) { android.os.Message msg = new android.os.Message(); - msg.what = MSG_SYNC_MESSAGES; - msg.obj = new Object[] { folder, messages }; + msg.obj = newFolders; + msg.what = MSG_NEW_FOLDERS; sendMessage(msg); } @@ -285,55 +271,32 @@ public class FolderList extends K9ListActivity { * queueing up a remote update of the folder. */ - class FolderUpdateWorker implements Runnable { - String mFolder; - FolderInfoHolder mHolder; - boolean mSynchronizeRemote; - - /** - * Create a worker for the given folder and specifying whether the worker - * should synchronize the remote folder or just the local one. - * - * @param folder - * @param synchronizeRemote - */ - public FolderUpdateWorker(FolderInfoHolder folder, boolean synchronizeRemote) { - mFolder = folder.name; - mHolder = folder; - mSynchronizeRemote = synchronizeRemote; - } - - public void run() { - // Lower our priority - Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); - PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); - WakeLock wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "Email - UpdateWorker"); - wakeLock.setReferenceCounted(false); - wakeLock.acquire(Email.WAKE_LOCK_TIMEOUT); - // Synchronously load the list of local messages - - try { - try { - Store localStore = Store.getInstance(mAccount.getLocalStoreUri(), getApplication()); - LocalFolder localFolder = (LocalFolder) localStore.getFolder(mFolder); - - if (localFolder.getMessageCount() == 0 && localFolder.getLastChecked() <= 0) { - mSynchronizeRemote = true; - } - } catch (MessagingException me) { - Log.e(Email.LOG_TAG, "Unable to get count of local messages for folder " + mFolder, me); + private void checkMail(FolderInfoHolder folder) + { + PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); + final WakeLock wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "Email - UpdateWorker"); + wakeLock.setReferenceCounted(false); + wakeLock.acquire(Email.WAKE_LOCK_TIMEOUT); + MessagingListener listener = new MessagingListener() + { + public void synchronizeMailboxFinished(Account account, String folder, int totalMessagesInMailbox, int numNewMessages) { + if (!account.equals(mAccount)) { + return; } - - if (mSynchronizeRemote) { - // Tell the MessagingController to run a remote update of this folder - // at it's leisure - MessagingController.getInstance(getApplication()).synchronizeMailbox(mAccount, mFolder, mAdapter.mListener); - } - } finally { - wakeLock.release(); + wakeLock.release(); } - - } + + @Override + public void synchronizeMailboxFailed(Account account, String folder, + String message) { + if (!account.equals(mAccount)) { + return; + } + wakeLock.release(); + } + }; + MessagingController.getInstance(getApplication()).synchronizeMailbox(mAccount, folder.name, listener); + sendMail(mAccount); } private static void actionHandleAccount(Context context, Account account, String initialFolder, boolean startup) { @@ -381,38 +344,39 @@ public class FolderList extends K9ListActivity { @Override public void onCreate(Bundle savedInstanceState) { + + String initialFolder; + super.onCreate(savedInstanceState); String savedFolderName = null; Intent intent = getIntent(); mAccount = (Account)intent.getSerializableExtra(EXTRA_ACCOUNT); Log.v(Email.LOG_TAG, "savedInstanceState: " + (savedInstanceState==null)); if (savedInstanceState == null) { - mInitialFolder = intent.getStringExtra(EXTRA_INITIAL_FOLDER); - Log.v(Email.LOG_TAG, "EXTRA_INITIAL_FOLDER: " + mInitialFolder); + initialFolder = intent.getStringExtra(EXTRA_INITIAL_FOLDER); + Log.v(Email.LOG_TAG, "EXTRA_INITIAL_FOLDER: " + initialFolder); mStartup = (boolean) intent.getBooleanExtra(EXTRA_STARTUP, false); Log.v(Email.LOG_TAG, "startup: " + mStartup); - if (mInitialFolder == null + if (initialFolder == null && mStartup) { - mInitialFolder = mAccount.getAutoExpandFolderName(); + initialFolder = mAccount.getAutoExpandFolderName(); } } else { - mInitialFolder = null; + initialFolder = null; mStartup = false; savedFolderName = savedInstanceState.getString(STATE_CURRENT_FOLDER); } - Log.v(Email.LOG_TAG, "mInitialFolder: " + mInitialFolder); - if (mInitialFolder != null - && !Email.FOLDER_NONE.equals(mInitialFolder)) { - onOpenFolder(mInitialFolder, true); + Log.v(Email.LOG_TAG, "mInitialFolder: " + initialFolder); + if (initialFolder != null + && !Email.FOLDER_NONE.equals(initialFolder)) { + onOpenFolder(initialFolder, true); finish(); } else { requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); - final FolderList xxx = this; - mListView = getListView(); mListView.setScrollBarStyle(View.SCROLLBARS_OUTSIDE_INSET); mListView.setLongClickable(true); @@ -421,7 +385,7 @@ public class FolderList extends K9ListActivity { mListView.setOnItemClickListener(new OnItemClickListener() { public void onItemClick(AdapterView parent, View v, int itemPosition, long id) { Log.v(Email.LOG_TAG,"We're clicking "+itemPosition+" -- "+id); - MessageList.actionHandleFolder(xxx, mAccount, ((FolderInfoHolder)mAdapter.getItem(id)).name, false); + MessageList.actionHandleFolder(FolderList.this, mAccount, ((FolderInfoHolder)mAdapter.getItem(id)).name, false); } }); registerForContextMenu(mListView); @@ -445,12 +409,6 @@ public class FolderList extends K9ListActivity { setListAdapter(mAdapter); - if (savedInstanceState != null) { - mRestoringState = true; - //onRestoreListState(savedInstanceState); - mRestoringState = false; - } - setTitle(mAccount.getDescription()); if (savedFolderName != null) @@ -480,7 +438,6 @@ public class FolderList extends K9ListActivity { MessagingController.getInstance(getApplication()).addListener(mAdapter.mListener); mAccount.refresh(Preferences.getPreferences(this)); - markAllRefresh(); onRefresh( !REFRESH_REMOTE ); @@ -503,7 +460,9 @@ public class FolderList extends K9ListActivity { //Shortcuts that work no matter what is selected switch (keyCode) { - case KeyEvent.KEYCODE_Q: { + case KeyEvent.KEYCODE_Q: + //case KeyEvent.KEYCODE_BACK: + { onAccounts(); return true; } @@ -525,22 +484,9 @@ public class FolderList extends K9ListActivity { }//onKeyDown private void onRefresh(final boolean forceRemote) { - if (forceRemote) { - mRefreshRemote = true; - } - new Thread() { - - public void run() { - Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); - MessagingController.getInstance(getApplication()).listFolders(mAccount, forceRemote, mAdapter.mListener); - - if (forceRemote) { - MessagingController.getInstance(getApplication()).sendPendingMessages(mAccount, null); - } - } - } - .start(); + MessagingController.getInstance(getApplication()).listFolders(mAccount, forceRemote, mAdapter.mListener); + } private void onEditAccount() { @@ -552,21 +498,14 @@ public class FolderList extends K9ListActivity { } private void onAccounts() { - // If we're a child activity (say because Welcome dropped us straight to the message list - // we won't have a parent activity and we'll need to get back to it - if (mStartup - || isTaskRoot()) { - Intent intent = new Intent(this, Accounts.class); - intent.putExtra(Accounts.EXTRA_STARTUP, false); - startActivity(intent); + if (mStartup || isTaskRoot()) + { + Accounts.listAccounts(this); } + finish(); } - private void markAllRefresh() { - mAdapter.mListener.accountReset(mAccount); - } - private void onEmptyTrash(final Account account) { mHandler.dataChanged(); @@ -583,6 +522,10 @@ public class FolderList extends K9ListActivity { private void checkMail(final Account account) { MessagingController.getInstance(getApplication()).checkMail(this, account, true, true, mAdapter.mListener); } + + private void sendMail(Account account) { + MessagingController.getInstance(getApplication()).sendPendingMessages(account, mAdapter.mListener); + } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { @@ -596,6 +539,11 @@ public class FolderList extends K9ListActivity { return true; + case R.id.send_messages: + Log.i(Email.LOG_TAG, "sending pending messages"); + + MessagingController.getInstance(getApplication()).sendPendingMessages(mAccount, null); + return true; case R.id.accounts: onAccounts(); @@ -666,15 +614,13 @@ public class FolderList extends K9ListActivity { case R.id.send_messages: Log.i(Email.LOG_TAG, "sending pending messages from " + folder.name); - - MessagingController.getInstance(getApplication()).sendPendingMessages(mAccount, null); + sendMail(mAccount); break; case R.id.check_mail: Log.i(Email.LOG_TAG, "refresh folder " + folder.name); - - threadPool.execute(new FolderUpdateWorker(folder, true)); + checkMail(folder); break; @@ -741,10 +687,6 @@ public class FolderList extends K9ListActivity { MessagingController.getInstance(getApplication()).markAllMessagesRead(mAccount, mSelectedContextFolder.name); - for (MessageInfoHolder holder : mSelectedContextFolder.messages) { - holder.read = true; - } - mSelectedContextFolder.unreadMessageCount = 0; mHandler.dataChanged(); @@ -854,7 +796,7 @@ public class FolderList extends K9ListActivity { } mHandler.progress(false); - + MessagingController.getInstance(getApplication()).refreshListener(mAdapter.mListener); mHandler.dataChanged(); } @@ -902,24 +844,9 @@ public class FolderList extends K9ListActivity { newFolders.add(holder); } - mFolders.clear(); - - mFolders.addAll(newFolders); - Collections.sort(mFolders); - mHandler.dataChanged(); - mRefreshRemote = false; - } - - - @Override - public void accountReset(Account account) { - if (!account.equals(mAccount)) { - return; - } - - for (FolderInfoHolder folder : mFolders) { - folder.needsRefresh = true; - } + Collections.sort(newFolders); + mHandler.newFolders(newFolders); + } public void synchronizeMailboxStarted(Account account, String folder) { @@ -930,6 +857,7 @@ public class FolderList extends K9ListActivity { mHandler.progress(true); mHandler.folderLoading(folder, true); mHandler.folderSyncing(folder); + mHandler.dataChanged(); } @Override @@ -937,23 +865,37 @@ public class FolderList extends K9ListActivity { if (!account.equals(mAccount)) { return; } - - // There has to be a cheaper way to get at the localFolder object than this - try { - Folder localFolder = (Folder) Store.getInstance(account.getLocalStoreUri(), getApplication()).getFolder(folder); - getFolder(folder).populate(localFolder); - } - catch (MessagingException e) { - - } - - mHandler.progress(false); mHandler.folderLoading(folder, false); // mHandler.folderStatus(folder, null); mHandler.folderSyncing(null); + + refreshFolder(account, folder); - onRefresh( ! REFRESH_REMOTE ); + } + + private void refreshFolder(Account account, String folderName) + { + // There has to be a cheaper way to get at the localFolder object than this + try { + if (account != null && folderName != null) + { + Folder localFolder = (Folder) Store.getInstance(account.getLocalStoreUri(), getApplication()).getFolder(folderName); + if (localFolder != null) + { + FolderInfoHolder folderHolder = getFolder(folderName); + if (folderHolder != null) + { + folderHolder.populate(localFolder); + mHandler.dataChanged(); + } + } + } + } + catch (Exception e) { + Log.e(Email.LOG_TAG, "Exception while populating folder", e); + } + } @Override @@ -978,6 +920,23 @@ public class FolderList extends K9ListActivity { } mHandler.folderSyncing(null); + + mHandler.dataChanged(); + } + + @Override + public void setPushActive(Account account, String folderName, boolean enabled) + { + if (!account.equals(mAccount)) { + return; + } + FolderInfoHolder holder = getFolder(folderName); + + if (holder != null) { + holder.pushActive = enabled; + + mHandler.dataChanged(); + } } @@ -993,8 +952,7 @@ public class FolderList extends K9ListActivity { if (!account.equals(mAccount)) { return; } - - onRefresh( ! REFRESH_REMOTE); + refreshFolder(account, mAccount.getTrashFolderName()); } @Override @@ -1002,8 +960,7 @@ public class FolderList extends K9ListActivity { if (!account.equals(mAccount)) { return; } - - onRefresh( !REFRESH_REMOTE); + refreshFolder(account, folderName); } @Override @@ -1013,8 +970,7 @@ public class FolderList extends K9ListActivity { } mHandler.sendingOutbox(false); - - onRefresh( !REFRESH_REMOTE); + refreshFolder(account, mAccount.getOutboxFolderName()); } @Override @@ -1024,6 +980,8 @@ public class FolderList extends K9ListActivity { } mHandler.sendingOutbox(true); + + mHandler.dataChanged(); } @Override @@ -1033,6 +991,7 @@ public class FolderList extends K9ListActivity { } mHandler.sendingOutbox(false); + refreshFolder(account, mAccount.getOutboxFolderName()); } public void accountSizeChanged(Account account, long oldSize, long newSize) { @@ -1115,6 +1074,11 @@ public class FolderList extends K9ListActivity { statusText = (getDateFormat().format(lastCheckedDate) + " " + getTimeFormat() .format(lastCheckedDate)); } + + if (folder.pushActive) + { + statusText = getString(R.string.folder_push_active_symbol) + statusText; + } if (statusText != null) { holder.folderStatus.setText(statusText); @@ -1150,8 +1114,6 @@ public class FolderList extends K9ListActivity { public String displayName; - public ArrayList messages; - public long lastChecked; public int unreadMessageCount; @@ -1159,11 +1121,11 @@ public class FolderList extends K9ListActivity { public boolean loading; public String status; + + public boolean pushActive; public boolean lastCheckFailed; - public boolean needsRefresh = false; - /** * Outbox is handled differently from any other folder. */ @@ -1241,12 +1203,8 @@ public class FolderList extends K9ListActivity { if (this.name.equals(mAccount.getSentFolderName())) { this.displayName = String.format( getString(R.string.special_mailbox_name_sent_fmt), this.name); } - - if (this.messages == null) { - this.messages = new ArrayList(); - } - - this.lastChecked = folder.getLastChecked(); + + this.lastChecked = folder.getLastUpdate(); String mess = truncateStatus(folder.getStatus()); diff --git a/src/com/android/email/activity/MessageCompose.java b/src/com/android/email/activity/MessageCompose.java index abd21632b..c51e9e546 100644 --- a/src/com/android/email/activity/MessageCompose.java +++ b/src/com/android/email/activity/MessageCompose.java @@ -495,41 +495,41 @@ public class MessageCompose extends K9Activity implements OnClickListener, OnFoc upperSignature.setVisibility(View.GONE); } mSignatureView.addTextChangedListener(sigwatcher); - + if (!mSourceMessageProcessed) { - updateFrom(); - updateSignature(); - - if (ACTION_REPLY.equals(action) || ACTION_REPLY_ALL.equals(action) || ACTION_FORWARD.equals(action) || ACTION_EDIT_DRAFT.equals(action)) { - /* - * If we need to load the message we add ourself as a message listener here - * so we can kick it off. Normally we add in onResume but we don't - * want to reload the message every time the activity is resumed. - * There is no harm in adding twice. - */ - MessagingController.getInstance(getApplication()).addListener(mListener); - MessagingController.getInstance(getApplication()).loadMessageForView( mAccount, mFolder, mSourceMessageUid, null); - } - - if (!ACTION_EDIT_DRAFT.equals(action)) { - String bccAddress = mAccount.getAlwaysBcc(); - if (bccAddress!=null - && !"".equals(bccAddress)) { - addAddress(mBccView, new Address(mAccount.getAlwaysBcc(), "")); - } - } - - Log.d(Email.LOG_TAG, "action = " + action + ", mAccount = " + mAccount + ", mFolder = " + mFolder + ", mSourceMessageUid = " + mSourceMessageUid); - if ((ACTION_REPLY.equals(action) || ACTION_REPLY_ALL.equals(action)) && mAccount != null && mFolder != null && mSourceMessageUid != null) { - Log.d(Email.LOG_TAG, "Setting message ANSWERED flag to true"); - // TODO: Really, we should wait until we send the message, but that would require saving the original - // message info along with a Draft copy, in case it is left in Drafts for a while before being sent - MessagingController.getInstance(getApplication()).setMessageFlag(mAccount, mFolder, mSourceMessageUid, Flag.ANSWERED, true); - } - - updateTitle(); + updateFrom(); + updateSignature(); + + if (ACTION_REPLY.equals(action) || ACTION_REPLY_ALL.equals(action) || ACTION_FORWARD.equals(action) || ACTION_EDIT_DRAFT.equals(action)) { + /* + * If we need to load the message we add ourself as a message listener here + * so we can kick it off. Normally we add in onResume but we don't + * want to reload the message every time the activity is resumed. + * There is no harm in adding twice. + */ + MessagingController.getInstance(getApplication()).addListener(mListener); + MessagingController.getInstance(getApplication()).loadMessageForView( mAccount, mFolder, mSourceMessageUid, null); } + if (!ACTION_EDIT_DRAFT.equals(action)) { + String bccAddress = mAccount.getAlwaysBcc(); + if (bccAddress!=null + && !"".equals(bccAddress)) { + addAddress(mBccView, new Address(mAccount.getAlwaysBcc(), "")); + } + } + + Log.d(Email.LOG_TAG, "action = " + action + ", mAccount = " + mAccount + ", mFolder = " + mFolder + ", mSourceMessageUid = " + mSourceMessageUid); + if ((ACTION_REPLY.equals(action) || ACTION_REPLY_ALL.equals(action)) && mAccount != null && mFolder != null && mSourceMessageUid != null) { + Log.d(Email.LOG_TAG, "Setting message ANSWERED flag to true"); + // TODO: Really, we should wait until we send the message, but that would require saving the original + // message info along with a Draft copy, in case it is left in Drafts for a while before being sent + MessagingController.getInstance(getApplication()).setMessageFlag(mAccount, mFolder, mSourceMessageUid, Flag.ANSWERED, true); + } + + updateTitle(); + } + if (ACTION_REPLY.equals(action) || ACTION_REPLY_ALL.equals(action) || ACTION_EDIT_DRAFT.equals(action)) { //change focus to message body. mMessageContentView.requestFocus(); diff --git a/src/com/android/email/activity/MessageList.java b/src/com/android/email/activity/MessageList.java index f7a4cd7a5..c5aeb55cb 100644 --- a/src/com/android/email/activity/MessageList.java +++ b/src/com/android/email/activity/MessageList.java @@ -59,11 +59,6 @@ import com.android.email.mail.store.LocalStore; import com.android.email.mail.store.LocalStore.LocalFolder; import com.android.email.mail.store.LocalStore.LocalMessage; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; - - /** * MessageList is the primary user interface for the program. This @@ -82,21 +77,15 @@ import java.util.concurrent.TimeUnit; public class MessageList extends K9ListActivity { - private static final String INTENT_DATA_PATH_SUFFIX = "/accounts"; - private static final int DIALOG_MARK_ALL_AS_READ = 1; private static final int ACTIVITY_CHOOSE_FOLDER_MOVE = 1; private static final int ACTIVITY_CHOOSE_FOLDER_COPY = 2; - - private static final boolean FORCE_REMOTE_SYNC = true; private static final String EXTRA_ACCOUNT = "account"; private static final String EXTRA_STARTUP = "startup"; - private static final String EXTRA_CLEAR_NOTIFICATION = "clearNotification"; - private static final String EXTRA_FOLDER = "folder"; private static final String STATE_KEY_LIST = "com.android.email.activity.messagelist_state"; @@ -146,11 +135,6 @@ public class MessageList extends K9ListActivity { */ private String mFolderName; - - private boolean mRestoringState; - - private boolean mRefreshRemote; - private MessageListHandler mHandler = new MessageListHandler(); private DateFormat dateFormat = null; @@ -165,8 +149,6 @@ public class MessageList extends K9ListActivity { private boolean mStartup = false; - private static final ThreadPoolExecutor threadPool = new ThreadPoolExecutor(1, 1, 120000L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue()); - private DateFormat getDateFormat() { if (dateFormat == null) { String dateFormatS = android.provider.Settings.System.getString(getContentResolver(), @@ -232,23 +214,28 @@ public class MessageList extends K9ListActivity { break; case MSG_REMOVE_MESSAGE: { - MessageInfoHolder message = (MessageInfoHolder)((Object[]) msg.obj)[1]; - mAdapter.messages.remove(message); + List messages = (List)((Object[]) msg.obj)[0]; + for (MessageInfoHolder message : messages) + { + mAdapter.messages.remove(message); + } mAdapter.notifyDataSetChanged(); break; } case MSG_ADD_MESSAGE: { - MessageInfoHolder message = (MessageInfoHolder)((Object[]) msg.obj)[1]; - - int index = Collections.binarySearch( mAdapter.messages, message); - - if (index < 0) + List messages = (List)((Object[]) msg.obj)[0]; + for (MessageInfoHolder message : messages) { - index = (index * -1) - 1; + int index = Collections.binarySearch( mAdapter.messages, message); + + if (index < 0) + { + index = (index * -1) - 1; + } + + mAdapter.messages.add(index, message); } - - mAdapter.messages.add(index, message); mAdapter.notifyDataSetChanged(); break; } @@ -270,7 +257,7 @@ public class MessageList extends K9ListActivity { case MSG_FOLDER_LOADING: { - FolderInfoHolder folder = mAdapter.getFolder((String) msg.obj); + FolderInfoHolder folder = mCurrentFolder; if (folder != null) { folder.loading = msg.arg1 != 0; @@ -297,17 +284,17 @@ public class MessageList extends K9ListActivity { } } - public void removeMessage(MessageInfoHolder message) { + public void removeMessage(List messages) { android.os.Message msg = new android.os.Message(); msg.what = MSG_REMOVE_MESSAGE; - msg.obj = new Object[] { message.folder, message }; + msg.obj = new Object[] { messages }; sendMessage(msg); } - public void addMessage(MessageInfoHolder message) { + public void addMessages(List messages) { android.os.Message msg = new android.os.Message(); msg.what = MSG_ADD_MESSAGE; - msg.obj = new Object[] { message.folder, message }; + msg.obj = new Object[] { messages }; sendMessage(msg); } @@ -412,9 +399,8 @@ public class MessageList extends K9ListActivity { MessagingController.getInstance(getApplication()).loadMoreMessages( mAccount, mFolderName, - mAdapter.mListener); - - onRefresh(FORCE_REMOTE_SYNC); + mAdapter.mListener); + return; } else { MessageInfoHolder message = (MessageInfoHolder) mAdapter.getItem( itemPosition); @@ -432,15 +418,20 @@ public class MessageList extends K9ListActivity { colorChipResId = colorChipResIds[mAccount.getAccountNumber() % colorChipResIds.length]; mAdapter = new MessageListAdapter(); + + final Object previousData = getLastNonConfigurationInstance(); + + if (previousData != null) { + //noinspection unchecked + mAdapter.messages.addAll((List) previousData); + } mCurrentFolder = mAdapter.getFolder(mFolderName); setListAdapter(mAdapter); if (savedInstanceState != null) { - mRestoringState = true; onRestoreListState(savedInstanceState); - mRestoringState = false; } setTitle( @@ -488,7 +479,7 @@ public class MessageList extends K9ListActivity { MessagingController.getInstance(getApplication()).addListener( mAdapter.mListener); - onRefresh(!FORCE_REMOTE_SYNC); + MessagingController.getInstance(getApplication()).listLocalMessages(mAccount, mFolderName, mAdapter.mListener); NotificationManager notifMgr = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); notifMgr.cancel(mAccount.getAccountNumber()); @@ -505,8 +496,9 @@ public class MessageList extends K9ListActivity { } - - + @Override public Object onRetainNonConfigurationInstance() { + return mAdapter.messages; + } @Override public boolean onKeyDown(int keyCode, KeyEvent event) { @@ -515,7 +507,9 @@ public class MessageList extends K9ListActivity { switch (keyCode) { case KeyEvent.KEYCODE_C: { onCompose(); return true;} - case KeyEvent.KEYCODE_Q: { onShowFolderList(); return true; } + case KeyEvent.KEYCODE_Q: + //case KeyEvent.KEYCODE_BACK: + { onShowFolderList(); return true; } case KeyEvent.KEYCODE_O: { onCycleSort(); return true; } @@ -563,28 +557,20 @@ public class MessageList extends K9ListActivity { - private void onRefresh(final boolean forceRemote) { - if (forceRemote) { - mRefreshRemote = true; - } - - new Thread() { - - public void run() { - Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); - if (forceRemote) { - MessagingController.getInstance(getApplication()).synchronizeMailbox(mAccount, mFolderName, mAdapter.mListener); - MessagingController.getInstance(getApplication()).sendPendingMessages(mAccount, null); - } - MessagingController.getInstance(getApplication()).listLocalMessages(mAccount, mFolderName, mAdapter.mListener); - - } - } - - .start(); - } private void onOpenMessage( MessageInfoHolder message) { + if (message.folder.name.equals(mAccount.getDraftsFolderName())) { + MessageCompose.actionEditDraft(this, mAccount, message.message); + } else { + // Need to get the list before the sort starts + ArrayList folderUids = new ArrayList(); + + for (MessageInfoHolder holder : mAdapter.messages) { + folderUids.add(holder.uid); + } + + MessageView.actionView(this, mAccount, message.folder.name, message.uid, folderUids); + } /* * We set read=true here for UI performance reasons. The actual value will * get picked up on the refresh when the Activity is resumed but that may @@ -598,26 +584,14 @@ public class MessageList extends K9ListActivity { mHandler.sortMessages(); } - if (message.folder.name.equals(mAccount.getDraftsFolderName())) { - MessageCompose.actionEditDraft(this, mAccount, message.message); - } else { - ArrayList folderUids = new ArrayList(); - - for (MessageInfoHolder holder : mAdapter.messages) { - folderUids.add(holder.uid); - } - - MessageView.actionView(this, mAccount, message.folder.name, message.uid, folderUids); - } } private void onShowFolderList() { - // If we're a child activity (say because Welcome dropped us straight to the message list - // we won't have a parent activity and we'll need to get back to it - if (mStartup - || isTaskRoot()) { + if (mStartup || isTaskRoot()) + { FolderList.actionHandleAccount(this, mAccount, false); } + finish(); } @@ -642,7 +616,12 @@ public class MessageList extends K9ListActivity { mHandler.sortMessages(); } - + + private void onAccounts() { + Accounts.listAccounts(this); + finish(); + } + private void onCycleSort() { SORT_TYPE[] sorts = SORT_TYPE.values(); int curIndex = 0; @@ -677,11 +656,11 @@ public class MessageList extends K9ListActivity { holder.folder.unreadMessageCount--; } - FolderInfoHolder trashHolder = mAdapter.getFolder(mAccount.getTrashFolderName()); - - if (trashHolder != null) { - trashHolder.needsRefresh = true; - } +// FolderInfoHolder trashHolder = mAdapter.getFolder(mAccount.getTrashFolderName()); +// +// if (trashHolder != null) { +// trashHolder.needsRefresh = true; +// } mAdapter.removeMessage(holder); mListView.setSelection(position); @@ -741,26 +720,22 @@ public class MessageList extends K9ListActivity { String destFolderName = data.getStringExtra(ChooseFolder.EXTRA_NEW_FOLDER); - String srcFolderName = data.getStringExtra(ChooseFolder.EXTRA_CUR_FOLDER); - String uid = data.getStringExtra(ChooseFolder.EXTRA_MESSAGE_UID); - FolderInfoHolder srcHolder = mAdapter.getFolder(srcFolderName); + FolderInfoHolder srcHolder = mCurrentFolder; - FolderInfoHolder destHolder = mAdapter.getFolder(destFolderName); - - if (srcHolder != null && destHolder != null) { + if (srcHolder != null && destFolderName != null) { MessageInfoHolder m = mAdapter.getMessage( uid); if (m != null) { switch (requestCode) { case ACTIVITY_CHOOSE_FOLDER_MOVE: - onMoveChosen(m, destHolder); + onMoveChosen(m, destFolderName); break; case ACTIVITY_CHOOSE_FOLDER_COPY: - onCopyChosen(m, destHolder); + onCopyChosen(m, destFolderName); break; } @@ -770,15 +745,12 @@ public class MessageList extends K9ListActivity { } - private void onMoveChosen(MessageInfoHolder holder, FolderInfoHolder folder) { + private void onMoveChosen(MessageInfoHolder holder, String folderName) { if (MessagingController.getInstance(getApplication()).isMoveCapable(mAccount) == false) { return; } -// String destFolderName = folder.name; -// FolderInfoHolder destHolder = mAdapter.getFolder(destFolderName); -// - if (folder == null) { + if (folderName == null) { return; } @@ -786,35 +758,23 @@ public class MessageList extends K9ListActivity { if (holder.folder.unreadMessageCount > 0) { holder.folder.unreadMessageCount--; } - - folder.unreadMessageCount++; } - folder.needsRefresh = true; - mAdapter.removeMessage(holder); - MessagingController.getInstance(getApplication()).moveMessage(mAccount, holder.message.getFolder().getName(), holder.message, folder.name, null); + MessagingController.getInstance(getApplication()).moveMessage(mAccount, holder.message.getFolder().getName(), holder.message, folderName, null); } - private void onCopyChosen(MessageInfoHolder holder, FolderInfoHolder folder) { + private void onCopyChosen(MessageInfoHolder holder, String folderName) { if (MessagingController.getInstance(getApplication()).isCopyCapable(mAccount) == false) { return; } - - if (folder == null) { + if (folderName == null) { return; } - - if (holder.read == false) { - folder.unreadMessageCount++; - } - - folder.needsRefresh = true; - MessagingController.getInstance(getApplication()).copyMessage(mAccount, - holder.message.getFolder().getName(), holder.message, folder.name, null); + holder.message.getFolder().getName(), holder.message, folderName, null); } @@ -831,7 +791,6 @@ public class MessageList extends K9ListActivity { } private void onMarkAllAsRead(final Account account, final String folder) { - showDialog(DIALOG_MARK_ALL_AS_READ); } @@ -916,12 +875,13 @@ public class MessageList extends K9ListActivity { mHandler.sortMessages(); } -// private void checkMail(final Account account) { -// MessagingController.getInstance(getApplication()).checkMail(this, account, true, true, mAdapter.mListener); -// } - private void checkMail(Account account, String folderName) { MessagingController.getInstance(getApplication()).synchronizeMailbox(account, folderName, mAdapter.mListener); + sendMail(account); + } + + private void sendMail(Account account) { + MessagingController.getInstance(getApplication()).sendPendingMessages(account, mAdapter.mListener); } @Override @@ -930,12 +890,20 @@ public class MessageList extends K9ListActivity { case R.id.check_mail: checkMail(mAccount, mFolderName); return true; + case R.id.send_messages: + sendMail(mAccount); + return true; case R.id.compose: onCompose(); return true; + case R.id.accounts: + onAccounts(); + + return true; + case R.id.set_sort_date: changeSort(SORT_TYPE.SORT_DATE); @@ -995,6 +963,13 @@ public class MessageList extends K9ListActivity { public boolean onCreateOptionsMenu(Menu menu) { super.onCreateOptionsMenu(menu); getMenuInflater().inflate(R.menu.message_list_option, menu); + + if (mCurrentFolder.outbox) { + menu.findItem(R.id.check_mail).setVisible(false); + } else { + menu.findItem(R.id.send_messages).setVisible(false); + } + return true; } @@ -1193,7 +1168,7 @@ public class MessageList extends K9ListActivity { return; } - mHandler.sortMessages(); + mHandler.sortMessages(); mHandler.progress(false); mHandler.folderLoading(folder, false); } @@ -1204,7 +1179,8 @@ public class MessageList extends K9ListActivity { return; } - mHandler.sortMessages(); + mHandler.sortMessages(); + mHandler.progress(false); mHandler.folderLoading(folder, false); } @@ -1234,12 +1210,12 @@ public class MessageList extends K9ListActivity { @Override - public void listLocalMessagesAddMessage(Account account, String folder, Message message) { + public void listLocalMessagesAddMessages(Account account, String folder, List messages) { if (!account.equals(mAccount) || !folder.equals(mFolderName)) { return; } - addOrUpdateMessage(folder, message); + addOrUpdateMessages(folder, messages); } @@ -1263,40 +1239,23 @@ public class MessageList extends K9ListActivity { mAnsweredIcon = getResources().getDrawable( R.drawable.ic_mms_answered_small); } + public void removeMessages(List holders) { + if (holders == null) { + return; + } + + mHandler.removeMessage(holders); + + } + public void removeMessage(MessageInfoHolder holder) { - if (holder == null) { - return; - } - - if (holder.folder == null) { - return; - } - - mHandler.removeMessage(holder); - + List messages = new ArrayList(); + messages.add(holder); + removeMessages(messages); } - - private void addOrUpdateMessage(FolderInfoHolder folder, Message message) { - - MessageInfoHolder m = getMessage( message.getUid()); - - if (m == null) { - m = new MessageInfoHolder(message, folder); - mHandler.addMessage(m); - } else { - if (message.isSet(Flag.DELETED)) { - removeMessage(m); - } else { - m.populate(message, folder); - mHandler.sortMessages(); - } - } - - - } - + private void addOrUpdateMessage(String folder, Message message) { - FolderInfoHolder f = getFolder(folder); + FolderInfoHolder f = mCurrentFolder; if (f == null) { return; @@ -1304,6 +1263,60 @@ public class MessageList extends K9ListActivity { addOrUpdateMessage(f, message); } + + private void addOrUpdateMessage(FolderInfoHolder folder, Message message) { + List messages = new ArrayList(); + messages.add(message); + addOrUpdateMessages(folder, messages); + } + + private void addOrUpdateMessages(String folder, List messages) { + FolderInfoHolder f = mCurrentFolder; + + if (f == null) { + return; + } + + addOrUpdateMessages(f, messages); + } + private void addOrUpdateMessages(FolderInfoHolder folder, List messages) { + boolean needsSort = false; + List messagesToAdd = new ArrayList(); + List messagesToRemove = new ArrayList(); + + for (Message message : messages) + { + MessageInfoHolder m = getMessage( message.getUid()); + + if (m == null) + { + m = new MessageInfoHolder(message, folder); + messagesToAdd.add(m); + } else { + if (message.isSet(Flag.DELETED)) { + messagesToRemove.add(m); + + } else { + m.populate(message, folder); + needsSort = true; + + } + } + } + + if (messagesToRemove.size() > 0) + { + removeMessages(messagesToRemove); + } + if (messagesToAdd.size() > 0) + { + mHandler.addMessages(messagesToAdd); + } + if (needsSort) + { + mHandler.sortMessages(); + } + } // XXX TODO - make this not use a for loop public MessageInfoHolder getMessage( String messageUid) { @@ -1444,20 +1457,23 @@ public class MessageList extends K9ListActivity { footerView.setId(R.layout.message_list_item_footer); FooterViewHolder holder = new FooterViewHolder(); holder.progress = (ProgressBar)footerView.findViewById(R.id.message_list_progress); + holder.progress.setIndeterminate(true); holder.main = (TextView)footerView.findViewById(R.id.main_text); footerView.setTag(holder); } - + FooterViewHolder holder = (FooterViewHolder)footerView.getTag(); + if (mCurrentFolder.loading) { holder.main.setText(getString(R.string.status_loading_more)); - mHandler.progress(true); + holder.progress.setVisibility(ProgressBar.VISIBLE); } else { if (mCurrentFolder.lastCheckFailed == false) { holder.main.setText(String.format(getString(R.string.load_more_messages_fmt).toString(), mAccount.getDisplayCount())); } else { holder.main.setText(getString(R.string.status_loading_more_failed)); } + holder.progress.setVisibility(ProgressBar.INVISIBLE); } return footerView; diff --git a/src/com/android/email/activity/MessageView.java b/src/com/android/email/activity/MessageView.java index 536616093..5850c344a 100644 --- a/src/com/android/email/activity/MessageView.java +++ b/src/com/android/email/activity/MessageView.java @@ -113,10 +113,6 @@ public class MessageView extends K9Activity private DateFormat timeFormat = null; private Menu optionsMenu = null; - - //Shall we use more threads? How often will the user move from non-fully-downloaded - //messages to another non-fully-downloaded message more than 3 times? - // private final ExecutorService threadPool = Executors.newFixedThreadPool(3); private DateFormat getDateFormat() { @@ -154,7 +150,23 @@ public class MessageView extends K9Activity private Listener mListener = new Listener(); private MessageViewHandler mHandler = new MessageViewHandler(); - public boolean onKeyDown(int keyCode, KeyEvent event) { + + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + boolean ret = false; + + if (KeyEvent.ACTION_DOWN == event.getAction()) + { + ret = onKeyDown(event.getKeyCode(), event); + } + if (ret == false) + { + ret = super.dispatchKeyEvent(event); + } + return ret; + } + + public boolean onKeyDown(int keyCode, KeyEvent event) { switch (keyCode) { case KeyEvent.KEYCODE_DEL: { onDelete(); return true;} case KeyEvent.KEYCODE_D: { onDelete(); return true;} @@ -174,26 +186,26 @@ public class MessageView extends K9Activity if (event.isShiftPressed()) { mHandler.post(new Runnable() { public void run() { - mMessageContentView.zoomIn(); + mMessageContentView.zoomIn(); } }); - } else { + } else { mHandler.post(new Runnable() { public void run() { - mMessageContentView.zoomOut(); - } + mMessageContentView.zoomOut(); + } }); } return true; } - case KeyEvent.KEYCODE_H: { + case KeyEvent.KEYCODE_H: { Toast toast = Toast.makeText(this, R.string.message_help_key, Toast.LENGTH_LONG); toast.show(); return true; } } - return super.onKeyDown(keyCode, event); - } + return super.onKeyDown(keyCode, event); + } class MessageViewHandler extends Handler { private static final int MSG_PROGRESS = 2; @@ -448,11 +460,11 @@ public class MessageView extends K9Activity Uri uri = intent.getData(); if (uri==null) { - mAccount = (Account) intent.getSerializableExtra(EXTRA_ACCOUNT); - mFolder = intent.getStringExtra(EXTRA_FOLDER); - mMessageUid = intent.getStringExtra(EXTRA_MESSAGE); - mFolderUids = intent.getStringArrayListExtra(EXTRA_FOLDER_UIDS); - + mAccount = (Account) intent.getSerializableExtra(EXTRA_ACCOUNT); + mFolder = intent.getStringExtra(EXTRA_FOLDER); + mMessageUid = intent.getStringExtra(EXTRA_MESSAGE); + mFolderUids = intent.getStringArrayListExtra(EXTRA_FOLDER_UIDS); + Log.v(Email.LOG_TAG, "mAccount number: " + mAccount.getAccountNumber()); Log.v(Email.LOG_TAG, "mFolder: " + mFolder); Log.v(Email.LOG_TAG, "mMessageUid: " + mMessageUid); @@ -495,7 +507,7 @@ public class MessageView extends K9Activity next = findViewById(R.id.next); previous = findViewById(R.id.previous); - + setOnClickListener(R.id.next); setOnClickListener(R.id.previous); @@ -509,7 +521,7 @@ public class MessageView extends K9Activity Account.HideButtons hideButtons = mAccount.getHideMessageViewButtons(); - //MessagingController.getInstance(getApplication()).addListener(mListener); + // MessagingController.getInstance(getApplication()).addListener(mListener); if (Account.HideButtons.ALWAYS == hideButtons) { hideButtons(); @@ -521,7 +533,7 @@ public class MessageView extends K9Activity else // Account.HideButtons.KEYBOARD_AVAIL { final Configuration config = this.getResources().getConfiguration(); - if (config.keyboardHidden == Configuration.KEYBOARDHIDDEN_NO ) + if (config.hardKeyboardHidden == Configuration.HARDKEYBOARDHIDDEN_NO ) { hideButtons(); } @@ -595,7 +607,7 @@ public class MessageView extends K9Activity super.onResume(); clearFormats(); } - + private void onDelete() { if (mMessage != null) { Message messageToDelete = mMessage; @@ -1172,8 +1184,8 @@ public class MessageView extends K9Activity if (!message.isSet(Flag.X_DOWNLOADED_FULL)) { mHandler.post(new Runnable() { public void run() { - mMessageContentView.loadUrl("file:///android_asset/downloading.html"); - } + mMessageContentView.loadUrl("file:///android_asset/downloading.html"); + } }); } try { @@ -1232,8 +1244,8 @@ public class MessageView extends K9Activity else { mHandler.post(new Runnable() { public void run() { - mMessageContentView.loadUrl("file:///android_asset/empty.html"); - } + mMessageContentView.loadUrl("file:///android_asset/empty.html"); + } }); } @@ -1261,7 +1273,7 @@ public class MessageView extends K9Activity mHandler.invalidIdError(); } else { - mHandler.networkError(); + mHandler.networkError(); } mMessageContentView.loadUrl("file:///android_asset/empty.html"); } @@ -1291,7 +1303,7 @@ public class MessageView extends K9Activity mHandler.post(new Runnable() { public void run() { mMessageContentView.loadUrl("file:///android_asset/loading.html"); - setProgressBarIndeterminateVisibility(true); + setProgressBarIndeterminateVisibility(true); } }); } @@ -1357,7 +1369,7 @@ public class MessageView extends K9Activity } catch (Exception e) { - Toast toast = Toast.makeText(MessageView.this, e.getMessage(), Toast.LENGTH_LONG); + Toast toast = Toast.makeText(MessageView.this, getString(R.string.message_view_no_viewer, attachment.contentType), Toast.LENGTH_LONG); toast.show(); } } diff --git a/src/com/android/email/activity/setup/AccountSettings.java b/src/com/android/email/activity/setup/AccountSettings.java index dd2f0be4a..10c06ffba 100644 --- a/src/com/android/email/activity/setup/AccountSettings.java +++ b/src/com/android/email/activity/setup/AccountSettings.java @@ -5,6 +5,7 @@ import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.os.Bundle; +import android.util.Log; import android.view.KeyEvent; import android.view.View; import android.view.View.OnClickListener; @@ -25,6 +26,7 @@ import com.android.email.R; import com.android.email.activity.ChooseFolder; import com.android.email.activity.ChooseIdentity; import com.android.email.activity.ManageIdentities; +import com.android.email.mail.Store; public class AccountSettings extends K9PreferenceActivity { private static final String EXTRA_ACCOUNT = "account"; @@ -49,6 +51,7 @@ public class AccountSettings extends K9PreferenceActivity { private static final String PREFERENCE_OUTGOING = "outgoing"; private static final String PREFERENCE_DISPLAY_MODE = "folder_display_mode"; private static final String PREFERENCE_SYNC_MODE = "folder_sync_mode"; + private static final String PREFERENCE_PUSH_MODE = "folder_push_mode"; private static final String PREFERENCE_TARGET_MODE = "folder_target_mode"; private static final String PREFERENCE_DELETE_POLICY = "delete_policy"; private static final String PREFERENCE_AUTO_EXPAND_FOLDER = "account_setup_auto_expand_folder"; @@ -66,6 +69,7 @@ public class AccountSettings extends K9PreferenceActivity { private RingtonePreference mAccountRingtone; private ListPreference mDisplayMode; private ListPreference mSyncMode; + private ListPreference mPushMode; private ListPreference mTargetMode; private ListPreference mDeletePolicy; private Preference mAutoExpandFolder; @@ -82,6 +86,18 @@ public class AccountSettings extends K9PreferenceActivity { mAccount = (Account)getIntent().getSerializableExtra(EXTRA_ACCOUNT); + boolean isPushCapable = false; + Store store = null; + try + { + store = Store.getInstance(mAccount.getStoreUri(), getApplication()); + isPushCapable = store.isPushCapable(); + } + catch (Exception e) + { + Log.e(Email.LOG_TAG, "Could not get remote store", e); + } + addPreferencesFromResource(R.xml.account_settings_preferences); Preference category = findPreference(PREFERENCE_TOP_CATERGORY); @@ -139,6 +155,20 @@ public class AccountSettings extends K9PreferenceActivity { } }); + mPushMode = (ListPreference) findPreference(PREFERENCE_PUSH_MODE); + mPushMode.setEnabled(isPushCapable); + mPushMode.setValue(mAccount.getFolderPushMode().name()); + mPushMode.setSummary(mPushMode.getEntry()); + mPushMode.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { + public boolean onPreferenceChange(Preference preference, Object newValue) { + final String summary = newValue.toString(); + int index = mPushMode.findIndexOfValue(summary); + mPushMode.setSummary(mPushMode.getEntries()[index]); + mPushMode.setValue(summary); + return false; + } + }); + mTargetMode = (ListPreference) findPreference(PREFERENCE_TARGET_MODE); mTargetMode.setValue(mAccount.getFolderTargetMode().name()); mTargetMode.setSummary(mTargetMode.getEntry()); @@ -274,6 +304,7 @@ public class AccountSettings extends K9PreferenceActivity { mAccount.setVibrate(mAccountVibrate.isChecked()); mAccount.setFolderDisplayMode(Account.FolderMode.valueOf(mDisplayMode.getValue())); mAccount.setFolderSyncMode(Account.FolderMode.valueOf(mSyncMode.getValue())); + mAccount.setFolderPushMode(Account.FolderMode.valueOf(mPushMode.getValue())); mAccount.setFolderTargetMode(Account.FolderMode.valueOf(mTargetMode.getValue())); mAccount.setDeletePolicy(Integer.parseInt(mDeletePolicy.getValue())); SharedPreferences prefs = mAccountRingtone.getPreferenceManager().getSharedPreferences(); diff --git a/src/com/android/email/activity/setup/AccountSetupCheckSettings.java b/src/com/android/email/activity/setup/AccountSetupCheckSettings.java index 7809a7121..15b893e91 100644 --- a/src/com/android/email/activity/setup/AccountSetupCheckSettings.java +++ b/src/com/android/email/activity/setup/AccountSetupCheckSettings.java @@ -104,17 +104,10 @@ public class AccountSetupCheckSettings extends K9Activity implements OnClickList setMessage(R.string.account_setup_check_settings_check_incoming_msg); store = Store.getInstance(mAccount.getStoreUri(), getApplication()); store.checkSettings(); - new Thread() { - - public void run() { - Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); - MessagingController.getInstance(getApplication()).listFolders(mAccount, true, null); - MessagingController.getInstance(getApplication()).synchronizeMailbox(mAccount, Email.INBOX , null); + + MessagingController.getInstance(getApplication()).listFolders(mAccount, true, null); + MessagingController.getInstance(getApplication()).synchronizeMailbox( mAccount, Email.INBOX , null); - } - }.start(); - - } if (mDestroyed) { return; diff --git a/src/com/android/email/activity/setup/FolderSettings.java b/src/com/android/email/activity/setup/FolderSettings.java index 2930f6e18..f5394ad7f 100644 --- a/src/com/android/email/activity/setup/FolderSettings.java +++ b/src/com/android/email/activity/setup/FolderSettings.java @@ -32,11 +32,13 @@ public class FolderSettings extends K9PreferenceActivity { private static final String PREFERENCE_TOP_CATERGORY = "folder_settings"; private static final String PREFERENCE_DISPLAY_CLASS = "folder_settings_folder_display_mode"; private static final String PREFERENCE_SYNC_CLASS = "folder_settings_folder_sync_mode"; + private static final String PREFERENCE_PUSH_CLASS = "folder_settings_folder_push_mode"; private LocalFolder mFolder; private ListPreference mDisplayClass; private ListPreference mSyncClass; + private ListPreference mPushClass; public static void actionSettings(Context context, Account account, String folderName) { Intent i = new Intent(context, FolderSettings.class); @@ -52,18 +54,30 @@ public class FolderSettings extends K9PreferenceActivity { String folderName = (String)getIntent().getSerializableExtra(EXTRA_FOLDER_NAME); Account mAccount = (Account)getIntent().getSerializableExtra(EXTRA_ACCOUNT); - try - { - Store localStore = Store.getInstance(mAccount.getLocalStoreUri(), - getApplication()); - mFolder = (LocalFolder) localStore.getFolder(folderName); - mFolder.refresh(Preferences.getPreferences(this)); - } - catch (MessagingException me) - { - Log.e(Email.LOG_TAG, "Unable to edit folder " + folderName + " preferences", me); - return; - } + try + { + Store localStore = Store.getInstance(mAccount.getLocalStoreUri(), + getApplication()); + mFolder = (LocalFolder) localStore.getFolder(folderName); + mFolder.refresh(Preferences.getPreferences(this)); + } + catch (MessagingException me) + { + Log.e(Email.LOG_TAG, "Unable to edit folder " + folderName + " preferences", me); + return; + } + + boolean isPushCapable = false; + Store store = null; + try + { + store = Store.getInstance(mAccount.getStoreUri(), getApplication()); + isPushCapable = store.isPushCapable(); + } + catch (Exception e) + { + Log.e(Email.LOG_TAG, "Could not get remote store", e); + } addPreferencesFromResource(R.xml.folder_settings_preferences); @@ -95,6 +109,20 @@ public class FolderSettings extends K9PreferenceActivity { return false; } }); + + mPushClass = (ListPreference) findPreference(PREFERENCE_PUSH_CLASS); + mPushClass.setEnabled(isPushCapable); + mPushClass.setValue(mFolder.getRawPushClass().name()); + mPushClass.setSummary(mPushClass.getEntry()); + mPushClass.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { + public boolean onPreferenceChange(Preference preference, Object newValue) { + final String summary = newValue.toString(); + int index = mPushClass.findIndexOfValue(summary); + mPushClass.setSummary(mPushClass.getEntries()[index]); + mPushClass.setValue(summary); + return false; + } + }); } @Override @@ -113,10 +141,12 @@ public class FolderSettings extends K9PreferenceActivity { private void saveSettings() { mFolder.setDisplayClass(FolderClass.valueOf(mDisplayClass.getValue())); mFolder.setSyncClass(FolderClass.valueOf(mSyncClass.getValue())); + mFolder.setPushClass(FolderClass.valueOf(mPushClass.getValue())); try { mFolder.save(Preferences.getPreferences(this)); + Email.setServicesEnabled(this); } catch (MessagingException me) { diff --git a/src/com/android/email/mail/Folder.java b/src/com/android/email/mail/Folder.java index 80cabc884..80f0f762b 100644 --- a/src/com/android/email/mail/Folder.java +++ b/src/com/android/email/mail/Folder.java @@ -6,12 +6,13 @@ import com.android.email.Preferences; public abstract class Folder { private String status = null; private long lastChecked = 0; + private long lastPush = 0; public enum OpenMode { READ_WRITE, READ_ONLY, } - + // NONE is obsolete, it will be translated to NO_CLASS for display and to INHERITED for sync and push public enum FolderClass { - NONE, FIRST_CLASS, SECOND_CLASS; + NONE, NO_CLASS, INHERITED, FIRST_CLASS, SECOND_CLASS; } public enum FolderType { @@ -109,6 +110,17 @@ public abstract class Folder { public abstract String getName(); public abstract Flag[] getPermanentFlags() throws MessagingException; + + /** + * + * @param oldPushState + * @param message + * @return empty string to clear the pushState, null to leave the state as-is + */ + public String getNewPushState(String oldPushState, Message message) + { + return null; + } public boolean supportsFetchingFlags() { return true; @@ -129,15 +141,34 @@ public abstract class Folder { this.lastChecked = lastChecked; } + public long getLastPush() + { + return lastPush; + } + + public void setLastPush(long lastCheckedDisplay) throws MessagingException + { + this.lastPush = lastCheckedDisplay; + } + + public long getLastUpdate() + { + return Math.max(getLastChecked(), getLastPush()); + } + public FolderClass getDisplayClass() { - return FolderClass.NONE; + return FolderClass.NO_CLASS; } public FolderClass getSyncClass() { return getDisplayClass(); } + public FolderClass getPushClass() + { + return getSyncClass(); + } public void refresh(Preferences preferences) throws MessagingException { @@ -154,6 +185,5 @@ public abstract class Folder { this.status = status; } - - + } diff --git a/src/com/android/email/mail/MessageRetrievalListener.java b/src/com/android/email/mail/MessageRetrievalListener.java index fd070521d..b81385a3a 100644 --- a/src/com/android/email/mail/MessageRetrievalListener.java +++ b/src/com/android/email/mail/MessageRetrievalListener.java @@ -5,4 +5,6 @@ public interface MessageRetrievalListener { public void messageStarted(String uid, int number, int ofTotal); public void messageFinished(Message message, int number, int ofTotal); + + public void messagesFinished(int total); } diff --git a/src/com/android/email/mail/MessagingException.java b/src/com/android/email/mail/MessagingException.java index a1882ca82..9b67de1c2 100644 --- a/src/com/android/email/mail/MessagingException.java +++ b/src/com/android/email/mail/MessagingException.java @@ -9,10 +9,20 @@ public class MessagingException extends Exception { public MessagingException(String message) { super(message); } + + public MessagingException(String message, boolean perm) { + super(message); + permanentFailure = perm; + } public MessagingException(String message, Throwable throwable) { super(message, throwable); } + + public MessagingException(String message, boolean perm, Throwable throwable) { + super(message, throwable); + permanentFailure = perm; + } public boolean isPermanentFailure() { diff --git a/src/com/android/email/mail/PushReceiver.java b/src/com/android/email/mail/PushReceiver.java new file mode 100644 index 000000000..c1cb40e73 --- /dev/null +++ b/src/com/android/email/mail/PushReceiver.java @@ -0,0 +1,14 @@ +package com.android.email.mail; + +import java.util.List; + +public interface PushReceiver +{ + public void pushInProgress(); + public void pushComplete(); + public void messagesArrived(String folderName, List mess); + public void messagesFlagsChanged(String folderName, List mess); + public String getPushState(String folderName); + public void pushError(String errorMessage, Exception e); + public void setPushActive(String folderName, boolean enabled); +} diff --git a/src/com/android/email/mail/Pusher.java b/src/com/android/email/mail/Pusher.java new file mode 100644 index 000000000..c2d793937 --- /dev/null +++ b/src/com/android/email/mail/Pusher.java @@ -0,0 +1,14 @@ +package com.android.email.mail; + + +public interface Pusher +{ + public void start(); + public void refresh(); + public void stop(); + /** + * + * @return milliseconds of required refresh interval + */ + public int getRefreshInterval(); +} diff --git a/src/com/android/email/mail/Store.java b/src/com/android/email/mail/Store.java index 860a711d9..e8cf01734 100644 --- a/src/com/android/email/mail/Store.java +++ b/src/com/android/email/mail/Store.java @@ -2,6 +2,7 @@ package com.android.email.mail; import java.util.HashMap; +import java.util.List; import android.app.Application; @@ -85,5 +86,13 @@ public abstract class Store { public boolean isMoveCapable() { return false; } + public boolean isPushCapable() { + return false; + } + + public Pusher getPusher(PushReceiver receiver, List names) + { + return null; + } } diff --git a/src/com/android/email/mail/store/ImapResponseParser.java b/src/com/android/email/mail/store/ImapResponseParser.java index f8c19db2a..47e40d607 100644 --- a/src/com/android/email/mail/store/ImapResponseParser.java +++ b/src/com/android/email/mail/store/ImapResponseParser.java @@ -91,7 +91,7 @@ public class ImapResponseParser { public Object readToken() throws IOException { while (true) { Object token = parseToken(); - if (token == null || !token.equals(")")) { + if (token == null || !token.equals(")") || !token.equals("]")) { return token; } } @@ -107,9 +107,14 @@ public class ImapResponseParser { int ch = mIn.peek(); if (ch == '(') { return parseList(); + } else if (ch == '[') { + return parseSequence(); } else if (ch == ')') { expect(')'); return ")"; + } else if (ch == ']') { + expect(']'); + return "]"; } else if (ch == '"') { return parseQuoted(); } else if (ch == '{') { @@ -169,6 +174,26 @@ public class ImapResponseParser { } return list; } + + private ImapList parseSequence() throws IOException { + expect('['); + ImapList list = new ImapList(); + Object token; + while (true) { + token = parseToken(); + if (token == null) { + break; + } else if (token instanceof InputStream) { + list.add(token); + break; + } else if (token.equals("]")) { + break; + } else { + list.add(token); + } + } + return list; + } private String parseAtom() throws IOException { StringBuffer sb = new StringBuffer(); @@ -178,6 +203,7 @@ public class ImapResponseParser { if (ch == -1) { throw new IOException("parseAtom(): end of stream reached"); } else if (ch == '(' || ch == ')' || ch == '{' || ch == ' ' || + ch == '[' || ch == ']' || // docs claim that flags are \ atom but atom isn't supposed to // contain // * and some falgs contain * diff --git a/src/com/android/email/mail/store/ImapStore.java b/src/com/android/email/mail/store/ImapStore.java index 0c3345a69..792d6f043 100644 --- a/src/com/android/email/mail/store/ImapStore.java +++ b/src/com/android/email/mail/store/ImapStore.java @@ -12,6 +12,7 @@ import java.net.ConnectException; import java.net.InetSocketAddress; import java.net.Socket; import java.net.SocketAddress; +import java.net.SocketException; import java.net.URI; import java.net.URISyntaxException; import java.nio.ByteBuffer; @@ -24,17 +25,20 @@ import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.HashMap; +import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; +import java.util.Set; +import java.util.StringTokenizer; import java.util.UUID; +import java.util.concurrent.atomic.AtomicBoolean; import javax.net.ssl.SSLContext; import javax.net.ssl.TrustManager; import javax.net.ssl.SSLException; -import android.util.Config; import android.util.Log; import com.android.email.Email; @@ -48,6 +52,8 @@ import com.android.email.mail.Message; import com.android.email.mail.MessageRetrievalListener; import com.android.email.mail.MessagingException; import com.android.email.mail.Part; +import com.android.email.mail.PushReceiver; +import com.android.email.mail.Pusher; import com.android.email.mail.Store; import com.android.email.mail.CertificateValidationException; import com.android.email.mail.Folder.FolderType; @@ -85,6 +91,9 @@ public class ImapStore extends Store { public static final int CONNECTION_SECURITY_TLS_REQUIRED = 2; public static final int CONNECTION_SECURITY_SSL_REQUIRED = 3; public static final int CONNECTION_SECURITY_SSL_OPTIONAL = 4; + + private static final int IDLE_READ_TIMEOUT = 29 * 60 * 1000; // 29 minutes + private static final int IDLE_REFRESH_INTERVAL = 20 * 60 * 1000; // 20 minutes private static final Flag[] PERMANENT_FLAGS = { Flag.DELETED, Flag.SEEN }; @@ -323,11 +332,23 @@ public class ImapStore extends Store { { return true; } + @Override + public boolean isPushCapable() + { + return true; + } + + @Override + public Pusher getPusher(PushReceiver receiver, List names) + { + return new ImapPusher(this, receiver, names); + } class ImapFolder extends Folder { private String mName; - private int mMessageCount = -1; - private ImapConnection mConnection; + protected int mMessageCount = -1; + protected int uidNext = -1; + protected ImapConnection mConnection; private OpenMode mMode; private boolean mExists; private ImapStore store = null; @@ -339,89 +360,126 @@ public class ImapStore extends Store { public String getPrefixedName() { String prefixedName = ""; - if(mPathPrefix != null && mPathPrefix.length() > 0 && !Email.INBOX.equalsIgnoreCase(mName)){ - prefixedName += mPathPrefix + mPathDelimeter; + if (!Email.INBOX.equalsIgnoreCase(mName)) + { + String prefix = mPathPrefix; + String delim = mPathDelimeter; + + if (prefix != null && delim != null) + { + prefix = prefix.trim(); + delim = delim.trim(); + if (prefix.length() > 0 && delim.length() > 0) + { + prefixedName += mPathPrefix + mPathDelimeter; + } + } } - + prefixedName += mName; return prefixedName; } - private List executeSimpleCommand(String command) throws MessagingException, IOException + protected List executeSimpleCommand(String command) throws MessagingException, IOException { return handleUntaggedResponses(mConnection.executeSimpleCommand(command)); } - - public void open(OpenMode mode) throws MessagingException { - if (isOpen() && mMode == mode) { - // Make sure the connection is valid. If it's not we'll close it down and continue - // on to get a new one. - try { - executeSimpleCommand("NOOP"); - return; - } - catch (IOException ioe) { - ioExceptionHandler(mConnection, ioe); - } - } - synchronized (this) { - mConnection = getConnection(); - } - // * FLAGS (\Answered \Flagged \Deleted \Seen \Draft NonJunk - // $MDNSent) - // * OK [PERMANENTFLAGS (\Answered \Flagged \Deleted \Seen \Draft - // NonJunk $MDNSent \*)] Flags permitted. - // * 23 EXISTS - // * 0 RECENT - // * OK [UIDVALIDITY 1125022061] UIDs valid - // * OK [UIDNEXT 57576] Predicted next UID - // 2 OK [READ-WRITE] Select completed. - try { - if(mPathDelimeter == null){ - List nameResponses = - executeSimpleCommand(String.format("LIST \"\" \"*%s\"", encodeFolderName(mName))); - if(nameResponses.size() > 0){ - mPathDelimeter = nameResponses.get(0).getString(2); - } - } - - // executeSimpleCommand("CLOSE"); - - String command = String.format("SELECT \"%s\"", - encodeFolderName(getPrefixedName())); + protected List executeSimpleCommand(String command, boolean sensitve, UntaggedHandler untaggedHandler) throws MessagingException, IOException + { + return handleUntaggedResponses(mConnection.executeSimpleCommand(command, sensitve, untaggedHandler)); + } + + public void open(OpenMode mode) throws MessagingException + { + internalOpen(mode); + } - //mMessageCount = -1; - - List responses = executeSimpleCommand(command); - - /* - * If the command succeeds we expect the folder has been opened read-write - * unless we are notified otherwise in the responses. - */ - mMode = OpenMode.READ_WRITE; - - for (ImapResponse response : responses) { - if (response.mTag != null && response.size() >= 2) { - if ("[READ-ONLY]".equalsIgnoreCase(response.getString(1))) { - mMode = OpenMode.READ_ONLY; - } - else if ("[READ-WRITE]".equalsIgnoreCase(response.getString(1))) { - mMode = OpenMode.READ_WRITE; - } - } - } - - if (mMessageCount == -1) { - throw new MessagingException( - "Did not find message count with command '" + command + "'"); - } - mExists = true; - - } catch (IOException ioe) { - throw ioExceptionHandler(mConnection, ioe); - } - } + public List internalOpen(OpenMode mode) throws MessagingException { + if (isOpen() && mMode == mode) { + // Make sure the connection is valid. If it's not we'll close it down and continue + // on to get a new one. + try { + List responses = executeSimpleCommand("NOOP"); + return responses; + } + catch (IOException ioe) { + ioExceptionHandler(mConnection, ioe); + } + } + synchronized (this) { + mConnection = getConnection(); + } + // * FLAGS (\Answered \Flagged \Deleted \Seen \Draft NonJunk + // $MDNSent) + // * OK [PERMANENTFLAGS (\Answered \Flagged \Deleted \Seen \Draft + // NonJunk $MDNSent \*)] Flags permitted. + // * 23 EXISTS + // * 0 RECENT + // * OK [UIDVALIDITY 1125022061] UIDs valid + // * OK [UIDNEXT 57576] Predicted next UID + // 2 OK [READ-WRITE] Select completed. + try { + + if(mPathDelimeter == null){ + List nameResponses = + executeSimpleCommand(String.format("LIST \"\" \"*%s\"", encodeFolderName(mName))); + if(nameResponses.size() > 0){ + mPathDelimeter = nameResponses.get(0).getString(2); + } + } + + // executeSimpleCommand("CLOSE"); + + String command = String.format("SELECT \"%s\"", + encodeFolderName(getPrefixedName())); + + List responses = executeSimpleCommand(command); + + /* + * If the command succeeds we expect the folder has been opened read-write + * unless we are notified otherwise in the responses. + */ + mMode = OpenMode.READ_WRITE; + + for (ImapResponse response : responses) { + if (response.mTag != null && response.size() >= 2) { + Object bracketedObj = response.get(1); + if (bracketedObj instanceof ImapList) + { + ImapList bracketed = (ImapList)bracketedObj; + + if (bracketed.size() > 0) + { + Object keyObj = bracketed.get(0); + if (keyObj instanceof String) + { + String key = (String)keyObj; + + if ("READ-ONLY".equalsIgnoreCase(key)) { + mMode = OpenMode.READ_ONLY; + } + else if ("READ-WRITE".equalsIgnoreCase(key)) { + mMode = OpenMode.READ_WRITE; + } + } + } + } + + } + } + + if (mMessageCount == -1) { + throw new MessagingException( + "Did not find message count with command '" + command + "'"); + } + mExists = true; + return null; + } catch (IOException ioe) { + throw ioExceptionHandler(mConnection, ioe); + } + + } public boolean isOpen() { return mConnection != null; @@ -585,12 +643,20 @@ public class ImapStore extends Store { @Override public Message getMessage(String uid) throws MessagingException { - return new ImapMessage(uid, this); + return new ImapMessage(uid, this); } + @Override public Message[] getMessages(int start, int end, MessageRetrievalListener listener) throws MessagingException { + + + return getMessages(start, end, false, listener); + } + + protected Message[] getMessages(int start, int end, boolean includeDeleted, MessageRetrievalListener listener) + throws MessagingException { if (start < 1 || end < 1 || end < start) { throw new MessagingException( String.format("Invalid message set %d %d", @@ -601,7 +667,7 @@ public class ImapStore extends Store { try { boolean gotSearchValues = false; ArrayList uids = new ArrayList(); - List responses = executeSimpleCommand(String.format("UID SEARCH %d:%d NOT DELETED", start, end)); + List responses = executeSimpleCommand(String.format("UID SEARCH %d:%d" + (includeDeleted ? "" : " NOT DELETED"), start, end)); for (ImapResponse response : responses) { // Log.d(Email.LOG_TAG, "Got search response: " + response.get(0) + ", size " + response.size()); if (response.get(0).equals("SEARCH")) { @@ -734,13 +800,20 @@ public class ImapStore extends Store { do { response = mConnection.readResponse(); handleUntaggedResponse(response); -//Log.v(Email.LOG_TAG, "response for fetch: " + response); + if (Email.DEBUG) + { + Log.v(Email.LOG_TAG, "response for fetch: " + response); + } if (response.mTag == null && response.get(1).equals("FETCH")) { ImapList fetchList = (ImapList)response.getKeyedValue("FETCH"); String uid = fetchList.getKeyedString("UID"); Message message = messageMap.get(uid); - + if (message == null) + { + Log.w(Email.LOG_TAG, "Do not have message in messageMap for UID " + uid); + continue; + } if (listener != null) { listener.messageStarted(uid, messageNumber++, messageMap.size()); } @@ -784,8 +857,8 @@ public class ImapStore extends Store { parseBodyStructure(bs, message, "TEXT"); } catch (MessagingException e) { - if (Config.LOGV) { - Log.v(Email.LOG_TAG, "Error handling message", e); + if (Email.DEBUG) { + Log.d(Email.LOG_TAG, "Error handling message", e); } message.setBody(null); } @@ -820,7 +893,10 @@ public class ImapStore extends Store { { String bodyString = (String)literal; - Log.i(Email.LOG_TAG, "Part is an String: " + bodyString); + if (Email.DEBUG) + { + Log.v(Email.LOG_TAG, "Part is an String: " + bodyString); + } InputStream bodyStream = new ByteArrayInputStream(bodyString.getBytes()); String contentTransferEncoding = part.getHeader( MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING)[0]; @@ -854,7 +930,7 @@ public class ImapStore extends Store { * Handle any untagged responses that the caller doesn't care to handle themselves. * @param responses */ - private List handleUntaggedResponses(List responses) { + protected List handleUntaggedResponses(List responses) { for (ImapResponse response : responses) { handleUntaggedResponse(response); } @@ -865,16 +941,48 @@ public class ImapStore extends Store { * Handle an untagged response that the caller doesn't care to handle themselves. * @param response */ - private void handleUntaggedResponse(ImapResponse response) { + protected void handleUntaggedResponse(ImapResponse response) { if (response.mTag == null && response.size() > 1) { if (response.get(1).equals("EXISTS")) { mMessageCount = response.getNumber(0); - //Log.i(Email.LOG_TAG, "Got untagged EXISTS with value " + mMessageCount); + if (Email.DEBUG) + { + Log.d(Email.LOG_TAG, "Got untagged EXISTS with value " + mMessageCount); + } + } + if (response.get(0).equals("OK") && response.size() > 1) { + Object bracketedObj = response.get(1); + if (bracketedObj instanceof ImapList) + { + ImapList bracketed = (ImapList)bracketedObj; + + if (bracketed.size() > 1) + { + Object keyObj = bracketed.get(0); + if (keyObj instanceof String) + { + String key = (String)keyObj; + if ("UIDNEXT".equals(key)) + { + uidNext = bracketed.getNumber(1); + if (Email.DEBUG) + { + Log.d(Email.LOG_TAG, "Got UidNext = " + uidNext); + } + } + } + } + + + } } else if (response.get(1).equals("EXPUNGE") && mMessageCount > 0) { mMessageCount--; - // Log.i(Email.LOG_TAG, "Got untagged EXPUNGE with value " + mMessageCount); + if (Email.DEBUG) + { + Log.d(Email.LOG_TAG, "Got untagged EXPUNGE with value " + mMessageCount); + } } } //Log.i(Email.LOG_TAG, "mMessageCount = " + mMessageCount); @@ -1073,7 +1181,7 @@ public class ImapStore extends Store { } while(response.mTag == null); String newUid = getUidFromMessageId(message); - if (Config.LOGD) + if (Email.DEBUG) { Log.d(Email.LOG_TAG, "Got UID " + newUid + " for message"); } @@ -1102,14 +1210,14 @@ public class ImapStore extends Store { String[] messageIdHeader = message.getHeader("Message-ID"); if (messageIdHeader == null || messageIdHeader.length == 0) { - if (Config.LOGD) + if (Email.DEBUG) { Log.d(Email.LOG_TAG, "Did not get a message-id in order to search for UID"); } return null; } String messageId = messageIdHeader[0]; - if (Config.LOGD) + if (Email.DEBUG) { Log.d(Email.LOG_TAG, "Looking for UID for message with message-id " + messageId); } @@ -1188,6 +1296,35 @@ public class ImapStore extends Store { throw ioExceptionHandler(mConnection, ioe); } } + + public String getNewPushState(String oldPushStateS, Message message) + { + try + { + String messageUidS = message.getUid(); + int messageUid = Integer.parseInt(messageUidS); + ImapPushState oldPushState = ImapPushState.parse(oldPushStateS); +// Log.d(Email.LOG_TAG, "getNewPushState comparing oldUidNext " + oldPushState.uidNext +// + " to message uid " + messageUid); + if (messageUid >= oldPushState.uidNext) + { + int uidNext = messageUid + 1; + ImapPushState newPushState = new ImapPushState(uidNext); + //Log.d(Email.LOG_TAG, "newPushState = " + newPushState); + return newPushState.toString(); + } + else + { + return null; + } + } + catch (Exception e) + { + Log.e(Email.LOG_TAG, "Exception while updated push state", e); + return null; + } + } + public void setFlags(Message[] messages, Flag[] flags, boolean value) throws MessagingException { @@ -1259,6 +1396,7 @@ public class ImapStore extends Store { private OutputStream mOut; private ImapResponseParser mParser; private int mNextCommandTag; + protected Set capabilities = new HashSet(); public void open() throws IOException, MessagingException { if (isOpen()) { @@ -1297,23 +1435,44 @@ public class ImapStore extends Store { mSocket.connect(socketAddress, SOCKET_CONNECT_TIMEOUT); } - mSocket.setSoTimeout(Store.SOCKET_READ_TIMEOUT); + setReadTimeout(Store.SOCKET_READ_TIMEOUT); mIn = new PeekableInputStream(new BufferedInputStream(mSocket.getInputStream(), 1024)); mParser = new ImapResponseParser(mIn); mOut = mSocket.getOutputStream(); - // BANNER mParser.readResponse(); - + List responses = executeSimpleCommand("CAPABILITY"); + if (responses.size() != 2) { + throw new MessagingException("Invalid CAPABILITY response received"); + } + capabilities.clear(); + for (ImapResponse response : responses) + { + if (response.mTag == null) + { + if (response.size() > 0) + { + for (Object capability : response) + { + if (capability instanceof String) + { + if (Email.DEBUG) + { + Log.v(Email.LOG_TAG, "Saving capability '" + capability + "' for connection " + this.hashCode()); + } + capabilities.add((String)capability); + } + } + + } + } + } + if (mConnectionSecurity == CONNECTION_SECURITY_TLS_OPTIONAL || mConnectionSecurity == CONNECTION_SECURITY_TLS_REQUIRED) { - // CAPABILITY - List responses = executeSimpleCommand("CAPABILITY"); - if (responses.size() != 2) { - throw new MessagingException("Invalid CAPABILITY response received"); - } + if (responses.get(0).contains("STARTTLS")) { // STARTTLS executeSimpleCommand("STARTTLS"); @@ -1377,6 +1536,24 @@ public class ImapStore extends Store { } } } + + protected void setReadTimeout(int millis) throws SocketException + { + mSocket.setSoTimeout(millis); + } + + protected boolean isIdleCapable() + { + if (Email.DEBUG) + { + Log.v(Email.LOG_TAG, "Connection " + this.hashCode() + " has " + capabilities.size() + " capabilities"); + for (String capability : capabilities) + { + Log.v(Email.LOG_TAG, "Have capability '" + capability + "'"); + } + } + return capabilities.contains("IDLE"); + } public boolean isOpen() { return (mIn != null && mOut != null && mSocket != null && mSocket.isConnected() && !mSocket @@ -1432,6 +1609,19 @@ public class ImapStore extends Store { out = out.replaceAll("\"", "\\\\\""); return out; } + + public void sendContinuation(String continuation) throws IOException + { + mOut.write(continuation.getBytes()); + mOut.write('\r'); + mOut.write('\n'); + mOut.flush(); + + if (Email.DEBUG) { + Log.v(Email.LOG_TAG, ">>> " + continuation); + } + + } public String sendCommand(String command, boolean sensitive) throws MessagingException, IOException { @@ -1443,16 +1633,16 @@ public class ImapStore extends Store { mOut.write('\r'); mOut.write('\n'); mOut.flush(); - if (Config.LOGD) { - if (Email.DEBUG) { - if (sensitive && !Email.DEBUG_SENSITIVE) { - Log.d(Email.LOG_TAG, ">>> " - + "[Command Hidden, Enable Sensitive Debug Logging To Show]"); - } else { - Log.d(Email.LOG_TAG, ">>> " + commandToSend); - } + + if (Email.DEBUG) { + if (sensitive && !Email.DEBUG_SENSITIVE) { + Log.v(Email.LOG_TAG, ">>> " + + "[Command Hidden, Enable Sensitive Debug Logging To Show]"); + } else { + Log.v(Email.LOG_TAG, ">>> " + commandToSend); } } + return tag; } catch (IOException ioe) @@ -1476,15 +1666,20 @@ public class ImapStore extends Store { ImapException, MessagingException { return executeSimpleCommand(command, false); } - - public List executeSimpleCommand(String command, boolean sensitive) + + public List executeSimpleCommand(String command, boolean sensitive) throws IOException, + ImapException, MessagingException { + return executeSimpleCommand(command, sensitive, null); + } + + public List executeSimpleCommand(String command, boolean sensitive, UntaggedHandler untaggedHandler) throws IOException, ImapException, MessagingException { - if (Config.LOGV) + if (Email.DEBUG) { Log.v(Email.LOG_TAG, "Sending IMAP command " + command + " on connection " + this.hashCode()); } String tag = sendCommand(command, sensitive); - if (Config.LOGV) + if (Email.DEBUG) { Log.v(Email.LOG_TAG, "Sent IMAP command " + command + " with tag " + tag); } @@ -1492,7 +1687,7 @@ public class ImapStore extends Store { ImapResponse response; do { response = mParser.readResponse(); - if (Config.LOGV) + if (Email.DEBUG) { Log.v(Email.LOG_TAG, "Got IMAP response " + response); } @@ -1512,6 +1707,10 @@ public class ImapStore extends Store { response.mTag = null; continue; } + if (untaggedHandler != null) + { + untaggedHandler.handleAsyncUntaggedResponse(response); + } responses.add(response); } while (response.mTag == null); if (response.size() < 1 || !response.get(0).equals("OK")) { @@ -1538,7 +1737,8 @@ public class ImapStore extends Store { public void setFlagInternal(Flag flag, boolean set) throws MessagingException { super.setFlag(flag, set); } - + + @Override public void setFlag(Flag flag, boolean set) throws MessagingException { super.setFlag(flag, set); @@ -1556,10 +1756,11 @@ public class ImapStore extends Store { } else { - Folder remoteTrashFolder = iFolder.getStore().getFolder(trashFolderName); + ImapFolder remoteTrashFolder = (ImapFolder)iFolder.getStore().getFolder(trashFolderName); /* * Attempt to copy the remote message to the remote trash folder. */ + remoteTrashFolder.mExists = false; // Force redetection of Trash folder; some desktops delete it if (!remoteTrashFolder.exists()) { /* * If the remote trash folder doesn't exist we try to create it. @@ -1569,7 +1770,7 @@ public class ImapStore extends Store { } if (remoteTrashFolder.exists()) { - if (Config.LOGD) + if (Email.DEBUG) { Log.d(Email.LOG_TAG, "IMAPMessage.delete: copying remote message to " + trashFolderName); } @@ -1579,9 +1780,8 @@ public class ImapStore extends Store { } else { - // Toast.makeText(context, R.string.message_delete_failed, Toast.LENGTH_SHORT).show(); - - Log.e(Email.LOG_TAG, "IMAPMessage.delete: remote Trash folder " + trashFolderName + " does not exist and could not be created"); + throw new MessagingException("IMAPMessage.delete: remote Trash folder " + trashFolderName + " does not exist and could not be created" + , true); } } } @@ -1619,4 +1819,471 @@ public class ImapStore extends Store { mAlertText = alertText; } } + + public static int MAX_DELAY_TIME = 240000; + public static int NORMAL_DELAY_TIME = 10000; + + public class ImapFolderPusher extends ImapFolder implements UntaggedHandler + { + PushReceiver receiver = null; + Thread listeningThread = null; + AtomicBoolean stop = new AtomicBoolean(false); + AtomicBoolean idling = new AtomicBoolean(false); + AtomicBoolean doneSent = new AtomicBoolean(false); + private int delayTime = NORMAL_DELAY_TIME; + + public ImapFolderPusher(ImapStore store, String name, PushReceiver nReceiver) + { + super(store, name); + receiver = nReceiver; + } + public void refresh() throws IOException, MessagingException + { + if (idling.get()) + { + receiver.pushInProgress(); + sendDone(); + } + } + + private void sendDone() throws IOException, MessagingException + { + if (doneSent.compareAndSet(false, true) == true) + { + sendContinuation("DONE"); + } + } + + private void sendContinuation(String continuation) + throws MessagingException, IOException + { + if (mConnection != null) + { + mConnection.sendContinuation(continuation); + } + } + + public void start() throws MessagingException + { + + Runnable runner = new Runnable() + { + public void run() + { + Log.i(Email.LOG_TAG, "Pusher for " + getName() + " is running"); + while (stop.get() != true) + { + try + { + int oldUidNext = -1; + try + { + String pushStateS = receiver.getPushState(getName()); + ImapPushState pushState = ImapPushState.parse(pushStateS); + oldUidNext = pushState.uidNext; + Log.i(Email.LOG_TAG, "Got oldUidNext " + oldUidNext); + } + catch (Exception e) + { + Log.e(Email.LOG_TAG, "Unable to get oldUidNext", e); + } + + List responses = internalOpen(OpenMode.READ_ONLY); + if (mConnection == null) + { + receiver.pushError("Could not establish connection for IDLE", null); + throw new MessagingException("Could not establish connection for IDLE"); + + } + if (mConnection.isIdleCapable() == false) + { + stop.set(true); + receiver.pushError("IMAP server is not IDLE capable: " + mConnection.toString(), null); + throw new MessagingException("IMAP server is not IDLE capable:" + mConnection.toString()); + } + mConnection.setReadTimeout(IDLE_READ_TIMEOUT); + + if (responses != null) + { + handleUntaggedResponses(responses); + } + if (uidNext > oldUidNext) + { + int startUid = oldUidNext; + if (startUid < uidNext - 100) + { + startUid = uidNext - 100; + } + if (startUid < 1) + { + startUid = 1; + } + + Log.i(Email.LOG_TAG, "Needs sync from uid " + startUid + " to " + uidNext); + List messages = new ArrayList(); + for (int uid = startUid; uid < uidNext; uid++ ) + { + ImapMessage message = new ImapMessage("" + uid, ImapFolderPusher.this); + messages.add(message); + } + if (messages.size() > 0) + { + pushMessages(messages, true); + } + + } + else + { + Log.i(Email.LOG_TAG, "About to IDLE " + getName()); + + receiver.setPushActive(getName(), true); + idling.set(true); + doneSent.set(false); + executeSimpleCommand("IDLE", false, ImapFolderPusher.this); + idling.set(false); + receiver.setPushActive(getName(), false); + delayTime = NORMAL_DELAY_TIME; + } + } + catch (Exception e) + { + idling.set(false); + receiver.setPushActive(getName(), false); + try + { + close(false); + } + catch (Exception me) + { + Log.e(Email.LOG_TAG, "Got exception while closing for exception", me); + } + if (stop.get() == true) + { + Log.i(Email.LOG_TAG, "Got exception while idling, but stop is set"); + } + else + { + Log.e(Email.LOG_TAG, "Got exception while idling", e); + try + { + Thread.sleep(delayTime); + delayTime *= 2; + if (delayTime > MAX_DELAY_TIME) + { + delayTime = MAX_DELAY_TIME; + } + } + catch (Exception ie) + { + Log.e(Email.LOG_TAG, "Got exception while delaying after push exception", ie); + } + } + } + } + try + { + close(false); + receiver.pushComplete(); + Log.i(Email.LOG_TAG, "Pusher for " + getName() + " is exiting"); + } + catch (Exception me) + { + Log.e(Email.LOG_TAG, "Got exception while closing", me); + } + } + }; + listeningThread = new Thread(runner); + listeningThread.start(); + } + + List flagSyncMsgSeqs = new ArrayList(); + + protected List handleUntaggedResponses(List responses) { + flagSyncMsgSeqs.clear(); + int oldMessageCount = mMessageCount; + + super.handleUntaggedResponses(responses); + + List flagSyncMsgSeqsCopy = new ArrayList(); + flagSyncMsgSeqsCopy.addAll(flagSyncMsgSeqs); + + + if (Email.DEBUG) + { + Log.d(Email.LOG_TAG, "oldMessageCount = " + oldMessageCount + ", new mMessageCount = " + mMessageCount); + } + if (oldMessageCount > 0 && mMessageCount > oldMessageCount) + { + syncMessages(oldMessageCount + 1, mMessageCount, true); + } + if (Email.DEBUG) + { + Log.d(Email.LOG_TAG, "There are " + flagSyncMsgSeqsCopy + " messages needing flag sync"); + } + // TODO: Identify ranges and call syncMessages on said identified ranges + for (Integer msgSeq : flagSyncMsgSeqsCopy) + { + syncMessages(msgSeq, msgSeq, false); + } + + return responses; + } + + private void syncMessages(int start, int end, boolean newArrivals) + { + try + { + Message[] messageArray = null; + + messageArray = getMessages(start, end, true, null); + + List messages = new ArrayList(); + for (Message message : messageArray) + { + messages.add(message); + } + pushMessages(messages, newArrivals); + + } + catch (Exception e) + { + receiver.pushError("Exception while processing Push untagged responses", e); + } + } + + protected void handleUntaggedResponse(ImapResponse response) { + super.handleUntaggedResponse(response); + if (response.mTag == null && response.size() > 1) + { + try + { + Object responseType = response.get(1); + if ("FETCH".equals(responseType)) + { + int msgSeq = response.getNumber(0); + if (Email.DEBUG) + { + Log.d(Email.LOG_TAG, "Got untagged FETCH for msgseq " + msgSeq); + } + flagSyncMsgSeqs.add(msgSeq); + } + } + catch (Exception e) + { + Log.e(Email.LOG_TAG, "Could not handle untagged FETCH", e); + } + } + } + + + private void pushMessages(List messages, boolean newArrivals) + { + RuntimeException holdException = null; + try + { + if (newArrivals) + { + receiver.messagesArrived(getName(), messages); + } + else + { + receiver.messagesFlagsChanged(getName(), messages); + } + } + catch (RuntimeException e) + { + holdException = e; + } + + if (holdException != null) + { + throw holdException; + } + } + + public void stop() throws MessagingException + { + stop.set(true); + + if (mConnection != null) + { + if (Email.DEBUG) + { + Log.v(Email.LOG_TAG, "Closing mConnection to stop pushing"); + } + mConnection.close(); + } + else + { + Log.w(Email.LOG_TAG, "Attempt to interrupt null mConnection to stop pushing" + " on folderPusher " + getName()); + } + } + + public void handleAsyncUntaggedResponse(ImapResponse response) + { + if (Email.DEBUG) + { + Log.v(Email.LOG_TAG, "Got async response: " + response); + } + if (response.mTag == null) + { + if (response.size() > 1) + { + boolean started = false; + Object responseType = response.get(1); + if ("EXISTS".equals(responseType) || "EXPUNGE".equals(responseType) || + "FETCH".equals(responseType)) + { + if (started == false) + { + receiver.pushInProgress(); + started = true; + } + if (Email.DEBUG) + { + Log.d(Email.LOG_TAG, "Got useful async untagged response: " + response); + } + try + { + sendDone(); + } + catch (Exception e) + { + Log.e(Email.LOG_TAG, "Exception while sending DONE", e); + } + } + } + else if (response.size() > 0) + { + if ("idling".equals(response.get(0))) + { + if (Email.DEBUG) + { + Log.d(Email.LOG_TAG, "Idling"); + } + receiver.pushComplete(); + } + } + } + } + } + + public class ImapPusher implements Pusher + { + List folderPushers = new ArrayList(); + + public ImapPusher(ImapStore store, PushReceiver receiver, List folderNames) + { + for (String folderName : folderNames) + { + ImapFolderPusher pusher = new ImapFolderPusher(store, folderName, receiver); + folderPushers.add(pusher); + } + } + + public void refresh() + { + for (ImapFolderPusher folderPusher : folderPushers) + { + try + { + folderPusher.refresh(); + } + catch (Exception e) + { + Log.e(Email.LOG_TAG, "Got exception while refreshing"); + } + } + + } + + public void start() + { + for (ImapFolderPusher folderPusher : folderPushers) + { + try + { + folderPusher.start(); + } + catch (Exception e) + { + Log.e(Email.LOG_TAG, "Got exception while starting"); + } + } + } + + public void stop() + { + Log.i(Email.LOG_TAG, "Requested stop of IMAP pusher"); + for (ImapFolderPusher folderPusher : folderPushers) + { + try + { + Log.i(Email.LOG_TAG, "Requesting stop of IMAP folderPusher " + folderPusher.getName()); + folderPusher.stop(); + } + catch (Exception e) + { + Log.e(Email.LOG_TAG, "Got exception while stopping", e); + } + } + folderPushers.clear(); + } + + public int getRefreshInterval() + { + return IDLE_REFRESH_INTERVAL; + } + + } + private interface UntaggedHandler + { + void handleAsyncUntaggedResponse(ImapResponse respose); + } + + protected static class ImapPushState + { + protected int uidNext; + protected ImapPushState(int nUidNext) + { + uidNext = nUidNext; + } + protected static ImapPushState parse(String pushState) + { + int newUidNext = -1; + if (pushState != null) + { + StringTokenizer tokenizer = new StringTokenizer(pushState, ";"); + while (tokenizer.hasMoreTokens()) + { + StringTokenizer thisState = new StringTokenizer(tokenizer.nextToken(), "="); + if (thisState.hasMoreTokens()) + { + String key = thisState.nextToken(); + + if ("uidNext".equals(key) && thisState.hasMoreTokens()) + { + String value = thisState.nextToken(); + try + { + newUidNext = Integer.parseInt(value); + // Log.i(Email.LOG_TAG, "Parsed uidNext " + newUidNext); + } + catch (Exception e) + { + Log.e(Email.LOG_TAG, "Unable to part uidNext value " + value, e); + } + + } + } + } + } + return new ImapPushState(newUidNext); + } + public String toString() + { + return "uidNext=" + uidNext; + } + + } } diff --git a/src/com/android/email/mail/store/LocalStore.java b/src/com/android/email/mail/store/LocalStore.java index 6c00b32be..859f8849b 100644 --- a/src/com/android/email/mail/store/LocalStore.java +++ b/src/com/android/email/mail/store/LocalStore.java @@ -67,7 +67,7 @@ import java.io.StringReader; * */ public class LocalStore extends Store implements Serializable { - private static final int DB_VERSION = 26; + private static final int DB_VERSION = 29; private static final Flag[] PERMANENT_FLAGS = { Flag.DELETED, Flag.X_DESTROYED, Flag.SEEN }; private String mPath; @@ -133,7 +133,7 @@ public class LocalStore extends Store implements Serializable { mDb.execSQL("DROP TABLE IF EXISTS folders"); mDb.execSQL("CREATE TABLE folders (id INTEGER PRIMARY KEY, name TEXT, " - + "last_updated INTEGER, unread_count INTEGER, visible_limit INTEGER, status TEXT)"); + + "last_updated INTEGER, unread_count INTEGER, visible_limit INTEGER, status TEXT, push_state TEXT, last_pushed INTEGER)"); mDb.execSQL("CREATE INDEX IF NOT EXISTS folder_name ON folders (name)"); mDb.execSQL("DROP TABLE IF EXISTS messages"); @@ -273,10 +273,10 @@ public class LocalStore extends Store implements Serializable { try { - cursor = mDb.rawQuery("SELECT name, id, unread_count, visible_limit, last_updated, status FROM folders", null); + cursor = mDb.rawQuery("SELECT name, id, unread_count, visible_limit, last_updated, status, push_state, last_pushed FROM folders", null); while (cursor.moveToNext()) { LocalFolder folder = new LocalFolder(cursor.getString(0)); - folder.open(cursor.getInt(1), cursor.getInt(2), cursor.getInt(3), cursor.getLong(4), cursor.getString(5)); + folder.open(cursor.getInt(1), cursor.getInt(2), cursor.getInt(3), cursor.getLong(4), cursor.getString(5), cursor.getString(6), cursor.getLong(7)); folders.add(folder); } @@ -489,9 +489,11 @@ public class LocalStore extends Store implements Serializable { private long mFolderId = -1; private int mUnreadMessageCount = -1; private int mVisibleLimit = -1; - private FolderClass displayClass = FolderClass.NONE; - private FolderClass syncClass = FolderClass.NONE; + private FolderClass displayClass = FolderClass.NO_CLASS; + private FolderClass syncClass = FolderClass.INHERITED; + private FolderClass pushClass = FolderClass.SECOND_CLASS; private String prefId = null; + private String mPushState = null; public LocalFolder(String name) { this.mName = name; @@ -499,7 +501,9 @@ public class LocalStore extends Store implements Serializable { if (Email.INBOX.equals(getName())) { syncClass = FolderClass.FIRST_CLASS; + pushClass = FolderClass.FIRST_CLASS; } + } @@ -514,7 +518,7 @@ public class LocalStore extends Store implements Serializable { } Cursor cursor = null; try { - cursor = mDb.rawQuery("SELECT id, unread_count, visible_limit, last_updated, status FROM folders " + cursor = mDb.rawQuery("SELECT id, unread_count, visible_limit, last_updated, status, push_state, last_pushed FROM folders " + "where folders.name = ?", new String[] { mName @@ -524,7 +528,7 @@ public class LocalStore extends Store implements Serializable { int folderId = cursor.getInt(0); if (folderId > 0) { - open(cursor.getInt(0), cursor.getInt(1), cursor.getInt(2), cursor.getLong(3), cursor.getString(4)); + open(cursor.getInt(0), cursor.getInt(1), cursor.getInt(2), cursor.getLong(3), cursor.getString(4), cursor.getString(5), cursor.getLong(6) ); } } else { create(FolderType.HOLDS_MESSAGES); @@ -538,15 +542,17 @@ public class LocalStore extends Store implements Serializable { } } - private void open(int id, int unreadCount, int visibleLimit, long lastChecked, String status) throws MessagingException + private void open(int id, int unreadCount, int visibleLimit, long lastChecked, String status, String pushState, long lastPushed) throws MessagingException { mFolderId = id; mUnreadMessageCount = unreadCount; mVisibleLimit = visibleLimit; + mPushState = pushState; super.setStatus(status); // Only want to set the local variable stored in the super class. This class // does a DB update on setLastChecked super.setLastChecked(lastChecked); + super.setLastPush(lastPushed); } @Override @@ -655,6 +661,13 @@ public class LocalStore extends Store implements Serializable { mDb.execSQL("UPDATE folders SET last_updated = ? WHERE id = ?", new Object[] { lastChecked, mFolderId }); } + + public void setLastPush(long lastChecked) throws MessagingException { + open(OpenMode.READ_WRITE); + super.setLastPush(lastChecked); + mDb.execSQL("UPDATE folders SET last_pushed = ? WHERE id = ?", + new Object[] { lastChecked, mFolderId }); + } public int getVisibleLimit() throws MessagingException { open(OpenMode.READ_WRITE); @@ -676,6 +689,17 @@ public class LocalStore extends Store implements Serializable { mDb.execSQL("UPDATE folders SET status = ? WHERE id = ?", new Object[] { status, mFolderId }); } + public void setPushState(String pushState) throws MessagingException + { + open(OpenMode.READ_WRITE); + mPushState = pushState; + mDb.execSQL("UPDATE folders SET push_state = ? WHERE id = ?", + new Object[] { pushState, mFolderId }); + } + public String getPushState() + { + return mPushState; + } @Override public FolderClass getDisplayClass() { @@ -685,9 +709,9 @@ public class LocalStore extends Store implements Serializable { @Override public FolderClass getSyncClass() { - if (FolderClass.NONE == syncClass) + if (FolderClass.INHERITED == syncClass) { - return displayClass; + return getDisplayClass(); } else { @@ -697,10 +721,27 @@ public class LocalStore extends Store implements Serializable { public FolderClass getRawSyncClass() { - return syncClass; } + + public FolderClass getPushClass() + { + if (FolderClass.INHERITED == pushClass) + { + return getSyncClass(); + } + else + { + return pushClass; + } + } + + public FolderClass getRawPushClass() + { + return pushClass; + + } public void setDisplayClass(FolderClass displayClass) { @@ -711,6 +752,10 @@ public class LocalStore extends Store implements Serializable { { this.syncClass = syncClass; } + public void setPushClass(FolderClass pushClass) + { + this.pushClass = pushClass; + } private String getPrefId() throws MessagingException { @@ -740,7 +785,7 @@ public class LocalStore extends Store implements Serializable { SharedPreferences.Editor editor = preferences.getPreferences().edit(); // there can be a lot of folders. For the defaults, let's not save prefs, saving space, except for INBOX - if (displayClass == FolderClass.NONE && !Email.INBOX.equals(getName())) + if (displayClass == FolderClass.NO_CLASS && !Email.INBOX.equals(getName())) { editor.remove(id + ".displayMode"); } @@ -749,7 +794,7 @@ public class LocalStore extends Store implements Serializable { editor.putString(id + ".displayMode", displayClass.name()); } - if (syncClass == FolderClass.NONE && !Email.INBOX.equals(getName())) + if (syncClass == FolderClass.INHERITED && !Email.INBOX.equals(getName())) { editor.remove(id + ".syncMode"); } @@ -757,6 +802,15 @@ public class LocalStore extends Store implements Serializable { { editor.putString(id + ".syncMode", syncClass.name()); } + + if (pushClass == FolderClass.SECOND_CLASS && !Email.INBOX.equals(getName())) + { + editor.remove(id + ".pushMode"); + } + else + { + editor.putString(id + ".pushMode", pushClass.name()); + } editor.commit(); } @@ -767,17 +821,21 @@ public class LocalStore extends Store implements Serializable { try { displayClass = FolderClass.valueOf(preferences.getPreferences().getString(id + ".displayMode", - FolderClass.NONE.name())); + FolderClass.NO_CLASS.name())); } catch (Exception e) { Log.e(Email.LOG_TAG, "Unable to load displayMode for " + getName(), e); - displayClass = FolderClass.NONE; + displayClass = FolderClass.NO_CLASS; + } + if (displayClass == FolderClass.NONE) + { + displayClass = FolderClass.NO_CLASS; } - FolderClass defSyncClass = FolderClass.NONE; + FolderClass defSyncClass = FolderClass.INHERITED; if (Email.INBOX.equals(getName())) { defSyncClass = FolderClass.FIRST_CLASS; @@ -794,6 +852,32 @@ public class LocalStore extends Store implements Serializable { syncClass = defSyncClass; } + if (syncClass == FolderClass.NONE) + { + syncClass = FolderClass.INHERITED; + } + + FolderClass defPushClass = FolderClass.SECOND_CLASS; + if (Email.INBOX.equals(getName())) + { + defPushClass = FolderClass.FIRST_CLASS; + } + + try + { + pushClass = FolderClass.valueOf(preferences.getPreferences().getString(id + ".pushMode", + defPushClass.name())); + } + catch (Exception e) + { + Log.e(Email.LOG_TAG, "Unable to load pushMode for " + getName(), e); + + pushClass = defPushClass; + } + if (pushClass == FolderClass.NONE) + { + pushClass = FolderClass.INHERITED; + } } @@ -1039,7 +1123,9 @@ public class LocalStore extends Store implements Serializable { i++; } populateHeaders(messagesForHeaders); - + if (listener != null) { + listener.messagesFinished(i); + } } finally { if (cursor != null) { @@ -1085,9 +1171,7 @@ public class LocalStore extends Store implements Serializable { LocalMessage lMessage = (LocalMessage)message; if (!message.isSet(Flag.SEEN)) { - if (getUnreadMessageCount() > 0) { - setUnreadMessageCount(getUnreadMessageCount() - 1); - } + setUnreadMessageCount(getUnreadMessageCount() - 1); lDestFolder.setUnreadMessageCount(lDestFolder.getUnreadMessageCount() + 1); } @@ -1105,6 +1189,7 @@ public class LocalStore extends Store implements Serializable { LocalMessage placeHolder = new LocalMessage(oldUID, this); placeHolder.setFlagInternal(Flag.DELETED, true); + placeHolder.setFlagInternal(Flag.SEEN, true); appendMessages(new Message[] { placeHolder }); } @@ -1139,6 +1224,11 @@ public class LocalStore extends Store implements Serializable { message.setUid(Email.LOCAL_UID_PREFIX + UUID.randomUUID().toString()); } else { + Message oldMessage = getMessage(message.getUid()); + if (oldMessage != null && oldMessage.isSet(Flag.SEEN) == false) + { + setUnreadMessageCount(getUnreadMessageCount() - 1); + } /* * The message may already exist in this Folder, so delete it first. */ @@ -1202,6 +1292,10 @@ public class LocalStore extends Store implements Serializable { saveAttachment(messageId, attachment, copy); } saveHeaders(messageId, (MimeMessage)message); + if (message.isSet(Flag.SEEN) == false) + { + setUnreadMessageCount(getUnreadMessageCount() + 1); + } } catch (Exception e) { throw new MessagingException("Error appending message", e); } @@ -1473,6 +1567,28 @@ public class LocalStore extends Store implements Serializable { open(OpenMode.READ_ONLY); mDb.execSQL("DELETE FROM messages WHERE folder_id = ? and date < ?", new Object[] { Long.toString(mFolderId), new Long(cutoff) } ); + resetUnreadCount(); + } + + private void resetUnreadCount() + { + try + { + int newUnread = 0; + Message[] messages = getMessages(null); + for (Message message : messages) + { + if (message.isSet(Flag.SEEN) == false) + { + newUnread++; + } + } + setUnreadMessageCount(newUnread); + } + catch (Exception e) + { + Log.e(Email.LOG_TAG, "Unable to fetch all messages from LocalStore", e); + } } @Override @@ -1760,7 +1876,8 @@ public class LocalStore extends Store implements Serializable { * Update the unread count on the folder. */ try { - if (flag == Flag.DELETED || flag == Flag.X_DESTROYED || flag == Flag.SEEN) { + if (flag == Flag.DELETED || flag == Flag.X_DESTROYED + || (flag == Flag.SEEN && !isSet(Flag.DELETED))) { LocalFolder folder = (LocalFolder)mFolder; if (set && !isSet(Flag.SEEN)) { folder.setUnreadMessageCount(folder.getUnreadMessageCount() - 1); diff --git a/src/com/android/email/service/BootReceiver.java b/src/com/android/email/service/BootReceiver.java index 155913f8e..6c008113b 100644 --- a/src/com/android/email/service/BootReceiver.java +++ b/src/com/android/email/service/BootReceiver.java @@ -2,8 +2,9 @@ package com.android.email.service; import com.android.email.Email; + +import android.net.ConnectivityManager; import android.util.Log; -import com.android.email.MessagingController; import android.content.BroadcastReceiver; import android.content.Context; @@ -22,5 +23,9 @@ public class BootReceiver extends BroadcastReceiver { else if (Intent.ACTION_DEVICE_STORAGE_OK.equals(intent.getAction())) { MailService.actionReschedule(context); } + else if (ConnectivityManager.CONNECTIVITY_ACTION.equals(intent.getAction())) { + boolean noConnectivity = intent.getBooleanExtra(ConnectivityManager.EXTRA_NO_CONNECTIVITY, false); + MailService.connectivityChange(context, !noConnectivity); + } } } diff --git a/src/com/android/email/service/MailService.java b/src/com/android/email/service/MailService.java index 1ed1551bf..7369e70b5 100644 --- a/src/com/android/email/service/MailService.java +++ b/src/com/android/email/service/MailService.java @@ -1,6 +1,7 @@ package com.android.email.service; +import java.util.Collection; import java.util.Date; import java.util.HashMap; @@ -11,14 +12,15 @@ import android.app.PendingIntent; import android.app.Service; import android.content.Context; import android.content.Intent; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.net.Uri; +import android.net.NetworkInfo.State; import android.os.IBinder; import android.os.PowerManager; import android.os.PowerManager.WakeLock; -import android.os.SystemClock; import android.util.Config; import android.util.Log; -import android.text.TextUtils; -import android.net.Uri; import com.android.email.Account; import com.android.email.Email; @@ -26,29 +28,51 @@ import com.android.email.MessagingController; import com.android.email.MessagingListener; import com.android.email.Preferences; import com.android.email.R; -import com.android.email.activity.Accounts; -import com.android.email.activity.FolderList; -import com.android.email.mail.Folder; +import com.android.email.mail.Address; +import com.android.email.mail.Message; import com.android.email.mail.MessagingException; -import com.android.email.mail.Store; +import com.android.email.mail.Pusher; +import com.android.email.EmailReceivedIntent; /** */ public class MailService extends Service { + private static final String ACTION_APP_STARTED = "com.android.email.intent.action.MAIL_SERVICE_APP_STARTED"; 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_REFRESH_PUSHERS = "com.android.email.intent.action.MAIL_SERVICE_REFRESH_PUSHERS"; + private static final String CONNECTIVITY_CHANGE = "com.android.email.intent.action.MAIL_SERVICE_CONNECTIVITY_CHANGE"; + private static final String CANCEL_CONNECTIVITY_NOTICE = "com.android.email.intent.action.MAIL_SERVICE_CANCEL_CONNECTIVITY_NOTICE"; + private static final String HAS_CONNECTIVITY = "com.android.email.intent.action.MAIL_SERVICE_HAS_CONNECTIVITY"; + private Listener mListener = new Listener(); + + private State state = null; private int mStartId; - + 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 appStarted(Context context) { + Intent i = new Intent(); + i.setClass(context, MailService.class); + i.setAction(MailService.ACTION_APP_STARTED); + context.startService(i); + } + +// private static void checkMail(Context context) { +// Intent i = new Intent(); +// i.setClass(context, MailService.class); +// i.setAction(MailService.ACTION_CHECK_MAIL); +// context.startService(i); +// } public static void actionCancel(Context context) { Intent i = new Intent(); @@ -56,6 +80,14 @@ public class MailService extends Service { i.setAction(MailService.ACTION_CANCEL); context.startService(i); } + + public static void connectivityChange(Context context, boolean hasConnectivity) { + Intent i = new Intent(); + i.setClass(context, MailService.class); + i.setAction(MailService.CONNECTIVITY_CHANGE); + i.putExtra(HAS_CONNECTIVITY, hasConnectivity); + context.startService(i); + } @Override public void onCreate() { @@ -65,54 +97,173 @@ public class MailService extends Service { @Override public void onStart(Intent intent, int startId) { - setForeground(true); // if it gets killed once, it'll never restart - Log.v(Email.LOG_TAG, "***** MailService *****: onStart(" + intent + ", " + startId + ")"); - super.onStart(intent, startId); - this.mStartId = startId; - - // MessagingController.getInstance(getApplication()).addListener(mListener); - if (ACTION_CHECK_MAIL.equals(intent.getAction())) { - //if (Config.LOGV) { - MessagingController.getInstance(getApplication()).log("***** MailService *****: checking mail"); - Log.v(Email.LOG_TAG, "***** MailService *****: checking mail"); - //} - - MessagingController controller = MessagingController.getInstance(getApplication()); - Listener listener = (Listener)controller.getCheckMailListener(); - if (listener == null) + + PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); + WakeLock wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "Email"); + wakeLock.setReferenceCounted(false); + wakeLock.acquire(Email.MAIL_SERVICE_WAKE_LOCK_TIMEOUT); + try + { + + ConnectivityManager connectivityManager = (ConnectivityManager)getApplication().getSystemService(Context.CONNECTIVITY_SERVICE); + state = State.DISCONNECTED; + if (connectivityManager != null) { - MessagingController.getInstance(getApplication()).log("***** MailService *****: starting new check"); - - mListener.wakeLockAcquire(); - controller.setCheckMailListener(mListener); - controller.checkMail(this, null, false, false, mListener); + NetworkInfo netInfo = connectivityManager.getActiveNetworkInfo(); + if (netInfo != null) + { + state = netInfo.getState(); + + if (state == State.CONNECTED) + { + Log.i(Email.LOG_TAG, "Currently connected to a network"); + } + else + { + Log.i(Email.LOG_TAG, "Current network state = " + state); + } + } } - else + + + setForeground(true); // if it gets killed once, it'll never restart + Log.v(Email.LOG_TAG, "***** MailService *****: onStart(" + intent + ", " + startId + ")"); + super.onStart(intent, startId); + this.mStartId = startId; + + // MessagingController.getInstance(getApplication()).addListener(mListener); + if (ACTION_CHECK_MAIL.equals(intent.getAction())) { + //if (Config.LOGV) { + MessagingController.getInstance(getApplication()).log("***** MailService *****: checking mail"); + Log.v(Email.LOG_TAG, "***** MailService *****: checking mail"); + //} + if (state == State.CONNECTED) + { + MessagingController controller = MessagingController.getInstance(getApplication()); + Listener listener = (Listener)controller.getCheckMailListener(); + if (listener == null) + { + MessagingController.getInstance(getApplication()).log("***** MailService *****: starting new check"); + + mListener.wakeLockAcquire(); + controller.setCheckMailListener(mListener); + controller.checkMail(this, null, false, false, mListener); + } + else + { + MessagingController.getInstance(getApplication()).log("***** MailService *****: renewing WakeLock"); + + listener.wakeLockAcquire(); + } + } + + reschedule(); + // stopSelf(startId); + } + else if (ACTION_CANCEL.equals(intent.getAction())) { + if (Config.LOGV) { + Log.v(Email.LOG_TAG, "***** MailService *****: cancel"); + } + MessagingController.getInstance(getApplication()).log("***** MailService *****: cancel"); + + cancel(); + // stopSelf(startId); + } + else if (ACTION_RESCHEDULE.equals(intent.getAction())) { + if (Config.LOGV) { + Log.v(Email.LOG_TAG, "***** MailService *****: reschedule"); + } + MessagingController.getInstance(getApplication()).log("***** MailService *****: reschedule"); + boolean polling = reschedule(); + boolean pushing = reschedulePushers(); + if (polling == false && pushing == false) + { + Log.i(Email.LOG_TAG, "Neither pushing nor polling, so stopping"); + stopSelf(startId); + } + // stopSelf(startId); + } + else if (ACTION_REFRESH_PUSHERS.equals(intent.getAction())) { - MessagingController.getInstance(getApplication()).log("***** MailService *****: renewing WakeLock"); - - listener.wakeLockAcquire(); + schedulePushers(); + try + { + if (state == State.CONNECTED) + { + Log.i(Email.LOG_TAG, "Refreshing pushers"); + Collection pushers = MessagingController.getInstance(getApplication()).getPushers(); + for (Pusher pusher : pushers) + { + pusher.refresh(); + } + } + } + catch (Exception e) + { + Log.e(Email.LOG_TAG, "Exception while refreshing pushers", e); + } + } + else if (CONNECTIVITY_CHANGE.equals(intent.getAction())) + { + boolean hasConnectivity = intent.getBooleanExtra(HAS_CONNECTIVITY, true); + Log.i(Email.LOG_TAG, "Got connectivity action with hasConnectivity = " + hasConnectivity); + notifyConnectionStatus(hasConnectivity); + if (hasConnectivity) + { + reschedulePushers(); + // TODO: Make it send pending outgoing messages here + //checkMail(getApplication()); + } + else + { + stopPushers(); + } + } + else if (CANCEL_CONNECTIVITY_NOTICE.equals(intent.getAction())) + { + notifyConnectionStatus(true); + } + else if (ACTION_APP_STARTED.equals(intent.getAction())) + { + // Not needed for now, but might be useful later } - - reschedule(); - // stopSelf(startId); } - else if (ACTION_CANCEL.equals(intent.getAction())) { - if (Config.LOGV) { - Log.v(Email.LOG_TAG, "***** MailService *****: cancel"); + finally + { + if (wakeLock != null) + { + wakeLock.release(); } - MessagingController.getInstance(getApplication()).log("***** MailService *****: cancel"); - - cancel(); - // stopSelf(startId); } - else if (ACTION_RESCHEDULE.equals(intent.getAction())) { - if (Config.LOGV) { - Log.v(Email.LOG_TAG, "***** MailService *****: reschedule"); - } - MessagingController.getInstance(getApplication()).log("***** MailService *****: reschedule"); - reschedule(); - // stopSelf(startId); + } + + private void notifyConnectionStatus(boolean hasConnectivity) + { + NotificationManager notifMgr = + (NotificationManager)getApplication().getSystemService(Context.NOTIFICATION_SERVICE); + if (hasConnectivity == false) + { + String notice = getApplication().getString(R.string.no_connection_alert); + String header = getApplication().getString(R.string.alert_header); + + + Notification notif = new Notification(R.drawable.stat_notify_email_generic, + header, System.currentTimeMillis()); + + Intent i = new Intent(); + i.setClassName(getApplication().getPackageName(), "com.android.email.service.MailService"); + i.setAction(MailService.CANCEL_CONNECTIVITY_NOTICE); + + PendingIntent pi = PendingIntent.getService(this, 0, i, 0); + + notif.setLatestEventInfo(getApplication(), header, notice, pi); + notif.flags = Notification.FLAG_ONGOING_EVENT; + + notifMgr.notify(Email.CONNECTIVITY_ID, notif); + } + else + { + notifMgr.cancel(Email.CONNECTIVITY_ID); } } @@ -132,13 +283,14 @@ public class MailService extends Service { alarmMgr.cancel(pi); } - private void reschedule() { + private boolean reschedule() { + boolean polling = true; AlarmManager alarmMgr = (AlarmManager)getSystemService(Context.ALARM_SERVICE); Intent i = new Intent(); i.setClassName(getApplication().getPackageName(), "com.android.email.service.MailService"); i.setAction(ACTION_CHECK_MAIL); PendingIntent pi = PendingIntent.getService(this, 0, i, 0); - + int shortestInterval = -1; for (Account account : Preferences.getPreferences(this).getAccounts()) { @@ -151,7 +303,7 @@ public class MailService extends Service { if (shortestInterval == -1) { Log.v(Email.LOG_TAG, "No next check scheduled for package " + getApplication().getPackageName()); alarmMgr.cancel(pi); - stopSelf(mStartId); + polling = false; } else { @@ -172,7 +324,70 @@ public class MailService extends Service { alarmMgr.set(AlarmManager.RTC_WAKEUP, nextTime, pi); } + return polling; + } + + private void stopPushers() + { + MessagingController.getInstance(getApplication()).stopAllPushing(); + } + + private boolean reschedulePushers() + { + boolean pushing = false; + Log.i(Email.LOG_TAG, "Rescheduling pushers"); + stopPushers(); + if (state == State.CONNECTED) + { + for (Account account : Preferences.getPreferences(this).getAccounts()) { + Log.i(Email.LOG_TAG, "Setting up pushers for account " + account.getDescription()); + Pusher pusher = MessagingController.getInstance(getApplication()).setupPushing(account); + if (pusher != null) + { + pushing = true; + Log.i(Email.LOG_TAG, "Starting configured pusher for account " + account.getDescription()); + pusher.start(); + } + } + schedulePushers(); + } + return pushing; + + } + + private void schedulePushers() + { + int minInterval = -1; + + Collection pushers = MessagingController.getInstance(getApplication()).getPushers(); + for (Pusher pusher : pushers) + { + int interval = pusher.getRefreshInterval(); + if (interval != -1 && (interval < minInterval || minInterval == -1)) + { + minInterval = interval; + } + } + if (Email.DEBUG) + { + Log.v(Email.LOG_TAG, "Pusher refresh interval = " + minInterval); + } + if (minInterval != -1) + { + AlarmManager alarmMgr = (AlarmManager)getSystemService(Context.ALARM_SERVICE); + long nextTime = System.currentTimeMillis() + minInterval; + String checkString = "Next pusher refresh scheduled for " + new Date(nextTime); + if (Email.DEBUG) + { + Log.d(Email.LOG_TAG, checkString); + } + Intent i = new Intent(); + i.setClassName(getApplication().getPackageName(), "com.android.email.service.MailService"); + i.setAction(ACTION_REFRESH_PUSHERS); + PendingIntent pi = PendingIntent.getService(this, 0, i, 0); + alarmMgr.set(AlarmManager.RTC_WAKEUP, nextTime, pi); + } } public IBinder onBind(Intent intent) { @@ -244,46 +459,16 @@ public class MailService extends Service { NotificationManager notifMgr = (NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE); - int index = 0; for (Account thisAccount : Preferences.getPreferences(context).getAccounts()) { Integer newMailCount = accountsChecked.get(thisAccount.getUuid()); - int unreadMessageCount = -1; if (newMailCount != null) { try { - unreadMessageCount = thisAccount.getUnreadMessageCount(context, getApplication()); + int unreadMessageCount = thisAccount.getUnreadMessageCount(context, getApplication()); if (unreadMessageCount > 0 && newMailCount > 0) { - String notice = getString(R.string.notification_new_one_account_fmt, unreadMessageCount, - thisAccount.getDescription()); - Notification notif = new Notification(R.drawable.stat_notify_email_generic, - getString(R.string.notification_new_title), System.currentTimeMillis() + (index*1000)); - - notif.number = unreadMessageCount; - - Intent i = FolderList.actionHandleAccountIntent(context, thisAccount); - - PendingIntent pi = PendingIntent.getActivity(context, 0, i, 0); - - notif.setLatestEventInfo(context, getString(R.string.notification_new_title), notice, pi); - - // JRV XXX TODO - Do we also need to notify the messagelist here? - - - String ringtone = thisAccount.getRingtone(); - notif.sound = TextUtils.isEmpty(ringtone) ? null : Uri.parse(ringtone); - - if (thisAccount.isVibrate()) { - notif.defaults |= Notification.DEFAULT_VIBRATE; - } - - notif.flags |= Notification.FLAG_SHOW_LIGHTS; - notif.ledARGB = Email.NOTIFICATION_LED_COLOR; - notif.ledOnMS = Email.NOTIFICATION_LED_ON_TIME; - notif.ledOffMS = Email.NOTIFICATION_LED_OFF_TIME; - - notifMgr.notify(thisAccount.getAccountNumber(), notif); + MessagingController.getInstance(getApplication()).notifyAccount(context, thisAccount, unreadMessageCount); } else if (unreadMessageCount == 0) { @@ -298,7 +483,8 @@ public class MailService extends Service { } }//for accounts }//checkMailDone - + + private void release() { MessagingController controller = MessagingController.getInstance(getApplication()); @@ -322,4 +508,5 @@ public class MailService extends Service { } } } + }