Add methods to DbViewModel for loading and updating lists of items

This commit is contained in:
Torsten Grote
2020-12-17 17:28:01 -03:00
parent 480aaaa35e
commit 9048392d4e
8 changed files with 163 additions and 12 deletions

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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.
* <p>
* 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}.
* <p>
* 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 <T extends List<?>> void loadList(
DbCallable<T, DbException> task,
UiCallable<LiveResult<T>> 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<T> {
@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
* <ul>
* <li> LiveData does not have a value
* <li> LiveResult in the LiveData has an error
* <li> test function did return false for all items in the list
* </ul>
*/
@Nullable
protected <T> List<T> updateListItem(
LiveData<LiveResult<List<T>>> liveData, Function<T, Boolean> test,
Function<T, T> replacer) {
List<T> items = getList(liveData);
if (items == null) return null;
ListIterator<T> 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
* <ul>
* <li> LiveData does not have a value
* <li> LiveResult in the LiveData has an error
* <li> test function did return false for all items in the list
* </ul>
*/
@Nullable
protected <T> List<T> removeListItem(
LiveData<LiveResult<List<T>>> liveData, Function<T, Boolean> test) {
List<T> items = getList(liveData);
if (items == null) return null;
ListIterator<T> 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 <T> List<T> getList(LiveData<LiveResult<List<T>>> liveData) {
LiveResult<List<T>> value = liveData.getValue();
if (value == null) return null;
List<T> list = value.getResultOrNull();
if (list == null) return null;
return new ArrayList<>(list);
}
}

View File

@@ -8,9 +8,9 @@ import androidx.annotation.Nullable;
public class LiveResult<T> {
@Nullable
private T result;
private final T result;
@Nullable
private Exception exception;
private final Exception exception;
public LiveResult(T result) {
this.result = result;