mirror of
https://code.briarproject.org/briar/briar.git
synced 2026-02-12 18:59:06 +01:00
Merge branch '1800-group-list-view-model' into 'master'
Using ListAdapter for PrivateGroupList See merge request briar/briar!1327
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ public class BlogPostFragment extends BasePostFragment implements BlogListener {
|
||||
|
||||
@Override
|
||||
public void onExceptionUi(DbException exception) {
|
||||
handleDbException(exception);
|
||||
handleException(exception);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -79,7 +79,7 @@ public class FeedPostFragment extends BasePostFragment {
|
||||
|
||||
@Override
|
||||
public void onExceptionUi(DbException exception) {
|
||||
handleDbException(exception);
|
||||
handleException(exception);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -133,7 +133,7 @@ public abstract class BaseContactSelectorFragment<I extends SelectableContactIte
|
||||
|
||||
@Override
|
||||
public void onExceptionUi(DbException exception) {
|
||||
handleDbException(exception);
|
||||
handleException(exception);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -156,7 +156,7 @@ public class ForumActivity extends
|
||||
|
||||
@Override
|
||||
public void onExceptionUi(DbException exception) {
|
||||
handleDbException(exception);
|
||||
handleException(exception);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ public class CreateGroupActivity extends BriarActivity
|
||||
|
||||
@Override
|
||||
public void onExceptionUi(DbException exception) {
|
||||
handleDbException(exception);
|
||||
handleException(exception);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ public class GroupInviteActivity extends ContactSelectorActivity
|
||||
@Override
|
||||
public void onExceptionUi(DbException exception) {
|
||||
setResult(RESULT_CANCELED);
|
||||
handleDbException(exception);
|
||||
handleException(exception);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
|
||||
@@ -124,7 +124,7 @@ public class GroupMemberListActivity extends BriarActivity
|
||||
|
||||
@Override
|
||||
public void onExceptionUi(DbException exception) {
|
||||
handleDbException(exception);
|
||||
handleException(exception);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -82,6 +82,10 @@ public interface AndroidNotificationManager {
|
||||
|
||||
void unblockNotification(GroupId g);
|
||||
|
||||
void blockAllGroupMessageNotifications();
|
||||
|
||||
void unblockAllGroupMessageNotifications();
|
||||
|
||||
void blockAllBlogPostNotifications();
|
||||
|
||||
void unblockAllBlogPostNotifications();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user