Merge pull request #1174

Handle synchronizing empty folders

Fixes #1139
This commit is contained in:
cketti 2016-03-24 05:22:04 +01:00
commit d93a7de367
2 changed files with 410 additions and 17 deletions

View file

@ -30,6 +30,7 @@ import android.net.Uri;
import android.os.Build;
import android.os.PowerManager;
import android.os.Process;
import android.support.annotation.VisibleForTesting;
import android.util.Log;
import com.fsck.k9.Account;
@ -158,6 +159,7 @@ public class MessagingController implements Runnable {
private final Context context;
private final NotificationController notificationController;
private volatile boolean stopped = false;
private static final Set<Flag> SYNC_FLAGS = EnumSet.of(Flag.SEEN, Flag.FLAGGED, Flag.ANSWERED, Flag.FORWARDED);
@ -214,7 +216,8 @@ public class MessagingController implements Runnable {
}
private MessagingController(Context context, NotificationController notificationController) {
@VisibleForTesting
MessagingController(Context context, NotificationController notificationController) {
this.context = context;
this.notificationController = notificationController;
mThread = new Thread(this);
@ -225,6 +228,13 @@ public class MessagingController implements Runnable {
}
}
@VisibleForTesting
void stop() throws InterruptedException {
stopped = true;
mThread.interrupt();
mThread.join(1000L);
}
public synchronized static MessagingController getInstance(Context context) {
if (inst == null) {
Context appContext = context.getApplicationContext();
@ -241,7 +251,7 @@ public class MessagingController implements Runnable {
@Override
public void run() {
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
while (true) {
while (!stopped) {
String commandDescription = null;
try {
final Command command = mCommands.take();
@ -765,7 +775,9 @@ public class MessagingController implements Runnable {
* TODO Break this method up into smaller chunks.
* @param providedRemoteFolder TODO
*/
private void synchronizeMailboxSynchronous(final Account account, final String folder, final MessagingListener listener, Folder providedRemoteFolder) {
@VisibleForTesting
void synchronizeMailboxSynchronous(final Account account, final String folder, final MessagingListener listener,
Folder providedRemoteFolder) {
Folder remoteFolder = null;
LocalFolder tLocalFolder = null;
@ -886,7 +898,7 @@ public class MessagingController implements Runnable {
final Date earliestDate = account.getEarliestPollDate();
int remoteStart;
int remoteStart = 1;
if (remoteMessageCount > 0) {
/* Message numbers start at 1. */
if (visibleLimit > 0) {
@ -927,7 +939,7 @@ public class MessagingController implements Runnable {
l.synchronizeMailboxHeadersFinished(account, folder, headerProgress.get(), remoteUidMap.size());
}
} else {
} else if(remoteMessageCount < 0) {
throw new Exception("Message count " + remoteMessageCount + " for folder " + folder);
}
@ -1164,7 +1176,6 @@ public class MessagingController implements Runnable {
if (K9.DEBUG)
Log.d(K9.LOG_TAG, "SYNC: About to fetch " + unsyncedMessages.size() + " unsynced messages for folder " + folder);
fetchUnsyncedMessages(account, remoteFolder, unsyncedMessages, smallMessages, largeMessages, progress, todo, fp);
String updatedPushState = localFolder.getPushState();
@ -1179,10 +1190,7 @@ public class MessagingController implements Runnable {
if (K9.DEBUG) {
Log.d(K9.LOG_TAG, "SYNC: Synced unsynced messages for folder " + folder);
}
}
if (K9.DEBUG)
Log.d(K9.LOG_TAG, "SYNC: Have "
+ largeMessages.size() + " large messages and "
@ -1190,26 +1198,24 @@ public class MessagingController implements Runnable {
+ 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();
//TODO: Only fetch small and large messages if we have some
fp.add(FetchProfile.Item.BODY);
// fp.add(FetchProfile.Item.FLAGS);
// fp.add(FetchProfile.Item.ENVELOPE);
downloadSmallMessages(account, remoteFolder, localFolder, smallMessages, progress, unreadBeforeStart, newMessages, todo, fp);
smallMessages.clear();
/*
* Now do the large messages that require more round trips.
*/
fp.clear();
fp = new FetchProfile();
fp.add(FetchProfile.Item.STRUCTURE);
downloadLargeMessages(account, remoteFolder, localFolder, largeMessages, progress, unreadBeforeStart, newMessages, todo, fp);
downloadLargeMessages(account, remoteFolder, localFolder, largeMessages, progress, unreadBeforeStart, newMessages, todo, fp);
largeMessages.clear();
/*
@ -1322,7 +1328,6 @@ public class MessagingController implements Runnable {
final String folder = remoteFolder.getName();
final Date earliestDate = account.getEarliestPollDate();
remoteFolder.fetch(unsyncedMessages, fp,
new MessageRetrievalListener<T>() {
@Override
@ -1340,6 +1345,7 @@ public class MessagingController implements Runnable {
}
progress.incrementAndGet();
for (MessagingListener l : getListeners()) {
//TODO: This might be the source of poll count errors in the UI. Is todo always the same as ofTotal
l.synchronizeMailboxProgress(account, folder, progress.get(), todo);
}
return;
@ -1489,7 +1495,7 @@ public class MessagingController implements Runnable {
* incomplete so the entire thing can be downloaded later if the user
* wishes to download it.
*/
fp.clear();
fp = new FetchProfile();
fp.add(FetchProfile.Item.BODY_SANE);
/*
* TODO a good optimization here would be to make sure that all Stores set
@ -1907,7 +1913,6 @@ public class MessagingController implements Runnable {
/*
* Otherwise we'll upload our message and then delete the remote message.
*/
fp.clear();
fp = new FetchProfile();
fp.add(FetchProfile.Item.BODY);
localFolder.fetch(Collections.singletonList(localMessage), fp, null);

View file

@ -0,0 +1,388 @@
package com.fsck.k9.controller;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import android.content.Context;
import com.fsck.k9.Account;
import com.fsck.k9.AccountStats;
import com.fsck.k9.mail.FetchProfile;
import com.fsck.k9.mail.Folder;
import com.fsck.k9.mail.Message;
import com.fsck.k9.mail.MessageRetrievalListener;
import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mail.Store;
import com.fsck.k9.mailstore.LocalFolder;
import com.fsck.k9.mailstore.LocalMessage;
import com.fsck.k9.mailstore.LocalStore;
import com.fsck.k9.notification.NotificationController;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import org.robolectric.shadows.ShadowApplication;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Mockito.atLeast;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@SuppressWarnings("unchecked")
@RunWith(RobolectricTestRunner.class)
@Config(manifest = "src/main/AndroidManifest.xml", sdk = 21)
public class MessagingControllerTest {
private static final String FOLDER_NAME = "Folder";
private static final int MAXIMUM_SMALL_MESSAGE_SIZE = 1000;
private MessagingController controller;
@Mock
private Account account;
@Mock
private AccountStats accountStats;
@Mock
private MessagingListener listener;
@Mock
private LocalFolder localFolder;
@Mock
private Folder remoteFolder;
@Mock
private LocalStore localStore;
@Mock
private Store remoteStore;
@Mock
private NotificationController notificationController;
@Captor
private ArgumentCaptor<List<Message>> messageListCaptor;
@Captor
private ArgumentCaptor<FetchProfile> fetchProfileCaptor;
@Before
public void setUp() throws MessagingException {
MockitoAnnotations.initMocks(this);
Context appContext = ShadowApplication.getInstance().getApplicationContext();
controller = new MessagingController(appContext, notificationController);
configureAccount();
configureLocalStore();
}
@After
public void tearDown() throws Exception {
controller.stop();
}
@Test
public void synchronizeMailboxSynchronous_withOneMessageInRemoteFolder_shouldFinishWithoutError()
throws Exception {
messageCountInRemoteFolder(1);
controller.synchronizeMailboxSynchronous(account, FOLDER_NAME, listener, remoteFolder);
verify(listener).synchronizeMailboxFinished(account, FOLDER_NAME, 1, 0);
}
@Test
public void synchronizeMailboxSynchronous_withEmptyRemoteFolder_shouldFinishWithoutError()
throws Exception {
messageCountInRemoteFolder(0);
controller.synchronizeMailboxSynchronous(account, FOLDER_NAME, listener, remoteFolder);
verify(listener).synchronizeMailboxFinished(account, FOLDER_NAME, 0, 0);
}
@Test
public void synchronizeMailboxSynchronous_withNegativeMessageCountInRemoteFolder_shouldFinishWithError()
throws Exception {
messageCountInRemoteFolder(-1);
controller.synchronizeMailboxSynchronous(account, FOLDER_NAME, listener, remoteFolder);
verify(listener).synchronizeMailboxFailed(account, FOLDER_NAME,
"Exception: Message count -1 for folder Folder");
}
@Test
public void synchronizeMailboxSynchronous_withRemoteFolderProvided_shouldNotOpenRemoteFolder() throws Exception {
messageCountInRemoteFolder(1);
controller.synchronizeMailboxSynchronous(account, FOLDER_NAME, listener, remoteFolder);
verify(remoteFolder, never()).open(Folder.OPEN_MODE_RW);
}
@Test
public void synchronizeMailboxSynchronous_withNoRemoteFolderProvided_shouldOpenRemoteFolderFromStore()
throws Exception {
messageCountInRemoteFolder(1);
configureRemoteStoreWithFolder();
controller.synchronizeMailboxSynchronous(account, FOLDER_NAME, listener, null);
verify(remoteFolder).open(Folder.OPEN_MODE_RW);
}
@Test
public void synchronizeMailboxSynchronous_withRemoteFolderProvided_shouldNotCloseRemoteFolder() throws Exception {
messageCountInRemoteFolder(1);
controller.synchronizeMailboxSynchronous(account, FOLDER_NAME, listener, remoteFolder);
verify(remoteFolder, never()).close();
}
@Test
public void synchronizeMailboxSynchronous_withNoRemoteFolderProvided_shouldCloseRemoteFolderFromStore()
throws Exception {
messageCountInRemoteFolder(1);
configureRemoteStoreWithFolder();
controller.synchronizeMailboxSynchronous(account, FOLDER_NAME, listener, null);
verify(remoteFolder).close();
}
@Test
public void synchronizeMailboxSynchronous_withAccountPolicySetToExpungeOnPoll_shouldExpungeRemoteFolder()
throws Exception {
messageCountInRemoteFolder(1);
when(account.getExpungePolicy()).thenReturn(Account.Expunge.EXPUNGE_ON_POLL);
configureRemoteStoreWithFolder();
controller.synchronizeMailboxSynchronous(account, FOLDER_NAME, listener, null);
verify(remoteFolder).expunge();
}
@Test
public void synchronizeMailboxSynchronous_withAccountPolicySetToExpungeManually_shouldNotExpungeRemoteFolder()
throws Exception {
messageCountInRemoteFolder(1);
when(account.getExpungePolicy()).thenReturn(Account.Expunge.EXPUNGE_MANUALLY);
controller.synchronizeMailboxSynchronous(account, FOLDER_NAME, listener, null);
verify(remoteFolder, never()).expunge();
}
@Test
public void synchronizeMailboxSynchronous_withAccountSetToSyncRemoteDeletions_shouldDeleteLocalCopiesOfDeletedMessages()
throws Exception {
messageCountInRemoteFolder(0);
LocalMessage localCopyOfRemoteDeletedMessage = mock(LocalMessage.class);
when(account.syncRemoteDeletions()).thenReturn(true);
when(localFolder.getMessages(null)).thenReturn(Collections.singletonList(localCopyOfRemoteDeletedMessage));
controller.synchronizeMailboxSynchronous(account, FOLDER_NAME, listener, remoteFolder);
verify(localFolder).destroyMessages(messageListCaptor.capture());
assertEquals(localCopyOfRemoteDeletedMessage, messageListCaptor.getValue().get(0));
}
@Test
public void synchronizeMailboxSynchronous_withAccountSetToSyncRemoteDeletions_shouldNotDeleteLocalCopiesOfExistingMessagesAfterEarliestPollDate()
throws Exception {
messageCountInRemoteFolder(1);
Date dateOfEarliestPoll = new Date();
LocalMessage localMessage = localMessageWithCopyOnServer();
when(account.syncRemoteDeletions()).thenReturn(true);
when(account.getEarliestPollDate()).thenReturn(dateOfEarliestPoll);
when(localMessage.olderThan(dateOfEarliestPoll)).thenReturn(false);
when(localFolder.getMessages(null)).thenReturn(Collections.singletonList(localMessage));
controller.synchronizeMailboxSynchronous(account, FOLDER_NAME, listener, remoteFolder);
verify(localFolder, never()).destroyMessages(messageListCaptor.capture());
}
@Test
public void synchronizeMailboxSynchronous_withAccountSetToSyncRemoteDeletions_shouldDeleteLocalCopiesOfExistingMessagesBeforeEarliestPollDate()
throws Exception {
messageCountInRemoteFolder(1);
LocalMessage localMessage = localMessageWithCopyOnServer();
Date dateOfEarliestPoll = new Date();
when(account.syncRemoteDeletions()).thenReturn(true);
when(account.getEarliestPollDate()).thenReturn(dateOfEarliestPoll);
when(localMessage.olderThan(dateOfEarliestPoll)).thenReturn(true);
when(localFolder.getMessages(null)).thenReturn(Collections.singletonList(localMessage));
controller.synchronizeMailboxSynchronous(account, FOLDER_NAME, listener, remoteFolder);
verify(localFolder).destroyMessages(messageListCaptor.capture());
assertEquals(localMessage, messageListCaptor.getValue().get(0));
}
@Test
public void synchronizeMailboxSynchronous_withAccountSetNotToSyncRemoteDeletions_shouldNotDeleteLocalCopiesOfMessages()
throws Exception {
messageCountInRemoteFolder(0);
LocalMessage remoteDeletedMessage = mock(LocalMessage.class);
when(account.syncRemoteDeletions()).thenReturn(false);
when(localFolder.getMessages(null)).thenReturn(Collections.singletonList(remoteDeletedMessage));
controller.synchronizeMailboxSynchronous(account, FOLDER_NAME, listener, remoteFolder);
verify(localFolder, never()).destroyMessages(messageListCaptor.capture());
}
@Test
public void synchronizeMailboxSynchronous_withAccountSupportingFetchingFlags_shouldFetchUnsychronizedMessagesListAndFlags()
throws Exception {
messageCountInRemoteFolder(1);
hasUnsyncedRemoteMessage();
when(remoteFolder.supportsFetchingFlags()).thenReturn(true);
controller.synchronizeMailboxSynchronous(account, FOLDER_NAME, listener, remoteFolder);
verify(remoteFolder, atLeastOnce()).fetch(any(List.class), fetchProfileCaptor.capture(),
any(MessageRetrievalListener.class));
assertEquals(2, fetchProfileCaptor.getAllValues().get(0).size());
assertTrue(fetchProfileCaptor.getAllValues().get(0).contains(FetchProfile.Item.FLAGS));
assertTrue(fetchProfileCaptor.getAllValues().get(0).contains(FetchProfile.Item.ENVELOPE));
}
@Test
public void synchronizeMailboxSynchronous_withAccountNotSupportingFetchingFlags_shouldFetchUnsychronizedMessages()
throws Exception {
messageCountInRemoteFolder(1);
hasUnsyncedRemoteMessage();
when(remoteFolder.supportsFetchingFlags()).thenReturn(false);
controller.synchronizeMailboxSynchronous(account, FOLDER_NAME, listener, remoteFolder);
verify(remoteFolder, atLeastOnce()).fetch(any(List.class), fetchProfileCaptor.capture(),
any(MessageRetrievalListener.class));
assertEquals(1, fetchProfileCaptor.getAllValues().get(0).size());
assertTrue(fetchProfileCaptor.getAllValues().get(0).contains(FetchProfile.Item.ENVELOPE));
}
@Test
public void synchronizeMailboxSynchronous_withUnsyncedNewSmallMessage_shouldFetchBodyOfSmallMessage()
throws Exception {
Message smallMessage = buildSmallNewMessage();
messageCountInRemoteFolder(1);
hasUnsyncedRemoteMessage();
when(remoteFolder.supportsFetchingFlags()).thenReturn(false);
respondToFetchEnvelopesWithMessage(smallMessage);
controller.synchronizeMailboxSynchronous(account, FOLDER_NAME, listener, remoteFolder);
verify(remoteFolder, atLeast(2)).fetch(any(List.class), fetchProfileCaptor.capture(),
any(MessageRetrievalListener.class));
assertEquals(1, fetchProfileCaptor.getAllValues().get(1).size());
assertTrue(fetchProfileCaptor.getAllValues().get(1).contains(FetchProfile.Item.BODY));
}
@Test
public void synchronizeMailboxSynchronous_withUnsyncedNewSmallMessage_shouldFetchStructureAndLimitedBodyOfLargeMessage()
throws Exception {
Message largeMessage = buildLargeNewMessage();
messageCountInRemoteFolder(1);
hasUnsyncedRemoteMessage();
when(remoteFolder.supportsFetchingFlags()).thenReturn(false);
respondToFetchEnvelopesWithMessage(largeMessage);
controller.synchronizeMailboxSynchronous(account, FOLDER_NAME, listener, remoteFolder);
//TODO: Don't bother fetching messages of a size we don't have
verify(remoteFolder, atLeast(4)).fetch(any(List.class), fetchProfileCaptor.capture(),
any(MessageRetrievalListener.class));
assertEquals(1, fetchProfileCaptor.getAllValues().get(2).size());
assertEquals(FetchProfile.Item.STRUCTURE, fetchProfileCaptor.getAllValues().get(2).get(0));
assertEquals(1, fetchProfileCaptor.getAllValues().get(3).size());
assertEquals(FetchProfile.Item.BODY_SANE, fetchProfileCaptor.getAllValues().get(3).get(0));
}
private void respondToFetchEnvelopesWithMessage(final Message message) throws MessagingException {
doAnswer(new Answer() {
@Override
public Void answer(InvocationOnMock invocation) throws Throwable {
FetchProfile fetchProfile = (FetchProfile) invocation.getArguments()[1];
if (invocation.getArguments()[2] != null) {
MessageRetrievalListener listener = (MessageRetrievalListener) invocation.getArguments()[2];
if (fetchProfile.contains(FetchProfile.Item.ENVELOPE)) {
listener.messageStarted("UID", 1, 1);
listener.messageFinished(message, 1, 1);
listener.messagesFinished(1);
}
}
return null;
}
}).when(remoteFolder).fetch(any(List.class), any(FetchProfile.class), any(MessageRetrievalListener.class));
}
private Message buildSmallNewMessage() {
Message message = mock(Message.class);
when(message.olderThan(any(Date.class))).thenReturn(false);
when(message.getSize()).thenReturn(MAXIMUM_SMALL_MESSAGE_SIZE);
return message;
}
private Message buildLargeNewMessage() {
Message message = mock(Message.class);
when(message.olderThan(any(Date.class))).thenReturn(false);
when(message.getSize()).thenReturn(MAXIMUM_SMALL_MESSAGE_SIZE + 1);
return message;
}
private void messageCountInRemoteFolder(int value) throws MessagingException {
when(remoteFolder.getMessageCount()).thenReturn(value);
}
private LocalMessage localMessageWithCopyOnServer() throws MessagingException {
String messageUid = "UID";
Message remoteMessage = mock(Message.class);
LocalMessage localMessage = mock(LocalMessage.class);
when(remoteMessage.getUid()).thenReturn(messageUid);
when(localMessage.getUid()).thenReturn(messageUid);
when(remoteFolder.getMessages(anyInt(), anyInt(), any(Date.class), any(MessageRetrievalListener.class)))
.thenReturn(Collections.singletonList(remoteMessage));
return localMessage;
}
private void hasUnsyncedRemoteMessage() throws MessagingException {
String messageUid = "UID";
Message remoteMessage = mock(Message.class);
when(remoteMessage.getUid()).thenReturn(messageUid);
when(remoteFolder.getMessages(anyInt(), anyInt(), any(Date.class), any(MessageRetrievalListener.class)))
.thenReturn(Collections.singletonList(remoteMessage));
}
private void configureAccount() throws MessagingException {
when(account.getLocalStore()).thenReturn(localStore);
when(account.getStats(any(Context.class))).thenReturn(accountStats);
when(account.getMaximumAutoDownloadMessageSize()).thenReturn(MAXIMUM_SMALL_MESSAGE_SIZE);
}
private void configureLocalStore() {
when(localStore.getFolder(FOLDER_NAME)).thenReturn(localFolder);
}
private void configureRemoteStoreWithFolder() throws MessagingException {
when(account.getRemoteStore()).thenReturn(remoteStore);
when(remoteStore.getFolder(FOLDER_NAME)).thenReturn(remoteFolder);
}
}