diff --git a/res/layout/attachment_forward_failed_notification.xml b/res/layout/attachment_forward_failed_notification.xml deleted file mode 100644 index 04394a0fb..000000000 --- a/res/layout/attachment_forward_failed_notification.xml +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/res/values/strings.xml b/res/values/strings.xml index f8cfcd1e7..265e25463 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -378,11 +378,15 @@ save attachment. messages moved to %2$s - "An attachment couldn't be forwarded" + Could not forward one or more attachments - "The attachment "%s - " couldn't be sent with your outgoing mail because it couldn't be downloaded." - + Could not forward + %s + + %s + sign-in failed + + Touch to change account settings diff --git a/src/com/android/email/MessagingController.java b/src/com/android/email/MessagingController.java index a4156f6d8..44ec5db0a 100644 --- a/src/com/android/email/MessagingController.java +++ b/src/com/android/email/MessagingController.java @@ -16,6 +16,7 @@ package com.android.email; +import com.android.email.mail.AuthenticationFailedException; import com.android.email.mail.FetchProfile; import com.android.email.mail.Flag; import com.android.email.mail.Folder; @@ -376,6 +377,7 @@ public class MessagingController implements Runnable { private void synchronizeMailboxSynchronous(final EmailContent.Account account, final EmailContent.Mailbox folder) { mListeners.synchronizeMailboxStarted(account.mId, folder.mId); + NotificationController nc = NotificationController.getInstance(mContext); try { processPendingActionsSynchronous(account); @@ -393,10 +395,16 @@ public class MessagingController implements Runnable { mListeners.synchronizeMailboxFinished(account.mId, folder.mId, results.mTotalMessages, results.mNewMessages); + // Clear authentication notification for this account + nc.cancelLoginFailedNotification(account.mId); } catch (MessagingException e) { if (Email.LOGD) { Log.v(Email.LOG_TAG, "synchronizeMailbox", e); } + if (e instanceof AuthenticationFailedException) { + // Generate authentication notification + nc.showLoginFailedNotification(account.mId); + } mListeners.synchronizeMailboxFailed(account.mId, folder.mId, e); } } @@ -1974,6 +1982,7 @@ public class MessagingController implements Runnable { */ public void sendPendingMessagesSynchronous(final EmailContent.Account account, long sentFolderId) { + NotificationController nc = NotificationController.getInstance(mContext); // 1. Loop through all messages in the account's outbox long outboxId = Mailbox.findMailboxOfType(mContext, account.mId, Mailbox.TYPE_OUTBOX); if (outboxId == Mailbox.NO_MAILBOX) { @@ -2018,6 +2027,9 @@ public class MessagingController implements Runnable { sender.sendMessage(messageId); } catch (MessagingException me) { // report error for this message, but keep trying others + if (me instanceof AuthenticationFailedException) { + nc.showLoginFailedNotification(account.mId); + } mListeners.sendPendingMessagesFailed(account.mId, messageId, me); continue; } @@ -2045,8 +2057,11 @@ public class MessagingController implements Runnable { } // 6. report completion/success mListeners.sendPendingMessagesCompleted(account.mId); - + nc.cancelLoginFailedNotification(account.mId); } catch (MessagingException me) { + if (me instanceof AuthenticationFailedException) { + nc.showLoginFailedNotification(account.mId); + } mListeners.sendPendingMessagesFailed(account.mId, -1, me); } finally { c.close(); diff --git a/src/com/android/email/NotificationController.java b/src/com/android/email/NotificationController.java index dd7e35e89..80a24990f 100644 --- a/src/com/android/email/NotificationController.java +++ b/src/com/android/email/NotificationController.java @@ -18,15 +18,18 @@ package com.android.email; import com.android.email.activity.ContactStatusLoader; import com.android.email.activity.Welcome; +import com.android.email.activity.setup.AccountSettingsXL; import com.android.email.mail.Address; import com.android.email.provider.EmailContent; import com.android.email.provider.EmailContent.Account; +import com.android.email.provider.EmailContent.Attachment; import com.android.email.provider.EmailContent.Message; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.content.Context; +import android.content.Intent; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.media.AudioManager; @@ -41,8 +44,10 @@ import android.text.TextUtils; public class NotificationController { public static final int NOTIFICATION_ID_SECURITY_NEEDED = 1; public static final int NOTIFICATION_ID_EXCHANGE_CALENDAR_ADDED = 2; - public static final int NOTIFICATION_ID_WARNING = 3; - private static final int NOTIFICATION_ID_NEW_MESSAGES_BASE = 10; + public static final int NOTIFICATION_ID_ATTACHMENT_WARNING = 3; + + private static final int NOTIFICATION_ID_BASE_NEW_MESSAGES = 0x10000000; + private static final int NOTIFICATION_ID_BASE_LOGIN_WARNING = 0x20000000; private static NotificationController sInstance; private final Context mContext; @@ -70,7 +75,7 @@ public class NotificationController { * accountID won't be too huge. Any other smarter/cleaner way? */ private int getNewMessageNotificationId(long accountId) { - return (int) (NOTIFICATION_ID_NEW_MESSAGES_BASE + accountId); + return (int) (NOTIFICATION_ID_BASE_NEW_MESSAGES + accountId); } /** @@ -224,4 +229,51 @@ public class NotificationController { notification.flags |= Notification.FLAG_SHOW_LIGHTS; notification.defaults |= Notification.DEFAULT_LIGHTS; } + + /** + * Generic warning notification + */ + public void showWarningNotification(int id, String tickerText, String notificationText, + Intent intent) { + PendingIntent pendingIntent = + PendingIntent.getActivity(mContext, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); + Notification n = new Notification(android.R.drawable.stat_notify_error, tickerText, + System.currentTimeMillis()); + n.setLatestEventInfo(mContext, tickerText, notificationText, pendingIntent); + n.flags = Notification.FLAG_AUTO_CANCEL; + mNotificationManager.notify(id, n); + } + + /** + * Alert the user that an attachment couldn't be forwarded. This is a very unusual case, and + * perhaps we shouldn't even send a notification. For now, it's helpful for debugging. + */ + public void showDownloadForwardFailedNotification(Attachment att) { + showWarningNotification(NOTIFICATION_ID_ATTACHMENT_WARNING, + mContext.getString(R.string.forward_download_failed_ticker), + mContext.getString(R.string.forward_download_failed_notification, + att.mFileName), + Welcome.createOpenCombinedOutboxIntent(mContext)); + } + + /** + * Alert the user that login failed for the specified account + */ + private int getLoginFailedNotificationId(long accountId) { + return NOTIFICATION_ID_BASE_LOGIN_WARNING + (int)accountId; + } + + // NOTE: DO NOT CALL THIS METHOD FROM THE UI THREAD (DATABASE ACCESS) + public void showLoginFailedNotification(long accountId) { + final Account account = Account.restoreAccountWithId(mContext, accountId); + if (account == null) return; + showWarningNotification(getLoginFailedNotificationId(accountId), + mContext.getString(R.string.login_failed_ticker, account.mDisplayName), + mContext.getString(R.string.login_failed_notification), + AccountSettingsXL.createAccountSettingsIntent(mContext, accountId)); + } + + public void cancelLoginFailedNotification(long accountId) { + mNotificationManager.cancel(getLoginFailedNotificationId(accountId)); + } } diff --git a/src/com/android/email/activity/Welcome.java b/src/com/android/email/activity/Welcome.java index 991566112..211879496 100644 --- a/src/com/android/email/activity/Welcome.java +++ b/src/com/android/email/activity/Welcome.java @@ -110,7 +110,7 @@ public class Welcome extends Activity { } /** - * Create an Intent to open "Combined Inbox". + * Create an Intent to open "Combined Outbox". */ public static Intent createOpenCombinedInboxIntent(Context context) { Intent i = new Intent(context, Welcome.class); @@ -118,6 +118,15 @@ public class Welcome extends Activity { return i; } + /** + * Create an Intent to open "Combined Inbox". + */ + public static Intent createOpenCombinedOutboxIntent(Context context) { + Intent i = new Intent(context, Welcome.class); + i.putExtra(EXTRA_MAILBOX_ID, Mailbox.QUERY_ALL_OUTBOX); + return i; + } + /** * Open account's inbox. */ diff --git a/src/com/android/email/activity/setup/AccountSettingsXL.java b/src/com/android/email/activity/setup/AccountSettingsXL.java index fdf4e9ea3..45b43ece0 100644 --- a/src/com/android/email/activity/setup/AccountSettingsXL.java +++ b/src/com/android/email/activity/setup/AccountSettingsXL.java @@ -121,6 +121,16 @@ public class AccountSettingsXL extends PreferenceActivity implements OnClickList fromActivity.startActivity(i); } + /** + * Create and return an intent to display (and edit) settings for a specific account, or -1 + * for any/all accounts + */ + public static Intent createAccountSettingsIntent(Context context, long accountId) { + Intent i = new Intent(context, AccountSettingsXL.class); + i.putExtra(EXTRA_ACCOUNT_ID, accountId); + return i; + } + /** * Launch generic settings and pre-enable the debug preferences */ diff --git a/src/com/android/email/mail/store/ImapStore.java b/src/com/android/email/mail/store/ImapStore.java index b7b9428af..71cd58211 100644 --- a/src/com/android/email/mail/store/ImapStore.java +++ b/src/com/android/email/mail/store/ImapStore.java @@ -418,9 +418,17 @@ public class ImapStore extends Store { } catch (IOException ioe) { connection.close(); throw new MessagingException("Unable to get folder list.", ioe); - } finally { + } catch (AuthenticationFailedException afe) { + // We do NOT want this connection pooled, or we will continue to send NOOP and SELECT + // commands to the server connection.destroyResponses(); - poolConnection(connection); + connection = null; + throw afe; + } finally { + if (connection != null) { + connection.destroyResponses(); + poolConnection(connection); + } } } @@ -601,6 +609,11 @@ public class ImapStore extends Store { } finally { destroyResponses(); } + } catch (AuthenticationFailedException e) { + // Don't cache this connection, so we're forced to try connecting/login again + mConnection = null; + close(false); + throw e; } catch (MessagingException e) { mExists = false; close(false); @@ -1633,6 +1646,8 @@ public class ImapStore extends Store { } static class ImapException extends MessagingException { + private static final long serialVersionUID = 1L; + String mAlertText; public ImapException(String message, String alertText, Throwable throwable) { diff --git a/src/com/android/email/service/AttachmentDownloadService.java b/src/com/android/email/service/AttachmentDownloadService.java index 9657af679..96d4190c8 100644 --- a/src/com/android/email/service/AttachmentDownloadService.java +++ b/src/com/android/email/service/AttachmentDownloadService.java @@ -18,11 +18,9 @@ package com.android.email.service; import com.android.email.Email; import com.android.email.NotificationController; -import com.android.email.R; import com.android.email.Utility; import com.android.email.Controller.ControllerService; import com.android.email.ExchangeUtils.NullEmailService; -import com.android.email.activity.Welcome; import com.android.email.provider.AttachmentProvider; import com.android.email.provider.EmailContent; import com.android.email.provider.EmailContent.Account; @@ -30,9 +28,6 @@ import com.android.email.provider.EmailContent.Attachment; import com.android.email.provider.EmailContent.Message; import com.android.exchange.ExchangeService; -import android.app.Notification; -import android.app.NotificationManager; -import android.app.PendingIntent; import android.app.Service; import android.content.ContentValues; import android.content.Context; @@ -42,7 +37,6 @@ import android.os.IBinder; import android.os.RemoteException; import android.text.format.DateUtils; import android.util.Log; -import android.widget.RemoteViews; import java.io.File; import java.io.FileDescriptor; @@ -349,7 +343,8 @@ public class AttachmentDownloadService extends Service implements Runnable { // message never get sent EmailContent.delete(mContext, Attachment.CONTENT_URI, attachment.mId); // TODO: Talk to UX about whether this is even worth doing - showDownloadForwardFailedNotification(attachment); + NotificationController nc = NotificationController.getInstance(mContext); + nc.showDownloadForwardFailedNotification(attachment); deleted = true; } // If we're an attachment on forwarded mail, and if we're not still blocked, @@ -457,31 +452,6 @@ public class AttachmentDownloadService extends Service implements Runnable { } } - /** - * Alert the user that an attachment couldn't be forwarded. This is a very unusual case, and - * perhaps we shouldn't even send a notification. For now, it's helpful for debugging. - * Note the STOPSHIP below... - */ - void showDownloadForwardFailedNotification(Attachment att) { - // STOPSHIP: Tentative UI; if we use a notification, replace this text with a resource - RemoteViews contentView = new RemoteViews(getPackageName(), - R.layout.attachment_forward_failed_notification); - contentView.setImageViewResource(R.id.image, R.drawable.ic_email_attachment); - contentView.setTextViewText(R.id.text, - getString(R.string.forward_download_failed_notification, att.mFileName)); - Notification n = new Notification(R.drawable.stat_notify_email_generic, - getString(R.string.forward_download_failed_ticker), System.currentTimeMillis()); - n.contentView = contentView; - Intent i = new Intent(mContext, Welcome.class); - PendingIntent pending = PendingIntent.getActivity(mContext, 0, i, - PendingIntent.FLAG_UPDATE_CURRENT); - n.contentIntent = pending; - n.flags = Notification.FLAG_AUTO_CANCEL; - NotificationManager nm = - (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); - nm.notify(NotificationController.NOTIFICATION_ID_WARNING, n); - } - /** * Return the class of the service used by the account type of the provided account id. We * cache the results to avoid repeated database access diff --git a/src/com/android/email/service/MailService.java b/src/com/android/email/service/MailService.java index eccf7ac80..b0aa0ab7f 100644 --- a/src/com/android/email/service/MailService.java +++ b/src/com/android/email/service/MailService.java @@ -454,7 +454,7 @@ public class MailService extends Service { * Create a watchdog alarm and set it. This is used in case a mail check fails (e.g. we are * killed by the system due to memory pressure.) Normally, a mail check will complete and * the watchdog will be replaced by the call to reschedule(). - * @param accountId the account we were trying to check + * @param accountId the account we were trying to check * @param alarmMgr system alarm manager */ private void setWatchdog(long accountId, AlarmManager alarmMgr) { @@ -701,7 +701,9 @@ public class MailService extends Service { @Override public void updateMailboxCallback(MessagingException result, long accountId, long mailboxId, int progress, int numNewMessages) { - if (result != null || progress == 100) { + // First, look for authentication failures and notify + //checkAuthenticationStatus(result, accountId); + if (result != null || progress == 100) { // We only track the inbox here in the service - ignore other mailboxes long inboxId = Mailbox.findMailboxOfType(MailService.this, accountId, Mailbox.TYPE_INBOX); diff --git a/src/com/android/exchange/ExchangeService.java b/src/com/android/exchange/ExchangeService.java index 7b87b75fa..6f103b0aa 100644 --- a/src/com/android/exchange/ExchangeService.java +++ b/src/com/android/exchange/ExchangeService.java @@ -19,6 +19,7 @@ package com.android.exchange; import com.android.email.AccountBackupRestore; import com.android.email.Email; +import com.android.email.NotificationController; import com.android.email.Utility; import com.android.email.mail.transport.SSLUtils; import com.android.email.provider.EmailContent; @@ -975,14 +976,17 @@ public class ExchangeService extends Service implements Runnable { * is null, mailboxes from all accounts with the specified hold will be released * @param reason the reason for the SyncError (AbstractSyncService.EXIT_XXX) * @param account an Account whose mailboxes should be released (or all if null) + * @return whether or not any mailboxes were released */ - /*package*/ void releaseSyncHolds(Context context, int reason, Account account) { - releaseSyncHoldsImpl(context, reason, account); + /*package*/ boolean releaseSyncHolds(Context context, int reason, Account account) { + boolean holdWasReleased = releaseSyncHoldsImpl(context, reason, account); kick("security release"); + return holdWasReleased; } - private void releaseSyncHoldsImpl(Context context, int reason, Account account) { + private boolean releaseSyncHoldsImpl(Context context, int reason, Account account) { synchronized(sSyncLock) { + boolean holdWasReleased = false; ArrayList releaseList = new ArrayList(); for (long mailboxId: mSyncErrorMap.keySet()) { if (account != null) { @@ -1000,7 +1004,9 @@ public class ExchangeService extends Service implements Runnable { } for (long mailboxId: releaseList) { mSyncErrorMap.remove(mailboxId); + holdWasReleased = true; } + return holdWasReleased; } } @@ -2376,6 +2382,20 @@ public class ExchangeService extends Service implements Runnable { SyncError syncError = errorMap.get(mailboxId); exchangeService.releaseMailbox(mailboxId); int exitStatus = svc.mExitStatus; + Mailbox m = Mailbox.restoreMailboxWithId(exchangeService, mailboxId); + if (m == null) return; + + if (exitStatus != AbstractSyncService.EXIT_LOGIN_FAILURE) { + long accountId = m.mAccountKey; + Account account = Account.restoreAccountWithId(exchangeService, accountId); + if (account == null) return; + if (exchangeService.releaseSyncHolds(exchangeService, + AbstractSyncService.EXIT_LOGIN_FAILURE, account)) { + NotificationController.getInstance(exchangeService) + .cancelLoginFailedNotification(accountId); + } + } + switch (exitStatus) { case AbstractSyncService.EXIT_DONE: if (svc.hasPendingRequests()) { @@ -2389,8 +2409,6 @@ public class ExchangeService extends Service implements Runnable { break; // I/O errors get retried at increasing intervals case AbstractSyncService.EXIT_IO_ERROR: - Mailbox m = Mailbox.restoreMailboxWithId(exchangeService, mailboxId); - if (m == null) return; if (syncError != null) { syncError.escalate(); log(m.mDisplayName + " held for " + syncError.holdDelay + "ms"); @@ -2400,8 +2418,11 @@ public class ExchangeService extends Service implements Runnable { } break; // These errors are not retried automatically - case AbstractSyncService.EXIT_SECURITY_FAILURE: case AbstractSyncService.EXIT_LOGIN_FAILURE: + NotificationController.getInstance(exchangeService) + .showLoginFailedNotification(m.mAccountKey); + // Fall through + case AbstractSyncService.EXIT_SECURITY_FAILURE: case AbstractSyncService.EXIT_EXCEPTION: errorMap.put(mailboxId, exchangeService.new SyncError(exitStatus, true)); break; diff --git a/tests/src/com/android/exchange/ExchangeServiceAccountTests.java b/tests/src/com/android/exchange/ExchangeServiceAccountTests.java index 425e9c482..9b9272695 100644 --- a/tests/src/com/android/exchange/ExchangeServiceAccountTests.java +++ b/tests/src/com/android/exchange/ExchangeServiceAccountTests.java @@ -73,7 +73,8 @@ public class ExchangeServiceAccountTests extends AccountTestCase { // We should have 4 assertEquals(4, errorMap.keySet().size()); // Release the holds on acct2 (there are two of them) - exchangeService.releaseSyncHolds(context, AbstractSyncService.EXIT_SECURITY_FAILURE, acct2); + assertTrue(exchangeService.releaseSyncHolds(context, + AbstractSyncService.EXIT_SECURITY_FAILURE, acct2)); // There should be two left assertEquals(2, errorMap.keySet().size()); // And these are the two... @@ -86,19 +87,22 @@ public class ExchangeServiceAccountTests extends AccountTestCase { // We should have 4 again assertEquals(4, errorMap.keySet().size()); // Release all of the security holds - exchangeService.releaseSyncHolds(context, AbstractSyncService.EXIT_SECURITY_FAILURE, null); + assertTrue(exchangeService.releaseSyncHolds(context, + AbstractSyncService.EXIT_SECURITY_FAILURE, null)); // There should be one left assertEquals(1, errorMap.keySet().size()); // And this is the one assertNotNull(errorMap.get(box2.mId)); // Release the i/o holds on account 2 (there aren't any) - exchangeService.releaseSyncHolds(context, AbstractSyncService.EXIT_IO_ERROR, acct2); + assertFalse(exchangeService.releaseSyncHolds(context, + AbstractSyncService.EXIT_IO_ERROR, acct2)); // There should still be one left assertEquals(1, errorMap.keySet().size()); // Release the i/o holds on account 1 (there's one) - exchangeService.releaseSyncHolds(context, AbstractSyncService.EXIT_IO_ERROR, acct1); + assertTrue(exchangeService.releaseSyncHolds(context, + AbstractSyncService.EXIT_IO_ERROR, acct1)); // There should still be one left assertEquals(0, errorMap.keySet().size()); }