From 9048392d4e3459c35b5631041126e8d8d8b37b44 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Thu, 17 Dec 2020 17:28:01 -0300 Subject: [PATCH] Add methods to DbViewModel for loading and updating lists of items --- .../add/remote/AddContactViewModel.java | 6 +- .../remote/PendingContactListViewModel.java | 4 +- .../conversation/ConversationViewModel.java | 2 +- .../android/conversation/ImageViewModel.java | 4 +- .../android/navdrawer/NavDrawerViewModel.java | 4 +- .../android/navdrawer/PluginViewModel.java | 5 +- .../briar/android/viewmodel/DbViewModel.java | 146 +++++++++++++++++- .../briar/android/viewmodel/LiveResult.java | 4 +- 8 files changed, 163 insertions(+), 12 deletions(-) diff --git a/briar-android/src/main/java/org/briarproject/briar/android/contact/add/remote/AddContactViewModel.java b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/remote/AddContactViewModel.java index 4d31de63a..e5152dd0a 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/contact/add/remote/AddContactViewModel.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/remote/AddContactViewModel.java @@ -9,6 +9,7 @@ import org.briarproject.bramble.api.contact.PendingContact; import org.briarproject.bramble.api.db.DatabaseExecutor; import org.briarproject.bramble.api.db.DbException; import org.briarproject.bramble.api.db.NoSuchPendingContactException; +import org.briarproject.bramble.api.db.TransactionManager; import org.briarproject.bramble.api.lifecycle.LifecycleManager; import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.briar.android.viewmodel.DbViewModel; @@ -52,8 +53,9 @@ public class AddContactViewModel extends DbViewModel { AddContactViewModel(Application application, ContactManager contactManager, @DatabaseExecutor Executor dbExecutor, - LifecycleManager lifecycleManager) { - super(application, dbExecutor, lifecycleManager); + LifecycleManager lifecycleManager, + TransactionManager db) { + super(application, dbExecutor, lifecycleManager, db); this.contactManager = contactManager; } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/contact/add/remote/PendingContactListViewModel.java b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/remote/PendingContactListViewModel.java index 55b2f6806..b1e2feb48 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/contact/add/remote/PendingContactListViewModel.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/remote/PendingContactListViewModel.java @@ -11,6 +11,7 @@ import org.briarproject.bramble.api.contact.event.PendingContactRemovedEvent; import org.briarproject.bramble.api.contact.event.PendingContactStateChangedEvent; import org.briarproject.bramble.api.db.DatabaseExecutor; import org.briarproject.bramble.api.db.DbException; +import org.briarproject.bramble.api.db.TransactionManager; import org.briarproject.bramble.api.event.Event; import org.briarproject.bramble.api.event.EventBus; import org.briarproject.bramble.api.event.EventListener; @@ -56,10 +57,11 @@ public class PendingContactListViewModel extends DbViewModel PendingContactListViewModel(Application application, @DatabaseExecutor Executor dbExecutor, LifecycleManager lifecycleManager, + TransactionManager db, ContactManager contactManager, RendezvousPoller rendezvousPoller, EventBus eventBus) { - super(application, dbExecutor, lifecycleManager); + super(application, dbExecutor, lifecycleManager, db); this.contactManager = contactManager; this.rendezvousPoller = rendezvousPoller; this.eventBus = eventBus; diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationViewModel.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationViewModel.java index 92ed61622..232983139 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationViewModel.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationViewModel.java @@ -113,7 +113,7 @@ public class ConversationViewModel extends DbViewModel PrivateMessageFactory privateMessageFactory, AttachmentRetriever attachmentRetriever, AttachmentCreator attachmentCreator) { - super(application, dbExecutor, lifecycleManager); + super(application, dbExecutor, lifecycleManager, db); this.db = db; this.eventBus = eventBus; this.messagingManager = messagingManager; diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageViewModel.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageViewModel.java index 904421754..63dddcd1d 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageViewModel.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageViewModel.java @@ -7,6 +7,7 @@ import android.view.View; import org.briarproject.bramble.api.db.DatabaseExecutor; import org.briarproject.bramble.api.db.DbException; +import org.briarproject.bramble.api.db.TransactionManager; import org.briarproject.bramble.api.event.Event; import org.briarproject.bramble.api.event.EventBus; import org.briarproject.bramble.api.event.EventListener; @@ -78,8 +79,9 @@ public class ImageViewModel extends DbViewModel implements EventListener { EventBus eventBus, @DatabaseExecutor Executor dbExecutor, LifecycleManager lifecycleManager, + TransactionManager db, @IoExecutor Executor ioExecutor) { - super(application, dbExecutor, lifecycleManager); + super(application, dbExecutor, lifecycleManager, db); this.messagingManager = messagingManager; this.eventBus = eventBus; this.ioExecutor = ioExecutor; diff --git a/briar-android/src/main/java/org/briarproject/briar/android/navdrawer/NavDrawerViewModel.java b/briar-android/src/main/java/org/briarproject/briar/android/navdrawer/NavDrawerViewModel.java index 731e62d46..99a494bf2 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/navdrawer/NavDrawerViewModel.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/navdrawer/NavDrawerViewModel.java @@ -4,6 +4,7 @@ import android.app.Application; import org.briarproject.bramble.api.db.DatabaseExecutor; import org.briarproject.bramble.api.db.DbException; +import org.briarproject.bramble.api.db.TransactionManager; import org.briarproject.bramble.api.lifecycle.LifecycleManager; import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.settings.Settings; @@ -51,8 +52,9 @@ public class NavDrawerViewModel extends DbViewModel { NavDrawerViewModel(Application app, @DatabaseExecutor Executor dbExecutor, LifecycleManager lifecycleManager, + TransactionManager db, SettingsManager settingsManager) { - super(app, dbExecutor, lifecycleManager); + super(app, dbExecutor, lifecycleManager, db); this.settingsManager = settingsManager; } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/navdrawer/PluginViewModel.java b/briar-android/src/main/java/org/briarproject/briar/android/navdrawer/PluginViewModel.java index 3dbc9c517..3f19c3f7d 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/navdrawer/PluginViewModel.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/navdrawer/PluginViewModel.java @@ -9,6 +9,7 @@ import android.content.IntentFilter; import org.briarproject.bramble.api.db.DatabaseExecutor; import org.briarproject.bramble.api.db.DbException; +import org.briarproject.bramble.api.db.TransactionManager; import org.briarproject.bramble.api.event.Event; import org.briarproject.bramble.api.event.EventBus; import org.briarproject.bramble.api.event.EventListener; @@ -85,10 +86,10 @@ public class PluginViewModel extends DbViewModel implements EventListener { @Inject PluginViewModel(Application app, @DatabaseExecutor Executor dbExecutor, - LifecycleManager lifecycleManager, + LifecycleManager lifecycleManager, TransactionManager db, SettingsManager settingsManager, PluginManager pluginManager, EventBus eventBus, NetworkManager networkManager) { - super(app, dbExecutor, lifecycleManager); + super(app, dbExecutor, lifecycleManager, db); this.app = app; this.settingsManager = settingsManager; this.pluginManager = pluginManager; diff --git a/briar-android/src/main/java/org/briarproject/briar/android/viewmodel/DbViewModel.java b/briar-android/src/main/java/org/briarproject/briar/android/viewmodel/DbViewModel.java index 33881a725..c928894df 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/viewmodel/DbViewModel.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/viewmodel/DbViewModel.java @@ -1,20 +1,36 @@ package org.briarproject.briar.android.viewmodel; import android.app.Application; +import android.os.Handler; import org.briarproject.bramble.api.db.DatabaseExecutor; +import org.briarproject.bramble.api.db.DbCallable; +import org.briarproject.bramble.api.db.DbException; +import org.briarproject.bramble.api.db.Transaction; +import org.briarproject.bramble.api.db.TransactionManager; import org.briarproject.bramble.api.lifecycle.LifecycleManager; import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import java.util.ArrayList; +import java.util.List; +import java.util.ListIterator; import java.util.concurrent.Executor; import java.util.logging.Logger; import javax.annotation.concurrent.Immutable; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.UiThread; +import androidx.arch.core.util.Function; import androidx.lifecycle.AndroidViewModel; +import androidx.lifecycle.LiveData; +import androidx.recyclerview.widget.RecyclerView; +import static android.os.Looper.getMainLooper; +import static java.util.logging.Level.WARNING; import static java.util.logging.Logger.getLogger; +import static org.briarproject.bramble.util.LogUtils.logException; @Immutable @NotNullByDefault @@ -25,17 +41,28 @@ public abstract class DbViewModel extends AndroidViewModel { @DatabaseExecutor private final Executor dbExecutor; private final LifecycleManager lifecycleManager; + private final TransactionManager db; public DbViewModel( @NonNull Application application, @DatabaseExecutor Executor dbExecutor, - LifecycleManager lifecycleManager) { + LifecycleManager lifecycleManager, + TransactionManager db) { super(application); this.dbExecutor = dbExecutor; this.lifecycleManager = lifecycleManager; + this.db = db; } - public void runOnDbThread(Runnable task) { + /** + * Runs the given task on the {@link DatabaseExecutor} + * and waits for the DB to open. + *

+ * If you need a list of items to be displayed in a + * {@link RecyclerView.Adapter}, + * use {@link #loadList(DbCallable, UiCallable)} instead. + */ + protected void runOnDbThread(Runnable task) { dbExecutor.execute(() -> { try { lifecycleManager.waitForDatabase(); @@ -47,4 +74,119 @@ public abstract class DbViewModel extends AndroidViewModel { }); } + /** + * Loads a list of items on the {@link DatabaseExecutor} within a single + * {@link Transaction} and publishes it as a {@link LiveResult} + * to the {@link UiThread}. + *

+ * Use this to ensure that modifications to your local list do not get + * overridden by database loads that were in progress while the modification + * was made. + * E.g. An event about the removal of a message causes the message item to + * be removed from the local list while all messages are reloaded. + * This method ensures that those operations can be processed on the + * UiThread in the correct order so that the removed message will not be + * re-added when the re-load completes. + */ + protected > void loadList( + DbCallable task, + UiCallable> uiUpdate) { + dbExecutor.execute(() -> { + try { + lifecycleManager.waitForDatabase(); + db.transaction(true, txn -> { + T t = task.call(txn); + txn.attach(() -> uiUpdate.call(new LiveResult<>(t))); + }); + } catch (InterruptedException e) { + LOG.warning("Interrupted while waiting for database"); + Thread.currentThread().interrupt(); + } catch (DbException e) { + logException(LOG, WARNING, e); + new Handler(getMainLooper()) + .post(() -> uiUpdate.call(new LiveResult<>(e))); + } + }); + } + + @NotNullByDefault + public interface UiCallable { + @UiThread + void call(T t); + } + + /** + * Creates a copy of the list available in the given LiveData + * and replaces items where the given test function returns true. + * + * @return a copy of the list in the LiveData with item(s) replaced + * or null when the + *

+ */ + @Nullable + protected List updateListItem( + LiveData>> liveData, Function test, + Function replacer) { + List items = getList(liveData); + if (items == null) return null; + + ListIterator iterator = items.listIterator(); + boolean changed = false; + while (iterator.hasNext()) { + T item = iterator.next(); + if (test.apply(item)) { + changed = true; + iterator.set(replacer.apply(item)); + } + } + return changed ? items : null; + } + + /** + * Creates a copy of the list available in the given LiveData + * and removes the items from it where the given test function returns true. + * + * @return a copy of the list in the LiveData with item(s) removed + * or null when the + *
    + *
  • LiveData does not have a value + *
  • LiveResult in the LiveData has an error + *
  • test function did return false for all items in the list + *
+ */ + @Nullable + protected List removeListItem( + LiveData>> liveData, Function test) { + List items = getList(liveData); + if (items == null) return null; + + ListIterator iterator = items.listIterator(); + boolean changed = false; + while (iterator.hasNext()) { + T item = iterator.next(); + if (test.apply(item)) { + changed = true; + iterator.remove(); + } + } + return changed ? items : null; + } + + /** + * Retrieves the list of items from the given LiveData + * or null if it is not available. + */ + @Nullable + private List getList(LiveData>> liveData) { + LiveResult> value = liveData.getValue(); + if (value == null) return null; + List list = value.getResultOrNull(); + if (list == null) return null; + return new ArrayList<>(list); + } + } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/viewmodel/LiveResult.java b/briar-android/src/main/java/org/briarproject/briar/android/viewmodel/LiveResult.java index 2d2d09da6..17bfe96f9 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/viewmodel/LiveResult.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/viewmodel/LiveResult.java @@ -8,9 +8,9 @@ import androidx.annotation.Nullable; public class LiveResult { @Nullable - private T result; + private final T result; @Nullable - private Exception exception; + private final Exception exception; public LiveResult(T result) { this.result = result;