Merge branch '1800-group-list-view-model' into 'master'

Using ListAdapter for PrivateGroupList

See merge request briar/briar!1327
This commit is contained in:
akwizgran
2021-01-05 11:25:33 +00:00
46 changed files with 822 additions and 433 deletions

View File

@@ -126,10 +126,11 @@ dependencies {
testImplementation 'androidx.test:runner:1.3.0'
testImplementation 'androidx.test.ext:junit:1.1.2'
testImplementation 'androidx.fragment:fragment-testing:1.2.5'
testImplementation "androidx.arch.core:core-testing:2.1.0"
testImplementation "androidx.test.espresso:espresso-core:$espressoVersion"
testImplementation 'org.robolectric:robolectric:4.3.1'
testImplementation 'org.mockito:mockito-core:3.1.0'
testImplementation 'junit:junit:4.12'
testImplementation 'junit:junit:4.13.1'
testImplementation "org.jmock:jmock:$jmockVersion"
testImplementation "org.jmock:jmock-junit4:$jmockVersion"
testImplementation "org.jmock:jmock-legacy:$jmockVersion"

View File

@@ -14,6 +14,7 @@ import org.briarproject.bramble.api.contact.ContactManager;
import org.briarproject.bramble.api.crypto.CryptoExecutor;
import org.briarproject.bramble.api.crypto.PasswordStrengthEstimator;
import org.briarproject.bramble.api.db.DatabaseExecutor;
import org.briarproject.bramble.api.db.TransactionManager;
import org.briarproject.bramble.api.event.EventBus;
import org.briarproject.bramble.api.identity.IdentityManager;
import org.briarproject.bramble.api.keyagreement.KeyAgreementTask;
@@ -85,6 +86,8 @@ public interface AndroidComponent
@DatabaseExecutor
Executor databaseExecutor();
TransactionManager transactionManager();
MessageTracker messageTracker();
LifecycleManager lifecycleManager();

View File

@@ -109,7 +109,7 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
@Nullable
private GroupId blockedGroup = null;
private boolean blockSignInReminder = false;
private boolean blockBlogs = false;
private boolean blockGroups = false, blockBlogs = false;
private long lastSound = 0;
private volatile Settings settings = new Settings();
@@ -223,8 +223,8 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
if (s.getNamespace().equals(SETTINGS_NAMESPACE))
settings = s.getSettings();
} else if (e instanceof ConversationMessageReceivedEvent) {
ConversationMessageReceivedEvent p =
(ConversationMessageReceivedEvent) e;
ConversationMessageReceivedEvent<?> p =
(ConversationMessageReceivedEvent<?>) e;
showContactNotification(p.getContactId());
} else if (e instanceof GroupMessageAddedEvent) {
GroupMessageAddedEvent g = (GroupMessageAddedEvent) e;
@@ -385,6 +385,7 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
@UiThread
private void showGroupMessageNotification(GroupId g) {
if (blockGroups) return;
if (g.equals(blockedGroup)) return;
groupCounts.add(g);
updateGroupMessageNotification(true);
@@ -681,6 +682,17 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
});
}
@Override
public void blockAllGroupMessageNotifications() {
androidExecutor.runOnUiThread((Runnable) () -> blockGroups = true);
}
@Override
public void unblockAllGroupMessageNotifications() {
androidExecutor.runOnUiThread((Runnable) () -> blockGroups = false);
}
@Override
public void blockAllBlogPostNotifications() {
androidExecutor.runOnUiThread((Runnable) () -> blockBlogs = true);

View File

@@ -31,6 +31,7 @@ import org.briarproject.briar.android.account.LockManagerImpl;
import org.briarproject.briar.android.keyagreement.ContactExchangeModule;
import org.briarproject.briar.android.login.LoginModule;
import org.briarproject.briar.android.navdrawer.NavDrawerModule;
import org.briarproject.briar.android.privategroup.list.GroupListModule;
import org.briarproject.briar.android.reporting.DevReportModule;
import org.briarproject.briar.android.viewmodel.ViewModelModule;
import org.briarproject.briar.api.android.AndroidNotificationManager;
@@ -65,7 +66,9 @@ import static org.briarproject.briar.android.TestingConstants.IS_DEBUG_BUILD;
LoginModule.class,
NavDrawerModule.class,
ViewModelModule.class,
DevReportModule.class
DevReportModule.class,
// below need to be within same scope as ViewModelProvider.Factory
GroupListModule.class,
})
public class AppModule {

View File

@@ -60,7 +60,6 @@ import org.briarproject.briar.android.privategroup.creation.GroupInviteFragment;
import org.briarproject.briar.android.privategroup.invitation.GroupInvitationActivity;
import org.briarproject.briar.android.privategroup.invitation.GroupInvitationModule;
import org.briarproject.briar.android.privategroup.list.GroupListFragment;
import org.briarproject.briar.android.privategroup.list.GroupListModule;
import org.briarproject.briar.android.privategroup.memberlist.GroupMemberListActivity;
import org.briarproject.briar.android.privategroup.memberlist.GroupMemberModule;
import org.briarproject.briar.android.privategroup.reveal.GroupRevealModule;
@@ -94,7 +93,6 @@ import dagger.Component;
ForumModule.class,
GroupInvitationModule.class,
GroupConversationModule.class,
GroupListModule.class,
GroupMemberModule.class,
GroupRevealModule.class,
SharingModule.class

View File

@@ -6,7 +6,6 @@ import android.view.View;
import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.briar.R;
@@ -240,7 +239,7 @@ public abstract class BaseActivity extends AppCompatActivity
}
@UiThread
public void handleDbException(DbException e) {
public void handleException(Exception e) {
supportFinishAfterTransition();
}

View File

@@ -232,7 +232,7 @@ public class BlogFragment extends BaseFragment
@Override
public void onExceptionUi(DbException exception) {
handleDbException(exception);
handleException(exception);
}
}
);
@@ -277,7 +277,7 @@ public class BlogFragment extends BaseFragment
@Override
public void onExceptionUi(DbException exception) {
handleDbException(exception);
handleException(exception);
}
});
}
@@ -296,7 +296,7 @@ public class BlogFragment extends BaseFragment
@Override
public void onExceptionUi(DbException exception) {
handleDbException(exception);
handleException(exception);
}
});
}
@@ -318,7 +318,7 @@ public class BlogFragment extends BaseFragment
@Override
public void onExceptionUi(DbException exception) {
handleDbException(exception);
handleException(exception);
}
});
}
@@ -398,7 +398,7 @@ public class BlogFragment extends BaseFragment
@Override
public void onExceptionUi(DbException exception) {
handleDbException(exception);
handleException(exception);
}
});
}

View File

@@ -58,7 +58,7 @@ public class BlogPostFragment extends BasePostFragment implements BlogListener {
@Override
public void onExceptionUi(DbException exception) {
handleDbException(exception);
handleException(exception);
}
});
}

View File

@@ -156,7 +156,7 @@ public class FeedFragment extends BaseFragment implements
@Override
public void onExceptionUi(DbException exception) {
handleDbException(exception);
handleException(exception);
}
});
}
@@ -187,7 +187,7 @@ public class FeedFragment extends BaseFragment implements
@Override
public void onExceptionUi(DbException exception) {
handleDbException(exception);
handleException(exception);
}
});
}
@@ -242,7 +242,7 @@ public class FeedFragment extends BaseFragment implements
@Override
public void onExceptionUi(DbException exception) {
handleDbException(exception);
handleException(exception);
}
}
);

View File

@@ -79,7 +79,7 @@ public class FeedPostFragment extends BasePostFragment {
@Override
public void onExceptionUi(DbException exception) {
handleDbException(exception);
handleException(exception);
}
});
}

View File

@@ -101,7 +101,7 @@ public class ReblogFragment extends BaseFragment implements SendListener {
@Override
public void onExceptionUi(DbException exception) {
handleDbException(exception);
handleException(exception);
}
});
@@ -128,7 +128,7 @@ public class ReblogFragment extends BaseFragment implements SendListener {
new UiExceptionHandler<DbException>(this) {
@Override
public void onExceptionUi(DbException exception) {
handleDbException(exception);
handleException(exception);
}
});
finish();

View File

@@ -9,8 +9,10 @@ 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.bramble.api.system.AndroidExecutor;
import org.briarproject.briar.android.viewmodel.DbViewModel;
import org.briarproject.briar.android.viewmodel.LiveEvent;
import org.briarproject.briar.android.viewmodel.LiveResult;
@@ -52,8 +54,10 @@ public class AddContactViewModel extends DbViewModel {
AddContactViewModel(Application application,
ContactManager contactManager,
@DatabaseExecutor Executor dbExecutor,
LifecycleManager lifecycleManager) {
super(application, dbExecutor, lifecycleManager);
LifecycleManager lifecycleManager,
TransactionManager db,
AndroidExecutor androidExecutor) {
super(application, dbExecutor, lifecycleManager, db, androidExecutor);
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;
@@ -18,6 +19,7 @@ import org.briarproject.bramble.api.lifecycle.LifecycleManager;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.rendezvous.RendezvousPoller;
import org.briarproject.bramble.api.rendezvous.event.RendezvousPollEvent;
import org.briarproject.bramble.api.system.AndroidExecutor;
import org.briarproject.briar.android.viewmodel.DbViewModel;
import java.util.ArrayList;
@@ -56,10 +58,12 @@ public class PendingContactListViewModel extends DbViewModel
PendingContactListViewModel(Application application,
@DatabaseExecutor Executor dbExecutor,
LifecycleManager lifecycleManager,
TransactionManager db,
AndroidExecutor androidExecutor,
ContactManager contactManager,
RendezvousPoller rendezvousPoller,
EventBus eventBus) {
super(application, dbExecutor, lifecycleManager);
super(application, dbExecutor, lifecycleManager, db, androidExecutor);
this.contactManager = contactManager;
this.rendezvousPoller = rendezvousPoller;
this.eventBus = eventBus;

View File

@@ -133,7 +133,7 @@ public abstract class BaseContactSelectorFragment<I extends SelectableContactIte
@Override
public void onExceptionUi(DbException exception) {
handleDbException(exception);
handleException(exception);
}
});
}

View File

@@ -22,6 +22,7 @@ import org.briarproject.bramble.api.settings.SettingsManager;
import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.sync.Message;
import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.bramble.api.system.AndroidExecutor;
import org.briarproject.briar.android.attachment.AttachmentCreator;
import org.briarproject.briar.android.attachment.AttachmentManager;
import org.briarproject.briar.android.attachment.AttachmentResult;
@@ -106,6 +107,7 @@ public class ConversationViewModel extends DbViewModel
@DatabaseExecutor Executor dbExecutor,
LifecycleManager lifecycleManager,
TransactionManager db,
AndroidExecutor androidExecutor,
EventBus eventBus,
MessagingManager messagingManager,
ContactManager contactManager,
@@ -113,7 +115,7 @@ public class ConversationViewModel extends DbViewModel
PrivateMessageFactory privateMessageFactory,
AttachmentRetriever attachmentRetriever,
AttachmentCreator attachmentCreator) {
super(application, dbExecutor, lifecycleManager);
super(application, dbExecutor, lifecycleManager, db, androidExecutor);
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;
@@ -14,6 +15,7 @@ import org.briarproject.bramble.api.lifecycle.IoExecutor;
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.bramble.api.system.AndroidExecutor;
import org.briarproject.briar.android.attachment.AttachmentItem;
import org.briarproject.briar.android.viewmodel.DbViewModel;
import org.briarproject.briar.android.viewmodel.LiveEvent;
@@ -78,8 +80,10 @@ public class ImageViewModel extends DbViewModel implements EventListener {
EventBus eventBus,
@DatabaseExecutor Executor dbExecutor,
LifecycleManager lifecycleManager,
TransactionManager db,
AndroidExecutor androidExecutor,
@IoExecutor Executor ioExecutor) {
super(application, dbExecutor, lifecycleManager);
super(application, dbExecutor, lifecycleManager, db, androidExecutor);
this.messagingManager = messagingManager;
this.eventBus = eventBus;
this.ioExecutor = ioExecutor;

View File

@@ -156,7 +156,7 @@ public class ForumActivity extends
@Override
public void onExceptionUi(DbException exception) {
handleDbException(exception);
handleException(exception);
}
});
}

View File

@@ -121,6 +121,7 @@ public class ForumListFragment extends BaseEventFragment implements
@Override
public void onStart() {
super.onStart();
// TODO block all forum post notifications as well
notificationManager.clearAllForumPostNotifications();
loadForums();
loadAvailableForums();

View File

@@ -5,7 +5,6 @@ import android.content.Context;
import android.os.Bundle;
import android.view.MenuItem;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.briar.android.DestroyableContext;
@@ -77,7 +76,7 @@ public abstract class BaseFragment extends Fragment
void showNextFragment(BaseFragment f);
@UiThread
void handleDbException(DbException e);
void handleException(Exception e);
}
@CallSuper
@@ -100,8 +99,8 @@ public abstract class BaseFragment extends Fragment
}
@UiThread
protected void handleDbException(DbException e) {
listener.handleDbException(e);
protected void handleException(Exception e) {
listener.handleException(e);
}
}

View File

@@ -17,7 +17,6 @@ import android.widget.TextView;
import com.google.android.material.navigation.NavigationView;
import com.google.android.material.navigation.NavigationView.OnNavigationItemSelectedListener;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
@@ -365,7 +364,7 @@ public class NavDrawerActivity extends BriarActivity implements
}
@Override
public void handleDbException(DbException e) {
public void handleException(Exception e) {
// Do nothing for now
}

View File

@@ -4,10 +4,12 @@ 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;
import org.briarproject.bramble.api.settings.SettingsManager;
import org.briarproject.bramble.api.system.AndroidExecutor;
import org.briarproject.briar.android.viewmodel.DbViewModel;
import java.util.concurrent.Executor;
@@ -51,8 +53,10 @@ public class NavDrawerViewModel extends DbViewModel {
NavDrawerViewModel(Application app,
@DatabaseExecutor Executor dbExecutor,
LifecycleManager lifecycleManager,
TransactionManager db,
AndroidExecutor androidExecutor,
SettingsManager settingsManager) {
super(app, dbExecutor, lifecycleManager);
super(app, dbExecutor, lifecycleManager, db, androidExecutor);
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;
@@ -28,6 +29,7 @@ import org.briarproject.bramble.api.plugin.event.TransportStateEvent;
import org.briarproject.bramble.api.settings.Settings;
import org.briarproject.bramble.api.settings.SettingsManager;
import org.briarproject.bramble.api.settings.event.SettingsUpdatedEvent;
import org.briarproject.bramble.api.system.AndroidExecutor;
import org.briarproject.briar.android.viewmodel.DbViewModel;
import java.util.concurrent.Executor;
@@ -85,10 +87,11 @@ public class PluginViewModel extends DbViewModel implements EventListener {
@Inject
PluginViewModel(Application app, @DatabaseExecutor Executor dbExecutor,
LifecycleManager lifecycleManager,
SettingsManager settingsManager, PluginManager pluginManager,
EventBus eventBus, NetworkManager networkManager) {
super(app, dbExecutor, lifecycleManager);
LifecycleManager lifecycleManager, TransactionManager db,
AndroidExecutor androidExecutor, SettingsManager settingsManager,
PluginManager pluginManager, EventBus eventBus,
NetworkManager networkManager) {
super(app, dbExecutor, lifecycleManager, db, androidExecutor);
this.app = app;
this.settingsManager = settingsManager;
this.pluginManager = pluginManager;

View File

@@ -106,7 +106,7 @@ public class GroupActivity extends
@Override
public void onExceptionUi(DbException exception) {
handleDbException(exception);
handleException(exception);
}
});
}
@@ -125,7 +125,7 @@ public class GroupActivity extends
@Override
public void onExceptionUi(DbException exception) {
handleDbException(exception);
handleException(exception);
}
});
}
@@ -264,7 +264,7 @@ public class GroupActivity extends
// GroupRemovedEvent being fired
@Override
public void onExceptionUi(DbException exception) {
handleDbException(exception);
handleException(exception);
}
});
}

View File

@@ -51,7 +51,7 @@ public class CreateGroupActivity extends BriarActivity
@Override
public void onExceptionUi(DbException exception) {
handleDbException(exception);
handleException(exception);
}
});
}

View File

@@ -69,7 +69,7 @@ public class GroupInviteActivity extends ContactSelectorActivity
@Override
public void onExceptionUi(DbException exception) {
setResult(RESULT_CANCELED);
handleDbException(exception);
handleException(exception);
}
});
}

View File

@@ -8,15 +8,19 @@ import org.briarproject.briar.api.client.MessageTracker.GroupCount;
import org.briarproject.briar.api.privategroup.GroupMessageHeader;
import org.briarproject.briar.api.privategroup.PrivateGroup;
// This class is not thread-safe
import javax.annotation.concurrent.Immutable;
import androidx.annotation.Nullable;
@Immutable
@NotNullByDefault
class GroupItem {
class GroupItem implements Comparable<GroupItem> {
private final PrivateGroup privateGroup;
private final AuthorInfo authorInfo;
private int messageCount, unreadCount;
private long timestamp;
private boolean dissolved;
private final int messageCount, unreadCount;
private final long timestamp;
private final boolean dissolved;
GroupItem(PrivateGroup privateGroup, AuthorInfo authorInfo,
GroupCount count, boolean dissolved) {
@@ -28,18 +32,22 @@ class GroupItem {
this.dissolved = dissolved;
}
void addMessageHeader(GroupMessageHeader header) {
messageCount++;
if (header.getTimestamp() > timestamp) {
timestamp = header.getTimestamp();
}
if (!header.isRead()) {
unreadCount++;
}
GroupItem(GroupItem item, GroupMessageHeader header) {
this.privateGroup = item.privateGroup;
this.authorInfo = item.authorInfo;
this.messageCount = item.messageCount + 1;
this.unreadCount = item.unreadCount + (header.isRead() ? 0 : 1);
this.timestamp = Math.max(header.getTimestamp(), item.timestamp);
this.dissolved = item.dissolved;
}
PrivateGroup getPrivateGroup() {
return privateGroup;
GroupItem(GroupItem item, boolean isDissolved) {
this.privateGroup = item.privateGroup;
this.authorInfo = item.authorInfo;
this.messageCount = item.messageCount;
this.unreadCount = item.unreadCount;
this.timestamp = item.timestamp;
this.dissolved = isDissolved;
}
GroupId getId() {
@@ -78,8 +86,27 @@ class GroupItem {
return dissolved;
}
void setDissolved() {
dissolved = true;
@Override
public int hashCode() {
return getId().hashCode();
}
@Override
public boolean equals(@Nullable Object o) {
return o instanceof GroupItem &&
getId().equals(((GroupItem) o).getId());
}
@Override
public int compareTo(GroupItem o) {
if (this == o) return 0;
// The group with the latest message comes first
long aTime = getTimestamp(), bTime = o.getTimestamp();
if (aTime > bTime) return -1;
if (aTime < bTime) return 1;
// Break ties by group name
String aName = getName();
String bName = o.getName();
return String.CASE_INSENSITIVE_ORDER.compare(aName, bName);
}
}

View File

@@ -1,81 +1,52 @@
package org.briarproject.briar.android.privategroup.list;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.briar.R;
import org.briarproject.briar.android.privategroup.list.GroupViewHolder.OnGroupRemoveClickListener;
import org.briarproject.briar.android.util.BriarAdapter;
import static androidx.recyclerview.widget.SortedList.INVALID_POSITION;
import androidx.recyclerview.widget.DiffUtil.ItemCallback;
import androidx.recyclerview.widget.ListAdapter;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
class GroupListAdapter extends BriarAdapter<GroupItem, GroupViewHolder> {
class GroupListAdapter extends ListAdapter<GroupItem, GroupViewHolder> {
private final OnGroupRemoveClickListener listener;
GroupListAdapter(Context ctx, OnGroupRemoveClickListener listener) {
super(ctx, GroupItem.class);
GroupListAdapter(OnGroupRemoveClickListener listener) {
super(new GroupItemCallback());
this.listener = listener;
}
@Override
public GroupViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View v = LayoutInflater.from(ctx).inflate(
R.layout.list_item_group, parent, false);
View v = LayoutInflater.from(parent.getContext())
.inflate(R.layout.list_item_group, parent, false);
return new GroupViewHolder(v);
}
@Override
public void onBindViewHolder(GroupViewHolder ui, int position) {
ui.bindView(ctx, items.get(position), listener);
ui.bindView(getItem(position), listener);
}
@Override
public int compare(GroupItem a, GroupItem b) {
if (a == b) return 0;
// The group with the latest message comes first
long aTime = a.getTimestamp(), bTime = b.getTimestamp();
if (aTime > bTime) return -1;
if (aTime < bTime) return 1;
// Break ties by group name
String aName = a.getName();
String bName = b.getName();
return String.CASE_INSENSITIVE_ORDER.compare(aName, bName);
}
@Override
public boolean areContentsTheSame(GroupItem a, GroupItem b) {
return a.getMessageCount() == b.getMessageCount() &&
a.getTimestamp() == b.getTimestamp() &&
a.getUnreadCount() == b.getUnreadCount() &&
a.isDissolved() == b.isDissolved();
}
@Override
public boolean areItemsTheSame(GroupItem a, GroupItem b) {
return a.getId().equals(b.getId());
}
int findItemPosition(GroupId g) {
for (int i = 0; i < items.size(); i++) {
GroupItem item = items.get(i);
if (item.getId().equals(g)) {
return i;
}
private static class GroupItemCallback extends ItemCallback<GroupItem> {
@Override
public boolean areItemsTheSame(GroupItem a, GroupItem b) {
return a.equals(b);
}
return INVALID_POSITION;
}
void removeItem(GroupId groupId) {
int pos = findItemPosition(groupId);
if (pos != INVALID_POSITION) items.removeItemAt(pos);
@Override
public boolean areContentsTheSame(GroupItem a, GroupItem b) {
return a.getMessageCount() == b.getMessageCount() &&
a.getTimestamp() == b.getTimestamp() &&
a.getUnreadCount() == b.getUnreadCount() &&
a.isDissolved() == b.isDissolved();
}
}
}

View File

@@ -1,60 +0,0 @@
package org.briarproject.briar.android.privategroup.list;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.briar.android.controller.DbController;
import org.briarproject.briar.android.controller.handler.ExceptionHandler;
import org.briarproject.briar.android.controller.handler.ResultExceptionHandler;
import org.briarproject.briar.api.privategroup.GroupMessageHeader;
import java.util.Collection;
import androidx.annotation.UiThread;
@NotNullByDefault
interface GroupListController extends DbController {
/**
* The listener must be set right after the controller was injected
*/
@UiThread
void setGroupListListener(GroupListListener listener);
@UiThread
void unsetGroupListListener(GroupListListener listener);
@UiThread
void onStart();
@UiThread
void onStop();
void loadGroups(
ResultExceptionHandler<Collection<GroupItem>, DbException> result);
void removeGroup(GroupId g, ExceptionHandler<DbException> result);
void loadAvailableGroups(
ResultExceptionHandler<Integer, DbException> result);
interface GroupListListener {
@UiThread
void onGroupMessageAdded(GroupMessageHeader header);
@UiThread
void onGroupInvitationReceived();
@UiThread
void onGroupAdded(GroupId groupId);
@UiThread
void onGroupRemoved(GroupId groupId);
@UiThread
void onGroupDissolved(GroupId groupId);
}
}

View File

@@ -12,57 +12,50 @@ import android.view.ViewGroup;
import com.google.android.material.snackbar.Snackbar;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.controller.handler.UiExceptionHandler;
import org.briarproject.briar.android.controller.handler.UiResultExceptionHandler;
import org.briarproject.briar.android.fragment.BaseFragment;
import org.briarproject.briar.android.privategroup.creation.CreateGroupActivity;
import org.briarproject.briar.android.privategroup.invitation.GroupInvitationActivity;
import org.briarproject.briar.android.privategroup.list.GroupListController.GroupListListener;
import org.briarproject.briar.android.privategroup.list.GroupViewHolder.OnGroupRemoveClickListener;
import org.briarproject.briar.android.util.BriarSnackbarBuilder;
import org.briarproject.briar.android.view.BriarRecyclerView;
import org.briarproject.briar.api.privategroup.GroupMessageHeader;
import java.util.Collection;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import javax.inject.Inject;
import androidx.annotation.UiThread;
import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.LinearLayoutManager;
import static com.google.android.material.snackbar.Snackbar.LENGTH_INDEFINITE;
import static java.util.Objects.requireNonNull;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class GroupListFragment extends BaseFragment implements
GroupListListener, OnGroupRemoveClickListener, OnClickListener {
OnGroupRemoveClickListener, OnClickListener {
public final static String TAG = GroupListFragment.class.getName();
private static final Logger LOG = Logger.getLogger(TAG);
public static GroupListFragment newInstance() {
return new GroupListFragment();
}
@Inject
GroupListController controller;
ViewModelProvider.Factory viewModelFactory;
private GroupListViewModel viewModel;
private BriarRecyclerView list;
private GroupListAdapter adapter;
private Snackbar snackbar;
@Override
public void injectFragment(ActivityComponent component) {
component.inject(this);
controller.setGroupListListener(this);
viewModel = new ViewModelProvider(this, viewModelFactory)
.get(GroupListViewModel.class);
}
@Nullable
@@ -75,17 +68,32 @@ public class GroupListFragment extends BaseFragment implements
View v = inflater.inflate(R.layout.list, container, false);
adapter = new GroupListAdapter(getActivity(), this);
adapter = new GroupListAdapter(this);
list = v.findViewById(R.id.list);
list.setEmptyImage(R.drawable.ic_empty_state_group_list);
list.setEmptyText(R.string.groups_list_empty);
list.setEmptyAction(R.string.groups_list_empty_action);
list.setLayoutManager(new LinearLayoutManager(getContext()));
list.setAdapter(adapter);
viewModel.getGroupItems().observe(getViewLifecycleOwner(), result ->
result.onError(this::handleException).onSuccess(items -> {
adapter.submitList(items);
if (requireNonNull(items).size() == 0) list.showData();
})
);
snackbar = new BriarSnackbarBuilder()
Snackbar snackbar = new BriarSnackbarBuilder()
.setAction(R.string.show, this)
.make(list, "", LENGTH_INDEFINITE);
viewModel.getNumInvitations().observe(getViewLifecycleOwner(), num -> {
if (num == 0) {
snackbar.dismiss();
} else {
snackbar.setText(getResources().getQuantityString(
R.plurals.groups_invitations_open, num, num));
if (!snackbar.isShownOrQueued()) snackbar.show();
}
});
return v;
}
@@ -93,25 +101,23 @@ public class GroupListFragment extends BaseFragment implements
@Override
public void onStart() {
super.onStart();
controller.onStart();
viewModel.blockAllGroupMessageNotifications();
viewModel.clearAllGroupMessageNotifications();
// The attributes and sorting of the groups may have changed while we
// were stopped and we have no way finding out about them, so re-load
// e.g. less unread messages in a group after viewing it.
viewModel.loadGroups();
// The number of invitations might have changed while we were stopped
// e.g. because of accepting an invitation which does not trigger event
viewModel.loadNumInvitations();
list.startPeriodicUpdate();
loadGroups();
loadAvailableGroups();
}
@Override
public void onStop() {
super.onStop();
controller.onStop();
list.stopPeriodicUpdate();
adapter.clear();
list.showProgressBar();
}
@Override
public void onDestroy() {
super.onDestroy();
controller.unsetGroupListListener(this);
viewModel.unblockAllGroupMessageNotifications();
}
@Override
@@ -122,68 +128,18 @@ public class GroupListFragment extends BaseFragment implements
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.action_add_group:
Intent i = new Intent(getContext(), CreateGroupActivity.class);
startActivity(i);
return true;
default:
return super.onOptionsItemSelected(item);
if (item.getItemId() == R.id.action_add_group) {
Intent i = new Intent(getContext(), CreateGroupActivity.class);
startActivity(i);
return true;
}
return super.onOptionsItemSelected(item);
}
@UiThread
@Override
public void onGroupRemoveClick(GroupItem item) {
controller.removeGroup(item.getId(),
new UiExceptionHandler<DbException>(this) {
// result handled by GroupRemovedEvent and onGroupRemoved()
@Override
public void onExceptionUi(DbException exception) {
handleDbException(exception);
}
});
}
@UiThread
@Override
public void onGroupMessageAdded(GroupMessageHeader header) {
adapter.incrementRevision();
int position = adapter.findItemPosition(header.getGroupId());
GroupItem item = adapter.getItemAt(position);
if (item != null) {
item.addMessageHeader(header);
adapter.updateItemAt(position, item);
}
}
@Override
public void onGroupInvitationReceived() {
loadAvailableGroups();
}
@UiThread
@Override
public void onGroupAdded(GroupId groupId) {
loadGroups();
}
@UiThread
@Override
public void onGroupRemoved(GroupId groupId) {
adapter.incrementRevision();
adapter.removeItem(groupId);
}
@Override
public void onGroupDissolved(GroupId groupId) {
adapter.incrementRevision();
int position = adapter.findItemPosition(groupId);
GroupItem item = adapter.getItemAt(position);
if (item != null) {
item.setDissolved();
adapter.updateItemAt(position, item);
}
viewModel.removeGroup(item.getId());
}
@Override
@@ -191,52 +147,6 @@ public class GroupListFragment extends BaseFragment implements
return TAG;
}
private void loadGroups() {
int revision = adapter.getRevision();
controller.loadGroups(
new UiResultExceptionHandler<Collection<GroupItem>, DbException>(
this) {
@Override
public void onResultUi(Collection<GroupItem> groups) {
if (revision == adapter.getRevision()) {
adapter.incrementRevision();
if (groups.isEmpty()) list.showData();
else adapter.replaceAll(groups);
} else {
LOG.info("Concurrent update, reloading");
loadGroups();
}
}
@Override
public void onExceptionUi(DbException exception) {
handleDbException(exception);
}
});
}
private void loadAvailableGroups() {
controller.loadAvailableGroups(
new UiResultExceptionHandler<Integer, DbException>(this) {
@Override
public void onResultUi(Integer num) {
if (num == 0) {
snackbar.dismiss();
} else {
snackbar.setText(getResources().getQuantityString(
R.plurals.groups_invitations_open, num,
num));
if (!snackbar.isShownOrQueued()) snackbar.show();
}
}
@Override
public void onExceptionUi(DbException exception) {
handleDbException(exception);
}
});
}
/**
* This method is handling the available groups snackbar action
*/

View File

@@ -1,17 +1,18 @@
package org.briarproject.briar.android.privategroup.list;
import org.briarproject.briar.android.activity.ActivityScope;
import org.briarproject.briar.android.viewmodel.ViewModelKey;
import androidx.lifecycle.ViewModel;
import dagger.Binds;
import dagger.Module;
import dagger.Provides;
import dagger.multibindings.IntoMap;
@Module
public class GroupListModule {
public abstract class GroupListModule {
@ActivityScope
@Provides
GroupListController provideGroupListController(
GroupListControllerImpl groupListController) {
return groupListController;
}
@Binds
@IntoMap
@ViewModelKey(GroupListViewModel.class)
abstract ViewModel bindGroupListViewModel(
GroupListViewModel groupListViewModel);
}

View File

@@ -1,9 +1,12 @@
package org.briarproject.briar.android.privategroup.list;
import android.app.Application;
import org.briarproject.bramble.api.contact.ContactManager;
import org.briarproject.bramble.api.db.DatabaseExecutor;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.db.NoSuchGroupException;
import org.briarproject.bramble.api.db.Transaction;
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;
@@ -16,11 +19,12 @@ import org.briarproject.bramble.api.sync.ClientId;
import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.sync.event.GroupAddedEvent;
import org.briarproject.bramble.api.sync.event.GroupRemovedEvent;
import org.briarproject.briar.android.controller.DbControllerImpl;
import org.briarproject.briar.android.controller.handler.ExceptionHandler;
import org.briarproject.briar.android.controller.handler.ResultExceptionHandler;
import org.briarproject.bramble.api.system.AndroidExecutor;
import org.briarproject.briar.android.viewmodel.DbViewModel;
import org.briarproject.briar.android.viewmodel.LiveResult;
import org.briarproject.briar.api.android.AndroidNotificationManager;
import org.briarproject.briar.api.client.MessageTracker.GroupCount;
import org.briarproject.briar.api.privategroup.GroupMessageHeader;
import org.briarproject.briar.api.privategroup.PrivateGroup;
import org.briarproject.briar.api.privategroup.PrivateGroupManager;
import org.briarproject.briar.api.privategroup.event.GroupDissolvedEvent;
@@ -30,6 +34,7 @@ import org.briarproject.briar.api.privategroup.invitation.GroupInvitationManager
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -38,10 +43,13 @@ import java.util.logging.Logger;
import javax.inject.Inject;
import androidx.annotation.CallSuper;
import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import static java.util.Objects.requireNonNull;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.LogUtils.logDuration;
import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.bramble.util.LogUtils.now;
@@ -49,11 +57,10 @@ import static org.briarproject.briar.api.privategroup.PrivateGroupManager.CLIENT
@MethodsNotNullByDefault
@ParametersNotNullByDefault
class GroupListControllerImpl extends DbControllerImpl
implements GroupListController, EventListener {
class GroupListViewModel extends DbViewModel implements EventListener {
private static final Logger LOG =
Logger.getLogger(GroupListControllerImpl.class.getName());
getLogger(GroupListViewModel.class.getName());
private final PrivateGroupManager groupManager;
private final GroupInvitationManager groupInvitationManager;
@@ -61,120 +68,137 @@ class GroupListControllerImpl extends DbControllerImpl
private final AndroidNotificationManager notificationManager;
private final EventBus eventBus;
// UI thread
@Nullable
private GroupListListener listener;
private final MutableLiveData<LiveResult<List<GroupItem>>> groupItems =
new MutableLiveData<>();
private final MutableLiveData<Integer> numInvitations =
new MutableLiveData<>();
@Inject
GroupListControllerImpl(@DatabaseExecutor Executor dbExecutor,
LifecycleManager lifecycleManager, PrivateGroupManager groupManager,
GroupListViewModel(Application application,
@DatabaseExecutor Executor dbExecutor,
LifecycleManager lifecycleManager,
TransactionManager db,
AndroidExecutor androidExecutor,
PrivateGroupManager groupManager,
GroupInvitationManager groupInvitationManager,
ContactManager contactManager,
AndroidNotificationManager notificationManager, EventBus eventBus) {
super(dbExecutor, lifecycleManager);
super(application, dbExecutor, lifecycleManager, db, androidExecutor);
this.groupManager = groupManager;
this.groupInvitationManager = groupInvitationManager;
this.contactManager = contactManager;
this.notificationManager = notificationManager;
this.eventBus = eventBus;
this.eventBus.addListener(this);
}
@Override
public void setGroupListListener(GroupListListener listener) {
this.listener = listener;
}
@Override
public void unsetGroupListListener(GroupListListener listener) {
if (this.listener == listener) this.listener = null;
}
@Override
@CallSuper
public void onStart() {
if (listener == null) throw new IllegalStateException();
eventBus.addListener(this);
notificationManager.clearAllGroupMessageNotifications();
}
@Override
@CallSuper
public void onStop() {
protected void onCleared() {
super.onCleared();
eventBus.removeListener(this);
}
void clearAllGroupMessageNotifications() {
notificationManager.clearAllGroupMessageNotifications();
}
void blockAllGroupMessageNotifications() {
notificationManager.blockAllGroupMessageNotifications();
}
void unblockAllGroupMessageNotifications() {
notificationManager.unblockAllGroupMessageNotifications();
}
@Override
@CallSuper
public void eventOccurred(Event e) {
if (listener == null) throw new IllegalStateException();
if (e instanceof GroupMessageAddedEvent) {
GroupMessageAddedEvent g = (GroupMessageAddedEvent) e;
LOG.info("Private group message added");
listener.onGroupMessageAdded(g.getHeader());
onGroupMessageAdded(g.getHeader());
} else if (e instanceof GroupInvitationRequestReceivedEvent) {
LOG.info("Private group invitation received");
listener.onGroupInvitationReceived();
loadNumInvitations();
} else if (e instanceof GroupAddedEvent) {
GroupAddedEvent g = (GroupAddedEvent) e;
ClientId id = g.getGroup().getClientId();
if (id.equals(CLIENT_ID)) {
LOG.info("Private group added");
listener.onGroupAdded(g.getGroup().getId());
loadGroups();
}
} else if (e instanceof GroupRemovedEvent) {
GroupRemovedEvent g = (GroupRemovedEvent) e;
ClientId id = g.getGroup().getClientId();
if (id.equals(CLIENT_ID)) {
LOG.info("Private group removed");
listener.onGroupRemoved(g.getGroup().getId());
onGroupRemoved(g.getGroup().getId());
}
} else if (e instanceof GroupDissolvedEvent) {
GroupDissolvedEvent g = (GroupDissolvedEvent) e;
LOG.info("Private group dissolved");
listener.onGroupDissolved(g.getGroupId());
onGroupDissolved(g.getGroupId());
}
}
@Override
public void loadGroups(
ResultExceptionHandler<Collection<GroupItem>, DbException> handler) {
runOnDbThread(() -> {
try {
long start = now();
Collection<PrivateGroup> groups =
groupManager.getPrivateGroups();
List<GroupItem> items = new ArrayList<>(groups.size());
Map<AuthorId, AuthorInfo> authorInfos = new HashMap<>();
for (PrivateGroup g : groups) {
try {
GroupId id = g.getId();
AuthorId authorId = g.getCreator().getId();
AuthorInfo authorInfo;
if (authorInfos.containsKey(authorId)) {
authorInfo = authorInfos.get(authorId);
} else {
authorInfo = contactManager.getAuthorInfo(authorId);
authorInfos.put(authorId, authorInfo);
}
GroupCount count = groupManager.getGroupCount(id);
boolean dissolved = groupManager.isDissolved(id);
items.add(
new GroupItem(g, authorInfo, count, dissolved));
} catch (NoSuchGroupException e) {
// Continue
}
}
logDuration(LOG, "Loading groups", start);
handler.onResult(items);
} catch (DbException e) {
logException(LOG, WARNING, e);
handler.onException(e);
}
});
void loadGroups() {
loadList(this::loadGroups, groupItems::setValue);
}
@Override
public void removeGroup(GroupId g, ExceptionHandler<DbException> handler) {
@DatabaseExecutor
private List<GroupItem> loadGroups(Transaction txn) throws DbException {
long start = now();
Collection<PrivateGroup> groups = groupManager.getPrivateGroups(txn);
List<GroupItem> items = new ArrayList<>(groups.size());
Map<AuthorId, AuthorInfo> authorInfos = new HashMap<>();
for (PrivateGroup g : groups) {
GroupId id = g.getId();
AuthorId authorId = g.getCreator().getId();
AuthorInfo authorInfo;
if (authorInfos.containsKey(authorId)) {
authorInfo = requireNonNull(authorInfos.get(authorId));
} else {
authorInfo = contactManager.getAuthorInfo(txn, authorId);
authorInfos.put(authorId, authorInfo);
}
GroupCount count = groupManager.getGroupCount(txn, id);
boolean dissolved = groupManager.isDissolved(txn, id);
items.add(new GroupItem(g, authorInfo, count, dissolved));
}
Collections.sort(items);
logDuration(LOG, "Loading groups", start);
return items;
}
@UiThread
private void onGroupMessageAdded(GroupMessageHeader header) {
GroupId g = header.getGroupId();
List<GroupItem> list = updateListItems(groupItems,
itemToTest -> itemToTest.getId().equals(g),
itemToUpdate -> new GroupItem(itemToUpdate, header));
if (list == null) return;
// re-sort as the order of items may have changed
Collections.sort(list);
groupItems.setValue(new LiveResult<>(list));
}
@UiThread
private void onGroupDissolved(GroupId groupId) {
List<GroupItem> list = updateListItems(groupItems,
itemToTest -> itemToTest.getId().equals(groupId),
itemToUpdate -> new GroupItem(itemToUpdate, true));
if (list == null) return;
groupItems.setValue(new LiveResult<>(list));
}
@UiThread
private void onGroupRemoved(GroupId groupId) {
List<GroupItem> list =
removeListItems(groupItems, i -> i.getId().equals(groupId));
if (list == null) return;
groupItems.setValue(new LiveResult<>(list));
}
void removeGroup(GroupId g) {
runOnDbThread(() -> {
try {
long start = now();
@@ -182,23 +206,26 @@ class GroupListControllerImpl extends DbControllerImpl
logDuration(LOG, "Removing group", start);
} catch (DbException e) {
logException(LOG, WARNING, e);
handler.onException(e);
}
});
}
@Override
public void loadAvailableGroups(
ResultExceptionHandler<Integer, DbException> handler) {
void loadNumInvitations() {
runOnDbThread(() -> {
try {
handler.onResult(
groupInvitationManager.getInvitations().size());
int i = groupInvitationManager.getInvitations().size();
numInvitations.postValue(i);
} catch (DbException e) {
logException(LOG, WARNING, e);
handler.onException(e);
}
});
}
LiveData<LiveResult<List<GroupItem>>> getGroupItems() {
return groupItems;
}
LiveData<Integer> getNumInvitations() {
return numInvitations;
}
}

View File

@@ -29,6 +29,7 @@ class GroupViewHolder extends RecyclerView.ViewHolder {
private final static float ALPHA = 0.42f;
private final Context ctx;
private final ViewGroup layout;
private final TextAvatarView avatar;
private final TextView name;
@@ -40,7 +41,7 @@ class GroupViewHolder extends RecyclerView.ViewHolder {
GroupViewHolder(View v) {
super(v);
ctx = v.getContext();
layout = (ViewGroup) v;
avatar = v.findViewById(R.id.avatarView);
name = v.findViewById(R.id.nameView);
@@ -51,8 +52,7 @@ class GroupViewHolder extends RecyclerView.ViewHolder {
remove = v.findViewById(R.id.removeButton);
}
void bindView(Context ctx, GroupItem group,
OnGroupRemoveClickListener listener) {
void bindView(GroupItem group, OnGroupRemoveClickListener listener) {
// Avatar
avatar.setText(group.getName().substring(0, 1));
avatar.setBackgroundBytes(group.getId().getBytes());

View File

@@ -124,7 +124,7 @@ public class GroupMemberListActivity extends BriarActivity
@Override
public void onExceptionUi(DbException exception) {
handleDbException(exception);
handleException(exception);
}
});
}

View File

@@ -80,7 +80,7 @@ public class RevealContactsActivity extends ContactSelectorActivity
@Override
public void onExceptionUi(DbException exception) {
handleDbException(exception);
handleException(exception);
}
});
}
@@ -120,7 +120,7 @@ public class RevealContactsActivity extends ContactSelectorActivity
new UiExceptionHandler<DbException>(this) {
@Override
public void onExceptionUi(DbException exception) {
handleDbException(exception);
handleException(exception);
}
});
}
@@ -137,7 +137,7 @@ public class RevealContactsActivity extends ContactSelectorActivity
new UiExceptionHandler<DbException>(this) {
@Override
public void onExceptionUi(DbException exception) {
handleDbException(exception);
handleException(exception);
}
});
supportFinishAfterTransition();

View File

@@ -98,7 +98,7 @@ public abstract class InvitationActivity<I extends InvitationItem>
@Override
public void onExceptionUi(DbException exception) {
handleDbException(exception);
handleException(exception);
}
});
}
@@ -110,7 +110,7 @@ public abstract class InvitationActivity<I extends InvitationItem>
new UiExceptionHandler<DbException>(this) {
@Override
public void onExceptionUi(DbException exception) {
handleDbException(exception);
handleException(exception);
}
});
}

View File

@@ -59,7 +59,7 @@ public class ShareBlogActivity extends ShareActivity {
Toast.makeText(ShareBlogActivity.this,
R.string.blogs_sharing_error, LENGTH_SHORT)
.show();
handleDbException(exception);
handleException(exception);
}
});

View File

@@ -59,7 +59,7 @@ public class ShareForumActivity extends ShareActivity {
Toast.makeText(ShareForumActivity.this,
R.string.forum_share_error, LENGTH_SHORT)
.show();
handleDbException(exception);
handleException(exception);
}
});
}

View File

@@ -152,7 +152,7 @@ public abstract class ThreadListActivity<G extends NamedGroup, I extends ThreadI
@Override
public void onExceptionUi(DbException exception) {
handleDbException(exception);
handleException(exception);
}
});
}
@@ -183,7 +183,7 @@ public abstract class ThreadListActivity<G extends NamedGroup, I extends ThreadI
@Override
public void onExceptionUi(DbException exception) {
handleDbException(exception);
handleException(exception);
}
});
}
@@ -214,7 +214,7 @@ public abstract class ThreadListActivity<G extends NamedGroup, I extends ThreadI
@Override
public void onExceptionUi(DbException exception) {
handleDbException(exception);
handleException(exception);
}
});
}
@@ -351,7 +351,7 @@ public abstract class ThreadListActivity<G extends NamedGroup, I extends ThreadI
@Override
public void onExceptionUi(DbException exception) {
handleDbException(exception);
handleException(exception);
}
};
getController().createAndStoreMessage(text, replyItem, handler);

View File

@@ -3,18 +3,33 @@ package org.briarproject.briar.android.viewmodel;
import android.app.Application;
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 org.briarproject.bramble.api.system.AndroidExecutor;
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 java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.LogUtils.logException;
@Immutable
@NotNullByDefault
@@ -25,17 +40,31 @@ public abstract class DbViewModel extends AndroidViewModel {
@DatabaseExecutor
private final Executor dbExecutor;
private final LifecycleManager lifecycleManager;
private final TransactionManager db;
private final AndroidExecutor androidExecutor;
public DbViewModel(
@NonNull Application application,
@DatabaseExecutor Executor dbExecutor,
LifecycleManager lifecycleManager) {
LifecycleManager lifecycleManager,
TransactionManager db,
AndroidExecutor androidExecutor) {
super(application);
this.dbExecutor = dbExecutor;
this.lifecycleManager = lifecycleManager;
this.db = db;
this.androidExecutor = androidExecutor;
}
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, UiConsumer)} instead.
*/
protected void runOnDbThread(Runnable task) {
dbExecutor.execute(() -> {
try {
lifecycleManager.waitForDatabase();
@@ -47,4 +76,120 @@ 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,
UiConsumer<LiveResult<T>> uiConsumer) {
dbExecutor.execute(() -> {
try {
lifecycleManager.waitForDatabase();
db.transaction(true, txn -> {
T t = task.call(txn);
txn.attach(() -> uiConsumer.accept(new LiveResult<>(t)));
});
} catch (InterruptedException e) {
LOG.warning("Interrupted while waiting for database");
Thread.currentThread().interrupt();
} catch (DbException e) {
logException(LOG, WARNING, e);
androidExecutor.runOnUiThread(
() -> uiConsumer.accept(new LiveResult<>(e)));
}
});
}
@NotNullByDefault
public interface UiConsumer<T> {
@UiThread
void accept(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> updateListItems(
LiveData<LiveResult<List<T>>> liveData, Function<T, Boolean> test,
Function<T, T> replacer) {
List<T> items = getListCopy(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> removeListItems(
LiveData<LiveResult<List<T>>> liveData, Function<T, Boolean> test) {
List<T> items = getListCopy(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 a copy of the list of items from the given LiveData
* or null if it is not available.
* The list copy can be safely mutated.
*/
@Nullable
private <T> List<T> getListCopy(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

@@ -3,14 +3,15 @@ package org.briarproject.briar.android.viewmodel;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import androidx.annotation.Nullable;
import androidx.core.util.Consumer;
@NotNullByDefault
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;
@@ -36,4 +37,20 @@ public class LiveResult<T> {
return exception != null;
}
/**
* Runs the given function, if {@link #hasError()} is true.
*/
public LiveResult<T> onError(Consumer<Exception> fun) {
if (exception != null) fun.accept(exception);
return this;
}
/**
* Runs the given function, if {@link #hasError()} is false.
*/
public LiveResult<T> onSuccess(Consumer<T> fun) {
if (result != null) fun.accept(result);
return this;
}
}

View File

@@ -82,6 +82,10 @@ public interface AndroidNotificationManager {
void unblockNotification(GroupId g);
void blockAllGroupMessageNotifications();
void unblockAllGroupMessageNotifications();
void blockAllBlogPostNotifications();
void unblockAllBlogPostNotifications();

View File

@@ -0,0 +1,36 @@
package org.briarproject.briar.android;
import org.briarproject.bramble.api.system.AndroidExecutor;
import java.util.concurrent.Callable;
import java.util.concurrent.Executor;
import java.util.concurrent.Future;
public class AndroidExecutorTestImpl implements AndroidExecutor {
private final Executor executor;
public AndroidExecutorTestImpl(Executor executor) {
this.executor = executor;
}
@Override
public <V> Future<V> runOnBackgroundThread(Callable<V> c) {
throw new IllegalStateException("not implemented");
}
@Override
public void runOnBackgroundThread(Runnable r) {
executor.execute(r);
}
@Override
public <V> Future<V> runOnUiThread(Callable<V> c) {
throw new IllegalStateException("not implemented");
}
@Override
public void runOnUiThread(Runnable r) {
executor.execute(r);
}
}

View File

@@ -0,0 +1,212 @@
package org.briarproject.briar.android.privategroup.list;
import android.app.Application;
import org.briarproject.bramble.api.contact.Contact;
import org.briarproject.bramble.api.contact.ContactManager;
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.event.Event;
import org.briarproject.bramble.api.event.EventBus;
import org.briarproject.bramble.api.event.EventListener;
import org.briarproject.bramble.api.identity.AuthorInfo;
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
import org.briarproject.bramble.api.sync.Group;
import org.briarproject.bramble.api.system.AndroidExecutor;
import org.briarproject.bramble.test.BrambleMockTestCase;
import org.briarproject.bramble.test.DbExpectations;
import org.briarproject.bramble.test.ImmediateExecutor;
import org.briarproject.briar.android.AndroidExecutorTestImpl;
import org.briarproject.briar.android.viewmodel.LiveResult;
import org.briarproject.briar.api.android.AndroidNotificationManager;
import org.briarproject.briar.api.privategroup.PrivateGroup;
import org.briarproject.briar.api.privategroup.PrivateGroupManager;
import org.briarproject.briar.api.privategroup.event.GroupDissolvedEvent;
import org.briarproject.briar.api.privategroup.invitation.GroupInvitationItem;
import org.briarproject.briar.api.privategroup.invitation.GroupInvitationManager;
import org.jmock.Expectations;
import org.jmock.lib.legacy.ClassImposteriser;
import org.junit.Rule;
import org.junit.Test;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.Executor;
import androidx.arch.core.executor.testing.InstantTaskExecutorRule;
import static edu.emory.mathcs.backport.java.util.Collections.emptyList;
import static edu.emory.mathcs.backport.java.util.Collections.singletonList;
import static org.briarproject.bramble.test.TestUtils.getAuthor;
import static org.briarproject.bramble.test.TestUtils.getContact;
import static org.briarproject.bramble.test.TestUtils.getGroup;
import static org.briarproject.bramble.test.TestUtils.getRandomBytes;
import static org.briarproject.briar.android.viewmodel.LiveDataTestUtil.getOrAwaitValue;
import static org.briarproject.briar.api.client.MessageTracker.GroupCount;
import static org.briarproject.briar.api.privategroup.invitation.GroupInvitationManager.CLIENT_ID;
import static org.briarproject.briar.api.privategroup.invitation.GroupInvitationManager.MAJOR_VERSION;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
public class GroupListViewModelTest extends BrambleMockTestCase {
@Rule
public final InstantTaskExecutorRule testRule =
new InstantTaskExecutorRule();
private final LifecycleManager lifecycleManager =
context.mock(LifecycleManager.class);
private final TransactionManager db =
context.mock(TransactionManager.class);
private final PrivateGroupManager groupManager =
context.mock(PrivateGroupManager.class);
private final GroupInvitationManager groupInvitationManager =
context.mock(GroupInvitationManager.class);
private final ContactManager contactManager =
context.mock(ContactManager.class);
private final AndroidNotificationManager notificationManager =
context.mock(AndroidNotificationManager.class);
private final EventBus eventBus = context.mock(EventBus.class);
private final GroupListViewModel viewModel;
private final Group g1 = getGroup(CLIENT_ID, MAJOR_VERSION);
private final Group g2 = getGroup(CLIENT_ID, MAJOR_VERSION);
private final PrivateGroup privateGroup1 =
new PrivateGroup(g1, "foo", getAuthor(), getRandomBytes(2));
private final PrivateGroup privateGroup2 =
new PrivateGroup(g2, "bar", getAuthor(), getRandomBytes(2));
private final AuthorInfo authorInfo1 =
new AuthorInfo(AuthorInfo.Status.UNVERIFIED);
private final AuthorInfo authorInfo2 =
new AuthorInfo(AuthorInfo.Status.VERIFIED);
private final GroupCount groupCount1 = new GroupCount(2, 1, 23L);
private final GroupCount groupCount2 = new GroupCount(5, 3, 42L);
private final GroupItem item1 =
new GroupItem(privateGroup1, authorInfo1, groupCount1, false);
private final GroupItem item2 =
new GroupItem(privateGroup2, authorInfo2, groupCount2, false);
public GroupListViewModelTest() {
context.setImposteriser(ClassImposteriser.INSTANCE);
Application app = context.mock(Application.class);
context.checking(new Expectations() {{
oneOf(eventBus).addListener(with(any(EventListener.class)));
}});
Executor dbExecutor = new ImmediateExecutor();
AndroidExecutor androidExecutor =
new AndroidExecutorTestImpl(dbExecutor);
viewModel = new GroupListViewModel(app, dbExecutor, lifecycleManager,
db, androidExecutor, groupManager, groupInvitationManager,
contactManager, notificationManager, eventBus);
}
@Test
public void testLoadGroupsException() throws Exception {
DbException dbException = new DbException();
Transaction txn = new Transaction(null, true);
context.checking(new DbExpectations() {{
oneOf(lifecycleManager).waitForDatabase();
oneOf(db).transaction(with(true), withDbRunnable(txn));
oneOf(groupManager).getPrivateGroups(txn);
will(throwException(dbException));
}});
viewModel.loadGroups();
LiveResult<List<GroupItem>> result =
getOrAwaitValue(viewModel.getGroupItems());
assertTrue(result.hasError());
assertEquals(dbException, result.getException());
assertNull(result.getResultOrNull());
}
@Test
public void testLoadGroups() throws Exception {
Transaction txn = new Transaction(null, true);
context.checking(new DbExpectations() {{
oneOf(lifecycleManager).waitForDatabase();
oneOf(db).transaction(with(true), withDbRunnable(txn));
oneOf(groupManager).getPrivateGroups(txn);
will(returnValue(Arrays.asList(privateGroup1, privateGroup2)));
}});
expectLoadGroup(txn, privateGroup1, authorInfo1, groupCount1, false);
expectLoadGroup(txn, privateGroup2, authorInfo2, groupCount2, false);
viewModel.loadGroups();
// unpack updated live data
LiveResult<List<GroupItem>> result =
getOrAwaitValue(viewModel.getGroupItems());
assertFalse(result.hasError());
List<GroupItem> liveList = result.getResultOrNull();
assertNotNull(liveList);
// list is sorted by last message timestamp
assertEquals(Arrays.asList(item2, item1), liveList);
// group 1 gets dissolved by creator
Event dissolvedEvent = new GroupDissolvedEvent(privateGroup1.getId());
viewModel.eventOccurred(dissolvedEvent);
result = getOrAwaitValue(viewModel.getGroupItems());
liveList = result.getResultOrNull();
assertNotNull(liveList);
assertEquals(2, liveList.size());
// assert that list update includes dissolved group item
for (GroupItem item : liveList) {
if (item.getId().equals(privateGroup1.getId())) {
assertTrue(item.isDissolved());
} else if (item.getId().equals(privateGroup2.getId())) {
assertFalse(item.isDissolved());
} else fail();
}
}
@Test
public void testLoadNumInvitations() throws Exception {
context.checking(new Expectations() {{
oneOf(lifecycleManager).waitForDatabase();
oneOf(groupInvitationManager).getInvitations();
will(returnValue(emptyList()));
}});
viewModel.loadNumInvitations();
int num = getOrAwaitValue(viewModel.getNumInvitations());
assertEquals(0, num);
PrivateGroup pg = context.mock(PrivateGroup.class);
Contact c = getContact();
GroupInvitationItem item = new GroupInvitationItem(pg, c);
context.checking(new Expectations() {{
oneOf(lifecycleManager).waitForDatabase();
oneOf(groupInvitationManager).getInvitations();
will(returnValue(singletonList(item)));
}});
viewModel.loadNumInvitations();
num = getOrAwaitValue(viewModel.getNumInvitations());
assertEquals(1, num);
}
private void expectLoadGroup(Transaction txn, PrivateGroup privateGroup,
AuthorInfo authorInfo, GroupCount groupCount, boolean dissolved)
throws DbException {
context.checking(new DbExpectations() {{
oneOf(contactManager)
.getAuthorInfo(txn, privateGroup.getCreator().getId());
will(returnValue(authorInfo));
oneOf(groupManager).getGroupCount(txn, privateGroup.getId());
will(returnValue(groupCount));
oneOf(groupManager).isDissolved(txn, privateGroup.getId());
will(returnValue(dissolved));
}});
}
}

View File

@@ -0,0 +1,36 @@
/* Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0
https://gist.github.com/JoseAlcerreca/1e9ee05dcdd6a6a6fa1cbfc125559bba
*/
package org.briarproject.briar.android.viewmodel;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import androidx.annotation.Nullable;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.Observer;
public class LiveDataTestUtil {
public static <T> T getOrAwaitValue(final LiveData<T> liveData)
throws InterruptedException {
final AtomicReference<T> data = new AtomicReference<>();
final CountDownLatch latch = new CountDownLatch(1);
Observer<T> observer = new Observer<T>() {
@Override
public void onChanged(@Nullable T o) {
data.set(o);
latch.countDown();
liveData.removeObserver(this);
}
};
liveData.observeForever(observer);
// Don't wait indefinitely if the LiveData is not set.
if (!latch.await(2, TimeUnit.SECONDS)) {
throw new RuntimeException("LiveData value was never set.");
}
return data.get();
}
}

View File

@@ -66,6 +66,11 @@ public interface PrivateGroupManager {
*/
void markGroupDissolved(Transaction txn, GroupId g) throws DbException;
/**
* Returns true if the given private group has been dissolved.
*/
boolean isDissolved(Transaction txn, GroupId g) throws DbException;
/**
* Returns true if the given private group has been dissolved.
*/
@@ -91,6 +96,12 @@ public interface PrivateGroupManager {
*/
Collection<PrivateGroup> getPrivateGroups() throws DbException;
/**
* Returns all private groups the user is a member of.
*/
Collection<PrivateGroup> getPrivateGroups(Transaction txn)
throws DbException;
/**
* Returns the text of the private group message with the given ID.
*/
@@ -111,6 +122,11 @@ public interface PrivateGroupManager {
*/
boolean isMember(Transaction txn, GroupId g, Author a) throws DbException;
/**
* Returns the group count for the given private group.
*/
GroupCount getGroupCount(Transaction txn, GroupId g) throws DbException;
/**
* Returns the group count for the given private group.
*/

View File

@@ -270,22 +270,31 @@ class PrivateGroupManagerImpl extends BdfIncomingMessageHook
}
@Override
public Collection<PrivateGroup> getPrivateGroups() throws DbException {
Collection<Group> groups;
Transaction txn = db.startTransaction(true);
public Collection<PrivateGroup> getPrivateGroups(Transaction txn)
throws DbException {
Collection<Group> groups = db.getGroups(txn, CLIENT_ID, MAJOR_VERSION);
Collection<PrivateGroup> privateGroups = new ArrayList<>(groups.size());
try {
groups = db.getGroups(txn, CLIENT_ID, MAJOR_VERSION);
db.commitTransaction(txn);
} finally {
db.endTransaction(txn);
}
try {
Collection<PrivateGroup> privateGroups =
new ArrayList<>(groups.size());
for (Group g : groups) {
privateGroups.add(privateGroupFactory.parsePrivateGroup(g));
}
return privateGroups;
} catch (FormatException e) {
throw new DbException(e);
}
return privateGroups;
}
@Override
public Collection<PrivateGroup> getPrivateGroups() throws DbException {
return db.transactionWithResult(true, this::getPrivateGroups);
}
@Override
public boolean isDissolved(Transaction txn, GroupId g) throws DbException {
try {
BdfDictionary meta =
clientHelper.getGroupMetadataAsDictionary(txn, g);
return meta.getBoolean(GROUP_KEY_DISSOLVED);
} catch (FormatException e) {
throw new DbException(e);
}
@@ -293,12 +302,7 @@ class PrivateGroupManagerImpl extends BdfIncomingMessageHook
@Override
public boolean isDissolved(GroupId g) throws DbException {
try {
BdfDictionary meta = clientHelper.getGroupMetadataAsDictionary(g);
return meta.getBoolean(GROUP_KEY_DISSOLVED);
} catch (FormatException e) {
throw new DbException(e);
}
return db.transactionWithResult(true, txn -> isDissolved(txn, g));
}
@Override
@@ -403,7 +407,8 @@ class PrivateGroupManagerImpl extends BdfIncomingMessageHook
PrivateGroup privateGroup = getPrivateGroup(txn, g);
for (Entry<Author, Visibility> m : authors.entrySet()) {
Author a = m.getKey();
AuthorInfo authorInfo = contactManager.getAuthorInfo(txn, a.getId());
AuthorInfo authorInfo =
contactManager.getAuthorInfo(txn, a.getId());
Status status = authorInfo.getStatus();
Visibility v = m.getValue();
ContactId c = null;
@@ -450,6 +455,12 @@ class PrivateGroupManagerImpl extends BdfIncomingMessageHook
return false;
}
@Override
public GroupCount getGroupCount(Transaction txn, GroupId g)
throws DbException {
return messageTracker.getGroupCount(txn, g);
}
@Override
public GroupCount getGroupCount(GroupId g) throws DbException {
return messageTracker.getGroupCount(g);