From ad20e5230a215d36e036bd88cc40dfd143e7f36f Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Mon, 11 Jan 2021 16:33:07 -0300 Subject: [PATCH 01/13] Allow blog posts to be loaded within one transaction --- .../briar/api/blog/BlogManager.java | 17 +++++++++++ .../briar/blog/BlogManagerImpl.java | 30 +++++++++++++++---- 2 files changed, 41 insertions(+), 6 deletions(-) diff --git a/briar-api/src/main/java/org/briarproject/briar/api/blog/BlogManager.java b/briar-api/src/main/java/org/briarproject/briar/api/blog/BlogManager.java index 78d4fc354..b2d76357e 100644 --- a/briar-api/src/main/java/org/briarproject/briar/api/blog/BlogManager.java +++ b/briar-api/src/main/java/org/briarproject/briar/api/blog/BlogManager.java @@ -10,6 +10,7 @@ import org.briarproject.bramble.api.sync.GroupId; import org.briarproject.bramble.api.sync.MessageId; import java.util.Collection; +import java.util.List; import javax.annotation.Nullable; @@ -98,6 +99,11 @@ public interface BlogManager { */ Collection getBlogs() throws DbException; + /** + * Returns the group IDs of all blogs to which the user subscribes. + */ + Collection getBlogIds(Transaction txn) throws DbException; + /** * Returns the header of the blog post with the given ID. */ @@ -108,11 +114,22 @@ public interface BlogManager { */ String getPostText(MessageId m) throws DbException; + /** + * Returns the text of the blog post with the given ID. + */ + String getPostText(Transaction txn, MessageId m) throws DbException; + /** * Returns the headers of all posts in the given blog. */ Collection getPostHeaders(GroupId g) throws DbException; + /** + * Returns the headers of all posts in the given blog. + */ + List getPostHeaders(Transaction txn, GroupId g) + throws DbException; + /** * Marks a blog post as read or unread. */ diff --git a/briar-core/src/main/java/org/briarproject/briar/blog/BlogManagerImpl.java b/briar-core/src/main/java/org/briarproject/briar/blog/BlogManagerImpl.java index cd6f2f428..157668e88 100644 --- a/briar-core/src/main/java/org/briarproject/briar/blog/BlogManagerImpl.java +++ b/briar-core/src/main/java/org/briarproject/briar/blog/BlogManagerImpl.java @@ -445,6 +445,14 @@ class BlogManagerImpl extends BdfIncomingMessageHook implements BlogManager, } } + @Override + public Collection getBlogIds(Transaction txn) throws DbException { + List groupIds = new ArrayList<>(); + Collection groups = db.getGroups(txn, CLIENT_ID, MAJOR_VERSION); + for (Group g : groups) groupIds.add(g.getId()); + return groupIds; + } + @Override public BlogPostHeader getPostHeader(GroupId g, MessageId m) throws DbException { @@ -471,6 +479,15 @@ class BlogManagerImpl extends BdfIncomingMessageHook implements BlogManager, } } + @Override + public String getPostText(Transaction txn, MessageId m) throws DbException { + try { + return getPostText(clientHelper.getMessageAsList(txn, m)); + } catch (FormatException e) { + throw new DbException(e); + } + } + private String getPostText(BdfList message) throws FormatException { MessageType type = MessageType.valueOf(message.getLong(0).intValue()); if (type == POST) { @@ -488,7 +505,12 @@ class BlogManagerImpl extends BdfIncomingMessageHook implements BlogManager, @Override public Collection getPostHeaders(GroupId g) throws DbException { + return db.transactionWithResult(true, txn -> getPostHeaders(txn, g)); + } + @Override + public List getPostHeaders(Transaction txn, GroupId g) + throws DbException { // Query for posts and comments only BdfDictionary query1 = BdfDictionary.of( new BdfEntry(KEY_TYPE, POST.getInt()) @@ -497,8 +519,7 @@ class BlogManagerImpl extends BdfIncomingMessageHook implements BlogManager, new BdfEntry(KEY_TYPE, COMMENT.getInt()) ); - Collection headers = new ArrayList<>(); - Transaction txn = db.startTransaction(true); + List headers = new ArrayList<>(); try { Map metadata1 = clientHelper.getMessageMetadataAsDictionary(txn, g, query1); @@ -528,13 +549,10 @@ class BlogManagerImpl extends BdfIncomingMessageHook implements BlogManager, entry.getKey(), meta, authorInfos); headers.add(h); } - db.commitTransaction(txn); - return headers; } catch (FormatException e) { throw new DbException(e); - } finally { - db.endTransaction(txn); } + return headers; } @Override From ab1ed0ff5a4b8603ead323681dee84b8b1e0ca27 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Mon, 11 Jan 2021 16:36:15 -0300 Subject: [PATCH 02/13] Turn FeedController into FeedViewModel --- .../briarproject/briar/android/AppModule.java | 2 + .../briar/android/blog/BaseController.java | 4 +- .../android/blog/BaseControllerImpl.java | 9 +- .../briar/android/blog/BaseViewModel.java | 219 ++++++++++++++++++ .../briar/android/blog/BlogController.java | 3 +- .../android/blog/BlogControllerImpl.java | 3 +- .../briar/android/blog/BlogFragment.java | 12 +- .../briar/android/blog/BlogModule.java | 18 +- .../briar/android/blog/BlogPostAdapter.java | 39 ++-- .../briar/android/blog/BlogPostItem.java | 5 +- .../briar/android/blog/FeedController.java | 32 --- .../android/blog/FeedControllerImpl.java | 143 ------------ .../briar/android/blog/FeedFragment.java | 196 +++++----------- .../briar/android/blog/FeedPostFragment.java | 44 ++-- .../briar/android/blog/FeedViewModel.java | 174 ++++++++++++++ .../briar/android/blog/ReblogFragment.java | 48 ++-- .../briar/android/viewmodel/DbViewModel.java | 18 ++ 17 files changed, 546 insertions(+), 423 deletions(-) create mode 100644 briar-android/src/main/java/org/briarproject/briar/android/blog/BaseViewModel.java delete mode 100644 briar-android/src/main/java/org/briarproject/briar/android/blog/FeedController.java delete mode 100644 briar-android/src/main/java/org/briarproject/briar/android/blog/FeedControllerImpl.java create mode 100644 briar-android/src/main/java/org/briarproject/briar/android/blog/FeedViewModel.java diff --git a/briar-android/src/main/java/org/briarproject/briar/android/AppModule.java b/briar-android/src/main/java/org/briarproject/briar/android/AppModule.java index 0729aa0f2..e0d4f31cc 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/AppModule.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/AppModule.java @@ -30,6 +30,7 @@ import org.briarproject.bramble.util.StringUtils; import org.briarproject.briar.android.account.DozeHelperModule; import org.briarproject.briar.android.account.LockManagerImpl; import org.briarproject.briar.android.account.SetupModule; +import org.briarproject.briar.android.blog.BlogModule; import org.briarproject.briar.android.contact.ContactListModule; import org.briarproject.briar.android.forum.ForumModule; import org.briarproject.briar.android.introduction.IntroductionModule; @@ -85,6 +86,7 @@ import static org.briarproject.briar.android.TestingConstants.IS_DEBUG_BUILD; ContactListModule.class, IntroductionModule.class, // below need to be within same scope as ViewModelProvider.Factory + BlogModule.BindsModule.class, ForumModule.class, GroupListModule.class, GroupConversationModule.class, diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/BaseController.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/BaseController.java index ad7bbdc23..2e6115ced 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/blog/BaseController.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/BaseController.java @@ -8,7 +8,7 @@ import org.briarproject.briar.android.controller.handler.ExceptionHandler; import org.briarproject.briar.android.controller.handler.ResultExceptionHandler; import org.briarproject.briar.api.blog.BlogPostHeader; -import java.util.Collection; +import java.util.List; import javax.annotation.Nullable; @@ -24,7 +24,7 @@ interface BaseController { void onStop(); void loadBlogPosts(GroupId g, - ResultExceptionHandler, DbException> handler); + ResultExceptionHandler, DbException> handler); void loadBlogPost(BlogPostHeader header, ResultExceptionHandler handler); diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/BaseControllerImpl.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/BaseControllerImpl.java index 036fe9ad2..13f5e5193 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/blog/BaseControllerImpl.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/BaseControllerImpl.java @@ -23,6 +23,7 @@ import org.briarproject.briar.util.HtmlUtils; import java.util.ArrayList; import java.util.Collection; +import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executor; @@ -80,10 +81,10 @@ abstract class BaseControllerImpl extends DbControllerImpl @Override public void loadBlogPosts(GroupId groupId, - ResultExceptionHandler, DbException> handler) { + ResultExceptionHandler, DbException> handler) { runOnDbThread(() -> { try { - Collection items = loadItems(groupId); + List items = loadItems(groupId); handler.onResult(items); } catch (DbException e) { logException(LOG, WARNING, e); @@ -92,12 +93,12 @@ abstract class BaseControllerImpl extends DbControllerImpl }); } - Collection loadItems(GroupId groupId) throws DbException { + List loadItems(GroupId groupId) throws DbException { long start = now(); Collection headers = blogManager.getPostHeaders(groupId); logDuration(LOG, "Loading headers", start); - Collection items = new ArrayList<>(headers.size()); + List items = new ArrayList<>(headers.size()); start = now(); for (BlogPostHeader h : headers) { headerCache.put(h.getId(), h); diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/BaseViewModel.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/BaseViewModel.java new file mode 100644 index 000000000..10e105b46 --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/BaseViewModel.java @@ -0,0 +1,219 @@ +package org.briarproject.briar.android.blog; + +import android.app.Application; + +import org.briarproject.bramble.api.db.DatabaseExecutor; +import org.briarproject.bramble.api.db.DbException; +import org.briarproject.bramble.api.db.NoSuchMessageException; +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.IdentityManager; +import org.briarproject.bramble.api.identity.LocalAuthor; +import org.briarproject.bramble.api.lifecycle.LifecycleManager; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.sync.GroupId; +import org.briarproject.bramble.api.sync.MessageId; +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.blog.Blog; +import org.briarproject.briar.api.blog.BlogCommentHeader; +import org.briarproject.briar.api.blog.BlogManager; +import org.briarproject.briar.api.blog.BlogPostHeader; +import org.briarproject.briar.util.HtmlUtils; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executor; +import java.util.logging.Logger; + +import javax.annotation.Nullable; +import javax.inject.Inject; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.Transformations; + +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; +import static org.briarproject.briar.util.HtmlUtils.ARTICLE; + +@NotNullByDefault +public class BaseViewModel extends DbViewModel implements EventListener { + + private static Logger LOG = getLogger(BaseViewModel.class.getName()); + + protected final TransactionManager db; + private final EventBus eventBus; + protected final IdentityManager identityManager; + protected final BlogManager blogManager; + + protected final MutableLiveData>> blogPosts = + new MutableLiveData<>(); + + // TODO do we still need those caches? + private final Map textCache = new ConcurrentHashMap<>(); + private final Map headerCache = + new ConcurrentHashMap<>(); + + @Inject + BaseViewModel(Application application, + @DatabaseExecutor Executor dbExecutor, + LifecycleManager lifecycleManager, + TransactionManager db, + AndroidExecutor androidExecutor, + EventBus eventBus, + IdentityManager identityManager, + BlogManager blogManager) { + super(application, dbExecutor, lifecycleManager, db, androidExecutor); + this.db = db; + this.eventBus = eventBus; + this.identityManager = identityManager; + this.blogManager = blogManager; + + eventBus.addListener(this); + } + + @Override + protected void onCleared() { + super.onCleared(); + eventBus.removeListener(this); + } + + @Override + public void eventOccurred(Event e) { + } + + void loadItems(GroupId groupId) { + loadList(txn -> loadBlogPosts(txn, groupId), blogPosts::setValue); + } + + @DatabaseExecutor + protected List loadBlogPosts(Transaction txn, GroupId groupId) + throws DbException { + long start = now(); + List headers = + blogManager.getPostHeaders(txn, groupId); + logDuration(LOG, "Loading headers", start); + List items = new ArrayList<>(headers.size()); + start = now(); + for (BlogPostHeader h : headers) { + headerCache.put(h.getId(), h); + BlogPostItem item = getItem(txn, h); + items.add(item); + } + logDuration(LOG, "Loading bodies", start); + return items; + } + + @DatabaseExecutor + protected BlogPostItem getItem(Transaction txn, BlogPostHeader h) + throws DbException { + String text; + if (h instanceof BlogCommentHeader) { + BlogCommentHeader c = (BlogCommentHeader) h; + BlogCommentItem item = new BlogCommentItem(c); + text = getPostText(txn, item.getPostHeader().getId()); + item.setText(text); + return item; + } else { + text = getPostText(txn, h.getId()); + return new BlogPostItem(h, text); + } + } + + @DatabaseExecutor + private String getPostText(Transaction txn, MessageId m) + throws DbException { + String text = textCache.get(m); + if (text == null) { + text = HtmlUtils.clean(blogManager.getPostText(txn, m), ARTICLE); + textCache.put(m, text); + } + return text; + } + + LiveData> loadBlogPost(GroupId g, MessageId m) { + MutableLiveData> result = + new MutableLiveData<>(); + runOnDbThread(() -> { + try { + long start = now(); + BlogPostHeader header1 = getPostHeader(g, m); + BlogPostItem item = db.transactionWithResult(true, txn -> + getItem(txn, header1) + ); + logDuration(LOG, "Loading post", start); + result.postValue(new LiveResult<>(item)); + } catch (DbException e) { + logException(LOG, WARNING, e); + result.postValue(new LiveResult<>(e)); + } + }); + return result; + } + + @DatabaseExecutor + private BlogPostHeader getPostHeader(GroupId g, MessageId m) + throws DbException { + BlogPostHeader header = headerCache.get(m); + if (header == null) { + header = blogManager.getPostHeader(g, m); + headerCache.put(m, header); + } + return header; + } + + void repeatPost(BlogPostItem item, @Nullable String comment) { + runOnDbThread(() -> { + try { + LocalAuthor a = identityManager.getLocalAuthor(); + Blog b = blogManager.getPersonalBlog(a); + BlogPostHeader h = item.getHeader(); + blogManager.addLocalComment(a, b.getId(), comment, h); + } catch (DbException e) { + logException(LOG, WARNING, e); + } + }); + } + + LiveData>> getAllBlogPosts() { + return blogPosts; + } + + LiveData>> getBlogPosts(GroupId g) { + return Transformations.map(blogPosts, result -> { + List allPosts = result.getResultOrNull(); + if (allPosts == null) return result; + List groupPosts = new ArrayList<>(); + for (BlogPostItem item : allPosts) { + if (item.getGroupId().equals(g)) groupPosts.add(item); + } + return new LiveResult<>(groupPosts); + }); + } + + LiveData> getBlogPost(MessageId m) { + return Transformations.map(blogPosts, result -> { + List allPosts = result.getResultOrNull(); + if (allPosts == null) { + Exception e = requireNonNull(result.getException()); + return new LiveResult<>(e); + } + for (BlogPostItem item : allPosts) { + if (item.getId().equals(m)) return new LiveResult<>(item); + } + return new LiveResult<>(new NoSuchMessageException()); + }); + } + +} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogController.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogController.java index 50f5b5b3d..7f4dba0b2 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogController.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogController.java @@ -8,6 +8,7 @@ import org.briarproject.bramble.api.sync.MessageId; import org.briarproject.briar.android.controller.handler.ResultExceptionHandler; import java.util.Collection; +import java.util.List; import androidx.annotation.UiThread; @@ -23,7 +24,7 @@ public interface BlogController extends BaseController { void unsetBlogSharingListener(BlogSharingListener listener); void loadBlogPosts( - ResultExceptionHandler, DbException> handler); + ResultExceptionHandler, DbException> handler); void loadBlogPost(MessageId m, ResultExceptionHandler handler); diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogControllerImpl.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogControllerImpl.java index 08f43d8bc..317a75134 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogControllerImpl.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogControllerImpl.java @@ -30,6 +30,7 @@ import org.briarproject.briar.api.sharing.event.ContactLeftShareableEvent; import java.util.ArrayList; import java.util.Collection; +import java.util.List; import java.util.concurrent.Executor; import java.util.logging.Logger; @@ -140,7 +141,7 @@ class BlogControllerImpl extends BaseControllerImpl @Override public void loadBlogPosts( - ResultExceptionHandler, DbException> handler) { + ResultExceptionHandler, DbException> handler) { if (groupId == null) throw new IllegalStateException(); loadBlogPosts(groupId, handler); } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogFragment.java index e138ec501..34c9034a3 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogFragment.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogFragment.java @@ -32,6 +32,7 @@ import org.briarproject.briar.android.view.BriarRecyclerView; import org.briarproject.briar.api.blog.BlogPostHeader; import java.util.Collection; +import java.util.List; import javax.inject.Inject; @@ -103,8 +104,7 @@ public class BlogFragment extends BaseFragment View v = inflater.inflate(R.layout.fragment_blog, container, false); - adapter = new BlogPostAdapter(requireActivity(), this, - getFragmentManager()); + adapter = new BlogPostAdapter(this, getParentFragmentManager()); list = v.findViewById(R.id.postList); layoutManager = new LinearLayoutManager(getActivity()); list.setLayoutManager(layoutManager); @@ -219,7 +219,7 @@ public class BlogFragment extends BaseFragment this) { @Override public void onResultUi(BlogPostItem post) { - adapter.add(post); +// adapter.add(post); if (local) { list.scrollToPosition(0); displaySnackbar(R.string.blogs_blog_post_created, @@ -258,14 +258,14 @@ public class BlogFragment extends BaseFragment private void loadBlogPosts(boolean reload) { blogController.loadBlogPosts( - new UiResultExceptionHandler, + new UiResultExceptionHandler, DbException>(this) { @Override - public void onResultUi(Collection posts) { + public void onResultUi(List posts) { if (posts.isEmpty()) { list.showData(); } else { - adapter.addAll(posts); + adapter.submitList(posts); if (reload || layoutManagerState == null) { list.scrollToPosition(0); } else { diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogModule.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogModule.java index e2db1a7cf..9ab9ff681 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogModule.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogModule.java @@ -4,13 +4,25 @@ import org.briarproject.briar.android.activity.ActivityScope; import org.briarproject.briar.android.activity.BaseActivity; import org.briarproject.briar.android.controller.SharingController; import org.briarproject.briar.android.controller.SharingControllerImpl; +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 BlogModule { + @Module + public abstract static class BindsModule { + @Binds + @IntoMap + @ViewModelKey(FeedViewModel.class) + abstract ViewModel bindFeedViewModel(FeedViewModel feedViewModel); + } + @ActivityScope @Provides BlogController provideBlogController(BaseActivity activity, @@ -19,12 +31,6 @@ public class BlogModule { return blogController; } - @ActivityScope - @Provides - FeedController provideFeedController(FeedControllerImpl feedController) { - return feedController; - } - @ActivityScope @Provides SharingController provideSharingController( diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogPostAdapter.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogPostAdapter.java index edddb09c9..53a8b301f 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogPostAdapter.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogPostAdapter.java @@ -1,6 +1,5 @@ package org.briarproject.briar.android.blog; -import android.content.Context; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -8,22 +7,33 @@ import android.view.ViewGroup; import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault; import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; import org.briarproject.briar.R; -import org.briarproject.briar.android.util.BriarAdapter; import androidx.annotation.Nullable; import androidx.fragment.app.FragmentManager; +import androidx.recyclerview.widget.DiffUtil; +import androidx.recyclerview.widget.ListAdapter; @MethodsNotNullByDefault @ParametersNotNullByDefault -class BlogPostAdapter extends BriarAdapter { +class BlogPostAdapter extends ListAdapter { private final OnBlogPostClickListener listener; @Nullable private final FragmentManager fragmentManager; - BlogPostAdapter(Context ctx, OnBlogPostClickListener listener, + BlogPostAdapter(OnBlogPostClickListener listener, @Nullable FragmentManager fragmentManager) { - super(ctx, BlogPostItem.class); + super(new DiffUtil.ItemCallback() { + @Override + public boolean areItemsTheSame(BlogPostItem a, BlogPostItem b) { + return a.getId().equals(b.getId()); + } + + @Override + public boolean areContentsTheSame(BlogPostItem a, BlogPostItem b) { + return a.isRead() == b.isRead(); + } + }); this.listener = listener; this.fragmentManager = fragmentManager; } @@ -31,29 +41,14 @@ class BlogPostAdapter extends BriarAdapter { @Override public BlogPostViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { - View v = LayoutInflater.from(ctx).inflate( + View v = LayoutInflater.from(parent.getContext()).inflate( R.layout.list_item_blog_post, parent, false); return new BlogPostViewHolder(v, false, listener, fragmentManager); } @Override public void onBindViewHolder(BlogPostViewHolder ui, int position) { - ui.bindItem(getItemAt(position)); - } - - @Override - public int compare(BlogPostItem a, BlogPostItem b) { - return a.compareTo(b); - } - - @Override - public boolean areContentsTheSame(BlogPostItem a, BlogPostItem b) { - return a.isRead() == b.isRead(); - } - - @Override - public boolean areItemsTheSame(BlogPostItem a, BlogPostItem b) { - return a.getId().equals(b.getId()); + ui.bindItem(getItem(position)); } } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogPostItem.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogPostItem.java index 7afaa77b4..3247a083c 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogPostItem.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogPostItem.java @@ -74,9 +74,6 @@ public class BlogPostItem implements Comparable { protected static int compare(BlogPostHeader h1, BlogPostHeader h2) { // The newest post comes first - long aTime = h1.getTimeReceived(), bTime = h2.getTimeReceived(); - if (aTime > bTime) return -1; - if (aTime < bTime) return 1; - return 0; + return Long.compare(h2.getTimeReceived(), h1.getTimeReceived()); } } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/FeedController.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/FeedController.java deleted file mode 100644 index e50b3ba17..000000000 --- a/briar-android/src/main/java/org/briarproject/briar/android/blog/FeedController.java +++ /dev/null @@ -1,32 +0,0 @@ -package org.briarproject.briar.android.blog; - -import org.briarproject.bramble.api.db.DbException; -import org.briarproject.bramble.api.nullsafety.NotNullByDefault; -import org.briarproject.briar.android.controller.handler.ResultExceptionHandler; -import org.briarproject.briar.api.blog.Blog; - -import java.util.Collection; - -import androidx.annotation.UiThread; - -@NotNullByDefault -public interface FeedController extends BaseController { - - void loadBlogPosts( - ResultExceptionHandler, DbException> handler); - - void loadPersonalBlog(ResultExceptionHandler handler); - - @UiThread - void setFeedListener(FeedListener listener); - - @UiThread - void unsetFeedListener(FeedListener listener); - - @NotNullByDefault - interface FeedListener extends BlogListener { - - @UiThread - void onBlogAdded(); - } -} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/FeedControllerImpl.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/FeedControllerImpl.java deleted file mode 100644 index 5a228891a..000000000 --- a/briar-android/src/main/java/org/briarproject/briar/android/blog/FeedControllerImpl.java +++ /dev/null @@ -1,143 +0,0 @@ -package org.briarproject.briar.android.blog; - -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.NoSuchMessageException; -import org.briarproject.bramble.api.event.Event; -import org.briarproject.bramble.api.event.EventBus; -import org.briarproject.bramble.api.identity.Author; -import org.briarproject.bramble.api.identity.IdentityManager; -import org.briarproject.bramble.api.lifecycle.LifecycleManager; -import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault; -import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; -import org.briarproject.bramble.api.sync.event.GroupAddedEvent; -import org.briarproject.bramble.api.sync.event.GroupRemovedEvent; -import org.briarproject.briar.android.controller.handler.ResultExceptionHandler; -import org.briarproject.briar.api.android.AndroidNotificationManager; -import org.briarproject.briar.api.blog.Blog; -import org.briarproject.briar.api.blog.BlogManager; -import org.briarproject.briar.api.blog.event.BlogPostAddedEvent; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.concurrent.Executor; -import java.util.logging.Logger; - -import javax.inject.Inject; - -import androidx.annotation.Nullable; - -import static java.util.logging.Level.WARNING; -import static org.briarproject.bramble.util.LogUtils.logDuration; -import static org.briarproject.bramble.util.LogUtils.logException; -import static org.briarproject.bramble.util.LogUtils.now; -import static org.briarproject.briar.api.blog.BlogManager.CLIENT_ID; - -@MethodsNotNullByDefault -@ParametersNotNullByDefault -class FeedControllerImpl extends BaseControllerImpl implements FeedController { - - private static final Logger LOG = - Logger.getLogger(FeedControllerImpl.class.getName()); - - // UI thread - @Nullable - private FeedListener listener; - - @Inject - FeedControllerImpl(@DatabaseExecutor Executor dbExecutor, - LifecycleManager lifecycleManager, EventBus eventBus, - AndroidNotificationManager notificationManager, - IdentityManager identityManager, BlogManager blogManager) { - super(dbExecutor, lifecycleManager, eventBus, notificationManager, - identityManager, blogManager); - } - - @Override - public void onStart() { - super.onStart(); - if (listener == null) throw new IllegalStateException(); - notificationManager.blockAllBlogPostNotifications(); - notificationManager.clearAllBlogPostNotifications(); - } - - @Override - public void onStop() { - super.onStop(); - notificationManager.unblockAllBlogPostNotifications(); - } - - @Override - public void setFeedListener(FeedListener listener) { - this.listener = listener; - } - - @Override - public void unsetFeedListener(FeedListener listener) { - if (this.listener == listener) this.listener = null; - } - - @Override - public void eventOccurred(Event e) { - if (listener == null) throw new IllegalStateException(); - if (e instanceof BlogPostAddedEvent) { - BlogPostAddedEvent b = (BlogPostAddedEvent) e; - LOG.info("Blog post added"); - listener.onBlogPostAdded(b.getHeader(), b.isLocal()); - } else if (e instanceof GroupAddedEvent) { - GroupAddedEvent g = (GroupAddedEvent) e; - if (g.getGroup().getClientId().equals(CLIENT_ID)) { - LOG.info("Blog added"); - listener.onBlogAdded(); - } - } else if (e instanceof GroupRemovedEvent) { - GroupRemovedEvent g = (GroupRemovedEvent) e; - if (g.getGroup().getClientId().equals(CLIENT_ID)) { - LOG.info("Blog removed"); - listener.onBlogRemoved(); - } - } - } - - @Override - public void loadBlogPosts( - ResultExceptionHandler, DbException> handler) { - runOnDbThread(() -> { - try { - long start = now(); - Collection posts = new ArrayList<>(); - for (Blog b : blogManager.getBlogs()) { - try { - posts.addAll(loadItems(b.getId())); - } catch (NoSuchGroupException | NoSuchMessageException e) { - logException(LOG, WARNING, e); - } - } - logDuration(LOG, "Loading all posts", start); - handler.onResult(posts); - } catch (DbException e) { - logException(LOG, WARNING, e); - handler.onException(e); - } - }); - } - - @Override - public void loadPersonalBlog( - ResultExceptionHandler handler) { - runOnDbThread(() -> { - try { - long start = now(); - Author a = identityManager.getLocalAuthor(); - Blog b = blogManager.getPersonalBlog(a); - logDuration(LOG, "Loading personal blog", start); - handler.onResult(b); - } catch (DbException e) { - logException(LOG, WARNING, e); - handler.onException(e); - } - }); - } - -} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/FeedFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/FeedFragment.java index 75026a034..97cca5ca6 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/blog/FeedFragment.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/FeedFragment.java @@ -10,52 +10,44 @@ import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; -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; import org.briarproject.briar.android.activity.ActivityComponent; -import org.briarproject.briar.android.blog.FeedController.FeedListener; -import org.briarproject.briar.android.controller.handler.UiResultExceptionHandler; import org.briarproject.briar.android.fragment.BaseFragment; import org.briarproject.briar.android.util.BriarSnackbarBuilder; import org.briarproject.briar.android.view.BriarRecyclerView; import org.briarproject.briar.api.blog.Blog; -import org.briarproject.briar.api.blog.BlogPostHeader; -import java.util.Collection; -import java.util.logging.Logger; +import java.util.List; import javax.inject.Inject; import androidx.annotation.Nullable; import androidx.annotation.UiThread; +import androidx.lifecycle.ViewModelProvider; import androidx.recyclerview.widget.LinearLayoutManager; -import static android.app.Activity.RESULT_OK; import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP; import static com.google.android.material.snackbar.Snackbar.LENGTH_LONG; import static org.briarproject.briar.android.activity.BriarActivity.GROUP_ID; -import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_WRITE_BLOG_POST; @UiThread @MethodsNotNullByDefault @ParametersNotNullByDefault -public class FeedFragment extends BaseFragment implements - OnBlogPostClickListener, FeedListener { +public class FeedFragment extends BaseFragment + implements OnBlogPostClickListener { public final static String TAG = FeedFragment.class.getName(); - private static final Logger LOG = Logger.getLogger(TAG); @Inject - FeedController feedController; + ViewModelProvider.Factory viewModelFactory; + private FeedViewModel viewModel; private BlogPostAdapter adapter; private LinearLayoutManager layoutManager; private BriarRecyclerView list; @Nullable - private Blog personalBlog; - @Nullable private Parcelable layoutManagerState; public static FeedFragment newInstance() { @@ -70,7 +62,12 @@ public class FeedFragment extends BaseFragment implements @Override public void injectFragment(ActivityComponent component) { component.inject(this); - feedController.setFeedListener(this); + // TODO don't use NavDrawerActivity scope here + viewModel = new ViewModelProvider(requireActivity(), viewModelFactory) + .get(FeedViewModel.class); + // TODO ideally we only do this once when the ViewModel gets created + viewModel.loadPersonalBlog(); + viewModel.loadAllBlogPosts(); } @Nullable @@ -82,8 +79,7 @@ public class FeedFragment extends BaseFragment implements View v = inflater.inflate(R.layout.fragment_blog, container, false); - adapter = - new BlogPostAdapter(getActivity(), this, getFragmentManager()); + adapter = new BlogPostAdapter(this, getParentFragmentManager()); layoutManager = new LinearLayoutManager(getActivity()); list = v.findViewById(R.id.postList); @@ -93,6 +89,12 @@ public class FeedFragment extends BaseFragment implements list.setEmptyText(R.string.blogs_feed_empty_state); list.setEmptyAction(R.string.blogs_feed_empty_state_action); + viewModel.getAllBlogPosts().observe(getViewLifecycleOwner(), result -> + result + .onError(this::handleException) + .onSuccess(this::onBlogPostsLoaded) + ); + if (savedInstanceState != null) { layoutManagerState = savedInstanceState.getParcelable("layoutManager"); @@ -101,40 +103,19 @@ public class FeedFragment extends BaseFragment implements return v; } - @Override - public void onActivityResult(int requestCode, int resultCode, - @Nullable Intent data) { - super.onActivityResult(requestCode, resultCode, data); - - // The BlogPostAddedEvent arrives when the controller is not listening - if (requestCode == REQUEST_WRITE_BLOG_POST && resultCode == RESULT_OK) { - showSnackBar(R.string.blogs_blog_post_created); - } - } - @Override public void onStart() { super.onStart(); - feedController.onStart(); + viewModel.blockAllBlogPostNotifications(); + viewModel.clearAllBlogPostNotifications(); list.startPeriodicUpdate(); - loadPersonalBlog(); - loadBlogPosts(false); } @Override public void onStop() { super.onStop(); - feedController.onStop(); - adapter.clear(); - list.showProgressBar(); + viewModel.unblockAllBlogPostNotifications(); list.stopPeriodicUpdate(); - // TODO save list position in database/preferences? - } - - @Override - public void onDestroy() { - super.onDestroy(); - feedController.unsetFeedListener(this); } @Override @@ -146,50 +127,16 @@ public class FeedFragment extends BaseFragment implements } } - private void loadPersonalBlog() { - feedController.loadPersonalBlog( - new UiResultExceptionHandler(this) { - @Override - public void onResultUi(Blog b) { - personalBlog = b; - } - - @Override - public void onExceptionUi(DbException exception) { - handleException(exception); - } - }); - } - - private void loadBlogPosts(boolean clear) { - int revision = adapter.getRevision(); - feedController.loadBlogPosts( - new UiResultExceptionHandler, DbException>( - this) { - @Override - public void onResultUi(Collection posts) { - if (revision == adapter.getRevision()) { - adapter.incrementRevision(); - if (clear) adapter.setItems(posts); - else adapter.addAll(posts); - if (posts.isEmpty()) list.showData(); - if (layoutManagerState == null) { - list.scrollToPosition(0); // Scroll to the top - } else { - layoutManager.onRestoreInstanceState( - layoutManagerState); - } - } else { - LOG.info("Concurrent update, reloading"); - loadBlogPosts(clear); - } - } - - @Override - public void onExceptionUi(DbException exception) { - handleException(exception); - } - }); + private void onBlogPostsLoaded(List items) { + if (items.isEmpty()) list.showData(); + else adapter.submitList(items, () -> { + Boolean wasLocal = viewModel.getPostAddedWasLocalAndReset(); + if (wasLocal != null && wasLocal) { + showSnackBar(R.string.blogs_blog_post_created); + } else if (wasLocal != null) { + showSnackBar(R.string.blogs_blog_post_received); + } + }); } @Override @@ -200,52 +147,27 @@ public class FeedFragment extends BaseFragment implements @Override public boolean onOptionsItemSelected(MenuItem item) { - if (personalBlog == null) return false; - switch (item.getItemId()) { - case R.id.action_write_blog_post: - Intent i1 = - new Intent(getActivity(), WriteBlogPostActivity.class); - i1.putExtra(GROUP_ID, personalBlog.getId().getBytes()); - startActivityForResult(i1, REQUEST_WRITE_BLOG_POST); - return true; - case R.id.action_rss_feeds_import: - Intent i2 = - new Intent(getActivity(), RssFeedImportActivity.class); - startActivity(i2); - return true; - case R.id.action_rss_feeds_manage: - Intent i3 = - new Intent(getActivity(), RssFeedManageActivity.class); - i3.putExtra(GROUP_ID, personalBlog.getId().getBytes()); - startActivity(i3); - return true; - default: - return super.onOptionsItemSelected(item); + int itemId = item.getItemId(); + if (itemId == R.id.action_write_blog_post) { + Blog personalBlog = viewModel.getPersonalBlog().getValue(); + if (personalBlog == null) return false; + Intent i = new Intent(getActivity(), WriteBlogPostActivity.class); + i.putExtra(GROUP_ID, personalBlog.getId().getBytes()); + startActivity(i); + return true; + } else if (itemId == R.id.action_rss_feeds_import) { + Intent i = new Intent(getActivity(), RssFeedImportActivity.class); + startActivity(i); + return true; + } else if (itemId == R.id.action_rss_feeds_manage) { + Blog personalBlog = viewModel.getPersonalBlog().getValue(); + if (personalBlog == null) return false; + Intent i = new Intent(getActivity(), RssFeedManageActivity.class); + i.putExtra(GROUP_ID, personalBlog.getId().getBytes()); + startActivity(i); + return true; } - } - - @Override - public void onBlogPostAdded(BlogPostHeader header, boolean local) { - feedController.loadBlogPost(header, - new UiResultExceptionHandler( - this) { - @Override - public void onResultUi(BlogPostItem post) { - adapter.incrementRevision(); - adapter.add(post); - if (local) { - showSnackBar(R.string.blogs_blog_post_created); - } else { - showSnackBar(R.string.blogs_blog_post_received); - } - } - - @Override - public void onExceptionUi(DbException exception) { - handleException(exception); - } - } - ); + return super.onOptionsItemSelected(item); } @Override @@ -257,10 +179,10 @@ public class FeedFragment extends BaseFragment implements @Override public void onAuthorClick(BlogPostItem post) { - Intent i = new Intent(getContext(), BlogActivity.class); + Intent i = new Intent(requireContext(), BlogActivity.class); i.putExtra(GROUP_ID, post.getGroupId().getBytes()); i.setFlags(FLAG_ACTIVITY_CLEAR_TOP); - getContext().startActivity(i); + requireContext().startActivity(i); } @Override @@ -283,14 +205,4 @@ public class FeedFragment extends BaseFragment implements sb.make(list, stringRes, LENGTH_LONG).show(); } - @Override - public void onBlogAdded() { - loadBlogPosts(false); - } - - @Override - public void onBlogRemoved() { - loadBlogPosts(true); - } - } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/FeedPostFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/FeedPostFragment.java index b9750a5a1..6e90bb6d7 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/blog/FeedPostFragment.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/FeedPostFragment.java @@ -5,19 +5,19 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -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.bramble.api.sync.MessageId; import org.briarproject.briar.android.activity.ActivityComponent; -import org.briarproject.briar.android.controller.handler.UiResultExceptionHandler; import javax.annotation.Nullable; import javax.inject.Inject; import androidx.annotation.UiThread; +import androidx.lifecycle.ViewModelProvider; +import static java.util.Objects.requireNonNull; import static org.briarproject.briar.android.activity.BriarActivity.GROUP_ID; @UiThread @@ -27,18 +27,16 @@ public class FeedPostFragment extends BasePostFragment { private static final String TAG = FeedPostFragment.class.getName(); - private GroupId blogId; - @Inject - FeedController feedController; + ViewModelProvider.Factory viewModelFactory; + + private FeedViewModel viewModel; static FeedPostFragment newInstance(GroupId blogId, MessageId postId) { FeedPostFragment f = new FeedPostFragment(); - Bundle bundle = new Bundle(); bundle.putByteArray(GROUP_ID, blogId.getBytes()); bundle.putByteArray(POST_ID, postId.getBytes()); - f.setArguments(bundle); return f; } @@ -46,6 +44,8 @@ public class FeedPostFragment extends BasePostFragment { @Override public void injectFragment(ActivityComponent component) { component.inject(this); + viewModel = new ViewModelProvider(requireActivity(), viewModelFactory) + .get(FeedViewModel.class); } @Nullable @@ -54,10 +54,14 @@ public class FeedPostFragment extends BasePostFragment { @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { Bundle args = requireArguments(); - byte[] b = args.getByteArray(GROUP_ID); - if (b == null) throw new IllegalStateException("No group ID in args"); - blogId = new GroupId(b); - + GroupId groupId = + new GroupId(requireNonNull(args.getByteArray(GROUP_ID))); + MessageId postId = + new MessageId(requireNonNull(args.getByteArray(POST_ID))); + viewModel.loadBlogPost(groupId, postId).observe(getViewLifecycleOwner(), + result -> result.onError(this::handleException) + .onSuccess(this::onBlogPostLoaded) + ); return super.onCreateView(inflater, container, savedInstanceState); } @@ -66,22 +70,4 @@ public class FeedPostFragment extends BasePostFragment { return TAG; } - @Override - public void onStart() { - super.onStart(); - feedController.loadBlogPost(blogId, postId, - new UiResultExceptionHandler( - this) { - @Override - public void onResultUi(BlogPostItem post) { - onBlogPostLoaded(post); - } - - @Override - public void onExceptionUi(DbException exception) { - handleException(exception); - } - }); - } - } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/FeedViewModel.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/FeedViewModel.java new file mode 100644 index 000000000..6f5f44f44 --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/FeedViewModel.java @@ -0,0 +1,174 @@ +package org.briarproject.briar.android.blog; + +import android.app.Application; + +import org.briarproject.bramble.api.db.DatabaseExecutor; +import org.briarproject.bramble.api.db.DbException; +import org.briarproject.bramble.api.db.NoSuchMessageException; +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.identity.Author; +import org.briarproject.bramble.api.identity.IdentityManager; +import org.briarproject.bramble.api.lifecycle.LifecycleManager; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +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.bramble.api.system.AndroidExecutor; +import org.briarproject.briar.android.viewmodel.LiveResult; +import org.briarproject.briar.api.android.AndroidNotificationManager; +import org.briarproject.briar.api.blog.Blog; +import org.briarproject.briar.api.blog.BlogManager; +import org.briarproject.briar.api.blog.BlogPostHeader; +import org.briarproject.briar.api.blog.event.BlogPostAddedEvent; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.Executor; +import java.util.logging.Logger; + +import javax.annotation.Nullable; +import javax.inject.Inject; + +import androidx.annotation.UiThread; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; + +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; +import static org.briarproject.briar.api.blog.BlogManager.CLIENT_ID; + +@NotNullByDefault +class FeedViewModel extends BaseViewModel { + + private static final Logger LOG = getLogger(FeedViewModel.class.getName()); + + protected final AndroidNotificationManager notificationManager; + + private final MutableLiveData personalBlog = new MutableLiveData<>(); + @Nullable + private Boolean postAddedWasLocal = null; + + @Inject + FeedViewModel(Application application, + @DatabaseExecutor Executor dbExecutor, + LifecycleManager lifecycleManager, + TransactionManager db, + AndroidExecutor androidExecutor, + EventBus eventBus, + IdentityManager identityManager, + BlogManager blogManager, + AndroidNotificationManager notificationManager) { + super(application, dbExecutor, lifecycleManager, db, androidExecutor, + eventBus, identityManager, blogManager); + this.notificationManager = notificationManager; + } + + @Override + public void eventOccurred(Event e) { + if (e instanceof BlogPostAddedEvent) { + BlogPostAddedEvent b = (BlogPostAddedEvent) e; + LOG.info("Blog post added"); + onBlogPostAdded(b.getHeader(), b.isLocal()); + } else if (e instanceof GroupAddedEvent) { + GroupAddedEvent g = (GroupAddedEvent) e; + if (g.getGroup().getClientId().equals(CLIENT_ID)) { + LOG.info("Blog added"); + loadAllBlogPosts(); + } + } else if (e instanceof GroupRemovedEvent) { + GroupRemovedEvent g = (GroupRemovedEvent) e; + if (g.getGroup().getClientId().equals(CLIENT_ID)) { + LOG.info("Blog removed"); + loadAllBlogPosts(); + } + } + } + + void blockAllBlogPostNotifications() { + notificationManager.blockAllBlogPostNotifications(); + } + + void clearAllBlogPostNotifications() { + notificationManager.clearAllBlogPostNotifications(); + } + + void unblockAllBlogPostNotifications() { + notificationManager.unblockAllBlogPostNotifications(); + } + + void loadPersonalBlog() { + runOnDbThread(() -> { + try { + long start = now(); + Author a = identityManager.getLocalAuthor(); + Blog b = blogManager.getPersonalBlog(a); + logDuration(LOG, "Loading personal blog", start); + personalBlog.postValue(b); + } catch (DbException e) { + logException(LOG, WARNING, e); + } + }); + } + + LiveData getPersonalBlog() { + return personalBlog; + } + + void loadAllBlogPosts() { + loadList(this::loadAllBlogPosts, blogPosts::setValue); + } + + @DatabaseExecutor + private List loadAllBlogPosts(Transaction txn) + throws DbException { + long start = now(); + List posts = new ArrayList<>(); + for (GroupId g : blogManager.getBlogIds(txn)) { + try { + posts.addAll(loadBlogPosts(txn, g)); + } catch (NoSuchMessageException e) { + logException(LOG, WARNING, e); + } + } + Collections.sort(posts); + logDuration(LOG, "Loading all posts", start); + return posts; + } + + private void onBlogPostAdded(BlogPostHeader header, boolean local) { + postAddedWasLocal = local; + runOnDbThread(() -> { + try { + db.transaction(true, txn -> { + BlogPostItem item = getItem(txn, header); + txn.attach(() -> { + List items = addListItem(blogPosts, item); + if (items != null) { + Collections.sort(items); + blogPosts.setValue(new LiveResult<>(items)); + } + }); + }); + } catch (DbException e) { + logException(LOG, WARNING, e); + } + }); + } + + @UiThread + @Nullable + Boolean getPostAddedWasLocalAndReset() { + if (postAddedWasLocal == null) return null; + boolean wasLocal = postAddedWasLocal; + postAddedWasLocal = null; + return wasLocal; + } + +} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/ReblogFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/ReblogFragment.java index 318bdecab..a4aecd098 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/blog/ReblogFragment.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/ReblogFragment.java @@ -7,15 +7,12 @@ import android.view.ViewGroup; import android.widget.ProgressBar; import android.widget.ScrollView; -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.bramble.api.sync.MessageId; 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.view.TextInputView; import org.briarproject.briar.android.view.TextSendController; @@ -27,6 +24,8 @@ import java.util.List; import javax.annotation.Nullable; import javax.inject.Inject; +import androidx.lifecycle.ViewModelProvider; + import static android.view.View.FOCUS_DOWN; import static android.view.View.GONE; import static android.view.View.INVISIBLE; @@ -42,12 +41,13 @@ public class ReblogFragment extends BaseFragment implements SendListener { public static final String TAG = ReblogFragment.class.getName(); + @Inject + ViewModelProvider.Factory viewModelFactory; + + private FeedViewModel viewModel; private ViewHolder ui; private BlogPostItem item; - @Inject - FeedController feedController; - static ReblogFragment newInstance(GroupId groupId, MessageId messageId) { ReblogFragment f = new ReblogFragment(); @@ -67,6 +67,8 @@ public class ReblogFragment extends BaseFragment implements SendListener { @Override public void injectFragment(ActivityComponent component) { component.inject(this); + viewModel = new ViewModelProvider(requireActivity(), viewModelFactory) + .get(FeedViewModel.class); } @Override @@ -90,30 +92,20 @@ public class ReblogFragment extends BaseFragment implements SendListener { ui.input.setMaxTextLength(MAX_BLOG_POST_TEXT_LENGTH); showProgressBar(); - feedController.loadBlogPost(blogId, postId, - new UiResultExceptionHandler( - this) { - @Override - public void onResultUi(BlogPostItem result) { - item = result; - bindViewHolder(); - } - - @Override - public void onExceptionUi(DbException exception) { - handleException(exception); - } - }); + viewModel.loadBlogPost(blogId, postId).observe(getViewLifecycleOwner(), + result -> result.onError(this::handleException) + .onSuccess(this::bindViewHolder) + ); return v; } - private void bindViewHolder() { - if (item == null) return; + private void bindViewHolder(BlogPostItem item) { + this.item = item; hideProgressBar(); - ui.post.bindItem(item); + ui.post.bindItem(this.item); ui.post.hideReblogButton(); ui.input.setReady(true); @@ -124,13 +116,7 @@ public class ReblogFragment extends BaseFragment implements SendListener { public void onSendClick(@Nullable String text, List headers) { ui.input.hideSoftKeyboard(); - feedController.repeatPost(item, text, - new UiExceptionHandler(this) { - @Override - public void onExceptionUi(DbException exception) { - handleException(exception); - } - }); + viewModel.repeatPost(item, text); finish(); } @@ -165,7 +151,7 @@ public class ReblogFragment extends BaseFragment implements SendListener { public void onAuthorClick(BlogPostItem post) { // probably don't want to allow author clicks here } - }, getFragmentManager()); + }, getParentFragmentManager()); input = v.findViewById(R.id.inputText); } } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/viewmodel/DbViewModel.java b/briar-android/src/main/java/org/briarproject/briar/android/viewmodel/DbViewModel.java index 7d3875723..a0aa00c8d 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/viewmodel/DbViewModel.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/viewmodel/DbViewModel.java @@ -142,6 +142,24 @@ public abstract class DbViewModel extends AndroidViewModel { void accept(T t); } + /** + * Creates a copy of the list available in the given LiveData + * and adds the given item to the copy. + * + * @return a copy of the list in the LiveData with item added or null when + *
    + *
  • LiveData does not have a value + *
  • LiveResult in the LiveData has an error + *
+ */ + @Nullable + protected List addListItem(LiveData>> liveData, T item) { + List items = getListCopy(liveData); + if (items == null) return null; + items.add(item); + return items; + } + /** * Creates a copy of the list available in the given LiveData * and replaces items where the given test function returns true. From b678de752914aee2389921c94414f41f961822bb Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Mon, 11 Jan 2021 16:46:10 -0300 Subject: [PATCH 03/13] Make BlogAdapter final and don't pass in a FragmentManager --- .../briar/android/blog/BasePostFragment.java | 9 ++++++++- .../briar/android/blog/BlogFragment.java | 10 ++++++++-- .../briar/android/blog/BlogPostAdapter.java | 10 ++-------- .../briar/android/blog/BlogPostViewHolder.java | 13 ++----------- .../briar/android/blog/FeedFragment.java | 11 ++++++++--- .../briar/android/blog/OnBlogPostClickListener.java | 2 ++ .../briar/android/blog/ReblogFragment.java | 9 ++++++++- .../briarproject/briar/android/util/UiUtils.java | 9 +++------ 8 files changed, 41 insertions(+), 32 deletions(-) diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/BasePostFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/BasePostFragment.java index 01d40619e..d09f63a5f 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/blog/BasePostFragment.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/BasePostFragment.java @@ -14,6 +14,7 @@ import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; import org.briarproject.bramble.api.sync.MessageId; import org.briarproject.briar.R; import org.briarproject.briar.android.fragment.BaseFragment; +import org.briarproject.briar.android.widget.LinkDialogFragment; import java.util.logging.Logger; @@ -76,7 +77,13 @@ abstract class BasePostFragment extends BaseFragment { i.setFlags(FLAG_ACTIVITY_CLEAR_TOP); getContext().startActivity(i); } - }, getFragmentManager()); + + @Override + public void onLinkClick(String url) { + LinkDialogFragment f = LinkDialogFragment.newInstance(url); + f.show(getParentFragmentManager(), f.getUniqueTag()); + } + }); return view; } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogFragment.java index 34c9034a3..a6ead22c1 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogFragment.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogFragment.java @@ -29,6 +29,7 @@ import org.briarproject.briar.android.sharing.BlogSharingStatusActivity; import org.briarproject.briar.android.sharing.ShareBlogActivity; import org.briarproject.briar.android.util.BriarSnackbarBuilder; import org.briarproject.briar.android.view.BriarRecyclerView; +import org.briarproject.briar.android.widget.LinkDialogFragment; import org.briarproject.briar.api.blog.BlogPostHeader; import java.util.Collection; @@ -69,7 +70,7 @@ public class BlogFragment extends BaseFragment private Parcelable layoutManagerState; private GroupId groupId; - private BlogPostAdapter adapter; + private final BlogPostAdapter adapter = new BlogPostAdapter(this); private LayoutManager layoutManager; private BriarRecyclerView list; private MenuItem writeButton, deleteButton; @@ -104,7 +105,6 @@ public class BlogFragment extends BaseFragment View v = inflater.inflate(R.layout.fragment_blog, container, false); - adapter = new BlogPostAdapter(this, getParentFragmentManager()); list = v.findViewById(R.id.postList); layoutManager = new LinearLayoutManager(getActivity()); list.setLayoutManager(layoutManager); @@ -256,6 +256,12 @@ public class BlogFragment extends BaseFragment getContext().startActivity(i); } + @Override + public void onLinkClick(String url) { + LinkDialogFragment f = LinkDialogFragment.newInstance(url); + f.show(getParentFragmentManager(), f.getUniqueTag()); + } + private void loadBlogPosts(boolean reload) { blogController.loadBlogPosts( new UiResultExceptionHandler, diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogPostAdapter.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogPostAdapter.java index 53a8b301f..00d051e39 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogPostAdapter.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogPostAdapter.java @@ -8,8 +8,6 @@ import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault; import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; import org.briarproject.briar.R; -import androidx.annotation.Nullable; -import androidx.fragment.app.FragmentManager; import androidx.recyclerview.widget.DiffUtil; import androidx.recyclerview.widget.ListAdapter; @@ -18,11 +16,8 @@ import androidx.recyclerview.widget.ListAdapter; class BlogPostAdapter extends ListAdapter { private final OnBlogPostClickListener listener; - @Nullable - private final FragmentManager fragmentManager; - BlogPostAdapter(OnBlogPostClickListener listener, - @Nullable FragmentManager fragmentManager) { + BlogPostAdapter(OnBlogPostClickListener listener) { super(new DiffUtil.ItemCallback() { @Override public boolean areItemsTheSame(BlogPostItem a, BlogPostItem b) { @@ -35,7 +30,6 @@ class BlogPostAdapter extends ListAdapter { } }); this.listener = listener; - this.fragmentManager = fragmentManager; } @Override @@ -43,7 +37,7 @@ class BlogPostAdapter extends ListAdapter { int viewType) { View v = LayoutInflater.from(parent.getContext()).inflate( R.layout.list_item_blog_post, parent, false); - return new BlogPostViewHolder(v, false, listener, fragmentManager); + return new BlogPostViewHolder(v, false, listener); } @Override diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogPostViewHolder.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogPostViewHolder.java index e10af04b9..ad4debe92 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogPostViewHolder.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogPostViewHolder.java @@ -20,7 +20,6 @@ import javax.annotation.Nullable; import androidx.annotation.NonNull; import androidx.annotation.UiThread; import androidx.core.view.ViewCompat; -import androidx.fragment.app.FragmentManager; import androidx.recyclerview.widget.RecyclerView; import static android.view.View.GONE; @@ -47,16 +46,12 @@ class BlogPostViewHolder extends RecyclerView.ViewHolder { @NonNull private final OnBlogPostClickListener listener; - @Nullable - private final FragmentManager fragmentManager; BlogPostViewHolder(View v, boolean fullText, - @NonNull OnBlogPostClickListener listener, - @Nullable FragmentManager fragmentManager) { + @NonNull OnBlogPostClickListener listener) { super(v); this.fullText = fullText; this.listener = listener; - this.fragmentManager = fragmentManager; ctx = v.getContext(); layout = v.findViewById(R.id.postLayout); @@ -67,10 +62,6 @@ class BlogPostViewHolder extends RecyclerView.ViewHolder { commentContainer = v.findViewById(R.id.commentContainer); } - void setVisibility(int visibility) { - layout.setVisibility(visibility); - } - void hideReblogButton() { reblogButton.setVisibility(GONE); } @@ -114,7 +105,7 @@ class BlogPostViewHolder extends RecyclerView.ViewHolder { if (fullText) { text.setText(postText); text.setTextIsSelectable(true); - makeLinksClickable(text, fragmentManager); + makeLinksClickable(text, listener::onLinkClick); } else { text.setTextIsSelectable(false); if (postText.length() > TEASER_LENGTH) diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/FeedFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/FeedFragment.java index 97cca5ca6..c876ca849 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/blog/FeedFragment.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/FeedFragment.java @@ -17,6 +17,7 @@ import org.briarproject.briar.android.activity.ActivityComponent; import org.briarproject.briar.android.fragment.BaseFragment; import org.briarproject.briar.android.util.BriarSnackbarBuilder; import org.briarproject.briar.android.view.BriarRecyclerView; +import org.briarproject.briar.android.widget.LinkDialogFragment; import org.briarproject.briar.api.blog.Blog; import java.util.List; @@ -44,7 +45,7 @@ public class FeedFragment extends BaseFragment ViewModelProvider.Factory viewModelFactory; private FeedViewModel viewModel; - private BlogPostAdapter adapter; + private final BlogPostAdapter adapter = new BlogPostAdapter(this); private LinearLayoutManager layoutManager; private BriarRecyclerView list; @Nullable @@ -79,8 +80,6 @@ public class FeedFragment extends BaseFragment View v = inflater.inflate(R.layout.fragment_blog, container, false); - adapter = new BlogPostAdapter(this, getParentFragmentManager()); - layoutManager = new LinearLayoutManager(getActivity()); list = v.findViewById(R.id.postList); list.setLayoutManager(layoutManager); @@ -185,6 +184,12 @@ public class FeedFragment extends BaseFragment requireContext().startActivity(i); } + @Override + public void onLinkClick(String url) { + LinkDialogFragment f = LinkDialogFragment.newInstance(url); + f.show(getParentFragmentManager(), f.getUniqueTag()); + } + @Override public String getUniqueTag() { return TAG; diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/OnBlogPostClickListener.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/OnBlogPostClickListener.java index 9920e4775..8e3d332e6 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/blog/OnBlogPostClickListener.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/OnBlogPostClickListener.java @@ -5,4 +5,6 @@ interface OnBlogPostClickListener { void onBlogPostClick(BlogPostItem post); void onAuthorClick(BlogPostItem post); + + void onLinkClick(String url); } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/ReblogFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/ReblogFragment.java index a4aecd098..f3746d7f7 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/blog/ReblogFragment.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/ReblogFragment.java @@ -17,6 +17,7 @@ import org.briarproject.briar.android.fragment.BaseFragment; import org.briarproject.briar.android.view.TextInputView; import org.briarproject.briar.android.view.TextSendController; import org.briarproject.briar.android.view.TextSendController.SendListener; +import org.briarproject.briar.android.widget.LinkDialogFragment; import org.briarproject.briar.api.attachment.AttachmentHeader; import java.util.List; @@ -151,7 +152,13 @@ public class ReblogFragment extends BaseFragment implements SendListener { public void onAuthorClick(BlogPostItem post) { // probably don't want to allow author clicks here } - }, getParentFragmentManager()); + + @Override + public void onLinkClick(String url) { + LinkDialogFragment f = LinkDialogFragment.newInstance(url); + f.show(getParentFragmentManager(), f.getUniqueTag()); + } + }); input = v.findViewById(R.id.inputText); } } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/util/UiUtils.java b/briar-android/src/main/java/org/briarproject/briar/android/util/UiUtils.java index 5cb811502..50459bb78 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/util/UiUtils.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/util/UiUtils.java @@ -34,7 +34,6 @@ import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; import org.briarproject.briar.R; import org.briarproject.briar.android.reporting.FeedbackActivity; import org.briarproject.briar.android.view.ArticleMovementMethod; -import org.briarproject.briar.android.widget.LinkDialogFragment; import java.util.Locale; @@ -49,8 +48,8 @@ import androidx.appcompat.app.AlertDialog; import androidx.core.content.ContextCompat; import androidx.core.hardware.fingerprint.FingerprintManagerCompat; import androidx.core.text.HtmlCompat; +import androidx.core.util.Consumer; import androidx.fragment.app.FragmentActivity; -import androidx.fragment.app.FragmentManager; import androidx.lifecycle.LifecycleOwner; import androidx.lifecycle.LiveData; import androidx.lifecycle.Observer; @@ -198,8 +197,7 @@ public class UiUtils { } public static void makeLinksClickable(TextView v, - @Nullable FragmentManager fm) { - if (fm == null) return; + Consumer onLinkClicked) { SpannableStringBuilder ssb = new SpannableStringBuilder(v.getText()); URLSpan[] spans = ssb.getSpans(0, ssb.length(), URLSpan.class); for (URLSpan span : spans) { @@ -210,8 +208,7 @@ public class UiUtils { ClickableSpan cSpan = new ClickableSpan() { @Override public void onClick(View v2) { - LinkDialogFragment f = LinkDialogFragment.newInstance(url); - f.show(fm, f.getUniqueTag()); + onLinkClicked.accept(url); } }; ssb.setSpan(cSpan, start, end, 0); From 1fa4b78474f1f207e74480f0b6bd56c501d44694 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Tue, 12 Jan 2021 09:43:08 -0300 Subject: [PATCH 04/13] Migrate BlogController to BlogViewModel --- .../briar/android/blog/BaseController.java | 48 ---- .../android/blog/BaseControllerImpl.java | 175 +------------ .../briar/android/blog/BaseViewModel.java | 81 +++--- .../briar/android/blog/BlogActivity.java | 18 +- .../briar/android/blog/BlogController.java | 30 +-- .../android/blog/BlogControllerImpl.java | 112 -------- .../briar/android/blog/BlogFragment.java | 239 ++++++------------ .../briar/android/blog/BlogModule.java | 5 + .../briar/android/blog/BlogPostFragment.java | 62 ++--- .../briar/android/blog/BlogViewModel.java | 168 ++++++++++++ .../briar/android/blog/FeedFragment.java | 6 +- .../briar/android/blog/FeedViewModel.java | 46 +--- 12 files changed, 344 insertions(+), 646 deletions(-) delete mode 100644 briar-android/src/main/java/org/briarproject/briar/android/blog/BaseController.java create mode 100644 briar-android/src/main/java/org/briarproject/briar/android/blog/BlogViewModel.java diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/BaseController.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/BaseController.java deleted file mode 100644 index 2e6115ced..000000000 --- a/briar-android/src/main/java/org/briarproject/briar/android/blog/BaseController.java +++ /dev/null @@ -1,48 +0,0 @@ -package org.briarproject.briar.android.blog; - -import org.briarproject.bramble.api.db.DbException; -import org.briarproject.bramble.api.nullsafety.NotNullByDefault; -import org.briarproject.bramble.api.sync.GroupId; -import org.briarproject.bramble.api.sync.MessageId; -import org.briarproject.briar.android.controller.handler.ExceptionHandler; -import org.briarproject.briar.android.controller.handler.ResultExceptionHandler; -import org.briarproject.briar.api.blog.BlogPostHeader; - -import java.util.List; - -import javax.annotation.Nullable; - -import androidx.annotation.UiThread; - -@NotNullByDefault -interface BaseController { - - @UiThread - void onStart(); - - @UiThread - void onStop(); - - void loadBlogPosts(GroupId g, - ResultExceptionHandler, DbException> handler); - - void loadBlogPost(BlogPostHeader header, - ResultExceptionHandler handler); - - void loadBlogPost(GroupId g, MessageId m, - ResultExceptionHandler handler); - - void repeatPost(BlogPostItem item, @Nullable String comment, - ExceptionHandler handler); - - @NotNullByDefault - interface BlogListener { - - @UiThread - void onBlogPostAdded(BlogPostHeader header, boolean local); - - @UiThread - void onBlogRemoved(); - } - -} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/BaseControllerImpl.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/BaseControllerImpl.java index 13f5e5193..d95a5c395 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/blog/BaseControllerImpl.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/BaseControllerImpl.java @@ -1,61 +1,28 @@ package org.briarproject.briar.android.blog; import org.briarproject.bramble.api.db.DatabaseExecutor; -import org.briarproject.bramble.api.db.DbException; import org.briarproject.bramble.api.event.EventBus; import org.briarproject.bramble.api.event.EventListener; import org.briarproject.bramble.api.identity.IdentityManager; -import org.briarproject.bramble.api.identity.LocalAuthor; import org.briarproject.bramble.api.lifecycle.LifecycleManager; import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault; import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; -import org.briarproject.bramble.api.sync.GroupId; -import org.briarproject.bramble.api.sync.MessageId; 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.briar.api.android.AndroidNotificationManager; -import org.briarproject.briar.api.blog.Blog; -import org.briarproject.briar.api.blog.BlogCommentHeader; import org.briarproject.briar.api.blog.BlogManager; -import org.briarproject.briar.api.blog.BlogPostHeader; -import org.briarproject.briar.util.HtmlUtils; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executor; -import java.util.logging.Logger; - -import javax.annotation.Nullable; - -import androidx.annotation.CallSuper; - -import static java.util.logging.Level.WARNING; -import static org.briarproject.bramble.util.LogUtils.logDuration; -import static org.briarproject.bramble.util.LogUtils.logException; -import static org.briarproject.bramble.util.LogUtils.now; -import static org.briarproject.briar.util.HtmlUtils.ARTICLE; @MethodsNotNullByDefault @ParametersNotNullByDefault abstract class BaseControllerImpl extends DbControllerImpl - implements BaseController, EventListener { - - private static final Logger LOG = - Logger.getLogger(BaseControllerImpl.class.getName()); + implements EventListener { protected final EventBus eventBus; protected final AndroidNotificationManager notificationManager; protected final IdentityManager identityManager; protected final BlogManager blogManager; - private final Map textCache = new ConcurrentHashMap<>(); - private final Map headerCache = - new ConcurrentHashMap<>(); - BaseControllerImpl(@DatabaseExecutor Executor dbExecutor, LifecycleManager lifecycleManager, EventBus eventBus, AndroidNotificationManager notificationManager, @@ -67,144 +34,4 @@ abstract class BaseControllerImpl extends DbControllerImpl this.blogManager = blogManager; } - @Override - @CallSuper - public void onStart() { - eventBus.addListener(this); - } - - @Override - @CallSuper - public void onStop() { - eventBus.removeListener(this); - } - - @Override - public void loadBlogPosts(GroupId groupId, - ResultExceptionHandler, DbException> handler) { - runOnDbThread(() -> { - try { - List items = loadItems(groupId); - handler.onResult(items); - } catch (DbException e) { - logException(LOG, WARNING, e); - handler.onException(e); - } - }); - } - - List loadItems(GroupId groupId) throws DbException { - long start = now(); - Collection headers = - blogManager.getPostHeaders(groupId); - logDuration(LOG, "Loading headers", start); - List items = new ArrayList<>(headers.size()); - start = now(); - for (BlogPostHeader h : headers) { - headerCache.put(h.getId(), h); - BlogPostItem item = getItem(h); - items.add(item); - } - logDuration(LOG, "Loading bodies", start); - return items; - } - - @Override - public void loadBlogPost(BlogPostHeader header, - ResultExceptionHandler handler) { - - String text = textCache.get(header.getId()); - if (text != null) { - LOG.info("Loaded text from cache"); - handler.onResult(new BlogPostItem(header, text)); - return; - } - runOnDbThread(() -> { - try { - long start = now(); - BlogPostItem item = getItem(header); - logDuration(LOG, "Loading text", start); - handler.onResult(item); - } catch (DbException e) { - logException(LOG, WARNING, e); - handler.onException(e); - } - }); - } - - @Override - public void loadBlogPost(GroupId g, MessageId m, - ResultExceptionHandler handler) { - - BlogPostHeader header = headerCache.get(m); - if (header != null) { - LOG.info("Loaded header from cache"); - loadBlogPost(header, handler); - return; - } - runOnDbThread(() -> { - try { - long start = now(); - BlogPostHeader header1 = getPostHeader(g, m); - BlogPostItem item = getItem(header1); - logDuration(LOG, "Loading post", start); - handler.onResult(item); - } catch (DbException e) { - logException(LOG, WARNING, e); - handler.onException(e); - } - }); - } - - @Override - public void repeatPost(BlogPostItem item, @Nullable String comment, - ExceptionHandler handler) { - runOnDbThread(() -> { - try { - LocalAuthor a = identityManager.getLocalAuthor(); - Blog b = blogManager.getPersonalBlog(a); - BlogPostHeader h = item.getHeader(); - blogManager.addLocalComment(a, b.getId(), comment, h); - } catch (DbException e) { - logException(LOG, WARNING, e); - handler.onException(e); - } - }); - } - - private BlogPostHeader getPostHeader(GroupId g, MessageId m) - throws DbException { - BlogPostHeader header = headerCache.get(m); - if (header == null) { - header = blogManager.getPostHeader(g, m); - headerCache.put(m, header); - } - return header; - } - - @DatabaseExecutor - private BlogPostItem getItem(BlogPostHeader h) throws DbException { - String text; - if (h instanceof BlogCommentHeader) { - BlogCommentHeader c = (BlogCommentHeader) h; - BlogCommentItem item = new BlogCommentItem(c); - text = getPostText(item.getPostHeader().getId()); - item.setText(text); - return item; - } else { - text = getPostText(h.getId()); - return new BlogPostItem(h, text); - } - } - - @DatabaseExecutor - private String getPostText(MessageId m) throws DbException { - String text = textCache.get(m); - if (text == null) { - text = HtmlUtils.clean(blogManager.getPostText(m), ARTICLE); - textCache.put(m, text); - } - return text; - } - } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/BaseViewModel.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/BaseViewModel.java index 10e105b46..d6690c4c2 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/blog/BaseViewModel.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/BaseViewModel.java @@ -4,7 +4,6 @@ import android.app.Application; import org.briarproject.bramble.api.db.DatabaseExecutor; import org.briarproject.bramble.api.db.DbException; -import org.briarproject.bramble.api.db.NoSuchMessageException; import org.briarproject.bramble.api.db.Transaction; import org.briarproject.bramble.api.db.TransactionManager; import org.briarproject.bramble.api.event.Event; @@ -19,6 +18,7 @@ import org.briarproject.bramble.api.sync.MessageId; 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.blog.Blog; import org.briarproject.briar.api.blog.BlogCommentHeader; import org.briarproject.briar.api.blog.BlogManager; @@ -26,6 +26,7 @@ import org.briarproject.briar.api.blog.BlogPostHeader; import org.briarproject.briar.util.HtmlUtils; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -35,11 +36,10 @@ import java.util.logging.Logger; import javax.annotation.Nullable; import javax.inject.Inject; +import androidx.annotation.UiThread; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; -import androidx.lifecycle.Transformations; -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; @@ -50,20 +50,23 @@ import static org.briarproject.briar.util.HtmlUtils.ARTICLE; @NotNullByDefault public class BaseViewModel extends DbViewModel implements EventListener { - private static Logger LOG = getLogger(BaseViewModel.class.getName()); + private static final Logger LOG = getLogger(BaseViewModel.class.getName()); protected final TransactionManager db; private final EventBus eventBus; protected final IdentityManager identityManager; + protected final AndroidNotificationManager notificationManager; protected final BlogManager blogManager; - protected final MutableLiveData>> blogPosts = + private final MutableLiveData>> blogPosts = new MutableLiveData<>(); // TODO do we still need those caches? private final Map textCache = new ConcurrentHashMap<>(); private final Map headerCache = new ConcurrentHashMap<>(); + @Nullable + private Boolean postAddedWasLocal = null; @Inject BaseViewModel(Application application, @@ -73,11 +76,13 @@ public class BaseViewModel extends DbViewModel implements EventListener { AndroidExecutor androidExecutor, EventBus eventBus, IdentityManager identityManager, + AndroidNotificationManager notificationManager, BlogManager blogManager) { super(application, dbExecutor, lifecycleManager, db, androidExecutor); this.db = db; this.eventBus = eventBus; this.identityManager = identityManager; + this.notificationManager = notificationManager; this.blogManager = blogManager; eventBus.addListener(this); @@ -93,10 +98,6 @@ public class BaseViewModel extends DbViewModel implements EventListener { public void eventOccurred(Event e) { } - void loadItems(GroupId groupId) { - loadList(txn -> loadBlogPosts(txn, groupId), blogPosts::setValue); - } - @DatabaseExecutor protected List loadBlogPosts(Transaction txn, GroupId groupId) throws DbException { @@ -148,9 +149,9 @@ public class BaseViewModel extends DbViewModel implements EventListener { runOnDbThread(() -> { try { long start = now(); - BlogPostHeader header1 = getPostHeader(g, m); + BlogPostHeader header = getPostHeader(g, m); BlogPostItem item = db.transactionWithResult(true, txn -> - getItem(txn, header1) + getItem(txn, header) ); logDuration(LOG, "Loading post", start); result.postValue(new LiveResult<>(item)); @@ -173,6 +174,31 @@ public class BaseViewModel extends DbViewModel implements EventListener { return header; } + @UiThread + protected void updateBlogPosts(LiveResult> posts) { + blogPosts.setValue(posts); + } + + protected void onBlogPostAdded(BlogPostHeader header, boolean local) { + postAddedWasLocal = local; + runOnDbThread(() -> { + try { + db.transaction(true, txn -> { + BlogPostItem item = getItem(txn, header); + txn.attach(() -> { + List items = addListItem(blogPosts, item); + if (items != null) { + Collections.sort(items); + blogPosts.setValue(new LiveResult<>(items)); + } + }); + }); + } catch (DbException e) { + logException(LOG, WARNING, e); + } + }); + } + void repeatPost(BlogPostItem item, @Nullable String comment) { runOnDbThread(() -> { try { @@ -186,34 +212,17 @@ public class BaseViewModel extends DbViewModel implements EventListener { }); } - LiveData>> getAllBlogPosts() { + LiveData>> getBlogPosts() { return blogPosts; } - LiveData>> getBlogPosts(GroupId g) { - return Transformations.map(blogPosts, result -> { - List allPosts = result.getResultOrNull(); - if (allPosts == null) return result; - List groupPosts = new ArrayList<>(); - for (BlogPostItem item : allPosts) { - if (item.getGroupId().equals(g)) groupPosts.add(item); - } - return new LiveResult<>(groupPosts); - }); - } - - LiveData> getBlogPost(MessageId m) { - return Transformations.map(blogPosts, result -> { - List allPosts = result.getResultOrNull(); - if (allPosts == null) { - Exception e = requireNonNull(result.getException()); - return new LiveResult<>(e); - } - for (BlogPostItem item : allPosts) { - if (item.getId().equals(m)) return new LiveResult<>(item); - } - return new LiveResult<>(new NoSuchMessageException()); - }); + @UiThread + @Nullable + Boolean getPostAddedWasLocalAndReset() { + if (postAddedWasLocal == null) return null; + boolean wasLocal = postAddedWasLocal; + postAddedWasLocal = null; + return wasLocal; } } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogActivity.java index 23a7ff252..bbe9b6961 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogActivity.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogActivity.java @@ -16,15 +16,27 @@ import javax.annotation.Nullable; import javax.inject.Inject; import androidx.appcompat.widget.Toolbar; +import androidx.lifecycle.ViewModelProvider; @MethodsNotNullByDefault @ParametersNotNullByDefault public class BlogActivity extends BriarActivity implements BaseFragmentListener { + @Inject + ViewModelProvider.Factory viewModelFactory; @Inject BlogController blogController; + private BlogViewModel viewModel; + + @Override + public void injectActivity(ActivityComponent component) { + component.inject(this); + viewModel = new ViewModelProvider(this, viewModelFactory) + .get(BlogViewModel.class); + } + @Override public void onCreate(@Nullable Bundle state) { super.onCreate(state); @@ -35,6 +47,7 @@ public class BlogActivity extends BriarActivity if (b == null) throw new IllegalStateException("No group ID in intent"); GroupId groupId = new GroupId(b); blogController.setGroupId(groupId); + viewModel.setGroupId(groupId); setContentView(R.layout.activity_fragment_container_toolbar); Toolbar toolbar = setUpCustomToolbar(false); @@ -54,9 +67,4 @@ public class BlogActivity extends BriarActivity } } - @Override - public void injectActivity(ActivityComponent component) { - component.inject(this); - } - } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogController.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogController.java index 7f4dba0b2..5ce9df167 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogController.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogController.java @@ -4,44 +4,16 @@ import org.briarproject.bramble.api.contact.ContactId; import org.briarproject.bramble.api.db.DbException; import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.sync.GroupId; -import org.briarproject.bramble.api.sync.MessageId; import org.briarproject.briar.android.controller.handler.ResultExceptionHandler; import java.util.Collection; -import java.util.List; - -import androidx.annotation.UiThread; @NotNullByDefault -public interface BlogController extends BaseController { +public interface BlogController { void setGroupId(GroupId g); - @UiThread - void setBlogSharingListener(BlogSharingListener listener); - - @UiThread - void unsetBlogSharingListener(BlogSharingListener listener); - - void loadBlogPosts( - ResultExceptionHandler, DbException> handler); - - void loadBlogPost(MessageId m, - ResultExceptionHandler handler); - - void loadBlog(ResultExceptionHandler handler); - - void deleteBlog(ResultExceptionHandler handler); - void loadSharingContacts( ResultExceptionHandler, DbException> handler); - interface BlogSharingListener extends BlogListener { - @UiThread - void onBlogInvitationAccepted(ContactId c); - - @UiThread - void onBlogLeft(ContactId c); - } - } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogControllerImpl.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogControllerImpl.java index 317a75134..10f59ab8b 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogControllerImpl.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogControllerImpl.java @@ -10,38 +10,25 @@ 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.IdentityManager; -import org.briarproject.bramble.api.identity.LocalAuthor; import org.briarproject.bramble.api.lifecycle.LifecycleManager; import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault; import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; import org.briarproject.bramble.api.sync.GroupId; -import org.briarproject.bramble.api.sync.MessageId; -import org.briarproject.bramble.api.sync.event.GroupRemovedEvent; import org.briarproject.briar.android.controller.ActivityLifecycleController; import org.briarproject.briar.android.controller.handler.ResultExceptionHandler; import org.briarproject.briar.api.android.AndroidNotificationManager; -import org.briarproject.briar.api.blog.Blog; -import org.briarproject.briar.api.blog.BlogInvitationResponse; import org.briarproject.briar.api.blog.BlogManager; import org.briarproject.briar.api.blog.BlogSharingManager; -import org.briarproject.briar.api.blog.event.BlogInvitationResponseReceivedEvent; -import org.briarproject.briar.api.blog.event.BlogPostAddedEvent; -import org.briarproject.briar.api.sharing.event.ContactLeftShareableEvent; import java.util.ArrayList; import java.util.Collection; -import java.util.List; import java.util.concurrent.Executor; import java.util.logging.Logger; import javax.inject.Inject; -import androidx.annotation.Nullable; - import static java.util.logging.Level.WARNING; -import static org.briarproject.bramble.util.LogUtils.logDuration; import static org.briarproject.bramble.util.LogUtils.logException; -import static org.briarproject.bramble.util.LogUtils.now; @MethodsNotNullByDefault @ParametersNotNullByDefault @@ -53,10 +40,6 @@ class BlogControllerImpl extends BaseControllerImpl private final BlogSharingManager blogSharingManager; - // UI thread - @Nullable - private BlogSharingListener listener; - private volatile GroupId groupId = null; @Inject @@ -76,15 +59,10 @@ class BlogControllerImpl extends BaseControllerImpl @Override public void onActivityStart() { - super.onStart(); - notificationManager.blockNotification(groupId); - notificationManager.clearBlogPostNotification(groupId); } @Override public void onActivityStop() { - super.onStop(); - notificationManager.unblockNotification(groupId); } @Override @@ -96,99 +74,9 @@ class BlogControllerImpl extends BaseControllerImpl groupId = g; } - @Override - public void setBlogSharingListener(BlogSharingListener listener) { - this.listener = listener; - } - - @Override - public void unsetBlogSharingListener(BlogSharingListener listener) { - if (this.listener == listener) this.listener = null; - } - @Override public void eventOccurred(Event e) { - if (groupId == null || listener == null) - throw new IllegalStateException(); - if (e instanceof BlogPostAddedEvent) { - BlogPostAddedEvent b = (BlogPostAddedEvent) e; - if (b.getGroupId().equals(groupId)) { - LOG.info("Blog post added"); - listener.onBlogPostAdded(b.getHeader(), b.isLocal()); - } - } else if (e instanceof BlogInvitationResponseReceivedEvent) { - BlogInvitationResponseReceivedEvent b = - (BlogInvitationResponseReceivedEvent) e; - BlogInvitationResponse r = b.getMessageHeader(); - if (r.getShareableId().equals(groupId) && r.wasAccepted()) { - LOG.info("Blog invitation accepted"); - listener.onBlogInvitationAccepted(b.getContactId()); - } - } else if (e instanceof ContactLeftShareableEvent) { - ContactLeftShareableEvent s = (ContactLeftShareableEvent) e; - if (s.getGroupId().equals(groupId)) { - LOG.info("Blog left by contact"); - listener.onBlogLeft(s.getContactId()); - } - } else if (e instanceof GroupRemovedEvent) { - GroupRemovedEvent g = (GroupRemovedEvent) e; - if (g.getGroup().getId().equals(groupId)) { - LOG.info("Blog removed"); - listener.onBlogRemoved(); - } - } - } - @Override - public void loadBlogPosts( - ResultExceptionHandler, DbException> handler) { - if (groupId == null) throw new IllegalStateException(); - loadBlogPosts(groupId, handler); - } - - @Override - public void loadBlogPost(MessageId m, - ResultExceptionHandler handler) { - if (groupId == null) throw new IllegalStateException(); - loadBlogPost(groupId, m, handler); - } - - @Override - public void loadBlog( - ResultExceptionHandler handler) { - if (groupId == null) throw new IllegalStateException(); - runOnDbThread(() -> { - try { - long start = now(); - LocalAuthor a = identityManager.getLocalAuthor(); - Blog b = blogManager.getBlog(groupId); - boolean ours = a.getId().equals(b.getAuthor().getId()); - boolean removable = blogManager.canBeRemoved(b); - BlogItem blog = new BlogItem(b, ours, removable); - logDuration(LOG, "Loading blog", start); - handler.onResult(blog); - } catch (DbException e) { - logException(LOG, WARNING, e); - handler.onException(e); - } - }); - } - - @Override - public void deleteBlog(ResultExceptionHandler handler) { - if (groupId == null) throw new IllegalStateException(); - runOnDbThread(() -> { - try { - long start = now(); - Blog b = blogManager.getBlog(groupId); - blogManager.removeBlog(b); - logDuration(LOG, "Removing blog", start); - handler.onResult(null); - } catch (DbException e) { - logException(LOG, WARNING, e); - handler.onException(e); - } - }); } @Override diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogFragment.java index a6ead22c1..2ab3bfc12 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogFragment.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogFragment.java @@ -1,6 +1,5 @@ package org.briarproject.briar.android.blog; -import android.content.DialogInterface; import android.content.Intent; import android.os.Bundle; import android.os.Parcelable; @@ -21,7 +20,6 @@ import org.briarproject.bramble.api.sync.GroupId; import org.briarproject.briar.R; import org.briarproject.briar.android.activity.ActivityComponent; import org.briarproject.briar.android.activity.BriarActivity; -import org.briarproject.briar.android.blog.BlogController.BlogSharingListener; import org.briarproject.briar.android.controller.SharingController; import org.briarproject.briar.android.controller.handler.UiResultExceptionHandler; import org.briarproject.briar.android.fragment.BaseFragment; @@ -30,7 +28,6 @@ import org.briarproject.briar.android.sharing.ShareBlogActivity; import org.briarproject.briar.android.util.BriarSnackbarBuilder; import org.briarproject.briar.android.view.BriarRecyclerView; import org.briarproject.briar.android.widget.LinkDialogFragment; -import org.briarproject.briar.api.blog.BlogPostHeader; import java.util.Collection; import java.util.List; @@ -41,6 +38,7 @@ import androidx.annotation.Nullable; import androidx.annotation.UiThread; import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AlertDialog; +import androidx.lifecycle.ViewModelProvider; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView.LayoutManager; @@ -57,11 +55,12 @@ import static org.briarproject.briar.android.controller.SharingController.Sharin @MethodsNotNullByDefault @ParametersNotNullByDefault public class BlogFragment extends BaseFragment - implements BlogSharingListener, SharingListener, - OnBlogPostClickListener { + implements SharingListener, OnBlogPostClickListener { private final static String TAG = BlogFragment.class.getName(); + @Inject + ViewModelProvider.Factory viewModelFactory; @Inject BlogController blogController; @Inject @@ -70,11 +69,10 @@ public class BlogFragment extends BaseFragment private Parcelable layoutManagerState; private GroupId groupId; + private BlogViewModel viewModel; private final BlogPostAdapter adapter = new BlogPostAdapter(this); private LayoutManager layoutManager; private BriarRecyclerView list; - private MenuItem writeButton, deleteButton; - private boolean isMyBlog = false, canDeleteBlog = false; static BlogFragment newInstance(GroupId groupId) { BlogFragment f = new BlogFragment(); @@ -89,7 +87,8 @@ public class BlogFragment extends BaseFragment @Override public void injectFragment(ActivityComponent component) { component.inject(this); - blogController.setBlogSharingListener(this); + viewModel = new ViewModelProvider(requireActivity(), viewModelFactory) + .get(BlogViewModel.class); sharingController.setSharingListener(this); } @@ -112,6 +111,14 @@ public class BlogFragment extends BaseFragment list.showProgressBar(); list.setEmptyText(getString(R.string.blogs_other_blog_empty_state)); + viewModel.getBlogPosts().observe(getViewLifecycleOwner(), result -> + result.onError(this::handleException) + .onSuccess(this::onBlogPostsLoaded) + ); + viewModel.getBlogRemoved().observe(getViewLifecycleOwner(), removed -> { + if (removed) finish(); + }); + if (savedInstanceState != null) { layoutManagerState = savedInstanceState.getParcelable("layoutManager"); @@ -123,16 +130,17 @@ public class BlogFragment extends BaseFragment @Override public void onStart() { super.onStart(); + viewModel.blockNotifications(); + viewModel.clearBlogPostNotifications(); sharingController.onStart(); - loadBlog(); loadSharedContacts(); - loadBlogPosts(false); list.startPeriodicUpdate(); } @Override public void onStop() { super.onStop(); + viewModel.unblockNotifications(); sharingController.onStop(); list.stopPeriodicUpdate(); } @@ -140,7 +148,6 @@ public class BlogFragment extends BaseFragment @Override public void onDestroy() { super.onDestroy(); - blogController.unsetBlogSharingListener(this); sharingController.unsetSharingListener(this); } @@ -156,42 +163,43 @@ public class BlogFragment extends BaseFragment @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { inflater.inflate(R.menu.blogs_blog_actions, menu); - writeButton = menu.findItem(R.id.action_write_blog_post); - if (isMyBlog) writeButton.setVisible(true); - deleteButton = menu.findItem(R.id.action_blog_delete); - if (canDeleteBlog) deleteButton.setEnabled(true); - + MenuItem writeButton = menu.findItem(R.id.action_write_blog_post); + MenuItem deleteButton = menu.findItem(R.id.action_blog_delete); + viewModel.getBlog().observe(getViewLifecycleOwner(), blog -> { + setToolbarTitle(blog.getBlog().getAuthor()); + if (blog.isOurs()) writeButton.setVisible(true); + if (blog.canBeRemoved()) deleteButton.setEnabled(true); + }); super.onCreateOptionsMenu(menu, inflater); } @Override public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case R.id.action_write_blog_post: - Intent i = new Intent(getActivity(), - WriteBlogPostActivity.class); - i.putExtra(GROUP_ID, groupId.getBytes()); - startActivityForResult(i, REQUEST_WRITE_BLOG_POST); - return true; - case R.id.action_blog_share: - Intent i2 = new Intent(getActivity(), ShareBlogActivity.class); - i2.setFlags(FLAG_ACTIVITY_CLEAR_TOP); - i2.putExtra(GROUP_ID, groupId.getBytes()); - startActivityForResult(i2, REQUEST_SHARE_BLOG); - return true; - case R.id.action_blog_sharing_status: - Intent i3 = new Intent(getActivity(), - BlogSharingStatusActivity.class); - i3.setFlags(FLAG_ACTIVITY_CLEAR_TOP); - i3.putExtra(GROUP_ID, groupId.getBytes()); - startActivity(i3); - return true; - case R.id.action_blog_delete: - showDeleteDialog(); - return true; - default: - return super.onOptionsItemSelected(item); + int itemId = item.getItemId(); + if (itemId == R.id.action_write_blog_post) { + Intent i = new Intent(getActivity(), + WriteBlogPostActivity.class); + i.putExtra(GROUP_ID, groupId.getBytes()); + startActivityForResult(i, REQUEST_WRITE_BLOG_POST); + return true; + } else if (itemId == R.id.action_blog_share) { + Intent i2 = new Intent(getActivity(), ShareBlogActivity.class); + i2.setFlags(FLAG_ACTIVITY_CLEAR_TOP); + i2.putExtra(GROUP_ID, groupId.getBytes()); + startActivityForResult(i2, REQUEST_SHARE_BLOG); + return true; + } else if (itemId == R.id.action_blog_sharing_status) { + Intent i3 = new Intent(getActivity(), + BlogSharingStatusActivity.class); + i3.setFlags(FLAG_ACTIVITY_CLEAR_TOP); + i3.putExtra(GROUP_ID, groupId.getBytes()); + startActivity(i3); + return true; + } else if (itemId == R.id.action_blog_delete) { + showDeleteDialog(); + return true; } + return super.onOptionsItemSelected(item); } @Override @@ -201,7 +209,7 @@ public class BlogFragment extends BaseFragment if (request == REQUEST_WRITE_BLOG_POST && result == RESULT_OK) { displaySnackbar(R.string.blogs_blog_post_created, true); - loadBlogPosts(true); + viewModel.loadBlogPosts(groupId); } else if (request == REQUEST_SHARE_BLOG && result == RESULT_OK) { displaySnackbar(R.string.blogs_sharing_snackbar, false); } @@ -212,30 +220,25 @@ public class BlogFragment extends BaseFragment return TAG; } - @Override - public void onBlogPostAdded(BlogPostHeader header, boolean local) { - blogController.loadBlogPost(header, - new UiResultExceptionHandler( - this) { - @Override - public void onResultUi(BlogPostItem post) { -// adapter.add(post); - if (local) { - list.scrollToPosition(0); - displaySnackbar(R.string.blogs_blog_post_created, - false); - } else { - displaySnackbar(R.string.blogs_blog_post_received, - true); - } - } - - @Override - public void onExceptionUi(DbException exception) { - handleException(exception); - } - } - ); + private void onBlogPostsLoaded(List items) { + adapter.submitList(items, () -> { + Boolean wasLocal = viewModel.getPostAddedWasLocalAndReset(); + if (wasLocal != null && wasLocal) { + list.scrollToPosition(0); + displaySnackbar(R.string.blogs_blog_post_created, + false); + } else if (wasLocal != null) { + displaySnackbar(R.string.blogs_blog_post_received, + true); + } + list.showData(); + if (layoutManagerState == null) { + list.scrollToPosition(0); + } else { + layoutManager.onRestoreInstanceState( + layoutManagerState); + } + }); } @Override @@ -262,53 +265,8 @@ public class BlogFragment extends BaseFragment f.show(getParentFragmentManager(), f.getUniqueTag()); } - private void loadBlogPosts(boolean reload) { - blogController.loadBlogPosts( - new UiResultExceptionHandler, - DbException>(this) { - @Override - public void onResultUi(List posts) { - if (posts.isEmpty()) { - list.showData(); - } else { - adapter.submitList(posts); - if (reload || layoutManagerState == null) { - list.scrollToPosition(0); - } else { - layoutManager.onRestoreInstanceState( - layoutManagerState); - } - } - } - - @Override - public void onExceptionUi(DbException exception) { - handleException(exception); - } - }); - } - - private void loadBlog() { - blogController.loadBlog( - new UiResultExceptionHandler(this) { - @Override - public void onResultUi(BlogItem blog) { - setToolbarTitle(blog.getBlog().getAuthor()); - if (blog.isOurs()) - showWriteButton(); - if (blog.canBeRemoved()) - enableDeleteButton(); - } - - @Override - public void onExceptionUi(DbException exception) { - handleException(exception); - } - }); - } - private void setToolbarTitle(Author a) { - getActivity().setTitle(a.getName()); + requireActivity().setTitle(a.getName()); } private void loadSharedContacts() { @@ -329,20 +287,6 @@ public class BlogFragment extends BaseFragment }); } - @Override - public void onBlogInvitationAccepted(ContactId c) { - sharingController.add(c); - setToolbarSubTitle(sharingController.getTotalCount(), - sharingController.getOnlineCount()); - } - - @Override - public void onBlogLeft(ContactId c) { - sharingController.remove(c); - setToolbarSubTitle(sharingController.getTotalCount(), - sharingController.getOnlineCount()); - } - @Override public void onSharingInfoUpdated(int total, int online) { setToolbarSubTitle(total, online); @@ -350,25 +294,13 @@ public class BlogFragment extends BaseFragment private void setToolbarSubTitle(int total, int online) { ActionBar actionBar = - ((BriarActivity) getActivity()).getSupportActionBar(); + ((BriarActivity) requireActivity()).getSupportActionBar(); if (actionBar != null) { actionBar.setSubtitle( getString(R.string.shared_with, total, online)); } } - private void showWriteButton() { - isMyBlog = true; - if (writeButton != null) - writeButton.setVisible(true); - } - - private void enableDeleteButton() { - canDeleteBlog = true; - if (deleteButton != null) - deleteButton.setEnabled(true); - } - private void displaySnackbar(int stringId, boolean scroll) { BriarSnackbarBuilder sb = new BriarSnackbarBuilder(); if (scroll) { @@ -379,38 +311,21 @@ public class BlogFragment extends BaseFragment } private void showDeleteDialog() { - DialogInterface.OnClickListener okListener = - (dialog, which) -> deleteBlog(); - AlertDialog.Builder builder = new AlertDialog.Builder(getActivity(), + AlertDialog.Builder builder = new AlertDialog.Builder(requireContext(), R.style.BriarDialogTheme); builder.setTitle(getString(R.string.blogs_remove_blog)); builder.setMessage( getString(R.string.blogs_remove_blog_dialog_message)); builder.setPositiveButton(R.string.cancel, null); - builder.setNegativeButton(R.string.blogs_remove_blog_ok, okListener); + builder.setNegativeButton(R.string.blogs_remove_blog_ok, + (dialog, which) -> deleteBlog()); builder.show(); } private void deleteBlog() { - blogController.deleteBlog( - new UiResultExceptionHandler(this) { - @Override - public void onResultUi(Void result) { - Toast.makeText(getActivity(), - R.string.blogs_blog_removed, LENGTH_SHORT) - .show(); - finish(); - } - - @Override - public void onExceptionUi(DbException exception) { - handleException(exception); - } - }); - } - - @Override - public void onBlogRemoved() { + viewModel.deleteBlog(); + Toast.makeText(getActivity(), R.string.blogs_blog_removed, LENGTH_SHORT) + .show(); finish(); } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogModule.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogModule.java index 9ab9ff681..a6cf55036 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogModule.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogModule.java @@ -21,6 +21,11 @@ public class BlogModule { @IntoMap @ViewModelKey(FeedViewModel.class) abstract ViewModel bindFeedViewModel(FeedViewModel feedViewModel); + + @Binds + @IntoMap + @ViewModelKey(BlogViewModel.class) + abstract ViewModel bindBlogViewModel(BlogViewModel blogViewModel); } @ActivityScope diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogPostFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogPostFragment.java index cb61b75de..7e201577c 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogPostFragment.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogPostFragment.java @@ -1,30 +1,35 @@ package org.briarproject.briar.android.blog; import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; -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.MessageId; import org.briarproject.briar.android.activity.ActivityComponent; -import org.briarproject.briar.android.blog.BaseController.BlogListener; -import org.briarproject.briar.android.controller.handler.UiResultExceptionHandler; -import org.briarproject.briar.api.blog.BlogPostHeader; +import javax.annotation.Nullable; import javax.inject.Inject; import androidx.annotation.UiThread; +import androidx.lifecycle.ViewModelProvider; @UiThread @MethodsNotNullByDefault @ParametersNotNullByDefault -public class BlogPostFragment extends BasePostFragment implements BlogListener { +public class BlogPostFragment extends BasePostFragment { private static final String TAG = BlogPostFragment.class.getName(); + @Inject + ViewModelProvider.Factory viewModelFactory; @Inject BlogController blogController; + private BlogViewModel viewModel; + static BlogPostFragment newInstance(MessageId postId) { BlogPostFragment f = new BlogPostFragment(); @@ -35,42 +40,29 @@ public class BlogPostFragment extends BasePostFragment implements BlogListener { return f; } + @Override + public void injectFragment(ActivityComponent component) { + component.inject(this); + viewModel = new ViewModelProvider(requireActivity(), viewModelFactory) + .get(BlogViewModel.class); + } + @Override public String getUniqueTag() { return TAG; } + @Nullable @Override - public void injectFragment(ActivityComponent component) { - component.inject(this); - } - - @Override - public void onStart() { - super.onStart(); - blogController.loadBlogPost(postId, - new UiResultExceptionHandler( - this) { - @Override - public void onResultUi(BlogPostItem post) { - onBlogPostLoaded(post); - } - - @Override - public void onExceptionUi(DbException exception) { - handleException(exception); - } - }); - } - - @Override - public void onBlogPostAdded(BlogPostHeader header, boolean local) { - // doesn't matter here - } - - @Override - public void onBlogRemoved() { - finish(); + public View onCreateView(LayoutInflater inflater, + @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + View v = super.onCreateView(inflater, container, savedInstanceState); + viewModel.loadBlogPost(postId).observe(getViewLifecycleOwner(), res -> + res.onError(this::handleException) + .onSuccess(this::onBlogPostLoaded) + ); + return v; } } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogViewModel.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogViewModel.java new file mode 100644 index 000000000..0d8e6d5c0 --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogViewModel.java @@ -0,0 +1,168 @@ +package org.briarproject.briar.android.blog; + +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.event.Event; +import org.briarproject.bramble.api.event.EventBus; +import org.briarproject.bramble.api.identity.IdentityManager; +import org.briarproject.bramble.api.identity.LocalAuthor; +import org.briarproject.bramble.api.lifecycle.LifecycleManager; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.sync.GroupId; +import org.briarproject.bramble.api.sync.MessageId; +import org.briarproject.bramble.api.sync.event.GroupRemovedEvent; +import org.briarproject.bramble.api.system.AndroidExecutor; +import org.briarproject.briar.android.viewmodel.LiveResult; +import org.briarproject.briar.api.android.AndroidNotificationManager; +import org.briarproject.briar.api.blog.Blog; +import org.briarproject.briar.api.blog.BlogInvitationResponse; +import org.briarproject.briar.api.blog.BlogManager; +import org.briarproject.briar.api.blog.event.BlogInvitationResponseReceivedEvent; +import org.briarproject.briar.api.blog.event.BlogPostAddedEvent; +import org.briarproject.briar.api.sharing.event.ContactLeftShareableEvent; + +import java.util.concurrent.Executor; +import java.util.logging.Logger; + +import javax.inject.Inject; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; + +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; + +@NotNullByDefault +class BlogViewModel extends BaseViewModel { + + private static final Logger LOG = getLogger(BlogViewModel.class.getName()); + + // implicitly non-null + private volatile GroupId groupId = null; + + private final MutableLiveData blog = new MutableLiveData<>(); + private final MutableLiveData blogRemoved = + new MutableLiveData<>(); + + @Inject + BlogViewModel(Application application, + @DatabaseExecutor Executor dbExecutor, + LifecycleManager lifecycleManager, + TransactionManager db, + AndroidExecutor androidExecutor, + EventBus eventBus, + IdentityManager identityManager, + AndroidNotificationManager notificationManager, + BlogManager blogManager) { + super(application, dbExecutor, lifecycleManager, db, androidExecutor, + eventBus, identityManager, notificationManager, blogManager); + } + + @Override + public void eventOccurred(Event e) { + if (e instanceof BlogPostAddedEvent) { + BlogPostAddedEvent b = (BlogPostAddedEvent) e; + if (b.getGroupId().equals(groupId)) { + LOG.info("Blog post added"); + onBlogPostAdded(b.getHeader(), b.isLocal()); + } + } else if (e instanceof BlogInvitationResponseReceivedEvent) { + BlogInvitationResponseReceivedEvent b = + (BlogInvitationResponseReceivedEvent) e; + BlogInvitationResponse r = b.getMessageHeader(); + if (r.getShareableId().equals(groupId) && r.wasAccepted()) { + LOG.info("Blog invitation accepted"); + // TODO +// onBlogInvitationAccepted(b.getContactId()); +// sharingController.add(c); + } + } else if (e instanceof ContactLeftShareableEvent) { + ContactLeftShareableEvent s = (ContactLeftShareableEvent) e; + if (s.getGroupId().equals(groupId)) { + LOG.info("Blog left by contact"); + // TODO +// onBlogLeft(s.getContactId()); +// sharingController.remove(c); + } + } else if (e instanceof GroupRemovedEvent) { + GroupRemovedEvent g = (GroupRemovedEvent) e; + if (g.getGroup().getId().equals(groupId)) { + LOG.info("Blog removed"); + blogRemoved.setValue(true); + } + } + } + + /** + * Set this before calling any other methods. + */ + public void setGroupId(GroupId groupId) { + this.groupId = groupId; + loadBlog(groupId); + loadBlogPosts(groupId); + } + + private void loadBlog(GroupId groupId) { + runOnDbThread(() -> { + try { + long start = now(); + LocalAuthor a = identityManager.getLocalAuthor(); + Blog b = blogManager.getBlog(groupId); + boolean ours = a.getId().equals(b.getAuthor().getId()); + boolean removable = blogManager.canBeRemoved(b); + blog.postValue(new BlogItem(b, ours, removable)); + logDuration(LOG, "Loading blog", start); + } catch (DbException e) { + logException(LOG, WARNING, e); + } + }); + } + + void blockNotifications() { + notificationManager.blockNotification(groupId); + } + + void clearBlogPostNotifications() { + notificationManager.clearBlogPostNotification(groupId); + } + + void unblockNotifications() { + notificationManager.unblockNotification(groupId); + } + + void loadBlogPosts(GroupId groupId) { + loadList(txn -> loadBlogPosts(txn, groupId), this::updateBlogPosts); + } + + void deleteBlog() { + runOnDbThread(() -> { + try { + long start = now(); + Blog b = blogManager.getBlog(groupId); + blogManager.removeBlog(b); + logDuration(LOG, "Removing blog", start); + } catch (DbException e) { + logException(LOG, WARNING, e); + } + }); + } + + LiveData> loadBlogPost(MessageId m) { + return loadBlogPost(groupId, m); + } + + LiveData getBlog() { + return blog; + } + + LiveData getBlogRemoved() { + return blogRemoved; + } + +} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/FeedFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/FeedFragment.java index c876ca849..218d6dfcc 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/blog/FeedFragment.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/FeedFragment.java @@ -63,8 +63,7 @@ public class FeedFragment extends BaseFragment @Override public void injectFragment(ActivityComponent component) { component.inject(this); - // TODO don't use NavDrawerActivity scope here - viewModel = new ViewModelProvider(requireActivity(), viewModelFactory) + viewModel = new ViewModelProvider(this, viewModelFactory) .get(FeedViewModel.class); // TODO ideally we only do this once when the ViewModel gets created viewModel.loadPersonalBlog(); @@ -88,7 +87,7 @@ public class FeedFragment extends BaseFragment list.setEmptyText(R.string.blogs_feed_empty_state); list.setEmptyAction(R.string.blogs_feed_empty_state_action); - viewModel.getAllBlogPosts().observe(getViewLifecycleOwner(), result -> + viewModel.getBlogPosts().observe(getViewLifecycleOwner(), result -> result .onError(this::handleException) .onSuccess(this::onBlogPostsLoaded) @@ -135,6 +134,7 @@ public class FeedFragment extends BaseFragment } else if (wasLocal != null) { showSnackBar(R.string.blogs_blog_post_received); } + list.showData(); }); } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/FeedViewModel.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/FeedViewModel.java index 6f5f44f44..3c18f5774 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/blog/FeedViewModel.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/FeedViewModel.java @@ -17,11 +17,9 @@ 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.bramble.api.system.AndroidExecutor; -import org.briarproject.briar.android.viewmodel.LiveResult; import org.briarproject.briar.api.android.AndroidNotificationManager; import org.briarproject.briar.api.blog.Blog; import org.briarproject.briar.api.blog.BlogManager; -import org.briarproject.briar.api.blog.BlogPostHeader; import org.briarproject.briar.api.blog.event.BlogPostAddedEvent; import java.util.ArrayList; @@ -30,10 +28,8 @@ import java.util.List; import java.util.concurrent.Executor; import java.util.logging.Logger; -import javax.annotation.Nullable; import javax.inject.Inject; -import androidx.annotation.UiThread; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; @@ -49,11 +45,7 @@ class FeedViewModel extends BaseViewModel { private static final Logger LOG = getLogger(FeedViewModel.class.getName()); - protected final AndroidNotificationManager notificationManager; - private final MutableLiveData personalBlog = new MutableLiveData<>(); - @Nullable - private Boolean postAddedWasLocal = null; @Inject FeedViewModel(Application application, @@ -63,11 +55,10 @@ class FeedViewModel extends BaseViewModel { AndroidExecutor androidExecutor, EventBus eventBus, IdentityManager identityManager, - BlogManager blogManager, - AndroidNotificationManager notificationManager) { + AndroidNotificationManager notificationManager, + BlogManager blogManager) { super(application, dbExecutor, lifecycleManager, db, androidExecutor, - eventBus, identityManager, blogManager); - this.notificationManager = notificationManager; + eventBus, identityManager, notificationManager, blogManager); } @Override @@ -122,7 +113,7 @@ class FeedViewModel extends BaseViewModel { } void loadAllBlogPosts() { - loadList(this::loadAllBlogPosts, blogPosts::setValue); + loadList(this::loadAllBlogPosts, this::updateBlogPosts); } @DatabaseExecutor @@ -142,33 +133,4 @@ class FeedViewModel extends BaseViewModel { return posts; } - private void onBlogPostAdded(BlogPostHeader header, boolean local) { - postAddedWasLocal = local; - runOnDbThread(() -> { - try { - db.transaction(true, txn -> { - BlogPostItem item = getItem(txn, header); - txn.attach(() -> { - List items = addListItem(blogPosts, item); - if (items != null) { - Collections.sort(items); - blogPosts.setValue(new LiveResult<>(items)); - } - }); - }); - } catch (DbException e) { - logException(LOG, WARNING, e); - } - }); - } - - @UiThread - @Nullable - Boolean getPostAddedWasLocalAndReset() { - if (postAddedWasLocal == null) return null; - boolean wasLocal = postAddedWasLocal; - postAddedWasLocal = null; - return wasLocal; - } - } From 33c24f8655ca77a9c00a409f9a1e3a4818cc2c2b Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Tue, 12 Jan 2021 09:45:29 -0300 Subject: [PATCH 05/13] Migrate blogs to new SharingController and get rid of the deprecated one --- .../briarproject/briar/android/AppModule.java | 2 +- .../android/activity/ActivityComponent.java | 2 - .../briar/android/blog/BlogActivity.java | 36 +++--- .../briar/android/blog/BlogController.java | 19 --- .../android/blog/BlogControllerImpl.java | 101 ---------------- .../briar/android/blog/BlogFragment.java | 62 +--------- .../briar/android/blog/BlogModule.java | 41 ++----- .../briar/android/blog/BlogPostFragment.java | 2 - .../briar/android/blog/BlogViewModel.java | 52 +++++++-- .../android/controller/SharingController.java | 77 ------------- .../controller/SharingControllerImpl.java | 109 ------------------ 11 files changed, 74 insertions(+), 429 deletions(-) delete mode 100644 briar-android/src/main/java/org/briarproject/briar/android/blog/BlogController.java delete mode 100644 briar-android/src/main/java/org/briarproject/briar/android/blog/BlogControllerImpl.java delete mode 100644 briar-android/src/main/java/org/briarproject/briar/android/controller/SharingController.java delete mode 100644 briar-android/src/main/java/org/briarproject/briar/android/controller/SharingControllerImpl.java diff --git a/briar-android/src/main/java/org/briarproject/briar/android/AppModule.java b/briar-android/src/main/java/org/briarproject/briar/android/AppModule.java index e0d4f31cc..b8db5726f 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/AppModule.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/AppModule.java @@ -86,7 +86,7 @@ import static org.briarproject.briar.android.TestingConstants.IS_DEBUG_BUILD; ContactListModule.class, IntroductionModule.class, // below need to be within same scope as ViewModelProvider.Factory - BlogModule.BindsModule.class, + BlogModule.class, ForumModule.class, GroupListModule.class, GroupConversationModule.class, diff --git a/briar-android/src/main/java/org/briarproject/briar/android/activity/ActivityComponent.java b/briar-android/src/main/java/org/briarproject/briar/android/activity/ActivityComponent.java index 3b1e91ef9..e0f0ce09b 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/activity/ActivityComponent.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/activity/ActivityComponent.java @@ -11,7 +11,6 @@ import org.briarproject.briar.android.account.SetupActivity; import org.briarproject.briar.android.account.UnlockActivity; import org.briarproject.briar.android.blog.BlogActivity; import org.briarproject.briar.android.blog.BlogFragment; -import org.briarproject.briar.android.blog.BlogModule; import org.briarproject.briar.android.blog.BlogPostFragment; import org.briarproject.briar.android.blog.FeedFragment; import org.briarproject.briar.android.blog.FeedPostFragment; @@ -85,7 +84,6 @@ import dagger.Component; @ActivityScope @Component(modules = { ActivityModule.class, - BlogModule.class, CreateGroupModule.class, GroupInvitationModule.class, GroupMemberModule.class, diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogActivity.java index bbe9b6961..be7b9b9ba 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogActivity.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogActivity.java @@ -18,6 +18,8 @@ import javax.inject.Inject; import androidx.appcompat.widget.Toolbar; import androidx.lifecycle.ViewModelProvider; +import static java.util.Objects.requireNonNull; + @MethodsNotNullByDefault @ParametersNotNullByDefault public class BlogActivity extends BriarActivity @@ -25,8 +27,6 @@ public class BlogActivity extends BriarActivity @Inject ViewModelProvider.Factory viewModelFactory; - @Inject - BlogController blogController; private BlogViewModel viewModel; @@ -43,28 +43,36 @@ public class BlogActivity extends BriarActivity // GroupId from Intent Intent i = getIntent(); - byte[] b = i.getByteArrayExtra(GROUP_ID); - if (b == null) throw new IllegalStateException("No group ID in intent"); - GroupId groupId = new GroupId(b); - blogController.setGroupId(groupId); + GroupId groupId = + new GroupId(requireNonNull(i.getByteArrayExtra(GROUP_ID))); viewModel.setGroupId(groupId); setContentView(R.layout.activity_fragment_container_toolbar); Toolbar toolbar = setUpCustomToolbar(false); // Open Sharing Status on Toolbar click - if (toolbar != null) { - toolbar.setOnClickListener(v -> { - Intent i1 = new Intent(BlogActivity.this, - BlogSharingStatusActivity.class); - i1.putExtra(GROUP_ID, groupId.getBytes()); - startActivity(i1); - }); - } + toolbar.setOnClickListener(v -> { + Intent i1 = new Intent(BlogActivity.this, + BlogSharingStatusActivity.class); + i1.putExtra(GROUP_ID, groupId.getBytes()); + startActivity(i1); + }); + + viewModel.getBlog().observe(this, blog -> + setTitle(blog.getBlog().getAuthor().getName()) + ); + viewModel.getSharingInfo().observe(this, info -> + setToolbarSubTitle(info.total, info.online) + ); if (state == null) { showInitialFragment(BlogFragment.newInstance(groupId)); } } + private void setToolbarSubTitle(int total, int online) { + requireNonNull(getSupportActionBar()) + .setSubtitle(getString(R.string.shared_with, total, online)); + } + } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogController.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogController.java deleted file mode 100644 index 5ce9df167..000000000 --- a/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogController.java +++ /dev/null @@ -1,19 +0,0 @@ -package org.briarproject.briar.android.blog; - -import org.briarproject.bramble.api.contact.ContactId; -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.handler.ResultExceptionHandler; - -import java.util.Collection; - -@NotNullByDefault -public interface BlogController { - - void setGroupId(GroupId g); - - void loadSharingContacts( - ResultExceptionHandler, DbException> handler); - -} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogControllerImpl.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogControllerImpl.java deleted file mode 100644 index 10f59ab8b..000000000 --- a/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogControllerImpl.java +++ /dev/null @@ -1,101 +0,0 @@ -package org.briarproject.briar.android.blog; - -import android.app.Activity; - -import org.briarproject.bramble.api.contact.Contact; -import org.briarproject.bramble.api.contact.ContactId; -import org.briarproject.bramble.api.db.DatabaseExecutor; -import org.briarproject.bramble.api.db.DbException; -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.IdentityManager; -import org.briarproject.bramble.api.lifecycle.LifecycleManager; -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.android.controller.ActivityLifecycleController; -import org.briarproject.briar.android.controller.handler.ResultExceptionHandler; -import org.briarproject.briar.api.android.AndroidNotificationManager; -import org.briarproject.briar.api.blog.BlogManager; -import org.briarproject.briar.api.blog.BlogSharingManager; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.concurrent.Executor; -import java.util.logging.Logger; - -import javax.inject.Inject; - -import static java.util.logging.Level.WARNING; -import static org.briarproject.bramble.util.LogUtils.logException; - -@MethodsNotNullByDefault -@ParametersNotNullByDefault -class BlogControllerImpl extends BaseControllerImpl - implements ActivityLifecycleController, BlogController, EventListener { - - private static final Logger LOG = - Logger.getLogger(BlogControllerImpl.class.getName()); - - private final BlogSharingManager blogSharingManager; - - private volatile GroupId groupId = null; - - @Inject - BlogControllerImpl(@DatabaseExecutor Executor dbExecutor, - LifecycleManager lifecycleManager, EventBus eventBus, - AndroidNotificationManager notificationManager, - IdentityManager identityManager, BlogManager blogManager, - BlogSharingManager blogSharingManager) { - super(dbExecutor, lifecycleManager, eventBus, notificationManager, - identityManager, blogManager); - this.blogSharingManager = blogSharingManager; - } - - @Override - public void onActivityCreate(Activity activity) { - } - - @Override - public void onActivityStart() { - } - - @Override - public void onActivityStop() { - } - - @Override - public void onActivityDestroy() { - } - - @Override - public void setGroupId(GroupId g) { - groupId = g; - } - - @Override - public void eventOccurred(Event e) { - - } - - @Override - public void loadSharingContacts( - ResultExceptionHandler, DbException> handler) { - if (groupId == null) throw new IllegalStateException(); - runOnDbThread(() -> { - try { - Collection contacts = - blogSharingManager.getSharedWith(groupId); - Collection contactIds = - new ArrayList<>(contacts.size()); - for (Contact c : contacts) contactIds.add(c.getId()); - handler.onResult(contactIds); - } catch (DbException e) { - logException(LOG, WARNING, e); - handler.onException(e); - } - }); - } - -} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogFragment.java index 2ab3bfc12..c8f27d41e 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogFragment.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogFragment.java @@ -11,17 +11,11 @@ import android.view.View; import android.view.ViewGroup; import android.widget.Toast; -import org.briarproject.bramble.api.contact.ContactId; -import org.briarproject.bramble.api.db.DbException; -import org.briarproject.bramble.api.identity.Author; 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.activity.BriarActivity; -import org.briarproject.briar.android.controller.SharingController; -import org.briarproject.briar.android.controller.handler.UiResultExceptionHandler; import org.briarproject.briar.android.fragment.BaseFragment; import org.briarproject.briar.android.sharing.BlogSharingStatusActivity; import org.briarproject.briar.android.sharing.ShareBlogActivity; @@ -29,14 +23,12 @@ import org.briarproject.briar.android.util.BriarSnackbarBuilder; import org.briarproject.briar.android.view.BriarRecyclerView; import org.briarproject.briar.android.widget.LinkDialogFragment; -import java.util.Collection; import java.util.List; import javax.inject.Inject; import androidx.annotation.Nullable; import androidx.annotation.UiThread; -import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AlertDialog; import androidx.lifecycle.ViewModelProvider; import androidx.recyclerview.widget.LinearLayoutManager; @@ -49,22 +41,17 @@ import static com.google.android.material.snackbar.Snackbar.LENGTH_LONG; import static org.briarproject.briar.android.activity.BriarActivity.GROUP_ID; import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_SHARE_BLOG; import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_WRITE_BLOG_POST; -import static org.briarproject.briar.android.controller.SharingController.SharingListener; @UiThread @MethodsNotNullByDefault @ParametersNotNullByDefault public class BlogFragment extends BaseFragment - implements SharingListener, OnBlogPostClickListener { + implements OnBlogPostClickListener { private final static String TAG = BlogFragment.class.getName(); @Inject ViewModelProvider.Factory viewModelFactory; - @Inject - BlogController blogController; - @Inject - SharingController sharingController; @Nullable private Parcelable layoutManagerState; @@ -89,7 +76,6 @@ public class BlogFragment extends BaseFragment component.inject(this); viewModel = new ViewModelProvider(requireActivity(), viewModelFactory) .get(BlogViewModel.class); - sharingController.setSharingListener(this); } @Nullable @@ -132,8 +118,6 @@ public class BlogFragment extends BaseFragment super.onStart(); viewModel.blockNotifications(); viewModel.clearBlogPostNotifications(); - sharingController.onStart(); - loadSharedContacts(); list.startPeriodicUpdate(); } @@ -141,16 +125,9 @@ public class BlogFragment extends BaseFragment public void onStop() { super.onStop(); viewModel.unblockNotifications(); - sharingController.onStop(); list.stopPeriodicUpdate(); } - @Override - public void onDestroy() { - super.onDestroy(); - sharingController.unsetSharingListener(this); - } - @Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); @@ -166,7 +143,6 @@ public class BlogFragment extends BaseFragment MenuItem writeButton = menu.findItem(R.id.action_write_blog_post); MenuItem deleteButton = menu.findItem(R.id.action_blog_delete); viewModel.getBlog().observe(getViewLifecycleOwner(), blog -> { - setToolbarTitle(blog.getBlog().getAuthor()); if (blog.isOurs()) writeButton.setVisible(true); if (blog.canBeRemoved()) deleteButton.setEnabled(true); }); @@ -265,42 +241,6 @@ public class BlogFragment extends BaseFragment f.show(getParentFragmentManager(), f.getUniqueTag()); } - private void setToolbarTitle(Author a) { - requireActivity().setTitle(a.getName()); - } - - private void loadSharedContacts() { - blogController.loadSharingContacts( - new UiResultExceptionHandler, - DbException>(this) { - @Override - public void onResultUi(Collection contacts) { - sharingController.addAll(contacts); - int online = sharingController.getOnlineCount(); - setToolbarSubTitle(contacts.size(), online); - } - - @Override - public void onExceptionUi(DbException exception) { - handleException(exception); - } - }); - } - - @Override - public void onSharingInfoUpdated(int total, int online) { - setToolbarSubTitle(total, online); - } - - private void setToolbarSubTitle(int total, int online) { - ActionBar actionBar = - ((BriarActivity) requireActivity()).getSupportActionBar(); - if (actionBar != null) { - actionBar.setSubtitle( - getString(R.string.shared_with, total, online)); - } - } - private void displaySnackbar(int stringId, boolean scroll) { BriarSnackbarBuilder sb = new BriarSnackbarBuilder(); if (scroll) { diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogModule.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogModule.java index a6cf55036..5eafcb2eb 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogModule.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogModule.java @@ -1,46 +1,23 @@ package org.briarproject.briar.android.blog; -import org.briarproject.briar.android.activity.ActivityScope; -import org.briarproject.briar.android.activity.BaseActivity; -import org.briarproject.briar.android.controller.SharingController; -import org.briarproject.briar.android.controller.SharingControllerImpl; 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 BlogModule { +public interface BlogModule { - @Module - public abstract static class BindsModule { - @Binds - @IntoMap - @ViewModelKey(FeedViewModel.class) - abstract ViewModel bindFeedViewModel(FeedViewModel feedViewModel); + @Binds + @IntoMap + @ViewModelKey(FeedViewModel.class) + abstract ViewModel bindFeedViewModel(FeedViewModel feedViewModel); - @Binds - @IntoMap - @ViewModelKey(BlogViewModel.class) - abstract ViewModel bindBlogViewModel(BlogViewModel blogViewModel); - } - - @ActivityScope - @Provides - BlogController provideBlogController(BaseActivity activity, - BlogControllerImpl blogController) { - activity.addLifecycleController(blogController); - return blogController; - } - - @ActivityScope - @Provides - SharingController provideSharingController( - SharingControllerImpl sharingController) { - return sharingController; - } + @Binds + @IntoMap + @ViewModelKey(BlogViewModel.class) + abstract ViewModel bindBlogViewModel(BlogViewModel blogViewModel); } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogPostFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogPostFragment.java index 7e201577c..114e73fe8 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogPostFragment.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogPostFragment.java @@ -25,8 +25,6 @@ public class BlogPostFragment extends BasePostFragment { @Inject ViewModelProvider.Factory viewModelFactory; - @Inject - BlogController blogController; private BlogViewModel viewModel; diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogViewModel.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogViewModel.java index 0d8e6d5c0..88e84e905 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogViewModel.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogViewModel.java @@ -2,6 +2,8 @@ package org.briarproject.briar.android.blog; import android.app.Application; +import org.briarproject.bramble.api.contact.Contact; +import org.briarproject.bramble.api.contact.ContactId; import org.briarproject.bramble.api.db.DatabaseExecutor; import org.briarproject.bramble.api.db.DbException; import org.briarproject.bramble.api.db.TransactionManager; @@ -10,20 +12,26 @@ import org.briarproject.bramble.api.event.EventBus; import org.briarproject.bramble.api.identity.IdentityManager; import org.briarproject.bramble.api.identity.LocalAuthor; import org.briarproject.bramble.api.lifecycle.LifecycleManager; -import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault; +import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; import org.briarproject.bramble.api.sync.GroupId; import org.briarproject.bramble.api.sync.MessageId; import org.briarproject.bramble.api.sync.event.GroupRemovedEvent; import org.briarproject.bramble.api.system.AndroidExecutor; +import org.briarproject.briar.android.sharing.SharingController; +import org.briarproject.briar.android.sharing.SharingController.SharingInfo; import org.briarproject.briar.android.viewmodel.LiveResult; import org.briarproject.briar.api.android.AndroidNotificationManager; import org.briarproject.briar.api.blog.Blog; import org.briarproject.briar.api.blog.BlogInvitationResponse; import org.briarproject.briar.api.blog.BlogManager; +import org.briarproject.briar.api.blog.BlogSharingManager; import org.briarproject.briar.api.blog.event.BlogInvitationResponseReceivedEvent; import org.briarproject.briar.api.blog.event.BlogPostAddedEvent; import org.briarproject.briar.api.sharing.event.ContactLeftShareableEvent; +import java.util.ArrayList; +import java.util.Collection; import java.util.concurrent.Executor; import java.util.logging.Logger; @@ -38,13 +46,16 @@ import static org.briarproject.bramble.util.LogUtils.logDuration; import static org.briarproject.bramble.util.LogUtils.logException; import static org.briarproject.bramble.util.LogUtils.now; -@NotNullByDefault +@MethodsNotNullByDefault +@ParametersNotNullByDefault class BlogViewModel extends BaseViewModel { private static final Logger LOG = getLogger(BlogViewModel.class.getName()); - // implicitly non-null - private volatile GroupId groupId = null; + private final BlogSharingManager blogSharingManager; + private final SharingController sharingController; + + private volatile GroupId groupId; private final MutableLiveData blog = new MutableLiveData<>(); private final MutableLiveData blogRemoved = @@ -59,9 +70,13 @@ class BlogViewModel extends BaseViewModel { EventBus eventBus, IdentityManager identityManager, AndroidNotificationManager notificationManager, - BlogManager blogManager) { + BlogManager blogManager, + BlogSharingManager blogSharingManager, + SharingController sharingController) { super(application, dbExecutor, lifecycleManager, db, androidExecutor, eventBus, identityManager, notificationManager, blogManager); + this.blogSharingManager = blogSharingManager; + this.sharingController = sharingController; } @Override @@ -78,17 +93,13 @@ class BlogViewModel extends BaseViewModel { BlogInvitationResponse r = b.getMessageHeader(); if (r.getShareableId().equals(groupId) && r.wasAccepted()) { LOG.info("Blog invitation accepted"); - // TODO -// onBlogInvitationAccepted(b.getContactId()); -// sharingController.add(c); + sharingController.add(b.getContactId()); } } else if (e instanceof ContactLeftShareableEvent) { ContactLeftShareableEvent s = (ContactLeftShareableEvent) e; if (s.getGroupId().equals(groupId)) { LOG.info("Blog left by contact"); - // TODO -// onBlogLeft(s.getContactId()); -// sharingController.remove(c); + sharingController.remove(s.getContactId()); } } else if (e instanceof GroupRemovedEvent) { GroupRemovedEvent g = (GroupRemovedEvent) e; @@ -106,6 +117,7 @@ class BlogViewModel extends BaseViewModel { this.groupId = groupId; loadBlog(groupId); loadBlogPosts(groupId); + loadSharingContacts(groupId); } private void loadBlog(GroupId groupId) { @@ -140,6 +152,21 @@ class BlogViewModel extends BaseViewModel { loadList(txn -> loadBlogPosts(txn, groupId), this::updateBlogPosts); } + private void loadSharingContacts(GroupId groupId) { + runOnDbThread(() -> { + try { + Collection contacts = + blogSharingManager.getSharedWith(groupId); + Collection contactIds = + new ArrayList<>(contacts.size()); + for (Contact c : contacts) contactIds.add(c.getId()); + sharingController.addAll(contactIds); + } catch (DbException e) { + logException(LOG, WARNING, e); + } + }); + } + void deleteBlog() { runOnDbThread(() -> { try { @@ -165,4 +192,7 @@ class BlogViewModel extends BaseViewModel { return blogRemoved; } + LiveData getSharingInfo() { + return sharingController.getSharingInfo(); + } } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/controller/SharingController.java b/briar-android/src/main/java/org/briarproject/briar/android/controller/SharingController.java deleted file mode 100644 index 3ebe7024f..000000000 --- a/briar-android/src/main/java/org/briarproject/briar/android/controller/SharingController.java +++ /dev/null @@ -1,77 +0,0 @@ -package org.briarproject.briar.android.controller; - -import org.briarproject.bramble.api.contact.ContactId; -import org.briarproject.bramble.api.nullsafety.NotNullByDefault; - -import java.util.Collection; - -import androidx.annotation.UiThread; - -@Deprecated -@NotNullByDefault -public interface SharingController { - - /** - * Sets the listener that is called when contacts go on or offline. - */ - @UiThread - void setSharingListener(SharingListener listener); - - /** - * Unsets the listener. - */ - @UiThread - void unsetSharingListener(SharingListener listener); - - /** - * Call this when your lifecycle starts, - * so the listener will be called when information changes. - */ - @UiThread - void onStart(); - - /** - * Call this when your lifecycle stops, - * so that the controller knows it can stops listening to events. - */ - @UiThread - void onStop(); - - /** - * Adds one contact to be tracked. - */ - @UiThread - void add(ContactId c); - - /** - * Adds a collection of contacts to be tracked. - */ - @UiThread - void addAll(Collection contacts); - - /** - * Call this when the contact identified by c is no longer sharing - * the given group identified by GroupId g. - */ - @UiThread - void remove(ContactId c); - - /** - * Returns the number of online contacts. - */ - @UiThread - int getOnlineCount(); - - /** - * Returns the total number of contacts that have been added. - */ - @UiThread - int getTotalCount(); - - interface SharingListener { - - @UiThread - void onSharingInfoUpdated(int total, int online); - } - -} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/controller/SharingControllerImpl.java b/briar-android/src/main/java/org/briarproject/briar/android/controller/SharingControllerImpl.java deleted file mode 100644 index b49eb6aa8..000000000 --- a/briar-android/src/main/java/org/briarproject/briar/android/controller/SharingControllerImpl.java +++ /dev/null @@ -1,109 +0,0 @@ -package org.briarproject.briar.android.controller; - -import org.briarproject.bramble.api.connection.ConnectionRegistry; -import org.briarproject.bramble.api.contact.ContactId; -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.nullsafety.NotNullByDefault; -import org.briarproject.bramble.api.plugin.event.ContactConnectedEvent; -import org.briarproject.bramble.api.plugin.event.ContactDisconnectedEvent; - -import java.util.Collection; -import java.util.HashSet; -import java.util.Set; - -import javax.annotation.Nullable; -import javax.inject.Inject; - -import androidx.annotation.UiThread; - -@Deprecated -@NotNullByDefault -public class SharingControllerImpl implements SharingController, EventListener { - - private final EventBus eventBus; - private final ConnectionRegistry connectionRegistry; - - // UI thread - private final Set contacts = new HashSet<>(); - - // UI thread - @Nullable - private SharingListener listener; - - @Inject - SharingControllerImpl(EventBus eventBus, - ConnectionRegistry connectionRegistry) { - this.eventBus = eventBus; - this.connectionRegistry = connectionRegistry; - } - - @Override - public void setSharingListener(SharingListener listener) { - this.listener = listener; - } - - @Override - public void unsetSharingListener(SharingListener listener) { - if (this.listener == listener) this.listener = null; - } - - @Override - public void onStart() { - eventBus.addListener(this); - } - - @Override - public void onStop() { - eventBus.removeListener(this); - } - - @Override - public void eventOccurred(Event e) { - if (e instanceof ContactConnectedEvent) { - setConnected(((ContactConnectedEvent) e).getContactId()); - } else if (e instanceof ContactDisconnectedEvent) { - setConnected(((ContactDisconnectedEvent) e).getContactId()); - } - } - - @UiThread - private void setConnected(ContactId c) { - if (listener == null) throw new IllegalStateException(); - if (contacts.contains(c)) { - int online = getOnlineCount(); - listener.onSharingInfoUpdated(contacts.size(), online); - } - } - - @Override - public void addAll(Collection c) { - contacts.addAll(c); - } - - @Override - public void add(ContactId c) { - contacts.add(c); - } - - @Override - public void remove(ContactId c) { - contacts.remove(c); - } - - @Override - public int getOnlineCount() { - int online = 0; - for (ContactId c : contacts) { - if (connectionRegistry.isConnected(c)) online++; - } - return online; - } - - @Override - public int getTotalCount() { - return contacts.size(); - } - -} From 6860a04e8b9560ff6ae70aa64ecd40ce227d6ef1 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Tue, 12 Jan 2021 10:21:50 -0300 Subject: [PATCH 06/13] Don't use layoutManager hack to restore scrolling position of blogs not needed anymore when posts are cached in viewmodels --- .../briar/android/blog/BlogFragment.java | 27 +------------------ .../briar/android/blog/FeedFragment.java | 17 ------------ 2 files changed, 1 insertion(+), 43 deletions(-) diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogFragment.java index c8f27d41e..a3c836097 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogFragment.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogFragment.java @@ -2,7 +2,6 @@ package org.briarproject.briar.android.blog; import android.content.Intent; import android.os.Bundle; -import android.os.Parcelable; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; @@ -52,13 +51,10 @@ public class BlogFragment extends BaseFragment @Inject ViewModelProvider.Factory viewModelFactory; - @Nullable - private Parcelable layoutManagerState; private GroupId groupId; private BlogViewModel viewModel; private final BlogPostAdapter adapter = new BlogPostAdapter(this); - private LayoutManager layoutManager; private BriarRecyclerView list; static BlogFragment newInstance(GroupId groupId) { @@ -91,7 +87,7 @@ public class BlogFragment extends BaseFragment View v = inflater.inflate(R.layout.fragment_blog, container, false); list = v.findViewById(R.id.postList); - layoutManager = new LinearLayoutManager(getActivity()); + LayoutManager layoutManager = new LinearLayoutManager(getActivity()); list.setLayoutManager(layoutManager); list.setAdapter(adapter); list.showProgressBar(); @@ -104,12 +100,6 @@ public class BlogFragment extends BaseFragment viewModel.getBlogRemoved().observe(getViewLifecycleOwner(), removed -> { if (removed) finish(); }); - - if (savedInstanceState != null) { - layoutManagerState = - savedInstanceState.getParcelable("layoutManager"); - } - return v; } @@ -128,15 +118,6 @@ public class BlogFragment extends BaseFragment list.stopPeriodicUpdate(); } - @Override - public void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - if (layoutManager != null) { - layoutManagerState = layoutManager.onSaveInstanceState(); - outState.putParcelable("layoutManager", layoutManagerState); - } - } - @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { inflater.inflate(R.menu.blogs_blog_actions, menu); @@ -208,12 +189,6 @@ public class BlogFragment extends BaseFragment true); } list.showData(); - if (layoutManagerState == null) { - list.scrollToPosition(0); - } else { - layoutManager.onRestoreInstanceState( - layoutManagerState); - } }); } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/FeedFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/FeedFragment.java index 218d6dfcc..66380a582 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/blog/FeedFragment.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/FeedFragment.java @@ -2,7 +2,6 @@ package org.briarproject.briar.android.blog; import android.content.Intent; import android.os.Bundle; -import android.os.Parcelable; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; @@ -48,8 +47,6 @@ public class FeedFragment extends BaseFragment private final BlogPostAdapter adapter = new BlogPostAdapter(this); private LinearLayoutManager layoutManager; private BriarRecyclerView list; - @Nullable - private Parcelable layoutManagerState; public static FeedFragment newInstance() { FeedFragment f = new FeedFragment(); @@ -93,11 +90,6 @@ public class FeedFragment extends BaseFragment .onSuccess(this::onBlogPostsLoaded) ); - if (savedInstanceState != null) { - layoutManagerState = - savedInstanceState.getParcelable("layoutManager"); - } - return v; } @@ -116,15 +108,6 @@ public class FeedFragment extends BaseFragment list.stopPeriodicUpdate(); } - @Override - public void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - if (layoutManager != null) { - layoutManagerState = layoutManager.onSaveInstanceState(); - outState.putParcelable("layoutManager", layoutManagerState); - } - } - private void onBlogPostsLoaded(List items) { if (items.isEmpty()) list.showData(); else adapter.submitList(items, () -> { From 95104d33833822b6481082772ab9ee16ddb2b18c Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Tue, 12 Jan 2021 14:54:17 -0300 Subject: [PATCH 07/13] Clean up after migrating blog controllers to view model --- .../android/activity/ActivityComponent.java | 3 - .../android/blog/BaseControllerImpl.java | 37 ----- .../briar/android/blog/BasePostFragment.java | 128 ------------------ .../briar/android/blog/BaseViewModel.java | 49 +------ .../briar/android/blog/BlogCommentItem.java | 4 +- .../briar/android/blog/BlogFragment.java | 4 +- .../briar/android/blog/BlogModule.java | 4 +- .../briar/android/blog/BlogPostFragment.java | 126 +++++++++++++++-- .../briar/android/blog/BlogPostItem.java | 2 +- .../android/blog/BlogPostViewHolder.java | 16 ++- .../briar/android/blog/BlogViewModel.java | 4 +- .../briar/android/blog/FeedFragment.java | 7 +- .../briar/android/blog/FeedPostFragment.java | 73 ---------- .../briar/android/blog/FeedViewModel.java | 41 +++++- .../briar/android/blog/ReblogActivity.java | 12 +- .../briar/android/blog/ReblogFragment.java | 43 +++--- .../android/forum/ForumListViewModel.java | 4 +- .../privategroup/list/GroupListViewModel.java | 5 +- .../briar/android/viewmodel/DbViewModel.java | 43 +++++- 19 files changed, 246 insertions(+), 359 deletions(-) delete mode 100644 briar-android/src/main/java/org/briarproject/briar/android/blog/BaseControllerImpl.java delete mode 100644 briar-android/src/main/java/org/briarproject/briar/android/blog/BasePostFragment.java delete mode 100644 briar-android/src/main/java/org/briarproject/briar/android/blog/FeedPostFragment.java diff --git a/briar-android/src/main/java/org/briarproject/briar/android/activity/ActivityComponent.java b/briar-android/src/main/java/org/briarproject/briar/android/activity/ActivityComponent.java index e0f0ce09b..e5914e1cf 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/activity/ActivityComponent.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/activity/ActivityComponent.java @@ -13,7 +13,6 @@ import org.briarproject.briar.android.blog.BlogActivity; import org.briarproject.briar.android.blog.BlogFragment; import org.briarproject.briar.android.blog.BlogPostFragment; import org.briarproject.briar.android.blog.FeedFragment; -import org.briarproject.briar.android.blog.FeedPostFragment; import org.briarproject.briar.android.blog.ReblogActivity; import org.briarproject.briar.android.blog.ReblogFragment; import org.briarproject.briar.android.blog.RssFeedImportActivity; @@ -150,8 +149,6 @@ public interface ActivityComponent { void inject(BlogPostFragment fragment); - void inject(FeedPostFragment fragment); - void inject(ReblogFragment fragment); void inject(ReblogActivity activity); diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/BaseControllerImpl.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/BaseControllerImpl.java deleted file mode 100644 index d95a5c395..000000000 --- a/briar-android/src/main/java/org/briarproject/briar/android/blog/BaseControllerImpl.java +++ /dev/null @@ -1,37 +0,0 @@ -package org.briarproject.briar.android.blog; - -import org.briarproject.bramble.api.db.DatabaseExecutor; -import org.briarproject.bramble.api.event.EventBus; -import org.briarproject.bramble.api.event.EventListener; -import org.briarproject.bramble.api.identity.IdentityManager; -import org.briarproject.bramble.api.lifecycle.LifecycleManager; -import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault; -import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; -import org.briarproject.briar.android.controller.DbControllerImpl; -import org.briarproject.briar.api.android.AndroidNotificationManager; -import org.briarproject.briar.api.blog.BlogManager; - -import java.util.concurrent.Executor; - -@MethodsNotNullByDefault -@ParametersNotNullByDefault -abstract class BaseControllerImpl extends DbControllerImpl - implements EventListener { - - protected final EventBus eventBus; - protected final AndroidNotificationManager notificationManager; - protected final IdentityManager identityManager; - protected final BlogManager blogManager; - - BaseControllerImpl(@DatabaseExecutor Executor dbExecutor, - LifecycleManager lifecycleManager, EventBus eventBus, - AndroidNotificationManager notificationManager, - IdentityManager identityManager, BlogManager blogManager) { - super(dbExecutor, lifecycleManager); - this.eventBus = eventBus; - this.notificationManager = notificationManager; - this.identityManager = identityManager; - this.blogManager = blogManager; - } - -} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/BasePostFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/BasePostFragment.java deleted file mode 100644 index d09f63a5f..000000000 --- a/briar-android/src/main/java/org/briarproject/briar/android/blog/BasePostFragment.java +++ /dev/null @@ -1,128 +0,0 @@ -package org.briarproject.briar.android.blog; - -import android.content.Intent; -import android.os.Bundle; -import android.os.Handler; -import android.os.Looper; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ProgressBar; - -import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault; -import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; -import org.briarproject.bramble.api.sync.MessageId; -import org.briarproject.briar.R; -import org.briarproject.briar.android.fragment.BaseFragment; -import org.briarproject.briar.android.widget.LinkDialogFragment; - -import java.util.logging.Logger; - -import javax.annotation.Nullable; - -import androidx.annotation.CallSuper; -import androidx.annotation.UiThread; - -import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP; -import static android.view.View.INVISIBLE; -import static android.view.View.VISIBLE; -import static java.util.logging.Logger.getLogger; -import static org.briarproject.briar.android.activity.BriarActivity.GROUP_ID; -import static org.briarproject.briar.android.util.UiUtils.MIN_DATE_RESOLUTION; - -@UiThread -@MethodsNotNullByDefault -@ParametersNotNullByDefault -abstract class BasePostFragment extends BaseFragment { - - static final String POST_ID = "briar.POST_ID"; - - private static final Logger LOG = - getLogger(BasePostFragment.class.getName()); - - private final Handler handler = new Handler(Looper.getMainLooper()); - - protected MessageId postId; - private ProgressBar progressBar; - private BlogPostViewHolder ui; - private BlogPostItem post; - private Runnable refresher; - - @CallSuper - @Nullable - @Override - public View onCreateView(LayoutInflater inflater, - @Nullable ViewGroup container, - @Nullable Bundle savedInstanceState) { - // retrieve MessageId of blog post from arguments - byte[] p = requireArguments().getByteArray(POST_ID); - if (p == null) throw new IllegalStateException("No post ID in args"); - postId = new MessageId(p); - - View view = inflater.inflate(R.layout.fragment_blog_post, container, - false); - progressBar = view.findViewById(R.id.progressBar); - progressBar.setVisibility(VISIBLE); - ui = new BlogPostViewHolder(view, true, new OnBlogPostClickListener() { - @Override - public void onBlogPostClick(BlogPostItem post) { - // We're already there - } - - @Override - public void onAuthorClick(BlogPostItem post) { - if (getContext() == null) return; - Intent i = new Intent(getContext(), BlogActivity.class); - i.putExtra(GROUP_ID, post.getGroupId().getBytes()); - i.setFlags(FLAG_ACTIVITY_CLEAR_TOP); - getContext().startActivity(i); - } - - @Override - public void onLinkClick(String url) { - LinkDialogFragment f = LinkDialogFragment.newInstance(url); - f.show(getParentFragmentManager(), f.getUniqueTag()); - } - }); - return view; - } - - @CallSuper - @Override - public void onStart() { - super.onStart(); - startPeriodicUpdate(); - } - - @CallSuper - @Override - public void onStop() { - super.onStop(); - stopPeriodicUpdate(); - } - - @UiThread - protected void onBlogPostLoaded(BlogPostItem post) { - progressBar.setVisibility(INVISIBLE); - this.post = post; - ui.bindItem(post); - } - - private void startPeriodicUpdate() { - refresher = () -> { - LOG.info("Updating Content..."); - ui.updateDate(post.getTimestamp()); - handler.postDelayed(refresher, MIN_DATE_RESOLUTION); - }; - LOG.info("Adding Handler Callback"); - handler.postDelayed(refresher, MIN_DATE_RESOLUTION); - } - - private void stopPeriodicUpdate() { - if (refresher != null) { - LOG.info("Removing Handler Callback"); - handler.removeCallbacks(refresher); - } - } - -} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/BaseViewModel.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/BaseViewModel.java index d6690c4c2..e61ae3c38 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/blog/BaseViewModel.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/BaseViewModel.java @@ -6,7 +6,6 @@ import org.briarproject.bramble.api.db.DatabaseExecutor; 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.IdentityManager; @@ -28,13 +27,10 @@ import org.briarproject.briar.util.HtmlUtils; import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executor; import java.util.logging.Logger; import javax.annotation.Nullable; -import javax.inject.Inject; import androidx.annotation.UiThread; import androidx.lifecycle.LiveData; @@ -48,27 +44,23 @@ import static org.briarproject.bramble.util.LogUtils.now; import static org.briarproject.briar.util.HtmlUtils.ARTICLE; @NotNullByDefault -public class BaseViewModel extends DbViewModel implements EventListener { +abstract class BaseViewModel extends DbViewModel implements EventListener { private static final Logger LOG = getLogger(BaseViewModel.class.getName()); - protected final TransactionManager db; private final EventBus eventBus; + protected final TransactionManager db; protected final IdentityManager identityManager; protected final AndroidNotificationManager notificationManager; protected final BlogManager blogManager; - private final MutableLiveData>> blogPosts = + protected final MutableLiveData>> blogPosts = new MutableLiveData<>(); - // TODO do we still need those caches? - private final Map textCache = new ConcurrentHashMap<>(); - private final Map headerCache = - new ConcurrentHashMap<>(); + // UI thread @Nullable private Boolean postAddedWasLocal = null; - @Inject BaseViewModel(Application application, @DatabaseExecutor Executor dbExecutor, LifecycleManager lifecycleManager, @@ -84,7 +76,6 @@ public class BaseViewModel extends DbViewModel implements EventListener { this.identityManager = identityManager; this.notificationManager = notificationManager; this.blogManager = blogManager; - eventBus.addListener(this); } @@ -94,10 +85,6 @@ public class BaseViewModel extends DbViewModel implements EventListener { eventBus.removeListener(this); } - @Override - public void eventOccurred(Event e) { - } - @DatabaseExecutor protected List loadBlogPosts(Transaction txn, GroupId groupId) throws DbException { @@ -108,7 +95,6 @@ public class BaseViewModel extends DbViewModel implements EventListener { List items = new ArrayList<>(headers.size()); start = now(); for (BlogPostHeader h : headers) { - headerCache.put(h.getId(), h); BlogPostItem item = getItem(txn, h); items.add(item); } @@ -135,12 +121,7 @@ public class BaseViewModel extends DbViewModel implements EventListener { @DatabaseExecutor private String getPostText(Transaction txn, MessageId m) throws DbException { - String text = textCache.get(m); - if (text == null) { - text = HtmlUtils.clean(blogManager.getPostText(txn, m), ARTICLE); - textCache.put(m, text); - } - return text; + return HtmlUtils.clean(blogManager.getPostText(txn, m), ARTICLE); } LiveData> loadBlogPost(GroupId g, MessageId m) { @@ -149,7 +130,7 @@ public class BaseViewModel extends DbViewModel implements EventListener { runOnDbThread(() -> { try { long start = now(); - BlogPostHeader header = getPostHeader(g, m); + BlogPostHeader header = blogManager.getPostHeader(g, m); BlogPostItem item = db.transactionWithResult(true, txn -> getItem(txn, header) ); @@ -163,24 +144,7 @@ public class BaseViewModel extends DbViewModel implements EventListener { return result; } - @DatabaseExecutor - private BlogPostHeader getPostHeader(GroupId g, MessageId m) - throws DbException { - BlogPostHeader header = headerCache.get(m); - if (header == null) { - header = blogManager.getPostHeader(g, m); - headerCache.put(m, header); - } - return header; - } - - @UiThread - protected void updateBlogPosts(LiveResult> posts) { - blogPosts.setValue(posts); - } - protected void onBlogPostAdded(BlogPostHeader header, boolean local) { - postAddedWasLocal = local; runOnDbThread(() -> { try { db.transaction(true, txn -> { @@ -189,6 +153,7 @@ public class BaseViewModel extends DbViewModel implements EventListener { List items = addListItem(blogPosts, item); if (items != null) { Collections.sort(items); + postAddedWasLocal = local; blogPosts.setValue(new LiveResult<>(items)); } }); diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogCommentItem.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogCommentItem.java index 89a6e7fa4..6894088d0 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogCommentItem.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogCommentItem.java @@ -8,7 +8,9 @@ import java.util.Collections; import java.util.Comparator; import java.util.List; -// This class is not thread-safe +import javax.annotation.concurrent.NotThreadSafe; + +@NotThreadSafe class BlogCommentItem extends BlogPostItem { private static final BlogCommentComparator COMPARATOR = diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogFragment.java index a3c836097..63ce4954d 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogFragment.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogFragment.java @@ -166,7 +166,6 @@ public class BlogFragment extends BaseFragment if (request == REQUEST_WRITE_BLOG_POST && result == RESULT_OK) { displaySnackbar(R.string.blogs_blog_post_created, true); - viewModel.loadBlogPosts(groupId); } else if (request == REQUEST_SHARE_BLOG && result == RESULT_OK) { displaySnackbar(R.string.blogs_sharing_snackbar, false); } @@ -194,7 +193,8 @@ public class BlogFragment extends BaseFragment @Override public void onBlogPostClick(BlogPostItem post) { - BlogPostFragment f = BlogPostFragment.newInstance(post.getId()); + BlogPostFragment f = + BlogPostFragment.newInstance(groupId, post.getId()); showNextFragment(f); } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogModule.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogModule.java index 5eafcb2eb..a12a5fc03 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogModule.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogModule.java @@ -13,11 +13,11 @@ public interface BlogModule { @Binds @IntoMap @ViewModelKey(FeedViewModel.class) - abstract ViewModel bindFeedViewModel(FeedViewModel feedViewModel); + ViewModel bindFeedViewModel(FeedViewModel feedViewModel); @Binds @IntoMap @ViewModelKey(BlogViewModel.class) - abstract ViewModel bindBlogViewModel(BlogViewModel blogViewModel); + ViewModel bindBlogViewModel(BlogViewModel blogViewModel); } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogPostFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogPostFragment.java index 114e73fe8..75001f45b 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogPostFragment.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogPostFragment.java @@ -1,39 +1,68 @@ package org.briarproject.briar.android.blog; +import android.content.Intent; import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.ProgressBar; import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault; import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; +import org.briarproject.bramble.api.sync.GroupId; import org.briarproject.bramble.api.sync.MessageId; +import org.briarproject.briar.R; import org.briarproject.briar.android.activity.ActivityComponent; +import org.briarproject.briar.android.fragment.BaseFragment; +import org.briarproject.briar.android.widget.LinkDialogFragment; + +import java.util.logging.Logger; import javax.annotation.Nullable; import javax.inject.Inject; +import androidx.annotation.CallSuper; import androidx.annotation.UiThread; +import androidx.lifecycle.LifecycleOwner; import androidx.lifecycle.ViewModelProvider; +import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP; +import static android.view.View.INVISIBLE; +import static android.view.View.VISIBLE; +import static java.util.Objects.requireNonNull; +import static java.util.logging.Logger.getLogger; +import static org.briarproject.briar.android.activity.BriarActivity.GROUP_ID; +import static org.briarproject.briar.android.util.UiUtils.MIN_DATE_RESOLUTION; + @UiThread @MethodsNotNullByDefault @ParametersNotNullByDefault -public class BlogPostFragment extends BasePostFragment { +public class BlogPostFragment extends BaseFragment + implements OnBlogPostClickListener { private static final String TAG = BlogPostFragment.class.getName(); + private static final Logger LOG = getLogger(TAG); + + static final String POST_ID = "briar.POST_ID"; + + protected BlogViewModel viewModel; + private final Handler handler = new Handler(Looper.getMainLooper()); + + private ProgressBar progressBar; + private BlogPostViewHolder ui; + private BlogPostItem post; + private Runnable refresher; @Inject ViewModelProvider.Factory viewModelFactory; - private BlogViewModel viewModel; - - static BlogPostFragment newInstance(MessageId postId) { + static BlogPostFragment newInstance(GroupId blogId, MessageId postId) { BlogPostFragment f = new BlogPostFragment(); - Bundle bundle = new Bundle(); + bundle.putByteArray(GROUP_ID, blogId.getBytes()); bundle.putByteArray(POST_ID, postId.getBytes()); - f.setArguments(bundle); return f; } @@ -45,22 +74,89 @@ public class BlogPostFragment extends BasePostFragment { .get(BlogViewModel.class); } - @Override - public String getUniqueTag() { - return TAG; - } - @Nullable @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - View v = super.onCreateView(inflater, container, savedInstanceState); - viewModel.loadBlogPost(postId).observe(getViewLifecycleOwner(), res -> - res.onError(this::handleException) + Bundle args = requireArguments(); + GroupId groupId = + new GroupId(requireNonNull(args.getByteArray(GROUP_ID))); + MessageId postId = new MessageId(args.getByteArray(POST_ID)); + + View view = inflater.inflate(R.layout.fragment_blog_post, container, + false); + progressBar = view.findViewById(R.id.progressBar); + progressBar.setVisibility(VISIBLE); + ui = new BlogPostViewHolder(view, true, this); + LifecycleOwner owner = getViewLifecycleOwner(); + viewModel.loadBlogPost(groupId, postId).observe(owner, result -> + result.onError(this::handleException) .onSuccess(this::onBlogPostLoaded) ); - return v; + return view; + } + + @CallSuper + @Override + public void onStart() { + super.onStart(); + startPeriodicUpdate(); + } + + @CallSuper + @Override + public void onStop() { + super.onStop(); + stopPeriodicUpdate(); + } + + @UiThread + private void onBlogPostLoaded(BlogPostItem post) { + progressBar.setVisibility(INVISIBLE); + this.post = post; + ui.bindItem(post); + } + + @Override + public void onBlogPostClick(BlogPostItem post) { + // We're already there + } + + @Override + public void onAuthorClick(BlogPostItem post) { + Intent i = new Intent(requireContext(), BlogActivity.class); + i.putExtra(GROUP_ID, post.getGroupId().getBytes()); + i.setFlags(FLAG_ACTIVITY_CLEAR_TOP); + getContext().startActivity(i); + } + + @Override + public void onLinkClick(String url) { + LinkDialogFragment f = LinkDialogFragment.newInstance(url); + f.show(getParentFragmentManager(), f.getUniqueTag()); + } + + private void startPeriodicUpdate() { + refresher = () -> { + LOG.info("Updating Content..."); + ui.updateDate(post.getTimestamp()); + handler.postDelayed(refresher, MIN_DATE_RESOLUTION); + }; + LOG.info("Adding Handler Callback"); + handler.postDelayed(refresher, MIN_DATE_RESOLUTION); + } + + private void stopPeriodicUpdate() { + if (refresher != null) { + LOG.info("Removing Handler Callback"); + handler.removeCallbacks(refresher); + } + } + + @Override + public String getUniqueTag() { + return TAG; } } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogPostItem.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogPostItem.java index 3247a083c..a4e4cf449 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogPostItem.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogPostItem.java @@ -17,7 +17,7 @@ public class BlogPostItem implements Comparable { private final BlogPostHeader header; @Nullable protected String text; - private boolean read; + private final boolean read; BlogPostItem(BlogPostHeader header, @Nullable String text) { this.header = header; diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogPostViewHolder.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogPostViewHolder.java index ad4debe92..1e948af57 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogPostViewHolder.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogPostViewHolder.java @@ -25,11 +25,14 @@ import androidx.recyclerview.widget.RecyclerView; import static android.view.View.GONE; import static android.view.View.VISIBLE; import static org.briarproject.briar.android.activity.BriarActivity.GROUP_ID; -import static org.briarproject.briar.android.blog.BasePostFragment.POST_ID; +import static org.briarproject.briar.android.blog.BlogPostFragment.POST_ID; import static org.briarproject.briar.android.util.UiUtils.TEASER_LENGTH; import static org.briarproject.briar.android.util.UiUtils.getSpanned; import static org.briarproject.briar.android.util.UiUtils.getTeaser; import static org.briarproject.briar.android.util.UiUtils.makeLinksClickable; +import static org.briarproject.briar.android.view.AuthorView.COMMENTER; +import static org.briarproject.briar.android.view.AuthorView.REBLOGGER; +import static org.briarproject.briar.android.view.AuthorView.RSS_FEED_REBLOGGED; import static org.briarproject.briar.api.blog.MessageType.POST; @UiThread @@ -138,17 +141,16 @@ class BlogPostViewHolder extends RecyclerView.ViewHolder { reblogger.setAuthorClickable(v -> listener.onAuthorClick(item)); } reblogger.setVisibility(VISIBLE); - reblogger.setPersona(AuthorView.REBLOGGER); + reblogger.setPersona(REBLOGGER); author.setPersona(item.getHeader().getRootPost().isRssFeed() ? - AuthorView.RSS_FEED_REBLOGGED : - AuthorView.COMMENTER); + RSS_FEED_REBLOGGED : COMMENTER); // comments + // TODO use nested RecyclerView instead like we do for Image Attachments for (BlogCommentHeader c : item.getComments()) { - View v = LayoutInflater.from(ctx) - .inflate(R.layout.list_item_blog_comment, - commentContainer, false); + View v = LayoutInflater.from(ctx).inflate( + R.layout.list_item_blog_comment, commentContainer, false); AuthorView author = v.findViewById(R.id.authorView); TextView text = v.findViewById(R.id.textView); diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogViewModel.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogViewModel.java index 88e84e905..173a627cc 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogViewModel.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogViewModel.java @@ -148,8 +148,8 @@ class BlogViewModel extends BaseViewModel { notificationManager.unblockNotification(groupId); } - void loadBlogPosts(GroupId groupId) { - loadList(txn -> loadBlogPosts(txn, groupId), this::updateBlogPosts); + private void loadBlogPosts(GroupId groupId) { + loadList(txn -> loadBlogPosts(txn, groupId), blogPosts::setValue); } private void loadSharingContacts(GroupId groupId) { diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/FeedFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/FeedFragment.java index 66380a582..8ef8d1b96 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/blog/FeedFragment.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/FeedFragment.java @@ -62,9 +62,6 @@ public class FeedFragment extends BaseFragment component.inject(this); viewModel = new ViewModelProvider(this, viewModelFactory) .get(FeedViewModel.class); - // TODO ideally we only do this once when the ViewModel gets created - viewModel.loadPersonalBlog(); - viewModel.loadAllBlogPosts(); } @Nullable @@ -154,8 +151,8 @@ public class FeedFragment extends BaseFragment @Override public void onBlogPostClick(BlogPostItem post) { - FeedPostFragment f = - FeedPostFragment.newInstance(post.getGroupId(), post.getId()); + BaseFragment f = + BlogPostFragment.newInstance(post.getGroupId(), post.getId()); showNextFragment(f); } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/FeedPostFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/FeedPostFragment.java deleted file mode 100644 index 6e90bb6d7..000000000 --- a/briar-android/src/main/java/org/briarproject/briar/android/blog/FeedPostFragment.java +++ /dev/null @@ -1,73 +0,0 @@ -package org.briarproject.briar.android.blog; - -import android.os.Bundle; -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.bramble.api.sync.MessageId; -import org.briarproject.briar.android.activity.ActivityComponent; - -import javax.annotation.Nullable; -import javax.inject.Inject; - -import androidx.annotation.UiThread; -import androidx.lifecycle.ViewModelProvider; - -import static java.util.Objects.requireNonNull; -import static org.briarproject.briar.android.activity.BriarActivity.GROUP_ID; - -@UiThread -@MethodsNotNullByDefault -@ParametersNotNullByDefault -public class FeedPostFragment extends BasePostFragment { - - private static final String TAG = FeedPostFragment.class.getName(); - - @Inject - ViewModelProvider.Factory viewModelFactory; - - private FeedViewModel viewModel; - - static FeedPostFragment newInstance(GroupId blogId, MessageId postId) { - FeedPostFragment f = new FeedPostFragment(); - Bundle bundle = new Bundle(); - bundle.putByteArray(GROUP_ID, blogId.getBytes()); - bundle.putByteArray(POST_ID, postId.getBytes()); - f.setArguments(bundle); - return f; - } - - @Override - public void injectFragment(ActivityComponent component) { - component.inject(this); - viewModel = new ViewModelProvider(requireActivity(), viewModelFactory) - .get(FeedViewModel.class); - } - - @Nullable - @Override - public View onCreateView(LayoutInflater inflater, - @Nullable ViewGroup container, - @Nullable Bundle savedInstanceState) { - Bundle args = requireArguments(); - GroupId groupId = - new GroupId(requireNonNull(args.getByteArray(GROUP_ID))); - MessageId postId = - new MessageId(requireNonNull(args.getByteArray(POST_ID))); - viewModel.loadBlogPost(groupId, postId).observe(getViewLifecycleOwner(), - result -> result.onError(this::handleException) - .onSuccess(this::onBlogPostLoaded) - ); - return super.onCreateView(inflater, container, savedInstanceState); - } - - @Override - public String getUniqueTag() { - return TAG; - } - -} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/FeedViewModel.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/FeedViewModel.java index 3c18f5774..320afa7cd 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/blog/FeedViewModel.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/FeedViewModel.java @@ -17,6 +17,7 @@ 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.bramble.api.system.AndroidExecutor; +import org.briarproject.briar.android.viewmodel.LiveResult; import org.briarproject.briar.api.android.AndroidNotificationManager; import org.briarproject.briar.api.blog.Blog; import org.briarproject.briar.api.blog.BlogManager; @@ -59,6 +60,8 @@ class FeedViewModel extends BaseViewModel { BlogManager blogManager) { super(application, dbExecutor, lifecycleManager, db, androidExecutor, eventBus, identityManager, notificationManager, blogManager); + loadPersonalBlog(); + loadAllBlogPosts(); } @Override @@ -71,13 +74,15 @@ class FeedViewModel extends BaseViewModel { GroupAddedEvent g = (GroupAddedEvent) e; if (g.getGroup().getClientId().equals(CLIENT_ID)) { LOG.info("Blog added"); - loadAllBlogPosts(); + // TODO how can this even happen? + // added RSS feeds should trigger BlogPostAddedEvent, no? + onBlogAdded(g.getGroup().getId()); } } else if (e instanceof GroupRemovedEvent) { GroupRemovedEvent g = (GroupRemovedEvent) e; if (g.getGroup().getClientId().equals(CLIENT_ID)) { LOG.info("Blog removed"); - loadAllBlogPosts(); + onBlogRemoved(g.getGroup().getId()); } } } @@ -94,7 +99,7 @@ class FeedViewModel extends BaseViewModel { notificationManager.unblockAllBlogPostNotifications(); } - void loadPersonalBlog() { + private void loadPersonalBlog() { runOnDbThread(() -> { try { long start = now(); @@ -112,8 +117,8 @@ class FeedViewModel extends BaseViewModel { return personalBlog; } - void loadAllBlogPosts() { - loadList(this::loadAllBlogPosts, this::updateBlogPosts); + private void loadAllBlogPosts() { + loadList(this::loadAllBlogPosts, blogPosts::setValue); } @DatabaseExecutor @@ -133,4 +138,30 @@ class FeedViewModel extends BaseViewModel { return posts; } + private void onBlogAdded(GroupId g) { + runOnDbThread(() -> { + try { + db.transaction(true, txn -> { + List posts = loadBlogPosts(txn, g); + txn.attach(() -> { + List items = + addListItems(blogPosts, posts); + if (items != null) { + Collections.sort(items); + blogPosts.setValue(new LiveResult<>(items)); + } + }); + }); + } catch (DbException e) { + logException(LOG, WARNING, e); + } + }); + } + + private void onBlogRemoved(GroupId g) { + removeAndUpdateListItems(blogPosts, item -> + item.getGroupId().equals(g) + ); + } + } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/ReblogActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/ReblogActivity.java index d45b10789..d73b1899b 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/blog/ReblogActivity.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/ReblogActivity.java @@ -11,7 +11,7 @@ import org.briarproject.briar.android.activity.ActivityComponent; import org.briarproject.briar.android.activity.BriarActivity; import org.briarproject.briar.android.fragment.BaseFragment.BaseFragmentListener; -import static org.briarproject.briar.android.blog.BasePostFragment.POST_ID; +import static org.briarproject.briar.android.blog.BlogPostFragment.POST_ID; public class ReblogActivity extends BriarActivity implements BaseFragmentListener { @@ -39,13 +39,11 @@ public class ReblogActivity extends BriarActivity implements @Override public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case android.R.id.home: - onBackPressed(); - return true; - default: - return super.onOptionsItemSelected(item); + if (item.getItemId() == android.R.id.home) { + onBackPressed(); + return true; } + return super.onOptionsItemSelected(item); } @Override diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/ReblogFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/ReblogFragment.java index f3746d7f7..a10a47bae 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/blog/ReblogFragment.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/ReblogFragment.java @@ -33,7 +33,7 @@ import static android.view.View.INVISIBLE; import static android.view.View.VISIBLE; import static java.util.Objects.requireNonNull; import static org.briarproject.briar.android.activity.BriarActivity.GROUP_ID; -import static org.briarproject.briar.android.blog.BasePostFragment.POST_ID; +import static org.briarproject.briar.android.blog.BlogPostFragment.POST_ID; import static org.briarproject.briar.api.blog.BlogConstants.MAX_BLOG_POST_TEXT_LENGTH; @MethodsNotNullByDefault @@ -45,7 +45,7 @@ public class ReblogFragment extends BaseFragment implements SendListener { @Inject ViewModelProvider.Factory viewModelFactory; - private FeedViewModel viewModel; + private BlogViewModel viewModel; private ViewHolder ui; private BlogPostItem item; @@ -69,7 +69,7 @@ public class ReblogFragment extends BaseFragment implements SendListener { public void injectFragment(ActivityComponent component) { component.inject(this); viewModel = new ViewModelProvider(requireActivity(), viewModelFactory) - .get(FeedViewModel.class); + .get(BlogViewModel.class); } @Override @@ -131,7 +131,7 @@ public class ReblogFragment extends BaseFragment implements SendListener { ui.input.setVisibility(VISIBLE); } - private class ViewHolder { + private class ViewHolder implements OnBlogPostClickListener { private final ScrollView scrollView; private final ProgressBar progressBar; @@ -142,24 +142,25 @@ public class ReblogFragment extends BaseFragment implements SendListener { scrollView = v.findViewById(R.id.scrollView); progressBar = v.findViewById(R.id.progressBar); post = new BlogPostViewHolder(v.findViewById(R.id.postLayout), - true, new OnBlogPostClickListener() { - @Override - public void onBlogPostClick(BlogPostItem post) { - // do nothing - } - - @Override - public void onAuthorClick(BlogPostItem post) { - // probably don't want to allow author clicks here - } - - @Override - public void onLinkClick(String url) { - LinkDialogFragment f = LinkDialogFragment.newInstance(url); - f.show(getParentFragmentManager(), f.getUniqueTag()); - } - }); + true, this); input = v.findViewById(R.id.inputText); } + + @Override + public void onBlogPostClick(BlogPostItem post) { + // do nothing + } + + @Override + public void onAuthorClick(BlogPostItem post) { + // probably don't want to allow author clicks here + } + + @Override + public void onLinkClick(String url) { + LinkDialogFragment f = LinkDialogFragment.newInstance(url); + f.show(getParentFragmentManager(), f.getUniqueTag()); + } } + } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumListViewModel.java b/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumListViewModel.java index af86a9d95..52ad8c449 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumListViewModel.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumListViewModel.java @@ -156,11 +156,9 @@ class ForumListViewModel extends DbViewModel implements EventListener { @UiThread private void onGroupRemoved(GroupId groupId) { - List list = removeListItems(forumItems, i -> + removeAndUpdateListItems(forumItems, i -> i.getForum().getId().equals(groupId) ); - if (list == null) return; - forumItems.setValue(new LiveResult<>(list)); } void loadForumInvitations() { diff --git a/briar-android/src/main/java/org/briarproject/briar/android/privategroup/list/GroupListViewModel.java b/briar-android/src/main/java/org/briarproject/briar/android/privategroup/list/GroupListViewModel.java index 041305a37..3eedd40fc 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/privategroup/list/GroupListViewModel.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/privategroup/list/GroupListViewModel.java @@ -193,10 +193,7 @@ class GroupListViewModel extends DbViewModel implements EventListener { @UiThread private void onGroupRemoved(GroupId groupId) { - List list = - removeListItems(groupItems, i -> i.getId().equals(groupId)); - if (list == null) return; - groupItems.setValue(new LiveResult<>(list)); + removeAndUpdateListItems(groupItems, i -> i.getId().equals(groupId)); } void removeGroup(GroupId g) { diff --git a/briar-android/src/main/java/org/briarproject/briar/android/viewmodel/DbViewModel.java b/briar-android/src/main/java/org/briarproject/briar/android/viewmodel/DbViewModel.java index a0aa00c8d..1c74d8f66 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/viewmodel/DbViewModel.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/viewmodel/DbViewModel.java @@ -13,6 +13,7 @@ import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.system.AndroidExecutor; import java.util.ArrayList; +import java.util.Collection; import java.util.List; import java.util.ListIterator; import java.util.concurrent.Executor; @@ -27,6 +28,7 @@ import androidx.arch.core.util.Function; import androidx.core.util.Consumer; import androidx.lifecycle.AndroidViewModel; import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; import androidx.recyclerview.widget.RecyclerView; import static java.util.logging.Level.WARNING; @@ -153,13 +155,33 @@ public abstract class DbViewModel extends AndroidViewModel { * */ @Nullable - protected List addListItem(LiveData>> liveData, T item) { + protected List addListItem(LiveData>> liveData, + T item) { List items = getListCopy(liveData); if (items == null) return null; items.add(item); return items; } + /** + * Creates a copy of the list available in the given LiveData + * and adds the given items to the copy. + * + * @return a copy of the list in the LiveData with items added or null when + *
    + *
  • LiveData does not have a value + *
  • LiveResult in the LiveData has an error + *
+ */ + @Nullable + protected List addListItems(LiveData>> liveData, + Collection items) { + List copiedItems = getListCopy(liveData); + if (copiedItems == null) return null; + copiedItems.addAll(items); + return copiedItems; + } + /** * Creates a copy of the list available in the given LiveData * and replaces items where the given test function returns true. @@ -221,6 +243,25 @@ public abstract class DbViewModel extends AndroidViewModel { return changed ? items : null; } + /** + * Updates the given LiveData with a copy of its list + * with the items removed where the given test function returns true. + *

+ * Nothing is updated, if the + *

    + *
  • LiveData does not have a value + *
  • LiveResult in the LiveData has an error + *
  • test function did return false for all items in the list + *
+ */ + @UiThread + protected void removeAndUpdateListItems( + MutableLiveData>> liveData, + Function test) { + List list = removeListItems(liveData, test); + if (list != null) liveData.setValue(new LiveResult<>(list)); + } + /** * Retrieves a copy of the list of items from the given LiveData * or null if it is not available. From d3b855318c1c3a213eea033ef1b3f77c19a80610 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Fri, 29 Jan 2021 14:21:43 -0300 Subject: [PATCH 08/13] Anticipate review feedback for blog view models after re-basing --- .../briar/android/blog/BaseViewModel.java | 53 ++++++++----------- .../briar/android/blog/BlogFragment.java | 24 ++++----- .../briar/android/blog/BlogPostFragment.java | 9 ++-- .../android/blog/BlogPostViewHolder.java | 6 +-- .../briar/android/blog/BlogViewModel.java | 36 +++++-------- .../briar/android/blog/FeedFragment.java | 8 +-- .../briar/android/blog/FeedViewModel.java | 43 ++++++--------- .../briar/api/blog/BlogManager.java | 3 +- .../briar/blog/BlogManagerImpl.java | 9 +--- .../headless/blogs/BlogControllerImpl.kt | 10 +++- .../briar/headless/ControllerTest.kt | 2 + .../headless/blogs/BlogControllerTest.kt | 27 +++++++--- 12 files changed, 107 insertions(+), 123 deletions(-) diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/BaseViewModel.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/BaseViewModel.java index e61ae3c38..95e859ff6 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/blog/BaseViewModel.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/BaseViewModel.java @@ -127,41 +127,34 @@ abstract class BaseViewModel extends DbViewModel implements EventListener { LiveData> loadBlogPost(GroupId g, MessageId m) { MutableLiveData> result = new MutableLiveData<>(); - runOnDbThread(() -> { - try { - long start = now(); - BlogPostHeader header = blogManager.getPostHeader(g, m); - BlogPostItem item = db.transactionWithResult(true, txn -> - getItem(txn, header) - ); - logDuration(LOG, "Loading post", start); - result.postValue(new LiveResult<>(item)); - } catch (DbException e) { - logException(LOG, WARNING, e); - result.postValue(new LiveResult<>(e)); - } + runOnDbThread(true, txn -> { + long start = now(); + BlogPostHeader header = blogManager.getPostHeader(txn, g, m); + BlogPostItem item = getItem(txn, header); + logDuration(LOG, "Loading post", start); + result.postValue(new LiveResult<>(item)); + }, e -> { + logException(LOG, WARNING, e); + result.postValue(new LiveResult<>(e)); }); return result; } protected void onBlogPostAdded(BlogPostHeader header, boolean local) { - runOnDbThread(() -> { - try { - db.transaction(true, txn -> { - BlogPostItem item = getItem(txn, header); - txn.attach(() -> { - List items = addListItem(blogPosts, item); - if (items != null) { - Collections.sort(items); - postAddedWasLocal = local; - blogPosts.setValue(new LiveResult<>(items)); - } - }); - }); - } catch (DbException e) { - logException(LOG, WARNING, e); - } - }); + runOnDbThread(true, txn -> { + BlogPostItem item = getItem(txn, header); + txn.attach(() -> onBlogPostItemAdded(item, local)); + }, e -> logException(LOG, WARNING, e)); + } + + @UiThread + private void onBlogPostItemAdded(BlogPostItem item, boolean local) { + List items = addListItem(blogPosts, item); + if (items != null) { + Collections.sort(items); + postAddedWasLocal = local; + blogPosts.setValue(new LiveResult<>(items)); + } } void repeatPost(BlogPostItem item, @Nullable String comment) { diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogFragment.java index 63ce4954d..079410e1c 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogFragment.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogFragment.java @@ -106,8 +106,7 @@ public class BlogFragment extends BaseFragment @Override public void onStart() { super.onStart(); - viewModel.blockNotifications(); - viewModel.clearBlogPostNotifications(); + viewModel.blockAndClearNotifications(); list.startPeriodicUpdate(); } @@ -134,23 +133,22 @@ public class BlogFragment extends BaseFragment public boolean onOptionsItemSelected(MenuItem item) { int itemId = item.getItemId(); if (itemId == R.id.action_write_blog_post) { - Intent i = new Intent(getActivity(), - WriteBlogPostActivity.class); + Intent i = new Intent(getActivity(), WriteBlogPostActivity.class); i.putExtra(GROUP_ID, groupId.getBytes()); startActivityForResult(i, REQUEST_WRITE_BLOG_POST); return true; } else if (itemId == R.id.action_blog_share) { - Intent i2 = new Intent(getActivity(), ShareBlogActivity.class); - i2.setFlags(FLAG_ACTIVITY_CLEAR_TOP); - i2.putExtra(GROUP_ID, groupId.getBytes()); - startActivityForResult(i2, REQUEST_SHARE_BLOG); + Intent i = new Intent(getActivity(), ShareBlogActivity.class); + i.setFlags(FLAG_ACTIVITY_CLEAR_TOP); + i.putExtra(GROUP_ID, groupId.getBytes()); + startActivityForResult(i, REQUEST_SHARE_BLOG); return true; } else if (itemId == R.id.action_blog_sharing_status) { - Intent i3 = new Intent(getActivity(), - BlogSharingStatusActivity.class); - i3.setFlags(FLAG_ACTIVITY_CLEAR_TOP); - i3.putExtra(GROUP_ID, groupId.getBytes()); - startActivity(i3); + Intent i = + new Intent(getActivity(), BlogSharingStatusActivity.class); + i.setFlags(FLAG_ACTIVITY_CLEAR_TOP); + i.putExtra(GROUP_ID, groupId.getBytes()); + startActivity(i); return true; } else if (itemId == R.id.action_blog_delete) { showDeleteDialog(); diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogPostFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogPostFragment.java index 75001f45b..77043d276 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogPostFragment.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogPostFragment.java @@ -23,7 +23,6 @@ import java.util.logging.Logger; import javax.annotation.Nullable; import javax.inject.Inject; -import androidx.annotation.CallSuper; import androidx.annotation.UiThread; import androidx.lifecycle.LifecycleOwner; import androidx.lifecycle.ViewModelProvider; @@ -36,7 +35,6 @@ import static java.util.logging.Logger.getLogger; import static org.briarproject.briar.android.activity.BriarActivity.GROUP_ID; import static org.briarproject.briar.android.util.UiUtils.MIN_DATE_RESOLUTION; -@UiThread @MethodsNotNullByDefault @ParametersNotNullByDefault public class BlogPostFragment extends BaseFragment @@ -82,7 +80,8 @@ public class BlogPostFragment extends BaseFragment Bundle args = requireArguments(); GroupId groupId = new GroupId(requireNonNull(args.getByteArray(GROUP_ID))); - MessageId postId = new MessageId(args.getByteArray(POST_ID)); + MessageId postId = + new MessageId(requireNonNull(args.getByteArray(POST_ID))); View view = inflater.inflate(R.layout.fragment_blog_post, container, false); @@ -97,14 +96,12 @@ public class BlogPostFragment extends BaseFragment return view; } - @CallSuper @Override public void onStart() { super.onStart(); startPeriodicUpdate(); } - @CallSuper @Override public void onStop() { super.onStop(); @@ -128,7 +125,7 @@ public class BlogPostFragment extends BaseFragment Intent i = new Intent(requireContext(), BlogActivity.class); i.putExtra(GROUP_ID, post.getGroupId().getBytes()); i.setFlags(FLAG_ACTIVITY_CLEAR_TOP); - getContext().startActivity(i); + requireContext().startActivity(i); } @Override diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogPostViewHolder.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogPostViewHolder.java index 1e948af57..a2d883f9f 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogPostViewHolder.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogPostViewHolder.java @@ -9,6 +9,7 @@ import android.view.ViewGroup; import android.widget.ImageButton; import android.widget.TextView; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.sync.MessageId; import org.briarproject.briar.R; import org.briarproject.briar.android.view.AuthorView; @@ -17,7 +18,6 @@ import org.briarproject.briar.api.blog.BlogPostHeader; import javax.annotation.Nullable; -import androidx.annotation.NonNull; import androidx.annotation.UiThread; import androidx.core.view.ViewCompat; import androidx.recyclerview.widget.RecyclerView; @@ -36,6 +36,7 @@ import static org.briarproject.briar.android.view.AuthorView.RSS_FEED_REBLOGGED; import static org.briarproject.briar.api.blog.MessageType.POST; @UiThread +@NotNullByDefault class BlogPostViewHolder extends RecyclerView.ViewHolder { private final Context ctx; @@ -47,11 +48,10 @@ class BlogPostViewHolder extends RecyclerView.ViewHolder { private final ViewGroup commentContainer; private final boolean fullText; - @NonNull private final OnBlogPostClickListener listener; BlogPostViewHolder(View v, boolean fullText, - @NonNull OnBlogPostClickListener listener) { + OnBlogPostClickListener listener) { super(v); this.fullText = fullText; this.listener = listener; diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogViewModel.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogViewModel.java index 173a627cc..5ac2c4143 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogViewModel.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogViewModel.java @@ -15,12 +15,10 @@ import org.briarproject.bramble.api.lifecycle.LifecycleManager; import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault; import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; import org.briarproject.bramble.api.sync.GroupId; -import org.briarproject.bramble.api.sync.MessageId; import org.briarproject.bramble.api.sync.event.GroupRemovedEvent; import org.briarproject.bramble.api.system.AndroidExecutor; import org.briarproject.briar.android.sharing.SharingController; import org.briarproject.briar.android.sharing.SharingController.SharingInfo; -import org.briarproject.briar.android.viewmodel.LiveResult; import org.briarproject.briar.api.android.AndroidNotificationManager; import org.briarproject.briar.api.blog.Blog; import org.briarproject.briar.api.blog.BlogInvitationResponse; @@ -37,6 +35,7 @@ import java.util.logging.Logger; import javax.inject.Inject; +import androidx.annotation.UiThread; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; @@ -136,11 +135,8 @@ class BlogViewModel extends BaseViewModel { }); } - void blockNotifications() { + void blockAndClearNotifications() { notificationManager.blockNotification(groupId); - } - - void clearBlogPostNotifications() { notificationManager.clearBlogPostNotification(groupId); } @@ -153,18 +149,18 @@ class BlogViewModel extends BaseViewModel { } private void loadSharingContacts(GroupId groupId) { - runOnDbThread(() -> { - try { - Collection contacts = - blogSharingManager.getSharedWith(groupId); - Collection contactIds = - new ArrayList<>(contacts.size()); - for (Contact c : contacts) contactIds.add(c.getId()); - sharingController.addAll(contactIds); - } catch (DbException e) { - logException(LOG, WARNING, e); - } - }); + runOnDbThread(true, txn -> { + Collection contacts = + blogSharingManager.getSharedWith(txn, groupId); + txn.attach(() -> onSharingContactsLoaded(contacts)); + }, e -> logException(LOG, WARNING, e)); + } + + @UiThread + private void onSharingContactsLoaded(Collection contacts) { + Collection contactIds = new ArrayList<>(contacts.size()); + for (Contact c : contacts) contactIds.add(c.getId()); + sharingController.addAll(contactIds); } void deleteBlog() { @@ -180,10 +176,6 @@ class BlogViewModel extends BaseViewModel { }); } - LiveData> loadBlogPost(MessageId m) { - return loadBlogPost(groupId, m); - } - LiveData getBlog() { return blog; } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/FeedFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/FeedFragment.java index 8ef8d1b96..6e4718576 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/blog/FeedFragment.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/FeedFragment.java @@ -24,7 +24,6 @@ import java.util.List; import javax.inject.Inject; import androidx.annotation.Nullable; -import androidx.annotation.UiThread; import androidx.lifecycle.ViewModelProvider; import androidx.recyclerview.widget.LinearLayoutManager; @@ -32,7 +31,6 @@ import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP; import static com.google.android.material.snackbar.Snackbar.LENGTH_LONG; import static org.briarproject.briar.android.activity.BriarActivity.GROUP_ID; -@UiThread @MethodsNotNullByDefault @ParametersNotNullByDefault public class FeedFragment extends BaseFragment @@ -82,8 +80,7 @@ public class FeedFragment extends BaseFragment list.setEmptyAction(R.string.blogs_feed_empty_state_action); viewModel.getBlogPosts().observe(getViewLifecycleOwner(), result -> - result - .onError(this::handleException) + result.onError(this::handleException) .onSuccess(this::onBlogPostsLoaded) ); @@ -93,8 +90,7 @@ public class FeedFragment extends BaseFragment @Override public void onStart() { super.onStart(); - viewModel.blockAllBlogPostNotifications(); - viewModel.clearAllBlogPostNotifications(); + viewModel.blockAndClearAllBlogPostNotifications(); list.startPeriodicUpdate(); } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/FeedViewModel.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/FeedViewModel.java index 320afa7cd..d5374a7bc 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/blog/FeedViewModel.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/FeedViewModel.java @@ -4,7 +4,6 @@ import android.app.Application; import org.briarproject.bramble.api.db.DatabaseExecutor; import org.briarproject.bramble.api.db.DbException; -import org.briarproject.bramble.api.db.NoSuchMessageException; import org.briarproject.bramble.api.db.Transaction; import org.briarproject.bramble.api.db.TransactionManager; import org.briarproject.bramble.api.event.Event; @@ -31,6 +30,7 @@ import java.util.logging.Logger; import javax.inject.Inject; +import androidx.annotation.UiThread; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; @@ -87,11 +87,8 @@ class FeedViewModel extends BaseViewModel { } } - void blockAllBlogPostNotifications() { + void blockAndClearAllBlogPostNotifications() { notificationManager.blockAllBlogPostNotifications(); - } - - void clearAllBlogPostNotifications() { notificationManager.clearAllBlogPostNotifications(); } @@ -127,11 +124,7 @@ class FeedViewModel extends BaseViewModel { long start = now(); List posts = new ArrayList<>(); for (GroupId g : blogManager.getBlogIds(txn)) { - try { - posts.addAll(loadBlogPosts(txn, g)); - } catch (NoSuchMessageException e) { - logException(LOG, WARNING, e); - } + posts.addAll(loadBlogPosts(txn, g)); } Collections.sort(posts); logDuration(LOG, "Loading all posts", start); @@ -139,23 +132,19 @@ class FeedViewModel extends BaseViewModel { } private void onBlogAdded(GroupId g) { - runOnDbThread(() -> { - try { - db.transaction(true, txn -> { - List posts = loadBlogPosts(txn, g); - txn.attach(() -> { - List items = - addListItems(blogPosts, posts); - if (items != null) { - Collections.sort(items); - blogPosts.setValue(new LiveResult<>(items)); - } - }); - }); - } catch (DbException e) { - logException(LOG, WARNING, e); - } - }); + runOnDbThread(true, txn -> { + List posts = loadBlogPosts(txn, g); + txn.attach(() -> onBlogPostItemsAdded(posts)); + }, e -> logException(LOG, WARNING, e)); + } + + @UiThread + private void onBlogPostItemsAdded(List posts) { + List items = addListItems(blogPosts, posts); + if (items != null) { + Collections.sort(items); + blogPosts.setValue(new LiveResult<>(items)); + } } private void onBlogRemoved(GroupId g) { diff --git a/briar-api/src/main/java/org/briarproject/briar/api/blog/BlogManager.java b/briar-api/src/main/java/org/briarproject/briar/api/blog/BlogManager.java index b2d76357e..9953cbac4 100644 --- a/briar-api/src/main/java/org/briarproject/briar/api/blog/BlogManager.java +++ b/briar-api/src/main/java/org/briarproject/briar/api/blog/BlogManager.java @@ -107,7 +107,8 @@ public interface BlogManager { /** * Returns the header of the blog post with the given ID. */ - BlogPostHeader getPostHeader(GroupId g, MessageId m) throws DbException; + BlogPostHeader getPostHeader(Transaction txn, GroupId g, MessageId m) + throws DbException; /** * Returns the text of the blog post with the given ID. diff --git a/briar-core/src/main/java/org/briarproject/briar/blog/BlogManagerImpl.java b/briar-core/src/main/java/org/briarproject/briar/blog/BlogManagerImpl.java index 157668e88..68ea45863 100644 --- a/briar-core/src/main/java/org/briarproject/briar/blog/BlogManagerImpl.java +++ b/briar-core/src/main/java/org/briarproject/briar/blog/BlogManagerImpl.java @@ -454,19 +454,14 @@ class BlogManagerImpl extends BdfIncomingMessageHook implements BlogManager, } @Override - public BlogPostHeader getPostHeader(GroupId g, MessageId m) + public BlogPostHeader getPostHeader(Transaction txn, GroupId g, MessageId m) throws DbException { - Transaction txn = db.startTransaction(true); try { BdfDictionary meta = clientHelper.getMessageMetadataAsDictionary(txn, m); - BlogPostHeader h = getPostHeaderFromMetadata(txn, g, m, meta); - db.commitTransaction(txn); - return h; + return getPostHeaderFromMetadata(txn, g, m, meta); } catch (FormatException e) { throw new DbException(e); - } finally { - db.endTransaction(txn); } } diff --git a/briar-headless/src/main/java/org/briarproject/briar/headless/blogs/BlogControllerImpl.kt b/briar-headless/src/main/java/org/briarproject/briar/headless/blogs/BlogControllerImpl.kt index ba4068050..2df0d6dcc 100644 --- a/briar-headless/src/main/java/org/briarproject/briar/headless/blogs/BlogControllerImpl.kt +++ b/briar-headless/src/main/java/org/briarproject/briar/headless/blogs/BlogControllerImpl.kt @@ -3,12 +3,15 @@ package org.briarproject.briar.headless.blogs import com.fasterxml.jackson.databind.ObjectMapper import io.javalin.http.BadRequestResponse import io.javalin.http.Context +import org.briarproject.bramble.api.db.DbException +import org.briarproject.bramble.api.db.TransactionManager import org.briarproject.bramble.api.identity.IdentityManager import org.briarproject.bramble.api.system.Clock import org.briarproject.bramble.util.StringUtils.utf8IsTooLong import org.briarproject.briar.api.blog.BlogConstants.MAX_BLOG_POST_TEXT_LENGTH import org.briarproject.briar.api.blog.BlogManager import org.briarproject.briar.api.blog.BlogPostFactory +import org.briarproject.briar.api.blog.BlogPostHeader import org.briarproject.briar.headless.getFromJson import javax.annotation.concurrent.Immutable import javax.inject.Inject @@ -21,6 +24,7 @@ internal class BlogControllerImpl constructor( private val blogManager: BlogManager, private val blogPostFactory: BlogPostFactory, + private val db: TransactionManager, private val identityManager: IdentityManager, private val objectMapper: ObjectMapper, private val clock: Clock @@ -45,8 +49,10 @@ constructor( val blog = blogManager.getPersonalBlog(author) val now = clock.currentTimeMillis() val post = blogPostFactory.createBlogPost(blog.id, now, null, author, text) - blogManager.addLocalPost(post) - val header = blogManager.getPostHeader(blog.id, post.message.id) + val header = db.transactionWithResult(true) { txn -> + blogManager.addLocalPost(txn, post) + return@transactionWithResult blogManager.getPostHeader(txn, blog.id, post.message.id) + } return ctx.json(header.output(text)) } diff --git a/briar-headless/src/test/java/org/briarproject/briar/headless/ControllerTest.kt b/briar-headless/src/test/java/org/briarproject/briar/headless/ControllerTest.kt index ed4aa8384..c7d2a6aaf 100644 --- a/briar-headless/src/test/java/org/briarproject/briar/headless/ControllerTest.kt +++ b/briar-headless/src/test/java/org/briarproject/briar/headless/ControllerTest.kt @@ -7,6 +7,7 @@ import io.mockk.mockk import org.briarproject.bramble.api.connection.ConnectionRegistry import org.briarproject.bramble.api.contact.Contact import org.briarproject.bramble.api.contact.ContactManager +import org.briarproject.bramble.api.db.TransactionManager import org.briarproject.bramble.api.identity.Author import org.briarproject.bramble.api.identity.IdentityManager import org.briarproject.bramble.api.identity.LocalAuthor @@ -24,6 +25,7 @@ import javax.servlet.http.HttpServletResponse abstract class ControllerTest { + protected val db = mockk() protected val contactManager = mockk() protected val conversationManager = mockk() protected val identityManager = mockk() diff --git a/briar-headless/src/test/java/org/briarproject/briar/headless/blogs/BlogControllerTest.kt b/briar-headless/src/test/java/org/briarproject/briar/headless/blogs/BlogControllerTest.kt index aae2aa100..94c6b0dae 100644 --- a/briar-headless/src/test/java/org/briarproject/briar/headless/blogs/BlogControllerTest.kt +++ b/briar-headless/src/test/java/org/briarproject/briar/headless/blogs/BlogControllerTest.kt @@ -6,14 +6,22 @@ import io.mockk.Runs import io.mockk.every import io.mockk.just import io.mockk.mockk -import org.briarproject.briar.api.identity.AuthorInfo -import org.briarproject.briar.api.identity.AuthorInfo.Status.OURSELVES +import io.mockk.slot +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.sync.MessageId import org.briarproject.bramble.identity.output import org.briarproject.bramble.util.StringUtils.getRandomString -import org.briarproject.briar.api.blog.* +import org.briarproject.briar.api.blog.Blog import org.briarproject.briar.api.blog.BlogConstants.MAX_BLOG_POST_TEXT_LENGTH +import org.briarproject.briar.api.blog.BlogManager +import org.briarproject.briar.api.blog.BlogPost +import org.briarproject.briar.api.blog.BlogPostFactory +import org.briarproject.briar.api.blog.BlogPostHeader import org.briarproject.briar.api.blog.MessageType.POST +import org.briarproject.briar.api.identity.AuthorInfo +import org.briarproject.briar.api.identity.AuthorInfo.Status.OURSELVES import org.briarproject.briar.headless.ControllerTest import org.junit.jupiter.api.Assertions.assertThrows import org.junit.jupiter.api.Test @@ -24,7 +32,7 @@ internal class BlogControllerTest : ControllerTest() { private val blogPostFactory = mockk() private val controller = - BlogControllerImpl(blogManager, blogPostFactory, identityManager, objectMapper, clock) + BlogControllerImpl(blogManager, blogPostFactory, db, identityManager, objectMapper, clock) private val blog = Blog(group, author, false) private val parentId: MessageId? = null @@ -46,6 +54,8 @@ internal class BlogControllerTest : ControllerTest() { @Test fun testCreate() { val post = BlogPost(message, null, localAuthor) + val dbSlot = slot>() + val txn = Transaction(Object(), true) every { ctx.body() } returns """{"text": "$text"}""" every { identityManager.localAuthor } returns localAuthor @@ -60,8 +70,13 @@ internal class BlogControllerTest : ControllerTest() { text ) } returns post - every { blogManager.addLocalPost(post) } just Runs - every { blogManager.getPostHeader(post.message.groupId, post.message.id) } returns header + every { db.transactionWithResult(true, capture(dbSlot)) } answers { + dbSlot.captured.call(txn) + } + every { blogManager.addLocalPost(txn, post) } just Runs + every { + blogManager.getPostHeader(txn, post.message.groupId, post.message.id) + } returns header every { ctx.json(header.output(text)) } returns ctx controller.createPost(ctx) From 2f969775d87470b0c05f9a0b3bbec868eadbd6c5 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Tue, 16 Mar 2021 16:33:56 -0300 Subject: [PATCH 09/13] Remove TransactionManager from blog's BaseViewModel --- .../java/org/briarproject/briar/android/blog/BaseViewModel.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/BaseViewModel.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/BaseViewModel.java index 95e859ff6..34a04123b 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/blog/BaseViewModel.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/BaseViewModel.java @@ -49,7 +49,6 @@ abstract class BaseViewModel extends DbViewModel implements EventListener { private static final Logger LOG = getLogger(BaseViewModel.class.getName()); private final EventBus eventBus; - protected final TransactionManager db; protected final IdentityManager identityManager; protected final AndroidNotificationManager notificationManager; protected final BlogManager blogManager; @@ -71,7 +70,6 @@ abstract class BaseViewModel extends DbViewModel implements EventListener { AndroidNotificationManager notificationManager, BlogManager blogManager) { super(application, dbExecutor, lifecycleManager, db, androidExecutor); - this.db = db; this.eventBus = eventBus; this.identityManager = identityManager; this.notificationManager = notificationManager; From 726ebcea3f6d6389538fdac18e3566300a235426 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Wed, 17 Mar 2021 10:33:29 -0300 Subject: [PATCH 10/13] Make blog post author clickable when not already in their blog --- .../org/briarproject/briar/android/blog/BlogFragment.java | 4 ++-- .../briarproject/briar/android/blog/BlogPostAdapter.java | 6 ++++-- .../briarproject/briar/android/blog/BlogPostFragment.java | 8 ++++++-- .../briar/android/blog/BlogPostViewHolder.java | 8 ++++---- .../org/briarproject/briar/android/blog/FeedFragment.java | 6 +++--- .../briarproject/briar/android/blog/ReblogFragment.java | 2 +- 6 files changed, 20 insertions(+), 14 deletions(-) diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogFragment.java index 079410e1c..d9a7a0284 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogFragment.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogFragment.java @@ -54,7 +54,7 @@ public class BlogFragment extends BaseFragment private GroupId groupId; private BlogViewModel viewModel; - private final BlogPostAdapter adapter = new BlogPostAdapter(this); + private final BlogPostAdapter adapter = new BlogPostAdapter(false, this); private BriarRecyclerView list; static BlogFragment newInstance(GroupId groupId) { @@ -192,7 +192,7 @@ public class BlogFragment extends BaseFragment @Override public void onBlogPostClick(BlogPostItem post) { BlogPostFragment f = - BlogPostFragment.newInstance(groupId, post.getId()); + BlogPostFragment.newInstance(groupId, post.getId(), false); showNextFragment(f); } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogPostAdapter.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogPostAdapter.java index 00d051e39..484a6b13e 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogPostAdapter.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogPostAdapter.java @@ -15,9 +15,10 @@ import androidx.recyclerview.widget.ListAdapter; @ParametersNotNullByDefault class BlogPostAdapter extends ListAdapter { + private final boolean authorClickable; private final OnBlogPostClickListener listener; - BlogPostAdapter(OnBlogPostClickListener listener) { + BlogPostAdapter(boolean authorClickable, OnBlogPostClickListener listener) { super(new DiffUtil.ItemCallback() { @Override public boolean areItemsTheSame(BlogPostItem a, BlogPostItem b) { @@ -29,6 +30,7 @@ class BlogPostAdapter extends ListAdapter { return a.isRead() == b.isRead(); } }); + this.authorClickable = authorClickable; this.listener = listener; } @@ -37,7 +39,7 @@ class BlogPostAdapter extends ListAdapter { int viewType) { View v = LayoutInflater.from(parent.getContext()).inflate( R.layout.list_item_blog_post, parent, false); - return new BlogPostViewHolder(v, false, listener); + return new BlogPostViewHolder(v, false, listener, authorClickable); } @Override diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogPostFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogPostFragment.java index 77043d276..605597e59 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogPostFragment.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogPostFragment.java @@ -44,6 +44,7 @@ public class BlogPostFragment extends BaseFragment private static final Logger LOG = getLogger(TAG); static final String POST_ID = "briar.POST_ID"; + static final String IS_FEED = "briar.IS_FEED"; protected BlogViewModel viewModel; private final Handler handler = new Handler(Looper.getMainLooper()); @@ -56,11 +57,13 @@ public class BlogPostFragment extends BaseFragment @Inject ViewModelProvider.Factory viewModelFactory; - static BlogPostFragment newInstance(GroupId blogId, MessageId postId) { + static BlogPostFragment newInstance(GroupId blogId, MessageId postId, + boolean isFeed) { BlogPostFragment f = new BlogPostFragment(); Bundle bundle = new Bundle(); bundle.putByteArray(GROUP_ID, blogId.getBytes()); bundle.putByteArray(POST_ID, postId.getBytes()); + bundle.putBoolean(IS_FEED, isFeed); f.setArguments(bundle); return f; } @@ -82,12 +85,13 @@ public class BlogPostFragment extends BaseFragment new GroupId(requireNonNull(args.getByteArray(GROUP_ID))); MessageId postId = new MessageId(requireNonNull(args.getByteArray(POST_ID))); + boolean isFeed = args.getBoolean(IS_FEED); View view = inflater.inflate(R.layout.fragment_blog_post, container, false); progressBar = view.findViewById(R.id.progressBar); progressBar.setVisibility(VISIBLE); - ui = new BlogPostViewHolder(view, true, this); + ui = new BlogPostViewHolder(view, true, this, isFeed); LifecycleOwner owner = getViewLifecycleOwner(); viewModel.loadBlogPost(groupId, postId).observe(owner, result -> result.onError(this::handleException) diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogPostViewHolder.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogPostViewHolder.java index a2d883f9f..5f1648e6a 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogPostViewHolder.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogPostViewHolder.java @@ -33,7 +33,6 @@ import static org.briarproject.briar.android.util.UiUtils.makeLinksClickable; import static org.briarproject.briar.android.view.AuthorView.COMMENTER; import static org.briarproject.briar.android.view.AuthorView.REBLOGGER; import static org.briarproject.briar.android.view.AuthorView.RSS_FEED_REBLOGGED; -import static org.briarproject.briar.api.blog.MessageType.POST; @UiThread @NotNullByDefault @@ -46,15 +45,16 @@ class BlogPostViewHolder extends RecyclerView.ViewHolder { private final ImageButton reblogButton; private final TextView text; private final ViewGroup commentContainer; - private final boolean fullText; + private final boolean fullText, authorClickable; private final OnBlogPostClickListener listener; BlogPostViewHolder(View v, boolean fullText, - OnBlogPostClickListener listener) { + OnBlogPostClickListener listener, boolean authorClickable) { super(v); this.fullText = fullText; this.listener = listener; + this.authorClickable = authorClickable; ctx = v.getContext(); layout = v.findViewById(R.id.postLayout); @@ -97,7 +97,7 @@ class BlogPostViewHolder extends RecyclerView.ViewHolder { author.setPersona( item.isRssFeed() ? AuthorView.RSS_FEED : AuthorView.NORMAL); // TODO make author clickable more often #624 - if (!fullText && item.getHeader().getType() == POST) { + if (authorClickable) { author.setAuthorClickable(v -> listener.onAuthorClick(item)); } else { author.setAuthorNotClickable(); diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/FeedFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/FeedFragment.java index 6e4718576..4ea4611a6 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/blog/FeedFragment.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/FeedFragment.java @@ -42,7 +42,7 @@ public class FeedFragment extends BaseFragment ViewModelProvider.Factory viewModelFactory; private FeedViewModel viewModel; - private final BlogPostAdapter adapter = new BlogPostAdapter(this); + private final BlogPostAdapter adapter = new BlogPostAdapter(true, this); private LinearLayoutManager layoutManager; private BriarRecyclerView list; @@ -147,8 +147,8 @@ public class FeedFragment extends BaseFragment @Override public void onBlogPostClick(BlogPostItem post) { - BaseFragment f = - BlogPostFragment.newInstance(post.getGroupId(), post.getId()); + BaseFragment f = BlogPostFragment + .newInstance(post.getGroupId(), post.getId(), true); showNextFragment(f); } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/ReblogFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/ReblogFragment.java index a10a47bae..003c7efe9 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/blog/ReblogFragment.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/ReblogFragment.java @@ -142,7 +142,7 @@ public class ReblogFragment extends BaseFragment implements SendListener { scrollView = v.findViewById(R.id.scrollView); progressBar = v.findViewById(R.id.progressBar); post = new BlogPostViewHolder(v.findViewById(R.id.postLayout), - true, this); + true, this, false); input = v.findViewById(R.id.inputText); } From e97478a21a891ab5c2ba91d5080dc90425ff83cc Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Wed, 17 Mar 2021 10:37:26 -0300 Subject: [PATCH 11/13] Don't reload blog data when configuration changes --- .../java/org/briarproject/briar/android/blog/BlogViewModel.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogViewModel.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogViewModel.java index 5ac2c4143..68c38d2d9 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogViewModel.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogViewModel.java @@ -112,7 +112,9 @@ class BlogViewModel extends BaseViewModel { /** * Set this before calling any other methods. */ + @UiThread public void setGroupId(GroupId groupId) { + if (this.groupId == groupId) return; // configuration change this.groupId = groupId; loadBlog(groupId); loadBlogPosts(groupId); From 4074ac8578ad6e0f3d3dd0e2d13f8dc1833dd910 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Mon, 22 Mar 2021 15:17:30 -0300 Subject: [PATCH 12/13] Add handleException() to DbViewModel and use it for blogs --- .../briar/android/blog/BaseViewModel.java | 4 +-- .../briar/android/blog/BlogViewModel.java | 8 +++--- .../briar/android/blog/FeedViewModel.java | 2 +- .../briar/android/viewmodel/DbViewModel.java | 25 +++++++++++++++++++ 4 files changed, 31 insertions(+), 8 deletions(-) diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/BaseViewModel.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/BaseViewModel.java index 34a04123b..03c02af29 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/blog/BaseViewModel.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/BaseViewModel.java @@ -142,7 +142,7 @@ abstract class BaseViewModel extends DbViewModel implements EventListener { runOnDbThread(true, txn -> { BlogPostItem item = getItem(txn, header); txn.attach(() -> onBlogPostItemAdded(item, local)); - }, e -> logException(LOG, WARNING, e)); + }, this::handleException); } @UiThread @@ -163,7 +163,7 @@ abstract class BaseViewModel extends DbViewModel implements EventListener { BlogPostHeader h = item.getHeader(); blogManager.addLocalComment(a, b.getId(), comment, h); } catch (DbException e) { - logException(LOG, WARNING, e); + handleException(e); } }); } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogViewModel.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogViewModel.java index 68c38d2d9..0ba0d185a 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogViewModel.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogViewModel.java @@ -39,10 +39,8 @@ import androidx.annotation.UiThread; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; -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; @MethodsNotNullByDefault @@ -132,7 +130,7 @@ class BlogViewModel extends BaseViewModel { blog.postValue(new BlogItem(b, ours, removable)); logDuration(LOG, "Loading blog", start); } catch (DbException e) { - logException(LOG, WARNING, e); + handleException(e); } }); } @@ -155,7 +153,7 @@ class BlogViewModel extends BaseViewModel { Collection contacts = blogSharingManager.getSharedWith(txn, groupId); txn.attach(() -> onSharingContactsLoaded(contacts)); - }, e -> logException(LOG, WARNING, e)); + }, this::handleException); } @UiThread @@ -173,7 +171,7 @@ class BlogViewModel extends BaseViewModel { blogManager.removeBlog(b); logDuration(LOG, "Removing blog", start); } catch (DbException e) { - logException(LOG, WARNING, e); + handleException(e); } }); } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/FeedViewModel.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/FeedViewModel.java index d5374a7bc..e4f6cbee5 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/blog/FeedViewModel.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/FeedViewModel.java @@ -105,7 +105,7 @@ class FeedViewModel extends BaseViewModel { logDuration(LOG, "Loading personal blog", start); personalBlog.postValue(b); } catch (DbException e) { - logException(LOG, WARNING, e); + handleException(e); } }); } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/viewmodel/DbViewModel.java b/briar-android/src/main/java/org/briarproject/briar/android/viewmodel/DbViewModel.java index 1c74d8f66..c99b1acf4 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/viewmodel/DbViewModel.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/viewmodel/DbViewModel.java @@ -1,6 +1,7 @@ package org.briarproject.briar.android.viewmodel; import android.app.Application; +import android.widget.Toast; import org.briarproject.bramble.api.db.DatabaseExecutor; import org.briarproject.bramble.api.db.DbCallable; @@ -11,6 +12,7 @@ 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.bramble.util.StringUtils; import java.util.ArrayList; import java.util.Collection; @@ -21,6 +23,7 @@ import java.util.logging.Logger; import javax.annotation.concurrent.Immutable; +import androidx.annotation.AnyThread; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.UiThread; @@ -31,6 +34,7 @@ import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; import androidx.recyclerview.widget.RecyclerView; +import static android.widget.Toast.LENGTH_LONG; import static java.util.logging.Level.WARNING; import static java.util.logging.Logger.getLogger; import static org.briarproject.bramble.util.LogUtils.logException; @@ -276,4 +280,25 @@ public abstract class DbViewModel extends AndroidViewModel { return new ArrayList<>(list); } + /** + * Logs the exception and shows a Toast to the user. + *

+ * Errors that are likely or expected to happen should not use this method + * and show proper error states in UI. + */ + @AnyThread + protected void handleException(Exception e) { + logException(LOG, WARNING, e); + androidExecutor.runOnUiThread(() -> { + String msg = "Error: " + e.getClass().getSimpleName(); + if (!StringUtils.isNullOrEmpty(e.getMessage())) { + msg += " " + e.getMessage(); + } + if (e.getCause() != null) { + msg += " caused by " + e.getCause().getClass().getSimpleName(); + } + Toast.makeText(getApplication(), msg, LENGTH_LONG).show(); + }); + } + } From f3210e3af28a53189ac00e0b4b1ff01e8b65b570 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Mon, 22 Mar 2021 16:48:16 -0300 Subject: [PATCH 13/13] Allow DbViewModel work on things other than lists. --- .../briar/android/blog/BaseViewModel.java | 48 +++++-- .../briar/android/blog/BlogFragment.java | 9 +- .../briar/android/blog/BlogViewModel.java | 3 +- .../briar/android/blog/FeedFragment.java | 10 +- .../briar/android/blog/FeedViewModel.java | 39 ++---- .../android/contact/ContactsViewModel.java | 8 +- .../android/forum/ForumListViewModel.java | 4 +- .../briar/android/forum/ForumViewModel.java | 2 +- .../conversation/GroupViewModel.java | 2 +- .../privategroup/list/GroupListViewModel.java | 7 +- .../briar/android/viewmodel/DbViewModel.java | 120 +++++++----------- 11 files changed, 109 insertions(+), 143 deletions(-) diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/BaseViewModel.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/BaseViewModel.java index 03c02af29..5902f1746 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/blog/BaseViewModel.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/BaseViewModel.java @@ -53,13 +53,9 @@ abstract class BaseViewModel extends DbViewModel implements EventListener { protected final AndroidNotificationManager notificationManager; protected final BlogManager blogManager; - protected final MutableLiveData>> blogPosts = + protected final MutableLiveData> blogPosts = new MutableLiveData<>(); - // UI thread - @Nullable - private Boolean postAddedWasLocal = null; - BaseViewModel(Application application, @DatabaseExecutor Executor dbExecutor, LifecycleManager lifecycleManager, @@ -147,11 +143,10 @@ abstract class BaseViewModel extends DbViewModel implements EventListener { @UiThread private void onBlogPostItemAdded(BlogPostItem item, boolean local) { - List items = addListItem(blogPosts, item); + List items = addListItem(getBlogPostItems(), item); if (items != null) { Collections.sort(items); - postAddedWasLocal = local; - blogPosts.setValue(new LiveResult<>(items)); + blogPosts.setValue(new LiveResult<>(new ListUpdate(local, items))); } } @@ -168,17 +163,42 @@ abstract class BaseViewModel extends DbViewModel implements EventListener { }); } - LiveData>> getBlogPosts() { + LiveData> getBlogPosts() { return blogPosts; } @UiThread @Nullable - Boolean getPostAddedWasLocalAndReset() { - if (postAddedWasLocal == null) return null; - boolean wasLocal = postAddedWasLocal; - postAddedWasLocal = null; - return wasLocal; + protected List getBlogPostItems() { + LiveResult value = blogPosts.getValue(); + if (value == null) return null; + ListUpdate result = value.getResultOrNull(); + return result == null ? null : result.getItems(); } + static class ListUpdate { + + @Nullable + private final Boolean postAddedWasLocal; + private final List items; + + ListUpdate(@Nullable Boolean postAddedWasLocal, + List items) { + this.postAddedWasLocal = postAddedWasLocal; + this.items = items; + } + + /** + * @return null when not a single post was added with this update. + * true when a single post was added locally and false if remotely. + */ + @Nullable + public Boolean getPostAddedWasLocal() { + return postAddedWasLocal; + } + + public List getItems() { + return items; + } + } } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogFragment.java index d9a7a0284..f43b209fd 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogFragment.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogFragment.java @@ -15,6 +15,7 @@ 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.blog.BaseViewModel.ListUpdate; import org.briarproject.briar.android.fragment.BaseFragment; import org.briarproject.briar.android.sharing.BlogSharingStatusActivity; import org.briarproject.briar.android.sharing.ShareBlogActivity; @@ -22,8 +23,6 @@ import org.briarproject.briar.android.util.BriarSnackbarBuilder; import org.briarproject.briar.android.view.BriarRecyclerView; import org.briarproject.briar.android.widget.LinkDialogFragment; -import java.util.List; - import javax.inject.Inject; import androidx.annotation.Nullable; @@ -174,9 +173,9 @@ public class BlogFragment extends BaseFragment return TAG; } - private void onBlogPostsLoaded(List items) { - adapter.submitList(items, () -> { - Boolean wasLocal = viewModel.getPostAddedWasLocalAndReset(); + private void onBlogPostsLoaded(ListUpdate update) { + adapter.submitList(update.getItems(), () -> { + Boolean wasLocal = update.getPostAddedWasLocal(); if (wasLocal != null && wasLocal) { list.scrollToPosition(0); displaySnackbar(R.string.blogs_blog_post_created, diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogViewModel.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogViewModel.java index 0ba0d185a..d3cd53c79 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogViewModel.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogViewModel.java @@ -145,7 +145,8 @@ class BlogViewModel extends BaseViewModel { } private void loadBlogPosts(GroupId groupId) { - loadList(txn -> loadBlogPosts(txn, groupId), blogPosts::setValue); + loadFromDb(txn -> new ListUpdate(null, loadBlogPosts(txn, groupId)), + blogPosts::setValue); } private void loadSharingContacts(GroupId groupId) { diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/FeedFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/FeedFragment.java index 4ea4611a6..bf7ce988c 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/blog/FeedFragment.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/FeedFragment.java @@ -13,14 +13,13 @@ import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault; import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; import org.briarproject.briar.R; import org.briarproject.briar.android.activity.ActivityComponent; +import org.briarproject.briar.android.blog.BaseViewModel.ListUpdate; import org.briarproject.briar.android.fragment.BaseFragment; import org.briarproject.briar.android.util.BriarSnackbarBuilder; import org.briarproject.briar.android.view.BriarRecyclerView; import org.briarproject.briar.android.widget.LinkDialogFragment; import org.briarproject.briar.api.blog.Blog; -import java.util.List; - import javax.inject.Inject; import androidx.annotation.Nullable; @@ -101,10 +100,9 @@ public class FeedFragment extends BaseFragment list.stopPeriodicUpdate(); } - private void onBlogPostsLoaded(List items) { - if (items.isEmpty()) list.showData(); - else adapter.submitList(items, () -> { - Boolean wasLocal = viewModel.getPostAddedWasLocalAndReset(); + private void onBlogPostsLoaded(ListUpdate update) { + adapter.submitList(update.getItems(), () -> { + Boolean wasLocal = update.getPostAddedWasLocal(); if (wasLocal != null && wasLocal) { showSnackBar(R.string.blogs_blog_post_created); } else if (wasLocal != null) { diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/FeedViewModel.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/FeedViewModel.java index e4f6cbee5..a14f4b4d4 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/blog/FeedViewModel.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/FeedViewModel.java @@ -13,7 +13,6 @@ import org.briarproject.bramble.api.identity.IdentityManager; import org.briarproject.bramble.api.lifecycle.LifecycleManager; import org.briarproject.bramble.api.nullsafety.NotNullByDefault; 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.bramble.api.system.AndroidExecutor; import org.briarproject.briar.android.viewmodel.LiveResult; @@ -34,10 +33,8 @@ import androidx.annotation.UiThread; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; -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; import static org.briarproject.briar.api.blog.BlogManager.CLIENT_ID; @@ -70,14 +67,6 @@ class FeedViewModel extends BaseViewModel { BlogPostAddedEvent b = (BlogPostAddedEvent) e; LOG.info("Blog post added"); onBlogPostAdded(b.getHeader(), b.isLocal()); - } else if (e instanceof GroupAddedEvent) { - GroupAddedEvent g = (GroupAddedEvent) e; - if (g.getGroup().getClientId().equals(CLIENT_ID)) { - LOG.info("Blog added"); - // TODO how can this even happen? - // added RSS feeds should trigger BlogPostAddedEvent, no? - onBlogAdded(g.getGroup().getId()); - } } else if (e instanceof GroupRemovedEvent) { GroupRemovedEvent g = (GroupRemovedEvent) e; if (g.getGroup().getClientId().equals(CLIENT_ID)) { @@ -115,11 +104,11 @@ class FeedViewModel extends BaseViewModel { } private void loadAllBlogPosts() { - loadList(this::loadAllBlogPosts, blogPosts::setValue); + loadFromDb(this::loadAllBlogPosts, blogPosts::setValue); } @DatabaseExecutor - private List loadAllBlogPosts(Transaction txn) + private ListUpdate loadAllBlogPosts(Transaction txn) throws DbException { long start = now(); List posts = new ArrayList<>(); @@ -128,29 +117,17 @@ class FeedViewModel extends BaseViewModel { } Collections.sort(posts); logDuration(LOG, "Loading all posts", start); - return posts; - } - - private void onBlogAdded(GroupId g) { - runOnDbThread(true, txn -> { - List posts = loadBlogPosts(txn, g); - txn.attach(() -> onBlogPostItemsAdded(posts)); - }, e -> logException(LOG, WARNING, e)); + return new ListUpdate(null, posts); } @UiThread - private void onBlogPostItemsAdded(List posts) { - List items = addListItems(blogPosts, posts); + private void onBlogRemoved(GroupId g) { + List items = removeListItems(getBlogPostItems(), item -> + item.getGroupId().equals(g) + ); if (items != null) { - Collections.sort(items); - blogPosts.setValue(new LiveResult<>(items)); + blogPosts.setValue(new LiveResult<>(new ListUpdate(null, items))); } } - private void onBlogRemoved(GroupId g) { - removeAndUpdateListItems(blogPosts, item -> - item.getGroupId().equals(g) - ); - } - } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/contact/ContactsViewModel.java b/briar-android/src/main/java/org/briarproject/briar/android/contact/ContactsViewModel.java index e0b1ab910..6d11e8e06 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/contact/ContactsViewModel.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/contact/ContactsViewModel.java @@ -86,7 +86,7 @@ public class ContactsViewModel extends DbViewModel implements EventListener { } protected void loadContacts() { - loadList(this::loadContacts, contactListItems::setValue); + loadFromDb(this::loadContacts, contactListItems::setValue); } private List loadContacts(Transaction txn) @@ -151,7 +151,7 @@ public class ContactsViewModel extends DbViewModel implements EventListener { @UiThread private void updateItem(ContactId c, Function replacer, boolean sort) { - List list = updateListItems(contactListItems, + List list = updateListItems(getList(contactListItems), itemToTest -> itemToTest.getContact().getId().equals(c), replacer); if (list == null) return; @@ -161,10 +161,8 @@ public class ContactsViewModel extends DbViewModel implements EventListener { @UiThread private void removeItem(ContactId c) { - List list = removeListItems(contactListItems, + removeAndUpdateListItems(contactListItems, itemToTest -> itemToTest.getContact().getId().equals(c)); - if (list == null) return; - contactListItems.setValue(new LiveResult<>(list)); } } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumListViewModel.java b/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumListViewModel.java index 52ad8c449..06eea08c9 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumListViewModel.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumListViewModel.java @@ -127,7 +127,7 @@ class ForumListViewModel extends DbViewModel implements EventListener { } public void loadForums() { - loadList(this::loadForums, forumItems::setValue); + loadFromDb(this::loadForums, forumItems::setValue); } @DatabaseExecutor @@ -145,7 +145,7 @@ class ForumListViewModel extends DbViewModel implements EventListener { @UiThread private void onForumPostReceived(GroupId g, ForumPostHeader header) { - List list = updateListItems(forumItems, + List list = updateListItems(getList(forumItems), itemToTest -> itemToTest.getForum().getId().equals(g), itemToUpdate -> new ForumListItem(itemToUpdate, header)); if (list == null) return; diff --git a/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumViewModel.java b/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumViewModel.java index af9254714..4b9186092 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumViewModel.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumViewModel.java @@ -135,7 +135,7 @@ class ForumViewModel extends ThreadListViewModel { @Override public void loadItems() { - loadList(txn -> { + loadFromDb(txn -> { long start = now(); List headers = forumManager.getPostHeaders(txn, groupId); diff --git a/briar-android/src/main/java/org/briarproject/briar/android/privategroup/conversation/GroupViewModel.java b/briar-android/src/main/java/org/briarproject/briar/android/privategroup/conversation/GroupViewModel.java index b3ce68229..9780cc12c 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/privategroup/conversation/GroupViewModel.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/privategroup/conversation/GroupViewModel.java @@ -159,7 +159,7 @@ class GroupViewModel extends ThreadListViewModel { @Override public void loadItems() { - loadList(txn -> { + loadFromDb(txn -> { // check first if group is dissolved isDissolved .postValue(privateGroupManager.isDissolved(txn, groupId)); diff --git a/briar-android/src/main/java/org/briarproject/briar/android/privategroup/list/GroupListViewModel.java b/briar-android/src/main/java/org/briarproject/briar/android/privategroup/list/GroupListViewModel.java index 3eedd40fc..edd3012fc 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/privategroup/list/GroupListViewModel.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/privategroup/list/GroupListViewModel.java @@ -2,7 +2,6 @@ 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.Transaction; @@ -142,7 +141,7 @@ class GroupListViewModel extends DbViewModel implements EventListener { } void loadGroups() { - loadList(this::loadGroups, groupItems::setValue); + loadFromDb(this::loadGroups, groupItems::setValue); } @DatabaseExecutor @@ -173,7 +172,7 @@ class GroupListViewModel extends DbViewModel implements EventListener { @UiThread private void onGroupMessageAdded(GroupMessageHeader header) { GroupId g = header.getGroupId(); - List list = updateListItems(groupItems, + List list = updateListItems(getList(groupItems), itemToTest -> itemToTest.getId().equals(g), itemToUpdate -> new GroupItem(itemToUpdate, header)); if (list == null) return; @@ -184,7 +183,7 @@ class GroupListViewModel extends DbViewModel implements EventListener { @UiThread private void onGroupDissolved(GroupId groupId) { - List list = updateListItems(groupItems, + List list = updateListItems(getList(groupItems), itemToTest -> itemToTest.getId().equals(groupId), itemToUpdate -> new GroupItem(itemToUpdate, true)); if (list == null) return; diff --git a/briar-android/src/main/java/org/briarproject/briar/android/viewmodel/DbViewModel.java b/briar-android/src/main/java/org/briarproject/briar/android/viewmodel/DbViewModel.java index c99b1acf4..681610da6 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/viewmodel/DbViewModel.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/viewmodel/DbViewModel.java @@ -70,7 +70,7 @@ public abstract class DbViewModel extends AndroidViewModel { *

* If you need a list of items to be displayed in a * {@link RecyclerView.Adapter}, - * use {@link #loadList(DbCallable, UiConsumer)} instead. + * use {@link #loadFromDb(DbCallable, UiConsumer)} instead. */ protected void runOnDbThread(Runnable task) { dbExecutor.execute(() -> { @@ -90,7 +90,7 @@ public abstract class DbViewModel extends AndroidViewModel { *

* If you need a list of items to be displayed in a * {@link RecyclerView.Adapter}, - * use {@link #loadList(DbCallable, UiConsumer)} instead. + * use {@link #loadFromDb(DbCallable, UiConsumer)} instead. */ protected void runOnDbThread(boolean readOnly, DbRunnable task, Consumer err) { @@ -108,21 +108,20 @@ public abstract class DbViewModel extends AndroidViewModel { } /** - * Loads a list of items on the {@link DatabaseExecutor} within a single + * Loads a data on the {@link DatabaseExecutor} within a single * {@link Transaction} and publishes it as a {@link LiveResult} * to the {@link UiThread}. *

- * Use this to ensure that modifications to your local list do not get + * Use this to ensure that modifications to your local UI data 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. + * be removed from the local data set while all messages are reloaded. * This method ensures that those operations can be processed on the * UiThread in the correct order so that the removed message will not be * re-added when the re-load completes. */ - protected > void loadList( - DbCallable task, + protected void loadFromDb(DbCallable task, UiConsumer> uiConsumer) { dbExecutor.execute(() -> { try { @@ -149,63 +148,46 @@ public abstract class DbViewModel extends AndroidViewModel { } /** - * Creates a copy of the list available in the given LiveData - * and adds the given item to the copy. + * Creates a copy of the given list and adds the given item to the copy. * - * @return a copy of the list in the LiveData with item added or null when - *

    - *
  • LiveData does not have a value - *
  • LiveResult in the LiveData has an error - *
+ * @return an updated copy of the list, or null if the list is null */ @Nullable - protected List addListItem(LiveData>> liveData, - T item) { - List items = getListCopy(liveData); - if (items == null) return null; - items.add(item); - return items; + protected List addListItem(@Nullable List list, T item) { + if (list == null) return null; + List copy = new ArrayList<>(list); + copy.add(item); + return copy; } /** - * Creates a copy of the list available in the given LiveData - * and adds the given items to the copy. + * Creates a copy of the given list and adds the given items to the copy. * - * @return a copy of the list in the LiveData with items added or null when - *
    - *
  • LiveData does not have a value - *
  • LiveResult in the LiveData has an error - *
+ * @return an updated copy of the list, or null if the list is null */ @Nullable - protected List addListItems(LiveData>> liveData, + protected List addListItems(@Nullable List list, Collection items) { - List copiedItems = getListCopy(liveData); - if (copiedItems == null) return null; - copiedItems.addAll(items); - return copiedItems; + if (list == null) return null; + List copy = new ArrayList<>(list); + copy.addAll(items); + return copy; } /** - * Creates a copy of the list available in the given LiveData - * and replaces items where the given test function returns true. + * Creates a copy of the given list, replacing 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 - *
    - *
  • LiveData does not have a value - *
  • LiveResult in the LiveData has an error - *
  • test function did return false for all items in the list - *
+ * @return an updated copy of the list, or null if either the list is null + * or the test function returns false for all items */ @Nullable - protected List updateListItems( - LiveData>> liveData, Function test, - Function replacer) { - List items = getListCopy(liveData); - if (items == null) return null; + protected List updateListItems(@Nullable List list, + Function test, Function replacer) { + if (list == null) return null; + List copy = new ArrayList<>(list); - ListIterator iterator = items.listIterator(); + ListIterator iterator = copy.listIterator(); boolean changed = false; while (iterator.hasNext()) { T item = iterator.next(); @@ -214,28 +196,23 @@ public abstract class DbViewModel extends AndroidViewModel { iterator.set(replacer.apply(item)); } } - return changed ? items : null; + return changed ? copy : 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. + * Creates a copy of the given list, removing items from it where the given + * test function returns true. * - * @return a copy of the list in the LiveData with item(s) removed - * or null when the - *
    - *
  • LiveData does not have a value - *
  • LiveResult in the LiveData has an error - *
  • test function did return false for all items in the list - *
+ * @return an updated copy of the list, or null if either the list is null + * or the test function returns false for all items */ @Nullable - protected List removeListItems( - LiveData>> liveData, Function test) { - List items = getListCopy(liveData); - if (items == null) return null; + protected List removeListItems(@Nullable List list, + Function test) { + if (list == null) return null; + List copy = new ArrayList<>(list); - ListIterator iterator = items.listIterator(); + ListIterator iterator = copy.listIterator(); boolean changed = false; while (iterator.hasNext()) { T item = iterator.next(); @@ -244,7 +221,7 @@ public abstract class DbViewModel extends AndroidViewModel { iterator.remove(); } } - return changed ? items : null; + return changed ? copy : null; } /** @@ -255,29 +232,26 @@ public abstract class DbViewModel extends AndroidViewModel { *
    *
  • LiveData does not have a value *
  • LiveResult in the LiveData has an error - *
  • test function did return false for all items in the list + *
  • test function returned false for all items in the list *
*/ @UiThread protected void removeAndUpdateListItems( MutableLiveData>> liveData, Function test) { - List list = removeListItems(liveData, test); - if (list != null) liveData.setValue(new LiveResult<>(list)); + List copy = removeListItems(getList(liveData), test); + if (copy != null) liveData.setValue(new LiveResult<>(copy)); } /** - * 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. + * Returns the list of items from the given LiveData, or null if no list is + * available. */ @Nullable - private List getListCopy(LiveData>> liveData) { + protected List getList(LiveData>> liveData) { LiveResult> value = liveData.getValue(); if (value == null) return null; - List list = value.getResultOrNull(); - if (list == null) return null; - return new ArrayList<>(list); + return value.getResultOrNull(); } /**