Merge pull request #4701 from k9mail/change_BackendStorageListener

Change the BackendStorage API to add/update/remove folders
This commit is contained in:
cketti 2020-04-27 12:28:02 +02:00 committed by GitHub
commit 17b9654fad
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 223 additions and 169 deletions

View file

@ -0,0 +1,29 @@
package com.fsck.k9.mailstore
import com.fsck.k9.Account
import com.fsck.k9.Preferences
/**
* Reset an Account's auto-expand folder when the currently configured folder was removed.
*/
class AutoExpandFolderBackendFoldersRefreshListener(
private val preferences: Preferences,
private val account: Account,
private val folderRepository: FolderRepository
) : BackendFoldersRefreshListener {
override fun onBeforeFolderListRefresh() = Unit
override fun onAfterFolderListRefresh() {
checkAutoExpandFolder()
}
private fun checkAutoExpandFolder() {
account.autoExpandFolder?.let { autoExpandFolderServerId ->
if (!folderRepository.isFolderPresent(autoExpandFolderServerId)) {
account.autoExpandFolder = null
preferences.saveAccount(account)
}
}
}
}

View file

@ -1,26 +0,0 @@
package com.fsck.k9.mailstore
import com.fsck.k9.Account
import com.fsck.k9.Preferences
import com.fsck.k9.backend.api.FolderInfo
import com.fsck.k9.mail.FolderType
/**
* Reset an Account's auto-expand folder when the currently configured folder was removed.
*/
class AutoExpandFolderBackendStorageListener(
private val preferences: Preferences,
private val account: Account
) : BackendStorageListener {
override fun onFoldersCreated(folders: List<FolderInfo>) = Unit
override fun onFoldersDeleted(folderServerIds: List<String>) {
if (account.autoExpandFolder in folderServerIds) {
account.autoExpandFolder = null
preferences.saveAccount(account)
}
}
override fun onFolderChanged(folderServerId: String, name: String, type: FolderType) = Unit
}

View file

@ -0,0 +1,6 @@
package com.fsck.k9.mailstore
interface BackendFoldersRefreshListener {
fun onBeforeFolderListRefresh()
fun onAfterFolderListRefresh()
}

View file

@ -1,10 +0,0 @@
package com.fsck.k9.mailstore
import com.fsck.k9.backend.api.FolderInfo
import com.fsck.k9.mail.FolderType
interface BackendStorageListener {
fun onFoldersCreated(folders: List<FolderInfo>)
fun onFoldersDeleted(folderServerIds: List<String>)
fun onFolderChanged(folderServerId: String, name: String, type: FolderType)
}

View file

@ -46,6 +46,23 @@ class FolderRepository(
return getFolderDetails(selection = null, selectionArgs = null)
}
fun isFolderPresent(folderServerId: String): Boolean {
val database = localStoreProvider.getInstance(account).database
return database.execute(false) { db ->
db.query(
"folders",
arrayOf("id"),
"server_id = ?",
arrayOf(folderServerId),
null,
null,
null
).use { cursor ->
cursor.count != 0
}
}
}
private fun getFolderDetails(selection: String?, selectionArgs: Array<String>?): List<FolderDetails> {
val database = localStoreProvider.getInstance(account).database
return database.execute(false) { db ->

View file

@ -7,6 +7,7 @@ import androidx.core.database.getStringOrNull
import com.fsck.k9.Account
import com.fsck.k9.Preferences
import com.fsck.k9.backend.api.BackendFolder
import com.fsck.k9.backend.api.BackendFolderUpdater
import com.fsck.k9.backend.api.BackendStorage
import com.fsck.k9.backend.api.FolderInfo
import com.fsck.k9.mail.FolderType as RemoteFolderType
@ -15,7 +16,7 @@ class K9BackendStorage(
private val preferences: Preferences,
private val account: Account,
private val localStore: LocalStore,
private val listeners: List<BackendStorageListener>
private val listeners: List<BackendFoldersRefreshListener>
) : BackendStorage {
private val database = localStore.database
@ -34,35 +35,8 @@ class K9BackendStorage(
}
}
override fun createFolders(folders: List<FolderInfo>) {
if (folders.isEmpty()) return
val localFolders = folders.map { localStore.getFolder(it.serverId, it.name, it.type) }
localStore.createFolders(localFolders, account.displayCount)
listeners.forEach { it.onFoldersCreated(folders) }
}
override fun deleteFolders(folderServerIds: List<String>) {
folderServerIds.asSequence()
.filterNot { account.isSpecialFolder(it) }
.map { localStore.getFolder(it) }
.forEach { it.delete() }
listeners.forEach { it.onFoldersDeleted(folderServerIds) }
}
override fun changeFolder(folderServerId: String, name: String, type: RemoteFolderType) {
database.execute(false) { db ->
val values = ContentValues().apply {
put("name", name)
put("type", type.toDatabaseFolderType())
}
db.update("folders", values, "server_id = ?", arrayOf(folderServerId))
}
listeners.forEach { it.onFolderChanged(folderServerId, name, type) }
override fun createFolderUpdater(): BackendFolderUpdater {
return K9BackendFolderUpdater()
}
override fun getExtraString(name: String): String? {
@ -122,4 +96,39 @@ class K9BackendStorage(
}
private fun Cursor.getLongOrNull(columnIndex: Int): Long? = if (isNull(columnIndex)) null else getLong(columnIndex)
private inner class K9BackendFolderUpdater : BackendFolderUpdater {
init {
listeners.forEach { it.onBeforeFolderListRefresh() }
}
override fun createFolders(folders: List<FolderInfo>) {
if (folders.isEmpty()) return
val localFolders = folders.map { localStore.getFolder(it.serverId, it.name, it.type) }
localStore.createFolders(localFolders, account.displayCount)
}
override fun deleteFolders(folderServerIds: List<String>) {
folderServerIds.asSequence()
.filterNot { account.isSpecialFolder(it) }
.map { localStore.getFolder(it) }
.forEach { it.delete() }
}
override fun changeFolder(folderServerId: String, name: String, type: RemoteFolderType) {
database.execute(false) { db ->
val values = ContentValues().apply {
put("name", name)
put("type", type.toDatabaseFolderType())
}
db.update("folders", values, "server_id = ?", arrayOf(folderServerId))
}
}
override fun close() {
listeners.forEach { it.onAfterFolderListRefresh() }
}
}
}

View file

@ -18,8 +18,8 @@ class K9BackendStorageFactory(
specialFolderSelectionStrategy,
account
)
val specialFolderListener = SpecialFolderBackendStorageListener(specialFolderUpdater)
val autoExpandFolderListener = AutoExpandFolderBackendStorageListener(preferences, account)
val specialFolderListener = SpecialFolderBackendFoldersRefreshListener(specialFolderUpdater)
val autoExpandFolderListener = AutoExpandFolderBackendFoldersRefreshListener(preferences, account, folderRepository)
val listeners = listOf(specialFolderListener, autoExpandFolderListener)
return K9BackendStorage(preferences, account, localStore, listeners)
}

View file

@ -0,0 +1,15 @@
package com.fsck.k9.mailstore
/**
* Update special folders when folders are added, removed, or changed.
*/
class SpecialFolderBackendFoldersRefreshListener(
private val specialFolderUpdater: SpecialFolderUpdater
) : BackendFoldersRefreshListener {
override fun onBeforeFolderListRefresh() = Unit
override fun onAfterFolderListRefresh() {
specialFolderUpdater.updateSpecialFolders()
}
}

View file

@ -1,26 +0,0 @@
package com.fsck.k9.mailstore
import com.fsck.k9.backend.api.FolderInfo
import com.fsck.k9.mail.FolderType
/**
* Update special folders when folders are added, removed, or changed.
*/
class SpecialFolderBackendStorageListener(
private val specialFolderUpdater: SpecialFolderUpdater
) : BackendStorageListener {
override fun onFoldersCreated(folders: List<FolderInfo>) {
if (folders.any { it.type != FolderType.REGULAR }) {
specialFolderUpdater.updateSpecialFolders()
}
}
override fun onFoldersDeleted(folderServerIds: List<String>) {
specialFolderUpdater.updateSpecialFolders()
}
override fun onFolderChanged(folderServerId: String, name: String, type: FolderType) {
specialFolderUpdater.updateSpecialFolders()
}
}

View file

@ -8,6 +8,7 @@ import com.fsck.k9.K9RobolectricTest
import com.fsck.k9.Preferences
import com.fsck.k9.backend.api.BackendFolder
import com.fsck.k9.backend.api.FolderInfo
import com.fsck.k9.backend.api.updateFolders
import com.fsck.k9.mail.Address
import com.fsck.k9.mail.Flag
import com.fsck.k9.mail.FolderType
@ -117,7 +118,9 @@ class K9BackendFolderTest : K9RobolectricTest() {
fun createBackendFolder(): BackendFolder {
val localStore: LocalStore = localStoreProvider.getInstance(account)
val backendStorage = K9BackendStorage(preferences, account, localStore, emptyList())
backendStorage.createFolders(listOf(FolderInfo(FOLDER_SERVER_ID, FOLDER_NAME, FOLDER_TYPE)))
backendStorage.updateFolders {
createFolders(listOf(FolderInfo(FOLDER_SERVER_ID, FOLDER_NAME, FOLDER_TYPE)))
}
val folderServerIds = backendStorage.getFolderServerIds()
assertTrue(FOLDER_SERVER_ID in folderServerIds)

View file

@ -1,18 +1,27 @@
package com.fsck.k9.backend.api
import com.fsck.k9.mail.FolderType
import java.io.Closeable
interface BackendStorage {
fun getFolder(folderServerId: String): BackendFolder
fun getFolderServerIds(): List<String>
fun createFolders(folders: List<FolderInfo>)
fun deleteFolders(folderServerIds: List<String>)
fun changeFolder(folderServerId: String, name: String, type: FolderType)
fun createFolderUpdater(): BackendFolderUpdater
fun getExtraString(name: String): String?
fun setExtraString(name: String, value: String)
fun getExtraNumber(name: String): Long?
fun setExtraNumber(name: String, value: Long)
}
interface BackendFolderUpdater : Closeable {
fun createFolders(folders: List<FolderInfo>)
fun deleteFolders(folderServerIds: List<String>)
fun changeFolder(folderServerId: String, name: String, type: FolderType)
}
inline fun BackendStorage.updateFolders(block: BackendFolderUpdater.() -> Unit) {
createFolderUpdater().use { it.block() }
}

View file

@ -2,6 +2,7 @@ package com.fsck.k9.backend.imap
import com.fsck.k9.backend.api.BackendStorage
import com.fsck.k9.backend.api.FolderInfo
import com.fsck.k9.backend.api.updateFolders
import com.fsck.k9.mail.store.imap.ImapStore
internal class CommandRefreshFolderList(
@ -12,21 +13,23 @@ internal class CommandRefreshFolderList(
val foldersOnServer = imapStore.folders
val oldFolderServerIds = backendStorage.getFolderServerIds()
val foldersToCreate = mutableListOf<FolderInfo>()
for (folder in foldersOnServer) {
// TODO: Start using the proper server ID. For now we still use the old server ID.
val serverId = folder.oldServerId ?: continue
backendStorage.updateFolders {
val foldersToCreate = mutableListOf<FolderInfo>()
for (folder in foldersOnServer) {
// TODO: Start using the proper server ID. For now we still use the old server ID.
val serverId = folder.oldServerId ?: continue
if (serverId !in oldFolderServerIds) {
foldersToCreate.add(FolderInfo(serverId, folder.name, folder.type))
} else {
backendStorage.changeFolder(serverId, folder.name, folder.type)
if (serverId !in oldFolderServerIds) {
foldersToCreate.add(FolderInfo(serverId, folder.name, folder.type))
} else {
changeFolder(serverId, folder.name, folder.type)
}
}
}
backendStorage.createFolders(foldersToCreate)
createFolders(foldersToCreate)
val newFolderServerIds = foldersOnServer.map { it.serverId }
val removedFolderServerIds = oldFolderServerIds - newFolderServerIds
backendStorage.deleteFolders(removedFolderServerIds)
val newFolderServerIds = foldersOnServer.map { it.serverId }
val removedFolderServerIds = oldFolderServerIds - newFolderServerIds
deleteFolders(removedFolderServerIds)
}
}
}

View file

@ -1,5 +1,6 @@
package com.fsck.k9.backend.jmap
import com.fsck.k9.backend.api.BackendFolderUpdater
import com.fsck.k9.backend.api.BackendStorage
import com.fsck.k9.backend.api.FolderInfo
import com.fsck.k9.mail.AuthenticationFailedException
@ -26,11 +27,13 @@ internal class CommandRefreshFolderList(
) {
fun refreshFolderList() {
try {
val state = backendStorage.getExtraString(STATE)
if (state == null) {
fetchMailboxes()
} else {
fetchMailboxUpdates(state)
backendStorage.createFolderUpdater().use { folderUpdater ->
val state = backendStorage.getExtraString(STATE)
if (state == null) {
fetchMailboxes(folderUpdater)
} else {
fetchMailboxUpdates(folderUpdater, state)
}
}
} catch (e: UnauthorizedException) {
throw AuthenticationFailedException("Authentication failed", e)
@ -45,7 +48,7 @@ internal class CommandRefreshFolderList(
}
}
private fun fetchMailboxes() {
private fun fetchMailboxes(folderUpdater: BackendFolderUpdater) {
val call = jmapClient.call(
GetMailboxMethodCall.builder().accountId(accountId).build()
)
@ -56,42 +59,42 @@ internal class CommandRefreshFolderList(
val (foldersToUpdate, foldersToCreate) = foldersOnServer.partition { it.id in oldFolderServerIds }
for (folder in foldersToUpdate) {
backendStorage.changeFolder(folder.id, folder.name, folder.type)
folderUpdater.changeFolder(folder.id, folder.name, folder.type)
}
val newFolders = foldersToCreate.map { folder ->
FolderInfo(folder.id, folder.name, folder.type)
}
backendStorage.createFolders(newFolders)
folderUpdater.createFolders(newFolders)
val newFolderServerIds = foldersOnServer.map { it.id }
val removedFolderServerIds = oldFolderServerIds - newFolderServerIds
backendStorage.deleteFolders(removedFolderServerIds)
folderUpdater.deleteFolders(removedFolderServerIds)
backendStorage.setExtraString(STATE, response.state)
}
private fun fetchMailboxUpdates(state: String) {
private fun fetchMailboxUpdates(folderUpdater: BackendFolderUpdater, state: String) {
try {
fetchAllMailboxChanges(state)
fetchAllMailboxChanges(folderUpdater, state)
} catch (e: MethodErrorResponseException) {
if (e.methodErrorResponse.type == ERROR_CANNOT_CALCULATE_CHANGES) {
fetchMailboxes()
fetchMailboxes(folderUpdater)
} else {
throw e
}
}
}
private fun fetchAllMailboxChanges(state: String) {
private fun fetchAllMailboxChanges(folderUpdater: BackendFolderUpdater, state: String) {
var currentState = state
do {
val (newState, hasMoreChanges) = fetchMailboxChanges(currentState)
val (newState, hasMoreChanges) = fetchMailboxChanges(folderUpdater, currentState)
currentState = newState
} while (hasMoreChanges)
}
private fun fetchMailboxChanges(state: String): UpdateState {
private fun fetchMailboxChanges(folderUpdater: BackendFolderUpdater, state: String): UpdateState {
val multiCall = jmapClient.newMultiCall()
val mailboxChangesCall = multiCall.call(
ChangesMailboxMethodCall.builder()
@ -120,15 +123,15 @@ internal class CommandRefreshFolderList(
val foldersToCreate = createdMailboxResponse.list.map { folder ->
FolderInfo(folder.id, folder.name, folder.type)
}
backendStorage.createFolders(foldersToCreate)
folderUpdater.createFolders(foldersToCreate)
for (folder in changedMailboxResponse.list) {
backendStorage.changeFolder(folder.id, folder.name, folder.type)
folderUpdater.changeFolder(folder.id, folder.name, folder.type)
}
val destroyed = mailboxChangesResponse.destroyed
destroyed?.let {
backendStorage.deleteFolders(it.toList())
folderUpdater.deleteFolders(it.toList())
}
backendStorage.setExtraString(STATE, mailboxChangesResponse.newState)

View file

@ -1,6 +1,8 @@
package com.fsck.k9.backend.jmap
import com.fsck.k9.backend.api.BackendFolderUpdater
import com.fsck.k9.backend.api.FolderInfo
import com.fsck.k9.backend.api.updateFolders
import com.fsck.k9.mail.AuthenticationFailedException
import com.fsck.k9.mail.FolderType
import com.fsck.k9.mail.MessagingException
@ -139,17 +141,19 @@ class CommandRefreshFolderListTest {
@Suppress("SameParameterValue")
private fun createFoldersInBackendStorage(state: String) {
createFolderInBackendStorage("id_inbox", "Inbox", FolderType.INBOX)
createFolderInBackendStorage("id_archive", "Archive", FolderType.ARCHIVE)
createFolderInBackendStorage("id_drafts", "Drafts", FolderType.DRAFTS)
createFolderInBackendStorage("id_sent", "Sent", FolderType.SENT)
createFolderInBackendStorage("id_trash", "Trash", FolderType.TRASH)
createFolderInBackendStorage("id_folder1", "folder1", FolderType.REGULAR)
backendStorage.updateFolders {
createFolder("id_inbox", "Inbox", FolderType.INBOX)
createFolder("id_archive", "Archive", FolderType.ARCHIVE)
createFolder("id_drafts", "Drafts", FolderType.DRAFTS)
createFolder("id_sent", "Sent", FolderType.SENT)
createFolder("id_trash", "Trash", FolderType.TRASH)
createFolder("id_folder1", "folder1", FolderType.REGULAR)
}
setMailboxState(state)
}
private fun createFolderInBackendStorage(serverId: String, name: String, type: FolderType) {
backendStorage.createFolders(listOf(FolderInfo(serverId, name, type)))
private fun BackendFolderUpdater.createFolder(serverId: String, name: String, type: FolderType) {
createFolders(listOf(FolderInfo(serverId, name, type)))
}
private fun setMailboxState(state: String) {

View file

@ -3,6 +3,7 @@ package com.fsck.k9.backend.jmap
import com.fsck.k9.backend.api.FolderInfo
import com.fsck.k9.backend.api.SyncConfig
import com.fsck.k9.backend.api.SyncConfig.ExpungePolicy
import com.fsck.k9.backend.api.updateFolders
import com.fsck.k9.mail.AuthenticationFailedException
import com.fsck.k9.mail.Flag
import com.fsck.k9.mail.FolderType
@ -234,7 +235,9 @@ class CommandSyncTest {
}
private fun createFolderInBackendStorage() {
backendStorage.createFolders(listOf(FolderInfo(FOLDER_SERVER_ID, "Regular folder", FolderType.REGULAR)))
backendStorage.updateFolders {
createFolders(listOf(FolderInfo(FOLDER_SERVER_ID, "Regular folder", FolderType.REGULAR)))
}
}
private fun MockWebServer.assertRequestUrlPath(expected: String) {

View file

@ -1,5 +1,6 @@
package com.fsck.k9.backend.jmap
import com.fsck.k9.backend.api.BackendFolderUpdater
import com.fsck.k9.backend.api.BackendStorage
import com.fsck.k9.backend.api.FolderInfo
import com.fsck.k9.mail.FolderType
@ -17,24 +18,8 @@ class InMemoryBackendStorage : BackendStorage {
return folders.keys.toList()
}
override fun createFolders(folders: List<FolderInfo>) {
folders.forEach { folder ->
if (this.folders.containsKey(folder.serverId)) error("Folder ${folder.serverId} already present")
this.folders[folder.serverId] = InMemoryBackendFolder(folder.name, folder.type)
}
}
override fun deleteFolders(folderServerIds: List<String>) {
for (folderServerId in folderServerIds) {
folders.remove(folderServerId) ?: error("Folder $folderServerId not found")
}
}
override fun changeFolder(folderServerId: String, name: String, type: FolderType) {
val folder = folders[folderServerId] ?: error("Folder $folderServerId not found")
folder.name = name
folder.type = type
override fun createFolderUpdater(): BackendFolderUpdater {
return InMemoryBackendFolderUpdater()
}
override fun getExtraString(name: String): String? = extraStrings[name]
@ -48,4 +33,28 @@ class InMemoryBackendStorage : BackendStorage {
override fun setExtraNumber(name: String, value: Long) {
extraNumbers[name] = value
}
private inner class InMemoryBackendFolderUpdater : BackendFolderUpdater {
override fun createFolders(foldersToCreate: List<FolderInfo>) {
foldersToCreate.forEach { folder ->
if (folders.containsKey(folder.serverId)) error("Folder ${folder.serverId} already present")
folders[folder.serverId] = InMemoryBackendFolder(folder.name, folder.type)
}
}
override fun deleteFolders(folderServerIds: List<String>) {
for (folderServerId in folderServerIds) {
folders.remove(folderServerId) ?: error("Folder $folderServerId not found")
}
}
override fun changeFolder(folderServerId: String, name: String, type: FolderType) {
val folder = folders[folderServerId] ?: error("Folder $folderServerId not found")
folder.name = name
folder.type = type
}
override fun close() = Unit
}
}

View file

@ -2,6 +2,7 @@ package com.fsck.k9.backend.pop3
import com.fsck.k9.backend.api.BackendStorage
import com.fsck.k9.backend.api.FolderInfo
import com.fsck.k9.backend.api.updateFolders
import com.fsck.k9.mail.FolderType
import com.fsck.k9.mail.store.pop3.Pop3Folder
@ -9,8 +10,10 @@ internal class CommandRefreshFolderList(private val backendStorage: BackendStora
fun refreshFolderList() {
val folderServerIds = backendStorage.getFolderServerIds()
if (Pop3Folder.INBOX !in folderServerIds) {
val inbox = FolderInfo(Pop3Folder.INBOX, Pop3Folder.INBOX, FolderType.INBOX)
backendStorage.createFolders(listOf(inbox))
backendStorage.updateFolders {
val inbox = FolderInfo(Pop3Folder.INBOX, Pop3Folder.INBOX, FolderType.INBOX)
createFolders(listOf(inbox))
}
}
}
}

View file

@ -2,6 +2,7 @@ package com.fsck.k9.backend.webdav
import com.fsck.k9.backend.api.BackendStorage
import com.fsck.k9.backend.api.FolderInfo
import com.fsck.k9.backend.api.updateFolders
import com.fsck.k9.mail.store.webdav.WebDavStore
internal class CommandRefreshFolderList(
@ -12,18 +13,20 @@ internal class CommandRefreshFolderList(
val foldersOnServer = webDavStore.personalNamespaces
val oldFolderServerIds = backendStorage.getFolderServerIds()
val foldersToCreate = mutableListOf<FolderInfo>()
for (folder in foldersOnServer) {
if (folder.serverId !in oldFolderServerIds) {
foldersToCreate.add(FolderInfo(folder.serverId, folder.name, folder.type))
} else {
backendStorage.changeFolder(folder.serverId, folder.name, folder.type)
backendStorage.updateFolders {
val foldersToCreate = mutableListOf<FolderInfo>()
for (folder in foldersOnServer) {
if (folder.serverId !in oldFolderServerIds) {
foldersToCreate.add(FolderInfo(folder.serverId, folder.name, folder.type))
} else {
changeFolder(folder.serverId, folder.name, folder.type)
}
}
}
backendStorage.createFolders(foldersToCreate)
createFolders(foldersToCreate)
val newFolderServerIds = foldersOnServer.map { it.serverId }
val removedFolderServerIds = oldFolderServerIds - newFolderServerIds
backendStorage.deleteFolders(removedFolderServerIds)
val newFolderServerIds = foldersOnServer.map { it.serverId }
val removedFolderServerIds = oldFolderServerIds - newFolderServerIds
deleteFolders(removedFolderServerIds)
}
}
}