Merge pull request #1248 from k9mail/GH-879_notify_on_authentication_failure

Notify user on authentication failure
This commit is contained in:
cketti 2016-04-11 22:57:00 +02:00
commit 78715ed29f
11 changed files with 340 additions and 23 deletions

View file

@ -14,6 +14,7 @@ public interface PushReceiver {
void messagesRemoved(Folder folder, List<Message> mess);
String getPushState(String folderName);
void pushError(String errorMessage, Exception e);
void authenticationFailed();
void setPushActive(String folderName, boolean enabled);
void sleep(TracingWakeLock wakeLock, long millis);
}

View file

@ -13,6 +13,7 @@ import android.content.Context;
import android.os.PowerManager;
import android.util.Log;
import com.fsck.k9.mail.AuthenticationFailedException;
import com.fsck.k9.mail.Flag;
import com.fsck.k9.mail.K9MailLib;
import com.fsck.k9.mail.Message;
@ -188,19 +189,18 @@ class ImapFolderPusher extends ImapFolder {
returnFromIdle();
}
} catch (Exception e) {
wakeLock.acquire(PUSH_WAKE_LOCK_TIMEOUT);
} catch (AuthenticationFailedException e) {
reacquireWakeLockAndCleanUp();
clearStoredUntaggedResponses();
idling = false;
pushReceiver.setPushActive(getName(), false);
try {
connection.close();
} catch (Exception me) {
Log.e(LOG_TAG, "Got exception while closing for exception for " + getLogId(), me);
if (K9MailLib.isDebug()) {
Log.e(K9MailLib.LOG_TAG, "Authentication failed. Stopping ImapFolderPusher.", e);
}
pushReceiver.authenticationFailed();
stop = true;
} catch (Exception e) {
reacquireWakeLockAndCleanUp();
if (stop) {
Log.i(LOG_TAG, "Got exception while idling, but stop is set for " + getLogId());
} else {
@ -241,6 +241,20 @@ class ImapFolderPusher extends ImapFolder {
}
}
private void reacquireWakeLockAndCleanUp() {
wakeLock.acquire(PUSH_WAKE_LOCK_TIMEOUT);
clearStoredUntaggedResponses();
idling = false;
pushReceiver.setPushActive(getName(), false);
try {
connection.close();
} catch (Exception me) {
Log.e(LOG_TAG, "Got exception while closing for exception for " + getLogId(), me);
}
}
private long getNewUidNext() throws MessagingException {
long newUidNext = uidNext;
if (newUidNext != -1L) {

View file

@ -44,6 +44,7 @@ import com.fsck.k9.R;
import com.fsck.k9.activity.MessageReference;
import com.fsck.k9.activity.setup.AccountSetupCheckSettings.CheckDirection;
import com.fsck.k9.cache.EmailProviderCache;
import com.fsck.k9.mail.AuthenticationFailedException;
import com.fsck.k9.mail.CertificateValidationException;
import com.fsck.k9.mail.power.TracingPowerManager;
import com.fsck.k9.mail.power.TracingPowerManager.TracingWakeLock;
@ -879,6 +880,8 @@ public class MessagingController implements Runnable {
}
notificationController.clearAuthenticationErrorNotification(account, true);
/*
* Get the remote message count.
*/
@ -1010,6 +1013,12 @@ public class MessagingController implements Runnable {
if (K9.DEBUG)
Log.i(K9.LOG_TAG, "Done synchronizing folder " + account.getDescription() + ":" + folder);
} catch (AuthenticationFailedException e) {
handleAuthenticationFailure(account, true);
for (MessagingListener l : getListeners(listener)) {
l.synchronizeMailboxFailed(account, folder, "Authentication failure");
}
} catch (Exception e) {
Log.e(K9.LOG_TAG, "synchronizeMailbox", e);
// If we don't set the last checked, it can try too often during
@ -1042,6 +1051,10 @@ public class MessagingController implements Runnable {
}
void handleAuthenticationFailure(Account account, boolean incoming) {
notificationController.showAuthenticationErrorNotification(account, incoming);
}
private void updateMoreMessages(Folder remoteFolder, LocalFolder localFolder, Date earliestDate, int remoteStart)
throws MessagingException, IOException {
@ -3142,7 +3155,12 @@ public class MessagingController implements Runnable {
queuePendingCommand(account, command);
processPendingCommands(account);
}
} catch (AuthenticationFailedException e) {
lastFailure = e;
wasPermanentFailure = false;
handleAuthenticationFailure(account, false);
handleSendFailure(account, localStore, localFolder, message, e, wasPermanentFailure);
} catch (CertificateValidationException e) {
lastFailure = e;
wasPermanentFailure = false;

View file

@ -83,6 +83,11 @@ public class MessagingControllerPushReceiver implements PushReceiver {
controller.addErrorMessage(account, errMess, e);
}
@Override
public void authenticationFailed() {
controller.handleAuthenticationFailure(account, true);
}
public String getPushState(String folderName) {
LocalFolder localFolder = null;
try {

View file

@ -0,0 +1,71 @@
package com.fsck.k9.notification;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.NotificationCompat.BigTextStyle;
import android.support.v4.app.NotificationManagerCompat;
import com.fsck.k9.Account;
import com.fsck.k9.R;
import com.fsck.k9.activity.setup.AccountSetupIncoming;
import com.fsck.k9.activity.setup.AccountSetupOutgoing;
import static com.fsck.k9.notification.NotificationController.NOTIFICATION_LED_BLINK_FAST;
import static com.fsck.k9.notification.NotificationController.NOTIFICATION_LED_FAILURE_COLOR;
class AuthenticationErrorNotifications {
private final NotificationController controller;
public AuthenticationErrorNotifications(NotificationController controller) {
this.controller = controller;
}
public void showAuthenticationErrorNotification(Account account, boolean incoming) {
int notificationId = NotificationIds.getAuthenticationErrorNotificationId(account, incoming);
Context context = controller.getContext();
PendingIntent editServerSettingsPendingIntent = createContentIntent(context, account, incoming);
String title = context.getString(R.string.notification_authentication_error_title);
String text = context.getString(R.string.notification_authentication_error_text, account.getDescription());
NotificationCompat.Builder builder = controller.createNotificationBuilder()
.setSmallIcon(R.drawable.notification_icon_warning)
.setWhen(System.currentTimeMillis())
.setAutoCancel(true)
.setTicker(title)
.setContentTitle(title)
.setContentText(text)
.setContentIntent(editServerSettingsPendingIntent)
.setStyle(new BigTextStyle().bigText(text))
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC);
controller.configureNotification(builder, null, null,
NOTIFICATION_LED_FAILURE_COLOR,
NOTIFICATION_LED_BLINK_FAST, true);
getNotificationManager().notify(notificationId, builder.build());
}
public void clearAuthenticationErrorNotification(Account account, boolean incoming) {
int notificationId = NotificationIds.getAuthenticationErrorNotificationId(account, incoming);
getNotificationManager().cancel(notificationId);
}
PendingIntent createContentIntent(Context context, Account account, boolean incoming) {
Intent editServerSettingsIntent = incoming ?
AccountSetupIncoming.intentActionEditIncomingSettings(context, account) :
AccountSetupOutgoing.intentActionEditOutgoingSettings(context, account);
return PendingIntent.getActivity(context, account.getAccountNumber(), editServerSettingsIntent,
PendingIntent.FLAG_UPDATE_CURRENT);
}
private NotificationManagerCompat getNotificationManager() {
return controller.getNotificationManager();
}
}

View file

@ -28,6 +28,7 @@ public class NotificationController {
private final Context context;
private final NotificationManagerCompat notificationManager;
private final CertificateErrorNotifications certificateErrorNotifications;
private final AuthenticationErrorNotifications authenticationErrorNotifications;
private final SyncNotifications syncNotifications;
private final SendFailedNotifications sendFailedNotifications;
private final NewMailNotifications newMailNotifications;
@ -54,6 +55,7 @@ public class NotificationController {
NotificationActionCreator actionBuilder = new NotificationActionCreator(context);
certificateErrorNotifications = new CertificateErrorNotifications(this);
authenticationErrorNotifications = new AuthenticationErrorNotifications(this);
syncNotifications = new SyncNotifications(this, actionBuilder);
sendFailedNotifications = new SendFailedNotifications(this, actionBuilder);
newMailNotifications = NewMailNotifications.newInstance(this, actionBuilder);
@ -67,6 +69,14 @@ public class NotificationController {
certificateErrorNotifications.clearCertificateErrorNotifications(account, incoming);
}
public void showAuthenticationErrorNotification(Account account, boolean incoming) {
authenticationErrorNotifications.showAuthenticationErrorNotification(account, incoming);
}
public void clearAuthenticationErrorNotification(Account account, boolean incoming) {
authenticationErrorNotifications.clearAuthenticationErrorNotification(account, incoming);
}
public void showSendingNotification(Account account) {
syncNotifications.showSendingNotification(account);
}

View file

@ -8,12 +8,14 @@ class NotificationIds {
private static final int OFFSET_SEND_FAILED_NOTIFICATION = 0;
private static final int OFFSET_CERTIFICATE_ERROR_INCOMING = 1;
private static final int OFFSET_CERTIFICATE_ERROR_OUTGOING = 2;
private static final int OFFSET_FETCHING_MAIL = 3;
private static final int OFFSET_NEW_MAIL_SUMMARY = 4;
private static final int OFFSET_AUTHENTICATION_ERROR_INCOMING = 3;
private static final int OFFSET_AUTHENTICATION_ERROR_OUTGOING = 4;
private static final int OFFSET_FETCHING_MAIL = 5;
private static final int OFFSET_NEW_MAIL_SUMMARY = 6;
private static final int OFFSET_NEW_MAIL_STACKED = 5;
private static final int OFFSET_NEW_MAIL_STACKED = 7;
private static final int NUMBER_OF_DEVICE_NOTIFICATIONS = 5;
private static final int NUMBER_OF_DEVICE_NOTIFICATIONS = 7;
private static final int NUMBER_OF_STACKED_NOTIFICATIONS = NotificationData.MAX_NUMBER_OF_STACKED_NOTIFICATIONS;
private static final int NUMBER_OF_NOTIFICATIONS_PER_ACCOUNT = NUMBER_OF_DEVICE_NOTIFICATIONS +
NUMBER_OF_STACKED_NOTIFICATIONS;
@ -44,6 +46,11 @@ class NotificationIds {
return getBaseNotificationId(account) + offset;
}
public static int getAuthenticationErrorNotificationId(Account account, boolean incoming) {
int offset = incoming ? OFFSET_AUTHENTICATION_ERROR_INCOMING : OFFSET_AUTHENTICATION_ERROR_OUTGOING;
return getBaseNotificationId(account) + offset;
}
private static int getBaseNotificationId(Account account) {
return account.getAccountNumber() * NUMBER_OF_NOTIFICATIONS_PER_ACCOUNT;
}

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#ffffff"
android:pathData="M1,21h22L12,2 1,21zM13,18h-2v-2h2v2zM13,14h-2v-4h2v4z"/>
</vector>

View file

@ -221,6 +221,9 @@ Please submit bug reports, contribute new features and ask questions at
<string name="notification_certificate_error_title">Certificate error for <xliff:g id="account">%s</xliff:g></string>
<string name="notification_certificate_error_text">Check your server settings</string>
<string name="notification_authentication_error_title">Authentication failed</string>
<string name="notification_authentication_error_text">Authentication failed for <xliff:g id="account">%s</xliff:g>. Update your server settings.</string>
<string name="notification_bg_sync_ticker">Checking mail: <xliff:g id="account">%s</xliff:g>:<xliff:g id="folder">%s</xliff:g></string>
<string name="notification_bg_sync_title">Checking mail</string>
<string name="notification_bg_send_ticker">Sending mail: <xliff:g id="account">%s</xliff:g></string>

View file

@ -0,0 +1,143 @@
package com.fsck.k9.notification;
import android.app.Notification;
import android.app.PendingIntent;
import android.content.Context;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.NotificationCompat.Builder;
import android.support.v4.app.NotificationManagerCompat;
import com.fsck.k9.Account;
import com.fsck.k9.MockHelper;
import com.fsck.k9.R;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@RunWith(RobolectricTestRunner.class)
@Config(manifest = "src/main/AndroidManifest.xml", sdk = 21)
public class AuthenticationErrorNotificationsTest {
private static final boolean INCOMING = true;
private static final boolean OUTGOING = false;
private static final int ACCOUNT_NUMBER = 1;
private static final String ACCOUNT_NAME = "TestAccount";
private NotificationManagerCompat notificationManager;
private NotificationCompat.Builder builder;
private NotificationController controller;
private Account account;
private AuthenticationErrorNotifications authenticationErrorNotifications;
private PendingIntent contentIntent;
@Before
public void setUp() throws Exception {
notificationManager = createFakeNotificationManager();
builder = createFakeNotificationBuilder();
controller = createFakeNotificationController(notificationManager, builder);
account = createFakeAccount();
contentIntent = createFakeContentIntent();
authenticationErrorNotifications = new TestAuthenticationErrorNotifications();
}
@Test
public void showAuthenticationErrorNotification_withIncomingServer_shouldCreateNotification() throws Exception {
int notificationId = NotificationIds.getAuthenticationErrorNotificationId(account, INCOMING);
authenticationErrorNotifications.showAuthenticationErrorNotification(account, INCOMING);
verify(notificationManager).notify(eq(notificationId), any(Notification.class));
assertAuthenticationErrorNotificationContents();
}
@Test
public void clearAuthenticationErrorNotification_withIncomingServer_shouldCancelNotification() throws Exception {
int notificationId = NotificationIds.getAuthenticationErrorNotificationId(account, INCOMING);
authenticationErrorNotifications.clearAuthenticationErrorNotification(account, INCOMING);
verify(notificationManager).cancel(notificationId);
}
@Test
public void showAuthenticationErrorNotification_withOutgoingServer_shouldCreateNotification() throws Exception {
int notificationId = NotificationIds.getAuthenticationErrorNotificationId(account, OUTGOING);
authenticationErrorNotifications.showAuthenticationErrorNotification(account, OUTGOING);
verify(notificationManager).notify(eq(notificationId), any(Notification.class));
assertAuthenticationErrorNotificationContents();
}
@Test
public void clearAuthenticationErrorNotification_withOutgoingServer_shouldCancelNotification() throws Exception {
int notificationId = NotificationIds.getAuthenticationErrorNotificationId(account, OUTGOING);
authenticationErrorNotifications.clearAuthenticationErrorNotification(account, OUTGOING);
verify(notificationManager).cancel(notificationId);
}
private void assertAuthenticationErrorNotificationContents() {
verify(builder).setSmallIcon(R.drawable.notification_icon_warning);
verify(builder).setTicker("Authentication failed");
verify(builder).setContentTitle("Authentication failed");
verify(builder).setContentText("Authentication failed for " + ACCOUNT_NAME + ". Update your server settings.");
verify(builder).setContentIntent(contentIntent);
verify(builder).setVisibility(NotificationCompat.VISIBILITY_PUBLIC);
}
private NotificationManagerCompat createFakeNotificationManager() {
return mock(NotificationManagerCompat.class);
}
private Builder createFakeNotificationBuilder() {
return MockHelper.mockBuilder(NotificationCompat.Builder.class);
}
private NotificationController createFakeNotificationController(NotificationManagerCompat notificationManager,
NotificationCompat.Builder builder) {
NotificationController controller = mock(NotificationController.class);
when(controller.getContext()).thenReturn(RuntimeEnvironment.application);
when(controller.getNotificationManager()).thenReturn(notificationManager);
when(controller.createNotificationBuilder()).thenReturn(builder);
return controller;
}
private Account createFakeAccount() {
Account account = mock(Account.class);
when(account.getAccountNumber()).thenReturn(ACCOUNT_NUMBER);
when(account.getDescription()).thenReturn(ACCOUNT_NAME);
return account;
}
private PendingIntent createFakeContentIntent() {
return mock(PendingIntent.class);
}
class TestAuthenticationErrorNotifications extends AuthenticationErrorNotifications {
public TestAuthenticationErrorNotifications() {
super(controller);
}
@Override
PendingIntent createContentIntent(Context context, Account account, boolean incoming) {
return contentIntent;
}
}
}

View file

@ -23,7 +23,7 @@ public class NotificationIdsTest {
int notificationId = NotificationIds.getNewMailSummaryNotificationId(account);
assertEquals(4, notificationId);
assertEquals(6, notificationId);
}
@Test
@ -33,7 +33,7 @@ public class NotificationIdsTest {
int notificationId = NotificationIds.getNewMailStackedNotificationId(account, notificationIndex);
assertEquals(5, notificationId);
assertEquals(7, notificationId);
}
@Test(expected = IndexOutOfBoundsException.class)
@ -56,7 +56,7 @@ public class NotificationIdsTest {
int notificationId = NotificationIds.getNewMailSummaryNotificationId(account);
assertEquals(17, notificationId);
assertEquals(21, notificationId);
}
@Test
@ -66,7 +66,7 @@ public class NotificationIdsTest {
int notificationId = NotificationIds.getNewMailStackedNotificationId(account, notificationIndex);
assertEquals(25, notificationId);
assertEquals(29, notificationId);
}
@Test
@ -75,7 +75,7 @@ public class NotificationIdsTest {
int notificationId = NotificationIds.getFetchingMailNotificationId(account);
assertEquals(3, notificationId);
assertEquals(5, notificationId);
}
@Test
@ -84,7 +84,7 @@ public class NotificationIdsTest {
int notificationId = NotificationIds.getFetchingMailNotificationId(account);
assertEquals(16, notificationId);
assertEquals(20, notificationId);
}
@Test
@ -102,7 +102,7 @@ public class NotificationIdsTest {
int notificationId = NotificationIds.getSendFailedNotificationId(account);
assertEquals(13, notificationId);
assertEquals(15, notificationId);
}
@Test
@ -120,7 +120,7 @@ public class NotificationIdsTest {
int notificationId = NotificationIds.getCertificateErrorNotificationId(account, INCOMING);
assertEquals(14, notificationId);
assertEquals(16, notificationId);
}
@Test
@ -138,7 +138,43 @@ public class NotificationIdsTest {
int notificationId = NotificationIds.getCertificateErrorNotificationId(account, OUTGOING);
assertEquals(15, notificationId);
assertEquals(17, notificationId);
}
@Test
public void getAuthenticationErrorNotificationId_forIncomingServerWithDefaultAccount() throws Exception {
Account account = createMockAccountWithAccountNumber(0);
int notificationId = NotificationIds.getAuthenticationErrorNotificationId(account, INCOMING);
assertEquals(3, notificationId);
}
@Test
public void getAuthenticationErrorNotificationId_forIncomingServerWithSecondAccount() throws Exception {
Account account = createMockAccountWithAccountNumber(1);
int notificationId = NotificationIds.getAuthenticationErrorNotificationId(account, INCOMING);
assertEquals(18, notificationId);
}
@Test
public void getAuthenticationErrorNotificationId_forOutgoingServerWithDefaultAccount() throws Exception {
Account account = createMockAccountWithAccountNumber(0);
int notificationId = NotificationIds.getAuthenticationErrorNotificationId(account, OUTGOING);
assertEquals(4, notificationId);
}
@Test
public void getAuthenticationErrorNotificationId_forOutgoingServerWithSecondAccount() throws Exception {
Account account = createMockAccountWithAccountNumber(1);
int notificationId = NotificationIds.getAuthenticationErrorNotificationId(account, OUTGOING);
assertEquals(19, notificationId);
}
private Account createMockAccountWithAccountNumber(int accountNumber) {