diff --git a/app/core/src/main/java/com/fsck/k9/KoinModule.kt b/app/core/src/main/java/com/fsck/k9/KoinModule.kt index 88db3d033..7e2e9e508 100644 --- a/app/core/src/main/java/com/fsck/k9/KoinModule.kt +++ b/app/core/src/main/java/com/fsck/k9/KoinModule.kt @@ -13,7 +13,15 @@ import com.fsck.k9.setup.ServerNameSuggester import org.koin.dsl.module val mainModule = module { - single { Preferences.getPreferences(get()) } + single { + Preferences( + context = get(), + storagePersister = get(), + localStoreProvider = get(), + localKeyStoreManager = get(), + accountPreferenceSerializer = get() + ) + } single { get().resources } single { get().contentResolver } single { LocalStoreProvider() } diff --git a/app/core/src/main/java/com/fsck/k9/Preferences.java b/app/core/src/main/java/com/fsck/k9/Preferences.java deleted file mode 100644 index a7cad0cab..000000000 --- a/app/core/src/main/java/com/fsck/k9/Preferences.java +++ /dev/null @@ -1,329 +0,0 @@ - -package com.fsck.k9; - - -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.UUID; -import java.util.concurrent.CopyOnWriteArrayList; - -import android.content.Context; -import androidx.annotation.GuardedBy; -import androidx.annotation.NonNull; -import androidx.annotation.RestrictTo; -import androidx.annotation.RestrictTo.Scope; - -import com.fsck.k9.backend.BackendManager; -import com.fsck.k9.mail.MessagingException; -import com.fsck.k9.mailstore.LocalStore; -import com.fsck.k9.mailstore.LocalStoreProvider; -import com.fsck.k9.preferences.Storage; -import com.fsck.k9.preferences.StorageEditor; -import com.fsck.k9.preferences.StoragePersister; -import timber.log.Timber; - - -public class Preferences { - - private static Preferences preferences; - private AccountPreferenceSerializer accountPreferenceSerializer; - - public static synchronized Preferences getPreferences(Context context) { - if (preferences == null) { - Context appContext = context.getApplicationContext(); - CoreResourceProvider resourceProvider = DI.get(CoreResourceProvider.class); - LocalKeyStoreManager localKeyStoreManager = DI.get(LocalKeyStoreManager.class); - AccountPreferenceSerializer accountPreferenceSerializer = DI.get(AccountPreferenceSerializer.class); - LocalStoreProvider localStoreProvider = DI.get(LocalStoreProvider.class); - StoragePersister storagePersister = DI.get(StoragePersister.class); - - preferences = new Preferences(appContext, resourceProvider, storagePersister, localStoreProvider, localKeyStoreManager, accountPreferenceSerializer); - } - return preferences; - } - - private Storage storage; - @GuardedBy("accountLock") - private Map accounts = null; - @GuardedBy("accountLock") - private List accountsInOrder = null; - @GuardedBy("accountLock") - private Account newAccount; - - private final List accountsChangeListeners = new CopyOnWriteArrayList<>(); - private final Context context; - private final LocalStoreProvider localStoreProvider; - private final CoreResourceProvider resourceProvider; - private final LocalKeyStoreManager localKeyStoreManager; - private final StoragePersister storagePersister; - - private final Object accountLock = new Object(); - - private Preferences(Context context, CoreResourceProvider resourceProvider, - StoragePersister storagePersister, LocalStoreProvider localStoreProvider, - LocalKeyStoreManager localKeyStoreManager, - AccountPreferenceSerializer accountPreferenceSerializer) { - this.storage = new Storage(); - this.storagePersister = storagePersister; - this.context = context; - this.resourceProvider = resourceProvider; - this.localStoreProvider = localStoreProvider; - this.localKeyStoreManager = localKeyStoreManager; - this.accountPreferenceSerializer = accountPreferenceSerializer; - - Map persistedStorageValues = storagePersister.loadValues(); - storage.replaceAll(persistedStorageValues); - - if (storage.isEmpty()) { - Timber.i("Preferences storage is zero-size, importing from Android-style preferences"); - StorageEditor editor = createStorageEditor(); - editor.copy(context.getSharedPreferences("AndroidMail.Main", Context.MODE_PRIVATE)); - editor.commit(); - } - } - - public StorageEditor createStorageEditor() { - return storagePersister.createStorageEditor(storage); - } - - @RestrictTo(Scope.TESTS) - public void clearAccounts() { - synchronized (accountLock) { - accounts = new HashMap<>(); - accountsInOrder = new LinkedList<>(); - } - } - - public void loadAccounts() { - synchronized (accountLock) { - accounts = new HashMap<>(); - accountsInOrder = new LinkedList<>(); - String accountUuids = getStorage().getString("accountUuids", null); - if ((accountUuids != null) && (accountUuids.length() != 0)) { - String[] uuids = accountUuids.split(","); - for (String uuid : uuids) { - Account newAccount = new Account(uuid); - accountPreferenceSerializer.loadAccount(newAccount, storage); - accounts.put(uuid, newAccount); - accountsInOrder.add(newAccount); - } - } - if ((newAccount != null) && newAccount.getAccountNumber() != -1) { - accounts.put(newAccount.getUuid(), newAccount); - if (!accountsInOrder.contains(newAccount)) { - accountsInOrder.add(newAccount); - } - newAccount = null; - } - } - } - - /** - * Returns an array of the accounts on the system. If no accounts are - * registered the method returns an empty array. - * - * @return all accounts - */ - public List getAccounts() { - synchronized (accountLock) { - if (accounts == null) { - loadAccounts(); - } - - return Collections.unmodifiableList(new ArrayList<>(accountsInOrder)); - } - } - - /** - * Returns an array of the accounts on the system. If no accounts are - * registered the method returns an empty array. - * - * @return all accounts with {@link Account#isAvailable(Context)} - */ - public Collection getAvailableAccounts() { - List allAccounts = getAccounts(); - Collection result = new ArrayList<>(allAccounts.size()); - for (Account account : allAccounts) { - if (account.isAvailable(context)) { - result.add(account); - } - } - - return result; - } - - public Account getAccount(String uuid) { - synchronized (accountLock) { - if (accounts == null) { - loadAccounts(); - } - - return accounts.get(uuid); - } - } - - public Account newAccount() { - synchronized (accountLock) { - String accountUuid = UUID.randomUUID().toString(); - newAccount = new Account(accountUuid); - accountPreferenceSerializer.loadDefaults(newAccount); - accounts.put(newAccount.getUuid(), newAccount); - accountsInOrder.add(newAccount); - - return newAccount; - } - } - - public void deleteAccount(Account account) { - synchronized (accountLock) { - if (accounts != null) { - accounts.remove(account.getUuid()); - } - if (accountsInOrder != null) { - accountsInOrder.remove(account); - } - - try { - getBackendManager().removeBackend(account); - } catch (Exception e) { - Timber.e(e, "Failed to reset remote store for account %s", account.getUuid()); - } - LocalStore.removeAccount(account); - - StorageEditor storageEditor = createStorageEditor(); - accountPreferenceSerializer.delete(storageEditor, storage, account); - storageEditor.commit(); - localKeyStoreManager.deleteCertificates(account); - - if (newAccount == account) { - newAccount = null; - } - } - - notifyListeners(); - } - - /** - * Returns the Account marked as default. If no account is marked as default - * the first account in the list is marked as default and then returned. If - * there are no accounts on the system the method returns null. - */ - public Account getDefaultAccount() { - Account defaultAccount; - synchronized (accountLock) { - String defaultAccountUuid = getStorage().getString("defaultAccountUuid", null); - defaultAccount = getAccount(defaultAccountUuid); - } - - if (defaultAccount == null) { - Collection accounts = getAvailableAccounts(); - if (!accounts.isEmpty()) { - defaultAccount = accounts.iterator().next(); - setDefaultAccount(defaultAccount); - } - } - - return defaultAccount; - } - - public void setDefaultAccount(Account account) { - createStorageEditor().putString("defaultAccountUuid", account.getUuid()).commit(); - } - - public Storage getStorage() { - return storage; - } - - private BackendManager getBackendManager() { - return DI.get(BackendManager.class); - } - - public void saveAccount(Account account) { - ensureAssignedAccountNumber(account); - processChangedValues(account); - - StorageEditor editor = createStorageEditor(); - accountPreferenceSerializer.save(editor, storage, account); - editor.commit(); - - notifyListeners(); - } - - private void ensureAssignedAccountNumber(Account account) { - if (account.getAccountNumber() != Account.UNASSIGNED_ACCOUNT_NUMBER) { - return; - } - - int accountNumber = generateAccountNumber(); - account.setAccountNumber(accountNumber); - } - - private void processChangedValues(Account account) { - if (account.isChangedVisibleLimits()) { - try { - localStoreProvider.getInstance(account).resetVisibleLimits(account.getDisplayCount()); - } catch (MessagingException e) { - Timber.e(e, "Failed to load LocalStore!"); - } - } - - account.resetChangeMarkers(); - } - - public int generateAccountNumber() { - List accountNumbers = getExistingAccountNumbers(); - return findNewAccountNumber(accountNumbers); - } - - private List getExistingAccountNumbers() { - List accounts = getAccounts(); - List accountNumbers = new ArrayList<>(accounts.size()); - for (Account a : accounts) { - accountNumbers.add(a.getAccountNumber()); - } - return accountNumbers; - } - - private static int findNewAccountNumber(List accountNumbers) { - int newAccountNumber = -1; - Collections.sort(accountNumbers); - for (int accountNumber : accountNumbers) { - if (accountNumber > newAccountNumber + 1) { - break; - } - newAccountNumber = accountNumber; - } - newAccountNumber++; - return newAccountNumber; - } - - public void move(Account account, boolean mUp) { - synchronized (accountLock) { - StorageEditor storageEditor = createStorageEditor(); - accountPreferenceSerializer.move(storageEditor, account, storage, mUp); - storageEditor.commit(); - loadAccounts(); - } - - notifyListeners(); - } - - private void notifyListeners() { - for (AccountsChangeListener listener : accountsChangeListeners) { - listener.onAccountsChanged(); - } - } - - public void addOnAccountsChangeListener(@NonNull AccountsChangeListener accountsChangeListener) { - accountsChangeListeners.add(accountsChangeListener); - } - - public void removeOnAccountsChangeListener(@NonNull AccountsChangeListener accountsChangeListener) { - accountsChangeListeners.remove(accountsChangeListener); - } -} diff --git a/app/core/src/main/java/com/fsck/k9/Preferences.kt b/app/core/src/main/java/com/fsck/k9/Preferences.kt new file mode 100644 index 000000000..efbb599e4 --- /dev/null +++ b/app/core/src/main/java/com/fsck/k9/Preferences.kt @@ -0,0 +1,263 @@ +package com.fsck.k9 + +import android.content.Context +import androidx.annotation.GuardedBy +import androidx.annotation.RestrictTo +import com.fsck.k9.backend.BackendManager +import com.fsck.k9.mail.MessagingException +import com.fsck.k9.mailstore.LocalStore +import com.fsck.k9.mailstore.LocalStoreProvider +import com.fsck.k9.preferences.Storage +import com.fsck.k9.preferences.StorageEditor +import com.fsck.k9.preferences.StoragePersister +import java.util.HashMap +import java.util.LinkedList +import java.util.UUID +import java.util.concurrent.CopyOnWriteArrayList +import org.koin.core.KoinComponent +import org.koin.core.inject +import timber.log.Timber + +class Preferences internal constructor( + private val context: Context, + private val storagePersister: StoragePersister, + private val localStoreProvider: LocalStoreProvider, + private val localKeyStoreManager: LocalKeyStoreManager, + private val accountPreferenceSerializer: AccountPreferenceSerializer +) : KoinComponent { + private val backendManager: BackendManager by inject() + + private val accountLock = Any() + + @GuardedBy("accountLock") + private var accountsMap: MutableMap? = null + + @GuardedBy("accountLock") + private var accountsInOrder = mutableListOf() + + @GuardedBy("accountLock") + private var newAccount: Account? = null + private val accountsChangeListeners = CopyOnWriteArrayList() + + val storage = Storage() + + init { + val persistedStorageValues = storagePersister.loadValues() + storage.replaceAll(persistedStorageValues) + + if (storage.isEmpty) { + Timber.i("Preferences storage is zero-size, importing from Android-style preferences") + + val editor = createStorageEditor() + editor.copy(context.getSharedPreferences("AndroidMail.Main", Context.MODE_PRIVATE)) + editor.commit() + } + } + + fun createStorageEditor(): StorageEditor { + return storagePersister.createStorageEditor(storage) + } + + @RestrictTo(RestrictTo.Scope.TESTS) + fun clearAccounts() { + synchronized(accountLock) { + accountsMap = HashMap() + accountsInOrder = LinkedList() + } + } + + fun loadAccounts() { + synchronized(accountLock) { + val accounts = mutableMapOf() + val accountsInOrder = mutableListOf() + + val accountUuids = storage.getString("accountUuids", null) + if (!accountUuids.isNullOrEmpty()) { + accountUuids.split(",").forEach { uuid -> + val newAccount = Account(uuid) + accountPreferenceSerializer.loadAccount(newAccount, storage) + + accounts[uuid] = newAccount + accountsInOrder.add(newAccount) + } + } + + newAccount?.takeIf { it.accountNumber != -1 }?.let { newAccount -> + accounts[newAccount.uuid] = newAccount + if (newAccount !in accountsInOrder) { + accountsInOrder.add(newAccount) + } + this.newAccount = null + } + + this.accountsMap = accounts + this.accountsInOrder = accountsInOrder + } + } + + val accounts: List + get() { + synchronized(accountLock) { + if (accountsMap == null) { + loadAccounts() + } + + return accountsInOrder.toList() + } + } + + val availableAccounts: Collection + get() = accounts.filter { it.isAvailable(context) } + + fun getAccount(uuid: String): Account? { + synchronized(accountLock) { + if (accountsMap == null) { + loadAccounts() + } + + return accountsMap!![uuid] + } + } + + fun newAccount(): Account { + val accountUuid = UUID.randomUUID().toString() + val account = Account(accountUuid) + accountPreferenceSerializer.loadDefaults(account) + + synchronized(accountLock) { + newAccount = account + accountsMap!![account.uuid] = account + accountsInOrder.add(account) + } + + return account + } + + fun deleteAccount(account: Account) { + synchronized(accountLock) { + accountsMap?.remove(account.uuid) + accountsInOrder.remove(account) + + try { + backendManager.removeBackend(account) + } catch (e: Exception) { + Timber.e(e, "Failed to reset remote store for account %s", account.uuid) + } + + LocalStore.removeAccount(account) + + val storageEditor = createStorageEditor() + accountPreferenceSerializer.delete(storageEditor, storage, account) + storageEditor.commit() + + localKeyStoreManager.deleteCertificates(account) + + if (account === newAccount) { + newAccount = null + } + } + + notifyListeners() + } + + var defaultAccount: Account? + get() { + return getDefaultAccountOrNull() ?: availableAccounts.firstOrNull()?.also { newDefaultAccount -> + defaultAccount = newDefaultAccount + } + } + set(account) { + requireNotNull(account) + + createStorageEditor() + .putString("defaultAccountUuid", account.uuid) + .commit() + } + + private fun getDefaultAccountOrNull(): Account? { + return synchronized(accountLock) { + storage.getString("defaultAccountUuid", null)?.let { defaultAccountUuid -> + getAccount(defaultAccountUuid) + } + } + } + + fun saveAccount(account: Account) { + ensureAssignedAccountNumber(account) + processChangedValues(account) + + val editor = createStorageEditor() + accountPreferenceSerializer.save(editor, storage, account) + editor.commit() + + notifyListeners() + } + + private fun ensureAssignedAccountNumber(account: Account) { + if (account.accountNumber != Account.UNASSIGNED_ACCOUNT_NUMBER) return + + account.accountNumber = generateAccountNumber() + } + + private fun processChangedValues(account: Account) { + if (account.isChangedVisibleLimits) { + try { + localStoreProvider.getInstance(account).resetVisibleLimits(account.displayCount) + } catch (e: MessagingException) { + Timber.e(e, "Failed to load LocalStore!") + } + } + account.resetChangeMarkers() + } + + fun generateAccountNumber(): Int { + val accountNumbers = accounts.map { it.accountNumber } + return findNewAccountNumber(accountNumbers) + } + + private fun findNewAccountNumber(accountNumbers: List): Int { + var newAccountNumber = -1 + for (accountNumber in accountNumbers.sorted()) { + if (accountNumber > newAccountNumber + 1) { + break + } + newAccountNumber = accountNumber + } + newAccountNumber++ + + return newAccountNumber + } + + fun move(account: Account, up: Boolean) { + synchronized(accountLock) { + val storageEditor = createStorageEditor() + accountPreferenceSerializer.move(storageEditor, account, storage, up) + storageEditor.commit() + + loadAccounts() + } + + notifyListeners() + } + + private fun notifyListeners() { + for (listener in accountsChangeListeners) { + listener.onAccountsChanged() + } + } + + fun addOnAccountsChangeListener(accountsChangeListener: AccountsChangeListener) { + accountsChangeListeners.add(accountsChangeListener) + } + + fun removeOnAccountsChangeListener(accountsChangeListener: AccountsChangeListener) { + accountsChangeListeners.remove(accountsChangeListener) + } + + companion object { + @JvmStatic + fun getPreferences(context: Context): Preferences { + return DI.get(Preferences::class.java) + } + } +} diff --git a/app/core/src/main/java/com/fsck/k9/job/K9JobManager.kt b/app/core/src/main/java/com/fsck/k9/job/K9JobManager.kt index 68545cc3c..f2193c7cf 100644 --- a/app/core/src/main/java/com/fsck/k9/job/K9JobManager.kt +++ b/app/core/src/main/java/com/fsck/k9/job/K9JobManager.kt @@ -27,7 +27,7 @@ class K9JobManager( private fun scheduleMailSync() { cancelAllMailSyncJobs() - preferences.availableAccounts?.forEach { account -> + preferences.availableAccounts.forEach { account -> mailSyncWorkerManager.scheduleMailSync(account) } } diff --git a/app/core/src/main/java/com/fsck/k9/preferences/SettingsExporter.kt b/app/core/src/main/java/com/fsck/k9/preferences/SettingsExporter.kt index d0d413f98..8fcdb6063 100644 --- a/app/core/src/main/java/com/fsck/k9/preferences/SettingsExporter.kt +++ b/app/core/src/main/java/com/fsck/k9/preferences/SettingsExporter.kt @@ -65,8 +65,9 @@ class SettingsExporter( serializer.startTag(null, ACCOUNTS_ELEMENT) for (accountUuid in accountUuids) { - val account = preferences.getAccount(accountUuid) - writeAccount(serializer, account, prefs) + preferences.getAccount(accountUuid)?.let { account -> + writeAccount(serializer, account, prefs) + } } serializer.endTag(null, ACCOUNTS_ELEMENT) diff --git a/app/core/src/test/java/com/fsck/k9/controller/MessagingControllerTest.java b/app/core/src/test/java/com/fsck/k9/controller/MessagingControllerTest.java index 9c6cfc7cd..dd4bb5264 100644 --- a/app/core/src/test/java/com/fsck/k9/controller/MessagingControllerTest.java +++ b/app/core/src/test/java/com/fsck/k9/controller/MessagingControllerTest.java @@ -1,18 +1,14 @@ package com.fsck.k9.controller; -import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.Map; import java.util.Set; import android.content.Context; import com.fsck.k9.Account; -import com.fsck.k9.AccountPreferenceSerializer; -import com.fsck.k9.DI; import com.fsck.k9.K9; import com.fsck.k9.K9RobolectricTest; import com.fsck.k9.Preferences; @@ -46,7 +42,6 @@ import org.mockito.ArgumentMatchers; import org.mockito.Captor; import org.mockito.InOrder; import org.mockito.Mock; -import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; @@ -62,7 +57,6 @@ import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; -import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when; @@ -74,7 +68,6 @@ public class MessagingControllerTest extends K9RobolectricTest { private static final String FOLDER_NAME = "Folder"; private static final long SENT_FOLDER_ID = 10; private static final int MAXIMUM_SMALL_MESSAGE_SIZE = 1000; - private static final String ACCOUNT_UUID = "1"; private MessagingController controller; private Account account; @@ -130,6 +123,9 @@ public class MessagingControllerTest extends K9RobolectricTest { } }; + private Preferences preferences; + private String accountUuid; + @Before public void setUp() throws MessagingException { @@ -137,7 +133,7 @@ public class MessagingControllerTest extends K9RobolectricTest { MockitoAnnotations.initMocks(this); appContext = RuntimeEnvironment.application; - Preferences preferences = Preferences.getPreferences(appContext); + preferences = Preferences.getPreferences(appContext); controller = new MessagingController(appContext, notificationController, notificationStrategy, localStoreProvider, unreadMessageCountProvider, backendManager, preferences, messageStoreProvider, @@ -186,7 +182,6 @@ public class MessagingControllerTest extends K9RobolectricTest { @Test public void searchLocalMessagesSynchronous_shouldCallSearchForMessagesOnLocalStore() throws Exception { - setAccountsInPreferences(Collections.singletonMap(ACCOUNT_UUID, account)); when(search.searchAllAccounts()).thenReturn(true); when(search.getAccountUuids()).thenReturn(new String[0]); @@ -198,7 +193,6 @@ public class MessagingControllerTest extends K9RobolectricTest { @Test public void searchLocalMessagesSynchronous_shouldNotifyWhenStoreFinishesRetrievingAMessage() throws Exception { - setAccountsInPreferences(Collections.singletonMap(ACCOUNT_UUID, account)); LocalMessage localMessage = mock(LocalMessage.class); when(localMessage.getFolder()).thenReturn(localFolder); when(search.searchAllAccounts()).thenReturn(true); @@ -215,8 +209,6 @@ public class MessagingControllerTest extends K9RobolectricTest { } private void setupRemoteSearch() throws Exception { - setAccountsInPreferences(Collections.singletonMap(ACCOUNT_UUID, account)); - remoteMessages = new ArrayList<>(); Collections.addAll(remoteMessages, "oldMessageUid", "newMessageUid1", "newMessageUid2"); List newRemoteMessages = new ArrayList<>(); @@ -261,7 +253,7 @@ public class MessagingControllerTest extends K9RobolectricTest { public void searchRemoteMessagesSynchronous_shouldNotifyStartedListingRemoteMessages() throws Exception { setupRemoteSearch(); - controller.searchRemoteMessagesSynchronous(ACCOUNT_UUID, FOLDER_ID, "query", reqFlags, forbiddenFlags, listener); + controller.searchRemoteMessagesSynchronous(accountUuid, FOLDER_ID, "query", reqFlags, forbiddenFlags, listener); verify(listener).remoteSearchStarted(FOLDER_ID); } @@ -270,7 +262,7 @@ public class MessagingControllerTest extends K9RobolectricTest { public void searchRemoteMessagesSynchronous_shouldQueryRemoteFolder() throws Exception { setupRemoteSearch(); - controller.searchRemoteMessagesSynchronous(ACCOUNT_UUID, FOLDER_ID, "query", reqFlags, forbiddenFlags, listener); + controller.searchRemoteMessagesSynchronous(accountUuid, FOLDER_ID, "query", reqFlags, forbiddenFlags, listener); verify(backend).search(FOLDER_NAME, "query", reqFlags, forbiddenFlags, false); } @@ -279,7 +271,7 @@ public class MessagingControllerTest extends K9RobolectricTest { public void searchRemoteMessagesSynchronous_shouldAskLocalFolderToDetermineNewMessages() throws Exception { setupRemoteSearch(); - controller.searchRemoteMessagesSynchronous(ACCOUNT_UUID, FOLDER_ID, "query", reqFlags, forbiddenFlags, listener); + controller.searchRemoteMessagesSynchronous(accountUuid, FOLDER_ID, "query", reqFlags, forbiddenFlags, listener); verify(localFolder).extractNewMessages(remoteMessages); } @@ -288,7 +280,7 @@ public class MessagingControllerTest extends K9RobolectricTest { public void searchRemoteMessagesSynchronous_shouldTryAndGetNewMessages() throws Exception { setupRemoteSearch(); - controller.searchRemoteMessagesSynchronous(ACCOUNT_UUID, FOLDER_ID, "query", reqFlags, forbiddenFlags, listener); + controller.searchRemoteMessagesSynchronous(accountUuid, FOLDER_ID, "query", reqFlags, forbiddenFlags, listener); verify(localFolder).getMessage("newMessageUid1"); } @@ -297,7 +289,7 @@ public class MessagingControllerTest extends K9RobolectricTest { public void searchRemoteMessagesSynchronous_shouldNotTryAndGetOldMessages() throws Exception { setupRemoteSearch(); - controller.searchRemoteMessagesSynchronous(ACCOUNT_UUID, FOLDER_ID, "query", reqFlags, forbiddenFlags, listener); + controller.searchRemoteMessagesSynchronous(accountUuid, FOLDER_ID, "query", reqFlags, forbiddenFlags, listener); verify(localFolder, never()).getMessage("oldMessageUid"); } @@ -306,7 +298,7 @@ public class MessagingControllerTest extends K9RobolectricTest { public void searchRemoteMessagesSynchronous_shouldFetchNewMessages() throws Exception { setupRemoteSearch(); - controller.searchRemoteMessagesSynchronous(ACCOUNT_UUID, FOLDER_ID, "query", reqFlags, forbiddenFlags, listener); + controller.searchRemoteMessagesSynchronous(accountUuid, FOLDER_ID, "query", reqFlags, forbiddenFlags, listener); verify(backend).fetchMessage(eq(FOLDER_NAME), eq("newMessageUid2"), fetchProfileCaptor.capture(), eq(MAXIMUM_SMALL_MESSAGE_SIZE)); @@ -316,7 +308,7 @@ public class MessagingControllerTest extends K9RobolectricTest { public void searchRemoteMessagesSynchronous_shouldNotFetchExistingMessages() throws Exception { setupRemoteSearch(); - controller.searchRemoteMessagesSynchronous(ACCOUNT_UUID, FOLDER_ID, "query", reqFlags, forbiddenFlags, listener); + controller.searchRemoteMessagesSynchronous(accountUuid, FOLDER_ID, "query", reqFlags, forbiddenFlags, listener); verify(backend, never()).fetchMessage(eq(FOLDER_NAME), eq("newMessageUid1"), fetchProfileCaptor.capture(), eq(MAXIMUM_SMALL_MESSAGE_SIZE)); @@ -328,7 +320,7 @@ public class MessagingControllerTest extends K9RobolectricTest { when(backend.search(anyString(), anyString(), nullable(Set.class), nullable(Set.class), eq(false))) .thenThrow(new MessagingException("Test")); - controller.searchRemoteMessagesSynchronous(ACCOUNT_UUID, FOLDER_ID, "query", reqFlags, forbiddenFlags, listener); + controller.searchRemoteMessagesSynchronous(accountUuid, FOLDER_ID, "query", reqFlags, forbiddenFlags, listener); verify(listener).remoteSearchFailed(null, "Test"); } @@ -339,14 +331,14 @@ public class MessagingControllerTest extends K9RobolectricTest { when(backend.search(anyString(), nullable(String.class), nullable(Set.class), nullable(Set.class), eq(false))) .thenThrow(new MessagingException("Test")); - controller.searchRemoteMessagesSynchronous(ACCOUNT_UUID, FOLDER_ID, "query", reqFlags, forbiddenFlags, listener); + controller.searchRemoteMessagesSynchronous(accountUuid, FOLDER_ID, "query", reqFlags, forbiddenFlags, listener); verify(listener).remoteSearchFinished(FOLDER_ID, 0, 50, Collections.emptyList()); } @Test public void sendPendingMessagesSynchronous_withNonExistentOutbox_shouldNotStartSync() throws MessagingException { - when(account.getOutboxFolderId()).thenReturn(FOLDER_ID); + account.setOutboxFolderId(FOLDER_ID); when(localFolder.exists()).thenReturn(false); controller.addListener(listener); @@ -433,7 +425,7 @@ public class MessagingControllerTest extends K9RobolectricTest { } private void setupAccountWithMessageToSend() throws MessagingException { - when(account.getOutboxFolderId()).thenReturn(FOLDER_ID); + account.setOutboxFolderId(FOLDER_ID); account.setSentFolderId(SENT_FOLDER_ID); when(localStore.getFolder(SENT_FOLDER_ID)).thenReturn(sentFolder); when(sentFolder.getDatabaseId()).thenReturn(SENT_FOLDER_ID); @@ -455,13 +447,12 @@ public class MessagingControllerTest extends K9RobolectricTest { when(backendManager.getBackend(account)).thenReturn(backend); } - private void configureAccount() throws MessagingException { - // TODO use simple account object without mocks - account = spy(new Account(ACCOUNT_UUID)); - DI.get(AccountPreferenceSerializer.class).loadDefaults(account); + private void configureAccount() { + account = preferences.newAccount(); + accountUuid = account.getUuid(); + account.setMaximumAutoDownloadMessageSize(MAXIMUM_SMALL_MESSAGE_SIZE); account.setEmail("user@host.com"); - Mockito.doReturn(true).when(account).isAvailable(appContext); } private void configureLocalStore() throws MessagingException { @@ -474,20 +465,7 @@ public class MessagingControllerTest extends K9RobolectricTest { when(localStoreProvider.getInstance(account)).thenReturn(localStore); } - private void setAccountsInPreferences(Map newAccounts) - throws Exception { - // TODO this affects other tests, try to get rid of it - Field accounts = Preferences.class.getDeclaredField("accounts"); - accounts.setAccessible(true); - accounts.set(Preferences.getPreferences(appContext), newAccounts); - - Field accountsInOrder = Preferences.class.getDeclaredField("accountsInOrder"); - accountsInOrder.setAccessible(true); - ArrayList newAccountsInOrder = new ArrayList<>(newAccounts.values()); - accountsInOrder.set(Preferences.getPreferences(appContext), newAccountsInOrder); - } - private void removeAccountsFromPreferences() { - Preferences.getPreferences(appContext).clearAccounts(); + preferences.clearAccounts(); } } diff --git a/app/k9mail/src/main/java/com/fsck/k9/widget/unread/UnreadWidgetConfigurationFragment.kt b/app/k9mail/src/main/java/com/fsck/k9/widget/unread/UnreadWidgetConfigurationFragment.kt index 3462a1c28..180c8e434 100644 --- a/app/k9mail/src/main/java/com/fsck/k9/widget/unread/UnreadWidgetConfigurationFragment.kt +++ b/app/k9mail/src/main/java/com/fsck/k9/widget/unread/UnreadWidgetConfigurationFragment.kt @@ -135,7 +135,9 @@ class UnreadWidgetConfigurationFragment : PreferenceFragmentCompat() { } private fun handleRegularAccount() { - val selectedAccount = preferences.getAccount(selectedAccountUuid) + val selectedAccount = preferences.getAccount(selectedAccountUuid!!) + ?: error("Account $selectedAccountUuid not found") + val accountDescription: String? = selectedAccount.description val summary = if (accountDescription.isNullOrEmpty()) selectedAccount.email else accountDescription diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/activity/EditIdentity.kt b/app/ui/legacy/src/main/java/com/fsck/k9/activity/EditIdentity.kt index 04c50e3dc..232fb9038 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/activity/EditIdentity.kt +++ b/app/ui/legacy/src/main/java/com/fsck/k9/activity/EditIdentity.kt @@ -33,8 +33,8 @@ class EditIdentity : K9Activity() { supportActionBar!!.setDisplayHomeAsUpEnabled(true) identityIndex = intent.getIntExtra(EXTRA_IDENTITY_INDEX, -1) - val accountUuid = intent.getStringExtra(EXTRA_ACCOUNT) - account = Preferences.getPreferences(this).getAccount(accountUuid) + val accountUuid = intent.getStringExtra(EXTRA_ACCOUNT) ?: error("Missing account UUID") + account = Preferences.getPreferences(this).getAccount(accountUuid) ?: error("Couldn't find account") identity = when { savedInstanceState != null -> savedInstanceState.getParcelable(EXTRA_IDENTITY) ?: error("Missing state") diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/activity/MessageList.kt b/app/ui/legacy/src/main/java/com/fsck/k9/activity/MessageList.kt index 824ce5d13..33f73bf29 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/activity/MessageList.kt +++ b/app/ui/legacy/src/main/java/com/fsck/k9/activity/MessageList.kt @@ -434,6 +434,11 @@ open class MessageList : if (accountUuid != null) { // We've most likely been started by an old unread widget or accounts shortcut val account = preferences.getAccount(accountUuid) + if (account == null) { + Timber.d("Account %s not found.", accountUuid) + return LaunchData(createDefaultLocalSearch()) + } + val folderId = defaultFolderProvider.getDefaultFolder(account) val search = LocalSearch().apply { addAccountUuid(accountUuid) @@ -455,7 +460,7 @@ open class MessageList : } private fun createDefaultLocalSearch(): LocalSearch { - val account = preferences.defaultAccount + val account = preferences.defaultAccount ?: error("No default account available") return LocalSearch().apply { addAccountUuid(account.uuid) addAllowedFolder(defaultFolderProvider.getDefaultFolder(account)) @@ -1141,7 +1146,7 @@ open class MessageList : } override fun openMessage(messageReference: MessageReference) { - val account = preferences.getAccount(messageReference.accountUuid) + val account = preferences.getAccount(messageReference.accountUuid) ?: error("Account not found") val folderId = messageReference.folderId val draftsFolderId = account.draftsFolderId diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/fragment/MessageListFragment.kt b/app/ui/legacy/src/main/java/com/fsck/k9/fragment/MessageListFragment.kt index fde3f361c..c13384ce8 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/fragment/MessageListFragment.kt +++ b/app/ui/legacy/src/main/java/com/fsck/k9/fragment/MessageListFragment.kt @@ -1048,7 +1048,7 @@ class MessageListFragment : } private fun groupMessagesByAccount(messages: List): Map> { - return messages.groupBy { preferences.getAccount(it.accountUuid) } + return messages.groupBy { preferences.getAccount(it.accountUuid)!! } } private fun onSpam(messages: List) { diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/endtoend/AutocryptKeyTransferPresenter.kt b/app/ui/legacy/src/main/java/com/fsck/k9/ui/endtoend/AutocryptKeyTransferPresenter.kt index 3d8b8c5b5..f69221040 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/ui/endtoend/AutocryptKeyTransferPresenter.kt +++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/endtoend/AutocryptKeyTransferPresenter.kt @@ -35,7 +35,7 @@ class AutocryptKeyTransferPresenter internal constructor( return } - account = preferences.getAccount(accountUuid) + account = preferences.getAccount(accountUuid) ?: error("Account $accountUuid not found") openPgpApiManager.setOpenPgpProvider( account.openPgpProvider, diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListExtractor.kt b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListExtractor.kt index f59502b71..0f241475a 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListExtractor.kt +++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListExtractor.kt @@ -24,7 +24,8 @@ class MessageListExtractor( ): MessageListItem { val position = cursor.position val accountUuid = cursor.getString(MLFProjectionInfo.ACCOUNT_UUID_COLUMN) - val account = preferences.getAccount(accountUuid) + val account = preferences.getAccount(accountUuid) ?: error("Account $accountUuid not found") + val fromList = cursor.getString(MLFProjectionInfo.SENDER_LIST_COLUMN) val toList = cursor.getString(MLFProjectionInfo.TO_LIST_COLUMN) val ccList = cursor.getString(MLFProjectionInfo.CC_LIST_COLUMN) diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/settings/account/AccountSettingsViewModel.kt b/app/ui/legacy/src/main/java/com/fsck/k9/ui/settings/account/AccountSettingsViewModel.kt index 7791c24cd..b24c8d835 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/ui/settings/account/AccountSettingsViewModel.kt +++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/settings/account/AccountSettingsViewModel.kt @@ -47,7 +47,9 @@ class AccountSettingsViewModel( } } - private fun loadAccount(accountUuid: String) = preferences.getAccount(accountUuid) + private fun loadAccount(accountUuid: String): Account { + return preferences.getAccount(accountUuid) ?: error("Account $accountUuid not found") + } fun getFolders(account: Account): LiveData { if (foldersLiveData.value == null) { diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/settings/import/AccountActivator.kt b/app/ui/legacy/src/main/java/com/fsck/k9/ui/settings/import/AccountActivator.kt index 5c70ce19d..f9f3ed262 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/ui/settings/import/AccountActivator.kt +++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/settings/import/AccountActivator.kt @@ -15,7 +15,7 @@ class AccountActivator( private val messagingController: MessagingController ) { fun enableAccount(accountUuid: String, incomingServerPassword: String?, outgoingServerPassword: String?) { - val account = preferences.getAccount(accountUuid) + val account = preferences.getAccount(accountUuid) ?: error("Account $accountUuid not found") setAccountPasswords(account, incomingServerPassword, outgoingServerPassword)