Add MessageListRepository

Remove the "message list changed" notification mechanism provided by `EmailProvider` and use a simple callback mechanism instead.
This commit is contained in:
cketti 2022-08-30 18:26:27 +02:00
parent 18b177e18e
commit e9b91f3654
11 changed files with 114 additions and 59 deletions

View file

@ -5,10 +5,10 @@ import java.util.List;
import java.util.Map;
import android.content.Context;
import android.net.Uri;
import com.fsck.k9.DI;
import com.fsck.k9.mailstore.LocalMessage;
import com.fsck.k9.provider.EmailProvider;
import com.fsck.k9.mailstore.MessageListRepository;
/**
* Cache to bridge the time needed to write (user-initiated) changes to the database.
@ -149,8 +149,7 @@ public class EmailProviderCache {
}
private void notifyChange() {
Uri uri = Uri.withAppendedPath(EmailProvider.CONTENT_URI, "account/" + mAccountUuid +
"/messages");
sContext.getContentResolver().notifyChange(uri, null);
MessageListRepository messageListRepository = DI.get(MessageListRepository.class);
messageListRepository.notifyMessageListChanged(mAccountUuid);
}
}

View file

@ -34,4 +34,5 @@ val mailStoreModule = module {
attachmentCounter = get()
)
}
single { MessageListRepository() }
}

View file

@ -23,7 +23,6 @@ import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;
import androidx.annotation.Nullable;
import android.text.TextUtils;
@ -51,7 +50,6 @@ import com.fsck.k9.mailstore.LockableDatabase.DbCallback;
import com.fsck.k9.mailstore.LockableDatabase.SchemaDefinition;
import com.fsck.k9.mailstore.StorageManager.InternalStorageProvider;
import com.fsck.k9.message.extractors.AttachmentInfoExtractor;
import com.fsck.k9.provider.EmailProvider;
import com.fsck.k9.provider.EmailProvider.MessageColumns;
import com.fsck.k9.search.LocalSearch;
import com.fsck.k9.search.SearchSpecification.Attribute;
@ -166,7 +164,6 @@ public class LocalStore {
private static final int THREAD_FLAG_UPDATE_BATCH_SIZE = 500;
private final Context context;
private final ContentResolver contentResolver;
private final PendingCommandSerializer pendingCommandSerializer;
private final AttachmentInfoExtractor attachmentInfoExtractor;
@ -184,7 +181,6 @@ public class LocalStore {
*/
private LocalStore(final Account account, final Context context) throws MessagingException {
this.context = context;
this.contentResolver = context.getContentResolver();
pendingCommandSerializer = PendingCommandSerializer.getInstance();
attachmentInfoExtractor = DI.get(AttachmentInfoExtractor.class);
@ -726,8 +722,8 @@ public class LocalStore {
}
public void notifyChange() {
Uri uri = Uri.withAppendedPath(EmailProvider.CONTENT_URI, "account/" + account.getUuid() + "/messages");
contentResolver.notifyChange(uri, null);
MessageListRepository messageListRepository = DI.get(MessageListRepository.class);
messageListRepository.notifyMessageListChanged(account.getUuid());
}
/**

View file

@ -0,0 +1,28 @@
package com.fsck.k9.mailstore
import java.util.concurrent.CopyOnWriteArraySet
class MessageListRepository {
private val listeners = CopyOnWriteArraySet<Pair<String, MessageListChangedListener>>()
fun addListener(accountUuid: String, listener: MessageListChangedListener) {
listeners.add(accountUuid to listener)
}
fun removeListener(listener: MessageListChangedListener) {
val entries = listeners.filter { it.second == listener }.toSet()
listeners.removeAll(entries)
}
fun notifyMessageListChanged(accountUuid: String) {
for (listener in listeners) {
if (listener.first == accountUuid) {
listener.second.onMessageListChanged()
}
}
}
}
fun interface MessageListChangedListener {
fun onMessageListChanged()
}

View file

@ -46,7 +46,7 @@ public class EmailProvider extends ContentProvider {
public static String AUTHORITY;
public static Uri CONTENT_URI;
public static Uri getNotificationUri(String accountUuid) {
private static Uri getNotificationUri(String accountUuid) {
return Uri.withAppendedPath(CONTENT_URI, "account/" + accountUuid + "/messages");
}

View file

@ -0,0 +1,47 @@
package com.fsck.k9.mailstore
import com.google.common.truth.Truth.assertThat
import org.junit.Test
private const val ACCOUNT_UUID = "00000000-0000-4000-0000-000000000000"
class MessageListRepositoryTest {
private val messageListRepository = MessageListRepository()
@Test
fun `adding and removing listener`() {
var messageListChanged = 0
val listener = MessageListChangedListener {
messageListChanged++
}
messageListRepository.addListener(ACCOUNT_UUID, listener)
messageListRepository.notifyMessageListChanged(ACCOUNT_UUID)
assertThat(messageListChanged).isEqualTo(1)
messageListRepository.removeListener(listener)
messageListRepository.notifyMessageListChanged(ACCOUNT_UUID)
assertThat(messageListChanged).isEqualTo(1)
}
@Test
fun `only notify listener when account UUID matches`() {
var messageListChanged = 0
val listener = MessageListChangedListener {
messageListChanged++
}
messageListRepository.addListener(ACCOUNT_UUID, listener)
messageListRepository.notifyMessageListChanged("otherAccountUuid")
assertThat(messageListChanged).isEqualTo(0)
}
@Test
fun `notifyMessageListChanged() without any listeners should not throw`() {
messageListRepository.notifyMessageListChanged(ACCOUNT_UUID)
}
}

View file

@ -1,17 +1,14 @@
package com.fsck.k9.ui.account
import android.content.ContentResolver
import android.database.ContentObserver
import android.os.Handler
import android.os.Looper
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.asLiveData
import com.fsck.k9.Account
import com.fsck.k9.controller.MessageCounts
import com.fsck.k9.controller.MessageCountsProvider
import com.fsck.k9.mailstore.MessageListChangedListener
import com.fsck.k9.mailstore.MessageListRepository
import com.fsck.k9.preferences.AccountManager
import com.fsck.k9.provider.EmailProvider
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.awaitClose
@ -26,7 +23,7 @@ import kotlinx.coroutines.launch
class AccountsViewModel(
accountManager: AccountManager,
private val messageCountsProvider: MessageCountsProvider,
private val contentResolver: ContentResolver
private val messageListRepository: MessageListRepository
) : ViewModel() {
private val displayAccountFlow: Flow<List<DisplayAccount>> = accountManager.getAccountsFlow()
.flatMapLatest { accounts ->
@ -47,21 +44,17 @@ class AccountsViewModel(
private fun getMessageCountsFlow(account: Account): Flow<MessageCounts> {
return callbackFlow {
val notificationUri = EmailProvider.getNotificationUri(account.uuid)
send(messageCountsProvider.getMessageCounts(account))
val contentObserver = object : ContentObserver(Handler(Looper.getMainLooper())) {
override fun onChange(selfChange: Boolean) {
launch {
send(messageCountsProvider.getMessageCounts(account))
}
val listener = MessageListChangedListener {
launch {
send(messageCountsProvider.getMessageCounts(account))
}
}
contentResolver.registerContentObserver(notificationUri, false, contentObserver)
messageListRepository.addListener(account.uuid, listener)
awaitClose {
contentResolver.unregisterContentObserver(contentObserver)
messageListRepository.removeListener(listener)
}
}.flowOn(Dispatchers.IO)
}

View file

@ -4,7 +4,9 @@ import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.module
val accountUiModule = module {
viewModel { AccountsViewModel(accountManager = get(), messageCountsProvider = get(), contentResolver = get()) }
viewModel {
AccountsViewModel(accountManager = get(), messageCountsProvider = get(), messageListRepository = get())
}
factory { AccountImageLoader(accountFallbackImageProvider = get()) }
factory { AccountFallbackImageProvider(context = get()) }
factory { AccountImageModelLoaderFactory(contactPhotoLoader = get(), accountFallbackImageProvider = get()) }

View file

@ -8,5 +8,7 @@ val messageListUiModule = module {
factory { DefaultFolderProvider() }
factory { MessageListExtractor(get(), get()) }
factory { MessageListLoader(get(), get(), get(), get()) }
factory { MessageListLiveDataFactory(get(), get(), get()) }
factory {
MessageListLiveDataFactory(messageListLoader = get(), preferences = get(), messageListRepository = get())
}
}

View file

@ -1,12 +1,9 @@
package com.fsck.k9.ui.messagelist
import android.content.ContentResolver
import android.database.ContentObserver
import android.net.Uri
import android.os.Handler
import androidx.lifecycle.LiveData
import com.fsck.k9.Preferences
import com.fsck.k9.provider.EmailProvider
import com.fsck.k9.mailstore.MessageListChangedListener
import com.fsck.k9.mailstore.MessageListRepository
import com.fsck.k9.search.getAccountUuids
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@ -16,53 +13,43 @@ import kotlinx.coroutines.withContext
class MessageListLiveData(
private val messageListLoader: MessageListLoader,
private val preferences: Preferences,
private val contentResolver: ContentResolver,
private val messageListRepository: MessageListRepository,
private val coroutineScope: CoroutineScope,
val config: MessageListConfig
) : LiveData<MessageListInfo>() {
private val contentObserver = object : ContentObserver(Handler()) {
override fun onChange(selfChange: Boolean) {
loadMessageListAsync()
}
private val messageListChangedListener = MessageListChangedListener {
loadMessageListAsync()
}
private fun loadMessageListAsync() {
coroutineScope.launch(Dispatchers.Main) {
value = withContext(Dispatchers.IO) {
val messageList = withContext(Dispatchers.IO) {
messageListLoader.getMessageList(config)
}
value = messageList
}
}
override fun onActive() {
super.onActive()
registerContentObserverAsync()
registerMessageListChangedListenerAsync()
loadMessageListAsync()
}
override fun onInactive() {
super.onInactive()
contentResolver.unregisterContentObserver(contentObserver)
messageListRepository.removeListener(messageListChangedListener)
}
private fun registerContentObserverAsync() {
coroutineScope.launch(Dispatchers.Main) {
val notificationUris = withContext(Dispatchers.IO) {
getNotificationUris()
}
private fun registerMessageListChangedListenerAsync() {
coroutineScope.launch(Dispatchers.IO) {
val accountUuids = config.search.getAccountUuids(preferences)
for (notificationUri in notificationUris) {
contentResolver.registerContentObserver(notificationUri, false, contentObserver)
for (accountUuid in accountUuids) {
messageListRepository.addListener(accountUuid, messageListChangedListener)
}
}
}
private fun getNotificationUris(): List<Uri> {
val accountUuids = config.search.getAccountUuids(preferences)
return accountUuids.map { accountUuid ->
EmailProvider.getNotificationUri(accountUuid)
}
}
}

View file

@ -1,15 +1,15 @@
package com.fsck.k9.ui.messagelist
import android.content.ContentResolver
import com.fsck.k9.Preferences
import com.fsck.k9.mailstore.MessageListRepository
import kotlinx.coroutines.CoroutineScope
class MessageListLiveDataFactory(
private val messageListLoader: MessageListLoader,
private val preferences: Preferences,
private val contentResolver: ContentResolver
private val messageListRepository: MessageListRepository
) {
fun create(coroutineScope: CoroutineScope, config: MessageListConfig): MessageListLiveData {
return MessageListLiveData(messageListLoader, preferences, contentResolver, coroutineScope, config)
return MessageListLiveData(messageListLoader, preferences, messageListRepository, coroutineScope, config)
}
}