Use notifications for login failures

* For now, clicking on the notification takes the user to the
  Welcome activity, as we don't have final flows for the new
  account setup UI
* Need comment on strings; the problem is that notification
  text must be rather short if we're to use the standard
  notification display.  It looks like newer UI will allow
  3 lines instead of 2, however.
* Tested w/ IMAP, POP3, EAS, and SMTP

Bug: 2322253
Change-Id: I7ed6fa5599179870cbcdb14af062e956eff37ec5
diff --git a/res/values/strings.xml b/res/values/strings.xml
index f8cfcd1..265e254 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -378,11 +378,15 @@
         messages moved to <xliff:g id="mailbox_name" example="Inbox" >%2$s</xliff:g></item>
     </plurals>
     <!-- Notification ticker when a forwarded attachment couldn't be sent [CHAR LIMIT=none] -->
-    <string name="forward_download_failed_ticker">"An attachment couldn't be forwarded"</string>
+    <string name="forward_download_failed_ticker">Could not forward one or more attachments</string>
     <!-- Notification text when a forwarded attachment couldn't be sent -->
-    <string name="forward_download_failed_notification">"The attachment "<xliff:g id="filename">%s
-        </xliff:g>" couldn't be sent with your outgoing mail because it couldn't be downloaded."
-        </string>
+    <string name="forward_download_failed_notification">Could not forward <xliff:g id="filename">
+        %s</xliff:g></string>
+    <!-- Notification ticker when email account authentication fails [CHAR LIMIT=20] -->
+    <string name="login_failed_ticker"><xliff:g id="account_name">%s
+        </xliff:g> sign-in failed</string>
+    <!-- Notification text when email account authentication fails [CHAR LIMIT=75]-->
+    <string name="login_failed_notification">Touch to change account settings</string>
 
     <!-- Size unit for bytes for attachments [CHAR LIMIT=10] -->
     <plurals name="message_view_attachment_bytes">
diff --git a/src/com/android/exchange/ExchangeService.java b/src/com/android/exchange/ExchangeService.java
index 7b87b75..6f103b0 100644
--- a/src/com/android/exchange/ExchangeService.java
+++ b/src/com/android/exchange/ExchangeService.java
@@ -19,6 +19,7 @@
 
 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 @@
      * 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<Long> releaseList = new ArrayList<Long>();
             for (long mailboxId: mSyncErrorMap.keySet()) {
                 if (account != null) {
@@ -1000,7 +1004,9 @@
             }
             for (long mailboxId: releaseList) {
                 mSyncErrorMap.remove(mailboxId);
+                holdWasReleased = true;
             }
+            return holdWasReleased;
         }
     }
 
@@ -2376,6 +2382,20 @@
             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 @@
                     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 @@
                     }
                     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 425e9c4..9b92726 100644
--- a/tests/src/com/android/exchange/ExchangeServiceAccountTests.java
+++ b/tests/src/com/android/exchange/ExchangeServiceAccountTests.java
@@ -73,7 +73,8 @@
         // 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 @@
         // 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());
     }