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..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 @@ -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.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..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 @@ -11,10 +11,8 @@ 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; import org.briarproject.briar.android.blog.ReblogActivity; import org.briarproject.briar.android.blog.ReblogFragment; import org.briarproject.briar.android.blog.RssFeedImportActivity; @@ -85,7 +83,6 @@ import dagger.Component; @ActivityScope @Component(modules = { ActivityModule.class, - BlogModule.class, CreateGroupModule.class, GroupInvitationModule.class, GroupMemberModule.class, @@ -152,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/BaseController.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/BaseController.java deleted file mode 100644 index ad7bbdc23..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.Collection; - -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 deleted file mode 100644 index 036fe9ad2..000000000 --- a/briar-android/src/main/java/org/briarproject/briar/android/blog/BaseControllerImpl.java +++ /dev/null @@ -1,209 +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.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.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()); - - 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, - IdentityManager identityManager, BlogManager blogManager) { - super(dbExecutor, lifecycleManager); - this.eventBus = eventBus; - this.notificationManager = notificationManager; - this.identityManager = identityManager; - 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 { - Collection items = loadItems(groupId); - handler.onResult(items); - } catch (DbException e) { - logException(LOG, WARNING, e); - handler.onException(e); - } - }); - } - - Collection loadItems(GroupId groupId) throws DbException { - long start = now(); - Collection headers = - blogManager.getPostHeaders(groupId); - logDuration(LOG, "Loading headers", start); - Collection 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/BasePostFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/BasePostFragment.java deleted file mode 100644 index 01d40619e..000000000 --- a/briar-android/src/main/java/org/briarproject/briar/android/blog/BasePostFragment.java +++ /dev/null @@ -1,121 +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 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); - } - }, getFragmentManager()); - 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 new file mode 100644 index 000000000..5902f1746 --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/BaseViewModel.java @@ -0,0 +1,204 @@ +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.Transaction; +import org.briarproject.bramble.api.db.TransactionManager; +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.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.Collections; +import java.util.List; +import java.util.concurrent.Executor; +import java.util.logging.Logger; + +import javax.annotation.Nullable; + +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.util.HtmlUtils.ARTICLE; + +@NotNullByDefault +abstract class BaseViewModel extends DbViewModel implements EventListener { + + private static final Logger LOG = getLogger(BaseViewModel.class.getName()); + + private final EventBus eventBus; + protected final IdentityManager identityManager; + protected final AndroidNotificationManager notificationManager; + protected final BlogManager blogManager; + + protected final MutableLiveData> blogPosts = + new MutableLiveData<>(); + + BaseViewModel(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); + this.eventBus = eventBus; + this.identityManager = identityManager; + this.notificationManager = notificationManager; + this.blogManager = blogManager; + eventBus.addListener(this); + } + + @Override + protected void onCleared() { + super.onCleared(); + eventBus.removeListener(this); + } + + @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) { + 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 { + return HtmlUtils.clean(blogManager.getPostText(txn, m), ARTICLE); + } + + LiveData> loadBlogPost(GroupId g, MessageId m) { + MutableLiveData> result = + new MutableLiveData<>(); + 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(true, txn -> { + BlogPostItem item = getItem(txn, header); + txn.attach(() -> onBlogPostItemAdded(item, local)); + }, this::handleException); + } + + @UiThread + private void onBlogPostItemAdded(BlogPostItem item, boolean local) { + List items = addListItem(getBlogPostItems(), item); + if (items != null) { + Collections.sort(items); + blogPosts.setValue(new LiveResult<>(new ListUpdate(local, items))); + } + } + + 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) { + handleException(e); + } + }); + } + + LiveData> getBlogPosts() { + return blogPosts; + } + + @UiThread + @Nullable + 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/BlogActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogActivity.java index 23a7ff252..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 @@ -16,6 +16,9 @@ import javax.annotation.Nullable; import javax.inject.Inject; import androidx.appcompat.widget.Toolbar; +import androidx.lifecycle.ViewModelProvider; + +import static java.util.Objects.requireNonNull; @MethodsNotNullByDefault @ParametersNotNullByDefault @@ -23,7 +26,16 @@ public class BlogActivity extends BriarActivity implements BaseFragmentListener { @Inject - BlogController blogController; + ViewModelProvider.Factory viewModelFactory; + + 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) { @@ -31,32 +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)); } } - @Override - public void injectActivity(ActivityComponent component) { - component.inject(this); + 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/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/BlogController.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogController.java deleted file mode 100644 index 50f5b5b3d..000000000 --- a/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogController.java +++ /dev/null @@ -1,46 +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.bramble.api.sync.MessageId; -import org.briarproject.briar.android.controller.handler.ResultExceptionHandler; - -import java.util.Collection; - -import androidx.annotation.UiThread; - -@NotNullByDefault -public interface BlogController extends BaseController { - - 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/BlogFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogFragment.java index e138ec501..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 @@ -1,9 +1,7 @@ package org.briarproject.briar.android.blog; -import android.content.DialogInterface; import android.content.Intent; import android.os.Bundle; -import android.os.Parcelable; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; @@ -12,33 +10,25 @@ 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.blog.BlogController.BlogSharingListener; -import org.briarproject.briar.android.controller.SharingController; -import org.briarproject.briar.android.controller.handler.UiResultExceptionHandler; +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; import org.briarproject.briar.android.util.BriarSnackbarBuilder; import org.briarproject.briar.android.view.BriarRecyclerView; -import org.briarproject.briar.api.blog.BlogPostHeader; - -import java.util.Collection; +import org.briarproject.briar.android.widget.LinkDialogFragment; 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; import androidx.recyclerview.widget.RecyclerView.LayoutManager; @@ -49,30 +39,22 @@ 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 BlogSharingListener, SharingListener, - OnBlogPostClickListener { + implements OnBlogPostClickListener { private final static String TAG = BlogFragment.class.getName(); @Inject - BlogController blogController; - @Inject - SharingController sharingController; - @Nullable - private Parcelable layoutManagerState; + ViewModelProvider.Factory viewModelFactory; private GroupId groupId; - private BlogPostAdapter adapter; - private LayoutManager layoutManager; + private BlogViewModel viewModel; + private final BlogPostAdapter adapter = new BlogPostAdapter(false, this); private BriarRecyclerView list; - private MenuItem writeButton, deleteButton; - private boolean isMyBlog = false, canDeleteBlog = false; static BlogFragment newInstance(GroupId groupId) { BlogFragment f = new BlogFragment(); @@ -87,8 +69,8 @@ public class BlogFragment extends BaseFragment @Override public void injectFragment(ActivityComponent component) { component.inject(this); - blogController.setBlogSharingListener(this); - sharingController.setSharingListener(this); + viewModel = new ViewModelProvider(requireActivity(), viewModelFactory) + .get(BlogViewModel.class); } @Nullable @@ -103,95 +85,75 @@ public class BlogFragment extends BaseFragment View v = inflater.inflate(R.layout.fragment_blog, container, false); - adapter = new BlogPostAdapter(requireActivity(), this, - getFragmentManager()); list = v.findViewById(R.id.postList); - layoutManager = new LinearLayoutManager(getActivity()); + LayoutManager layoutManager = new LinearLayoutManager(getActivity()); list.setLayoutManager(layoutManager); list.setAdapter(adapter); list.showProgressBar(); list.setEmptyText(getString(R.string.blogs_other_blog_empty_state)); - if (savedInstanceState != null) { - layoutManagerState = - savedInstanceState.getParcelable("layoutManager"); - } - + viewModel.getBlogPosts().observe(getViewLifecycleOwner(), result -> + result.onError(this::handleException) + .onSuccess(this::onBlogPostsLoaded) + ); + viewModel.getBlogRemoved().observe(getViewLifecycleOwner(), removed -> { + if (removed) finish(); + }); return v; } @Override public void onStart() { super.onStart(); - sharingController.onStart(); - loadBlog(); - loadSharedContacts(); - loadBlogPosts(false); + viewModel.blockAndClearNotifications(); list.startPeriodicUpdate(); } @Override public void onStop() { super.onStop(); - sharingController.onStop(); + viewModel.unblockNotifications(); list.stopPeriodicUpdate(); } - @Override - public void onDestroy() { - super.onDestroy(); - blogController.unsetBlogSharingListener(this); - sharingController.unsetSharingListener(this); - } - - @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); - 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 -> { + 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 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 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(); + return true; } + return super.onOptionsItemSelected(item); } @Override @@ -201,7 +163,6 @@ public class BlogFragment extends BaseFragment if (request == REQUEST_WRITE_BLOG_POST && result == RESULT_OK) { displaySnackbar(R.string.blogs_blog_post_created, true); - loadBlogPosts(true); } else if (request == REQUEST_SHARE_BLOG && result == RESULT_OK) { displaySnackbar(R.string.blogs_sharing_snackbar, false); } @@ -212,35 +173,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(ListUpdate update) { + adapter.submitList(update.getItems(), () -> { + Boolean wasLocal = update.getPostAddedWasLocal(); + 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(); + }); } @Override public void onBlogPostClick(BlogPostItem post) { - BlogPostFragment f = BlogPostFragment.newInstance(post.getId()); + BlogPostFragment f = + BlogPostFragment.newInstance(groupId, post.getId(), false); showNextFragment(f); } @@ -256,111 +207,10 @@ public class BlogFragment extends BaseFragment getContext().startActivity(i); } - private void loadBlogPosts(boolean reload) { - blogController.loadBlogPosts( - new UiResultExceptionHandler, - DbException>(this) { - @Override - public void onResultUi(Collection posts) { - if (posts.isEmpty()) { - list.showData(); - } else { - adapter.addAll(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()); - } - - 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 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); - } - - private void setToolbarSubTitle(int total, int online) { - ActionBar actionBar = - ((BriarActivity) getActivity()).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); + public void onLinkClick(String url) { + LinkDialogFragment f = LinkDialogFragment.newInstance(url); + f.show(getParentFragmentManager(), f.getUniqueTag()); } private void displaySnackbar(int stringId, boolean scroll) { @@ -373,38 +223,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 e2db1a7cf..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 @@ -1,35 +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 { - @ActivityScope - @Provides - BlogController provideBlogController(BaseActivity activity, - BlogControllerImpl blogController) { - activity.addLifecycleController(blogController); - return blogController; - } + @Binds + @IntoMap + @ViewModelKey(FeedViewModel.class) + ViewModel bindFeedViewModel(FeedViewModel feedViewModel); - @ActivityScope - @Provides - FeedController provideFeedController(FeedControllerImpl feedController) { - return feedController; - } - - @ActivityScope - @Provides - SharingController provideSharingController( - SharingControllerImpl sharingController) { - return sharingController; - } + @Binds + @IntoMap + @ViewModelKey(BlogViewModel.class) + ViewModel bindBlogViewModel(BlogViewModel blogViewModel); } 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..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 @@ -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,52 +7,44 @@ 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 boolean authorClickable; private final OnBlogPostClickListener listener; - @Nullable - private final FragmentManager fragmentManager; - BlogPostAdapter(Context ctx, OnBlogPostClickListener listener, - @Nullable FragmentManager fragmentManager) { - super(ctx, BlogPostItem.class); + BlogPostAdapter(boolean authorClickable, OnBlogPostClickListener listener) { + 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.authorClickable = authorClickable; this.listener = listener; - this.fragmentManager = fragmentManager; } @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); + return new BlogPostViewHolder(v, false, listener, authorClickable); } @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/BlogPostFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogPostFragment.java index cb61b75de..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 @@ -1,76 +1,163 @@ 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.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.blog.BaseController.BlogListener; -import org.briarproject.briar.android.controller.handler.UiResultExceptionHandler; -import org.briarproject.briar.api.blog.BlogPostHeader; +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.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 implements BlogListener { +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"; + static final String IS_FEED = "briar.IS_FEED"; + + protected BlogViewModel viewModel; + private final Handler handler = new Handler(Looper.getMainLooper()); + + private ProgressBar progressBar; + private BlogPostViewHolder ui; + private BlogPostItem post; + private Runnable refresher; @Inject - BlogController blogController; + ViewModelProvider.Factory viewModelFactory; - static BlogPostFragment newInstance(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; } + @Override + public void injectFragment(ActivityComponent component) { + component.inject(this); + viewModel = new ViewModelProvider(requireActivity(), viewModelFactory) + .get(BlogViewModel.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))); + 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, isFeed); + LifecycleOwner owner = getViewLifecycleOwner(); + viewModel.loadBlogPost(groupId, postId).observe(owner, result -> + result.onError(this::handleException) + .onSuccess(this::onBlogPostLoaded) + ); + return view; + } + + @Override + public void onStart() { + super.onStart(); + startPeriodicUpdate(); + } + + @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); + requireContext().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; } - @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(); - } - } 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..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; @@ -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/BlogPostViewHolder.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogPostViewHolder.java index e10af04b9..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 @@ -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,23 +18,24 @@ 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.fragment.app.FragmentManager; 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.api.blog.MessageType.POST; +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; @UiThread +@NotNullByDefault class BlogPostViewHolder extends RecyclerView.ViewHolder { private final Context ctx; @@ -43,20 +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; - @NonNull private final OnBlogPostClickListener listener; - @Nullable - private final FragmentManager fragmentManager; BlogPostViewHolder(View v, boolean fullText, - @NonNull OnBlogPostClickListener listener, - @Nullable FragmentManager fragmentManager) { + OnBlogPostClickListener listener, boolean authorClickable) { super(v); this.fullText = fullText; this.listener = listener; - this.fragmentManager = fragmentManager; + this.authorClickable = authorClickable; ctx = v.getContext(); layout = v.findViewById(R.id.postLayout); @@ -67,10 +65,6 @@ class BlogPostViewHolder extends RecyclerView.ViewHolder { commentContainer = v.findViewById(R.id.commentContainer); } - void setVisibility(int visibility) { - layout.setVisibility(visibility); - } - void hideReblogButton() { reblogButton.setVisibility(GONE); } @@ -103,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(); @@ -114,7 +108,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) @@ -147,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/BlogControllerImpl.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogViewModel.java similarity index 50% rename from briar-android/src/main/java/org/briarproject/briar/android/blog/BlogControllerImpl.java rename to briar-android/src/main/java/org/briarproject/briar/android/blog/BlogViewModel.java index 08f43d8bc..d3cd53c79 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/BlogViewModel.java @@ -1,24 +1,24 @@ package org.briarproject.briar.android.blog; -import android.app.Activity; +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; 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.bramble.api.system.AndroidExecutor; +import org.briarproject.briar.android.sharing.SharingController; +import org.briarproject.briar.android.sharing.SharingController.SharingInfo; import org.briarproject.briar.api.android.AndroidNotificationManager; import org.briarproject.briar.api.blog.Blog; import org.briarproject.briar.api.blog.BlogInvitationResponse; @@ -35,85 +35,54 @@ import java.util.logging.Logger; import javax.inject.Inject; -import androidx.annotation.Nullable; +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 @ParametersNotNullByDefault -class BlogControllerImpl extends BaseControllerImpl - implements ActivityLifecycleController, BlogController, EventListener { +class BlogViewModel extends BaseViewModel { - private static final Logger LOG = - Logger.getLogger(BlogControllerImpl.class.getName()); + private static final Logger LOG = getLogger(BlogViewModel.class.getName()); private final BlogSharingManager blogSharingManager; + private final SharingController sharingController; - // UI thread - @Nullable - private BlogSharingListener listener; + private volatile GroupId groupId; - private volatile GroupId groupId = null; + private final MutableLiveData blog = new MutableLiveData<>(); + private final MutableLiveData blogRemoved = + new MutableLiveData<>(); @Inject - BlogControllerImpl(@DatabaseExecutor Executor dbExecutor, - LifecycleManager lifecycleManager, EventBus eventBus, + BlogViewModel(Application application, + @DatabaseExecutor Executor dbExecutor, + LifecycleManager lifecycleManager, + TransactionManager db, + AndroidExecutor androidExecutor, + EventBus eventBus, + IdentityManager identityManager, AndroidNotificationManager notificationManager, - IdentityManager identityManager, BlogManager blogManager, - BlogSharingManager blogSharingManager) { - super(dbExecutor, lifecycleManager, eventBus, notificationManager, - identityManager, blogManager); + BlogManager blogManager, + BlogSharingManager blogSharingManager, + SharingController sharingController) { + super(application, dbExecutor, lifecycleManager, db, androidExecutor, + eventBus, identityManager, notificationManager, blogManager); this.blogSharingManager = blogSharingManager; - } - - @Override - public void onActivityCreate(Activity activity) { - } - - @Override - public void onActivityStart() { - super.onStart(); - notificationManager.blockNotification(groupId); - notificationManager.clearBlogPostNotification(groupId); - } - - @Override - public void onActivityStop() { - super.onStop(); - notificationManager.unblockNotification(groupId); - } - - @Override - public void onActivityDestroy() { - } - - @Override - public void setGroupId(GroupId g) { - groupId = g; - } - - @Override - public void setBlogSharingListener(BlogSharingListener listener) { - this.listener = listener; - } - - @Override - public void unsetBlogSharingListener(BlogSharingListener listener) { - if (this.listener == listener) this.listener = null; + this.sharingController = sharingController; } @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()); + onBlogPostAdded(b.getHeader(), b.isLocal()); } } else if (e instanceof BlogInvitationResponseReceivedEvent) { BlogInvitationResponseReceivedEvent b = @@ -121,41 +90,36 @@ class BlogControllerImpl extends BaseControllerImpl BlogInvitationResponse r = b.getMessageHeader(); if (r.getShareableId().equals(groupId) && r.wasAccepted()) { LOG.info("Blog invitation accepted"); - listener.onBlogInvitationAccepted(b.getContactId()); + sharingController.add(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()); + sharingController.remove(s.getContactId()); } } else if (e instanceof GroupRemovedEvent) { GroupRemovedEvent g = (GroupRemovedEvent) e; if (g.getGroup().getId().equals(groupId)) { LOG.info("Blog removed"); - listener.onBlogRemoved(); + blogRemoved.setValue(true); } } } - @Override - public void loadBlogPosts( - ResultExceptionHandler, DbException> handler) { - if (groupId == null) throw new IllegalStateException(); - loadBlogPosts(groupId, handler); + /** + * 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); + loadSharingContacts(groupId); } - @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(); + private void loadBlog(GroupId groupId) { runOnDbThread(() -> { try { long start = now(); @@ -163,50 +127,65 @@ class BlogControllerImpl extends BaseControllerImpl 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); + blog.postValue(new BlogItem(b, ours, removable)); logDuration(LOG, "Loading blog", start); - handler.onResult(blog); } catch (DbException e) { - logException(LOG, WARNING, e); - handler.onException(e); + handleException(e); } }); } - @Override - public void deleteBlog(ResultExceptionHandler handler) { - if (groupId == null) throw new IllegalStateException(); + void blockAndClearNotifications() { + notificationManager.blockNotification(groupId); + notificationManager.clearBlogPostNotification(groupId); + } + + void unblockNotifications() { + notificationManager.unblockNotification(groupId); + } + + private void loadBlogPosts(GroupId groupId) { + loadFromDb(txn -> new ListUpdate(null, loadBlogPosts(txn, groupId)), + blogPosts::setValue); + } + + private void loadSharingContacts(GroupId groupId) { + runOnDbThread(true, txn -> { + Collection contacts = + blogSharingManager.getSharedWith(txn, groupId); + txn.attach(() -> onSharingContactsLoaded(contacts)); + }, this::handleException); + } + + @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() { 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); + handleException(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); - } - }); + LiveData getBlog() { + return blog; } + LiveData getBlogRemoved() { + return blogRemoved; + } + + LiveData getSharingInfo() { + return sharingController.getSharingInfo(); + } } 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..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 @@ -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; @@ -10,53 +9,41 @@ 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.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 org.briarproject.briar.api.blog.BlogPostHeader; - -import java.util.Collection; -import java.util.logging.Logger; 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 BlogPostAdapter adapter; + private FeedViewModel viewModel; + private final BlogPostAdapter adapter = new BlogPostAdapter(true, this); private LinearLayoutManager layoutManager; private BriarRecyclerView list; - @Nullable - private Blog personalBlog; - @Nullable - private Parcelable layoutManagerState; public static FeedFragment newInstance() { FeedFragment f = new FeedFragment(); @@ -70,7 +57,8 @@ public class FeedFragment extends BaseFragment implements @Override public void injectFragment(ActivityComponent component) { component.inject(this); - feedController.setFeedListener(this); + viewModel = new ViewModelProvider(this, viewModelFactory) + .get(FeedViewModel.class); } @Nullable @@ -82,9 +70,6 @@ public class FeedFragment extends BaseFragment implements View v = inflater.inflate(R.layout.fragment_blog, container, false); - adapter = - new BlogPostAdapter(getActivity(), this, getFragmentManager()); - layoutManager = new LinearLayoutManager(getActivity()); list = v.findViewById(R.id.postList); list.setLayoutManager(layoutManager); @@ -93,103 +78,38 @@ public class FeedFragment extends BaseFragment implements list.setEmptyText(R.string.blogs_feed_empty_state); list.setEmptyAction(R.string.blogs_feed_empty_state_action); - if (savedInstanceState != null) { - layoutManagerState = - savedInstanceState.getParcelable("layoutManager"); - } + viewModel.getBlogPosts().observe(getViewLifecycleOwner(), result -> + result.onError(this::handleException) + .onSuccess(this::onBlogPostsLoaded) + ); 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.blockAndClearAllBlogPostNotifications(); 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 - public void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - if (layoutManager != null) { - layoutManagerState = layoutManager.onSaveInstanceState(); - outState.putParcelable("layoutManager", layoutManagerState); - } - } - - 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(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) { + showSnackBar(R.string.blogs_blog_post_received); + } + list.showData(); + }); } @Override @@ -200,67 +120,48 @@ 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 public void onBlogPostClick(BlogPostItem post) { - FeedPostFragment f = - FeedPostFragment.newInstance(post.getGroupId(), post.getId()); + BaseFragment f = BlogPostFragment + .newInstance(post.getGroupId(), post.getId(), true); showNextFragment(f); } @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 + public void onLinkClick(String url) { + LinkDialogFragment f = LinkDialogFragment.newInstance(url); + f.show(getParentFragmentManager(), f.getUniqueTag()); } @Override @@ -283,14 +184,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 deleted file mode 100644 index b9750a5a1..000000000 --- a/briar-android/src/main/java/org/briarproject/briar/android/blog/FeedPostFragment.java +++ /dev/null @@ -1,87 +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.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 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(); - - private GroupId blogId; - - @Inject - FeedController feedController; - - 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); - } - - @Nullable - @Override - public View onCreateView(LayoutInflater inflater, - @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); - - return super.onCreateView(inflater, container, savedInstanceState); - } - - @Override - public String getUniqueTag() { - 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..a14f4b4d4 --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/FeedViewModel.java @@ -0,0 +1,133 @@ +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.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.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.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.inject.Inject; + +import androidx.annotation.UiThread; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; + +import static java.util.logging.Logger.getLogger; +import static org.briarproject.bramble.util.LogUtils.logDuration; +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()); + + private final MutableLiveData personalBlog = new MutableLiveData<>(); + + @Inject + FeedViewModel(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); + loadPersonalBlog(); + loadAllBlogPosts(); + } + + @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 GroupRemovedEvent) { + GroupRemovedEvent g = (GroupRemovedEvent) e; + if (g.getGroup().getClientId().equals(CLIENT_ID)) { + LOG.info("Blog removed"); + onBlogRemoved(g.getGroup().getId()); + } + } + } + + void blockAndClearAllBlogPostNotifications() { + notificationManager.blockAllBlogPostNotifications(); + notificationManager.clearAllBlogPostNotifications(); + } + + void unblockAllBlogPostNotifications() { + notificationManager.unblockAllBlogPostNotifications(); + } + + private 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) { + handleException(e); + } + }); + } + + LiveData getPersonalBlog() { + return personalBlog; + } + + private void loadAllBlogPosts() { + loadFromDb(this::loadAllBlogPosts, blogPosts::setValue); + } + + @DatabaseExecutor + private ListUpdate loadAllBlogPosts(Transaction txn) + throws DbException { + long start = now(); + List posts = new ArrayList<>(); + for (GroupId g : blogManager.getBlogIds(txn)) { + posts.addAll(loadBlogPosts(txn, g)); + } + Collections.sort(posts); + logDuration(LOG, "Loading all posts", start); + return new ListUpdate(null, posts); + } + + @UiThread + private void onBlogRemoved(GroupId g) { + List items = removeListItems(getBlogPostItems(), item -> + item.getGroupId().equals(g) + ); + if (items != null) { + blogPosts.setValue(new LiveResult<>(new ListUpdate(null, items))); + } + } + +} 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/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 318bdecab..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 @@ -7,19 +7,17 @@ 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; 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; @@ -27,13 +25,15 @@ 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; 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 @@ -42,12 +42,13 @@ public class ReblogFragment extends BaseFragment implements SendListener { public static final String TAG = ReblogFragment.class.getName(); + @Inject + ViewModelProvider.Factory viewModelFactory; + + private BlogViewModel viewModel; private ViewHolder ui; private BlogPostItem item; - @Inject - FeedController feedController; - static ReblogFragment newInstance(GroupId groupId, MessageId messageId) { ReblogFragment f = new ReblogFragment(); @@ -67,6 +68,8 @@ public class ReblogFragment extends BaseFragment implements SendListener { @Override public void injectFragment(ActivityComponent component) { component.inject(this); + viewModel = new ViewModelProvider(requireActivity(), viewModelFactory) + .get(BlogViewModel.class); } @Override @@ -90,30 +93,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 +117,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(); } @@ -144,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; @@ -155,18 +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 - } - }, getFragmentManager()); + true, this, false); 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/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/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(); - } - -} 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..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; @@ -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/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 041305a37..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; @@ -193,10 +192,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/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); 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..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 @@ -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,8 +12,10 @@ 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; import java.util.List; import java.util.ListIterator; import java.util.concurrent.Executor; @@ -20,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; @@ -27,8 +31,10 @@ 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 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; @@ -64,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(() -> { @@ -84,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) { @@ -102,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 { @@ -143,25 +148,46 @@ public abstract class DbViewModel extends AndroidViewModel { } /** - * 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 and adds the given item to the copy. * - * @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 the list is null */ @Nullable - protected List updateListItems( - LiveData>> liveData, Function test, - Function replacer) { - List items = getListCopy(liveData); - if (items == null) return null; + protected List addListItem(@Nullable List list, T item) { + if (list == null) return null; + List copy = new ArrayList<>(list); + copy.add(item); + return copy; + } - ListIterator iterator = items.listIterator(); + /** + * Creates a copy of the given list and adds the given items to the copy. + * + * @return an updated copy of the list, or null if the list is null + */ + @Nullable + protected List addListItems(@Nullable List list, + Collection items) { + if (list == null) return null; + List copy = new ArrayList<>(list); + copy.addAll(items); + return copy; + } + + /** + * Creates a copy of the given list, replacing items where the given test + * function returns true. + * + * @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(@Nullable List list, + Function test, Function replacer) { + if (list == null) return null; + List copy = new ArrayList<>(list); + + ListIterator iterator = copy.listIterator(); boolean changed = false; while (iterator.hasNext()) { T item = iterator.next(); @@ -170,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(); @@ -200,21 +221,58 @@ public abstract class DbViewModel extends AndroidViewModel { iterator.remove(); } } - return changed ? items : null; + return changed ? copy : null; } /** - * Retrieves a copy of the list of items from the given LiveData - * or null if it is not available. - * The list copy can be safely mutated. + * 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 returned false for all items in the list + *
+ */ + @UiThread + protected void removeAndUpdateListItems( + MutableLiveData>> liveData, + Function test) { + List copy = removeListItems(getList(liveData), test); + if (copy != null) liveData.setValue(new LiveResult<>(copy)); + } + + /** + * 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(); + } + + /** + * 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(); + }); } } 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..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 @@ -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,21 +99,38 @@ 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. */ - 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. */ 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..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 @@ -446,19 +446,22 @@ class BlogManagerImpl extends BdfIncomingMessageHook implements BlogManager, } @Override - public BlogPostHeader getPostHeader(GroupId g, MessageId m) + 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(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); } } @@ -471,6 +474,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 +500,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 +514,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 +544,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 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)