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
This commit is contained in:
Marc Blank 2010-10-18 13:14:20 -07:00
parent 023285796b
commit d3e4f3ca7e
11 changed files with 157 additions and 90 deletions

View File

@ -1,35 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2010 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<!-- TODO: Use a theme style color for the text view -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:padding="3dp"
>
<ImageView android:id="@+id/image"
android:layout_width="wrap_content"
android:layout_height="fill_parent"
android:layout_marginRight="10dp"
/>
<TextView android:id="@+id/text"
android:layout_width="wrap_content"
android:layout_height="fill_parent"
android:textColor="#000"
/>
</LinearLayout>

View File

@ -378,11 +378,15 @@ save attachment.</string>
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">

View File

@ -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();

View File

@ -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));
}
}

View File

@ -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.
*/

View File

@ -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
*/

View File

@ -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) {

View File

@ -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

View File

@ -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);

View File

@ -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<Long> releaseList = new ArrayList<Long>();
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;

View File

@ -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());
}