Turn FeedController into FeedViewModel

This commit is contained in:
Torsten Grote
2021-01-11 16:36:15 -03:00
parent ad20e5230a
commit ab1ed0ff5a
17 changed files with 546 additions and 423 deletions

View File

@@ -30,6 +30,7 @@ import org.briarproject.bramble.util.StringUtils;
import org.briarproject.briar.android.account.DozeHelperModule;
import org.briarproject.briar.android.account.LockManagerImpl;
import org.briarproject.briar.android.account.SetupModule;
import org.briarproject.briar.android.blog.BlogModule;
import org.briarproject.briar.android.contact.ContactListModule;
import org.briarproject.briar.android.forum.ForumModule;
import org.briarproject.briar.android.introduction.IntroductionModule;
@@ -85,6 +86,7 @@ import static org.briarproject.briar.android.TestingConstants.IS_DEBUG_BUILD;
ContactListModule.class,
IntroductionModule.class,
// below need to be within same scope as ViewModelProvider.Factory
BlogModule.BindsModule.class,
ForumModule.class,
GroupListModule.class,
GroupConversationModule.class,

View File

@@ -8,7 +8,7 @@ import org.briarproject.briar.android.controller.handler.ExceptionHandler;
import org.briarproject.briar.android.controller.handler.ResultExceptionHandler;
import org.briarproject.briar.api.blog.BlogPostHeader;
import java.util.Collection;
import java.util.List;
import javax.annotation.Nullable;
@@ -24,7 +24,7 @@ interface BaseController {
void onStop();
void loadBlogPosts(GroupId g,
ResultExceptionHandler<Collection<BlogPostItem>, DbException> handler);
ResultExceptionHandler<List<BlogPostItem>, DbException> handler);
void loadBlogPost(BlogPostHeader header,
ResultExceptionHandler<BlogPostItem, DbException> handler);

View File

@@ -23,6 +23,7 @@ import org.briarproject.briar.util.HtmlUtils;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
@@ -80,10 +81,10 @@ abstract class BaseControllerImpl extends DbControllerImpl
@Override
public void loadBlogPosts(GroupId groupId,
ResultExceptionHandler<Collection<BlogPostItem>, DbException> handler) {
ResultExceptionHandler<List<BlogPostItem>, DbException> handler) {
runOnDbThread(() -> {
try {
Collection<BlogPostItem> items = loadItems(groupId);
List<BlogPostItem> items = loadItems(groupId);
handler.onResult(items);
} catch (DbException e) {
logException(LOG, WARNING, e);
@@ -92,12 +93,12 @@ abstract class BaseControllerImpl extends DbControllerImpl
});
}
Collection<BlogPostItem> loadItems(GroupId groupId) throws DbException {
List<BlogPostItem> loadItems(GroupId groupId) throws DbException {
long start = now();
Collection<BlogPostHeader> headers =
blogManager.getPostHeaders(groupId);
logDuration(LOG, "Loading headers", start);
Collection<BlogPostItem> items = new ArrayList<>(headers.size());
List<BlogPostItem> items = new ArrayList<>(headers.size());
start = now();
for (BlogPostHeader h : headers) {
headerCache.put(h.getId(), h);

View File

@@ -0,0 +1,219 @@
package org.briarproject.briar.android.blog;
import android.app.Application;
import org.briarproject.bramble.api.db.DatabaseExecutor;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.db.NoSuchMessageException;
import org.briarproject.bramble.api.db.Transaction;
import org.briarproject.bramble.api.db.TransactionManager;
import org.briarproject.bramble.api.event.Event;
import org.briarproject.bramble.api.event.EventBus;
import org.briarproject.bramble.api.event.EventListener;
import org.briarproject.bramble.api.identity.IdentityManager;
import org.briarproject.bramble.api.identity.LocalAuthor;
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.bramble.api.system.AndroidExecutor;
import org.briarproject.briar.android.viewmodel.DbViewModel;
import org.briarproject.briar.android.viewmodel.LiveResult;
import org.briarproject.briar.api.blog.Blog;
import org.briarproject.briar.api.blog.BlogCommentHeader;
import org.briarproject.briar.api.blog.BlogManager;
import org.briarproject.briar.api.blog.BlogPostHeader;
import org.briarproject.briar.util.HtmlUtils;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import javax.inject.Inject;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.Transformations;
import static java.util.Objects.requireNonNull;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.LogUtils.logDuration;
import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.bramble.util.LogUtils.now;
import static org.briarproject.briar.util.HtmlUtils.ARTICLE;
@NotNullByDefault
public class BaseViewModel extends DbViewModel implements EventListener {
private static Logger LOG = getLogger(BaseViewModel.class.getName());
protected final TransactionManager db;
private final EventBus eventBus;
protected final IdentityManager identityManager;
protected final BlogManager blogManager;
protected final MutableLiveData<LiveResult<List<BlogPostItem>>> blogPosts =
new MutableLiveData<>();
// TODO do we still need those caches?
private final Map<MessageId, String> textCache = new ConcurrentHashMap<>();
private final Map<MessageId, BlogPostHeader> headerCache =
new ConcurrentHashMap<>();
@Inject
BaseViewModel(Application application,
@DatabaseExecutor Executor dbExecutor,
LifecycleManager lifecycleManager,
TransactionManager db,
AndroidExecutor androidExecutor,
EventBus eventBus,
IdentityManager identityManager,
BlogManager blogManager) {
super(application, dbExecutor, lifecycleManager, db, androidExecutor);
this.db = db;
this.eventBus = eventBus;
this.identityManager = identityManager;
this.blogManager = blogManager;
eventBus.addListener(this);
}
@Override
protected void onCleared() {
super.onCleared();
eventBus.removeListener(this);
}
@Override
public void eventOccurred(Event e) {
}
void loadItems(GroupId groupId) {
loadList(txn -> loadBlogPosts(txn, groupId), blogPosts::setValue);
}
@DatabaseExecutor
protected List<BlogPostItem> loadBlogPosts(Transaction txn, GroupId groupId)
throws DbException {
long start = now();
List<BlogPostHeader> headers =
blogManager.getPostHeaders(txn, groupId);
logDuration(LOG, "Loading headers", start);
List<BlogPostItem> items = new ArrayList<>(headers.size());
start = now();
for (BlogPostHeader h : headers) {
headerCache.put(h.getId(), h);
BlogPostItem item = getItem(txn, h);
items.add(item);
}
logDuration(LOG, "Loading bodies", start);
return items;
}
@DatabaseExecutor
protected BlogPostItem getItem(Transaction txn, BlogPostHeader h)
throws DbException {
String text;
if (h instanceof BlogCommentHeader) {
BlogCommentHeader c = (BlogCommentHeader) h;
BlogCommentItem item = new BlogCommentItem(c);
text = getPostText(txn, item.getPostHeader().getId());
item.setText(text);
return item;
} else {
text = getPostText(txn, h.getId());
return new BlogPostItem(h, text);
}
}
@DatabaseExecutor
private String getPostText(Transaction txn, MessageId m)
throws DbException {
String text = textCache.get(m);
if (text == null) {
text = HtmlUtils.clean(blogManager.getPostText(txn, m), ARTICLE);
textCache.put(m, text);
}
return text;
}
LiveData<LiveResult<BlogPostItem>> loadBlogPost(GroupId g, MessageId m) {
MutableLiveData<LiveResult<BlogPostItem>> result =
new MutableLiveData<>();
runOnDbThread(() -> {
try {
long start = now();
BlogPostHeader header1 = getPostHeader(g, m);
BlogPostItem item = db.transactionWithResult(true, txn ->
getItem(txn, header1)
);
logDuration(LOG, "Loading post", start);
result.postValue(new LiveResult<>(item));
} catch (DbException e) {
logException(LOG, WARNING, e);
result.postValue(new LiveResult<>(e));
}
});
return result;
}
@DatabaseExecutor
private BlogPostHeader getPostHeader(GroupId g, MessageId m)
throws DbException {
BlogPostHeader header = headerCache.get(m);
if (header == null) {
header = blogManager.getPostHeader(g, m);
headerCache.put(m, header);
}
return header;
}
void repeatPost(BlogPostItem item, @Nullable String comment) {
runOnDbThread(() -> {
try {
LocalAuthor a = identityManager.getLocalAuthor();
Blog b = blogManager.getPersonalBlog(a);
BlogPostHeader h = item.getHeader();
blogManager.addLocalComment(a, b.getId(), comment, h);
} catch (DbException e) {
logException(LOG, WARNING, e);
}
});
}
LiveData<LiveResult<List<BlogPostItem>>> getAllBlogPosts() {
return blogPosts;
}
LiveData<LiveResult<List<BlogPostItem>>> getBlogPosts(GroupId g) {
return Transformations.map(blogPosts, result -> {
List<BlogPostItem> allPosts = result.getResultOrNull();
if (allPosts == null) return result;
List<BlogPostItem> groupPosts = new ArrayList<>();
for (BlogPostItem item : allPosts) {
if (item.getGroupId().equals(g)) groupPosts.add(item);
}
return new LiveResult<>(groupPosts);
});
}
LiveData<LiveResult<BlogPostItem>> getBlogPost(MessageId m) {
return Transformations.map(blogPosts, result -> {
List<BlogPostItem> allPosts = result.getResultOrNull();
if (allPosts == null) {
Exception e = requireNonNull(result.getException());
return new LiveResult<>(e);
}
for (BlogPostItem item : allPosts) {
if (item.getId().equals(m)) return new LiveResult<>(item);
}
return new LiveResult<>(new NoSuchMessageException());
});
}
}

View File

@@ -8,6 +8,7 @@ import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.briar.android.controller.handler.ResultExceptionHandler;
import java.util.Collection;
import java.util.List;
import androidx.annotation.UiThread;
@@ -23,7 +24,7 @@ public interface BlogController extends BaseController {
void unsetBlogSharingListener(BlogSharingListener listener);
void loadBlogPosts(
ResultExceptionHandler<Collection<BlogPostItem>, DbException> handler);
ResultExceptionHandler<List<BlogPostItem>, DbException> handler);
void loadBlogPost(MessageId m,
ResultExceptionHandler<BlogPostItem, DbException> handler);

View File

@@ -30,6 +30,7 @@ import org.briarproject.briar.api.sharing.event.ContactLeftShareableEvent;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.logging.Logger;
@@ -140,7 +141,7 @@ class BlogControllerImpl extends BaseControllerImpl
@Override
public void loadBlogPosts(
ResultExceptionHandler<Collection<BlogPostItem>, DbException> handler) {
ResultExceptionHandler<List<BlogPostItem>, DbException> handler) {
if (groupId == null) throw new IllegalStateException();
loadBlogPosts(groupId, handler);
}

View File

@@ -32,6 +32,7 @@ import org.briarproject.briar.android.view.BriarRecyclerView;
import org.briarproject.briar.api.blog.BlogPostHeader;
import java.util.Collection;
import java.util.List;
import javax.inject.Inject;
@@ -103,8 +104,7 @@ public class BlogFragment extends BaseFragment
View v = inflater.inflate(R.layout.fragment_blog, container, false);
adapter = new BlogPostAdapter(requireActivity(), this,
getFragmentManager());
adapter = new BlogPostAdapter(this, getParentFragmentManager());
list = v.findViewById(R.id.postList);
layoutManager = new LinearLayoutManager(getActivity());
list.setLayoutManager(layoutManager);
@@ -219,7 +219,7 @@ public class BlogFragment extends BaseFragment
this) {
@Override
public void onResultUi(BlogPostItem post) {
adapter.add(post);
// adapter.add(post);
if (local) {
list.scrollToPosition(0);
displaySnackbar(R.string.blogs_blog_post_created,
@@ -258,14 +258,14 @@ public class BlogFragment extends BaseFragment
private void loadBlogPosts(boolean reload) {
blogController.loadBlogPosts(
new UiResultExceptionHandler<Collection<BlogPostItem>,
new UiResultExceptionHandler<List<BlogPostItem>,
DbException>(this) {
@Override
public void onResultUi(Collection<BlogPostItem> posts) {
public void onResultUi(List<BlogPostItem> posts) {
if (posts.isEmpty()) {
list.showData();
} else {
adapter.addAll(posts);
adapter.submitList(posts);
if (reload || layoutManagerState == null) {
list.scrollToPosition(0);
} else {

View File

@@ -4,13 +4,25 @@ import org.briarproject.briar.android.activity.ActivityScope;
import org.briarproject.briar.android.activity.BaseActivity;
import org.briarproject.briar.android.controller.SharingController;
import org.briarproject.briar.android.controller.SharingControllerImpl;
import org.briarproject.briar.android.viewmodel.ViewModelKey;
import androidx.lifecycle.ViewModel;
import dagger.Binds;
import dagger.Module;
import dagger.Provides;
import dagger.multibindings.IntoMap;
@Module
public class BlogModule {
@Module
public abstract static class BindsModule {
@Binds
@IntoMap
@ViewModelKey(FeedViewModel.class)
abstract ViewModel bindFeedViewModel(FeedViewModel feedViewModel);
}
@ActivityScope
@Provides
BlogController provideBlogController(BaseActivity activity,
@@ -19,12 +31,6 @@ public class BlogModule {
return blogController;
}
@ActivityScope
@Provides
FeedController provideFeedController(FeedControllerImpl feedController) {
return feedController;
}
@ActivityScope
@Provides
SharingController provideSharingController(

View File

@@ -1,6 +1,5 @@
package org.briarproject.briar.android.blog;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -8,22 +7,33 @@ import android.view.ViewGroup;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.briar.R;
import org.briarproject.briar.android.util.BriarAdapter;
import androidx.annotation.Nullable;
import androidx.fragment.app.FragmentManager;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ListAdapter;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
class BlogPostAdapter extends BriarAdapter<BlogPostItem, BlogPostViewHolder> {
class BlogPostAdapter extends ListAdapter<BlogPostItem, BlogPostViewHolder> {
private final OnBlogPostClickListener listener;
@Nullable
private final FragmentManager fragmentManager;
BlogPostAdapter(Context ctx, OnBlogPostClickListener listener,
BlogPostAdapter(OnBlogPostClickListener listener,
@Nullable FragmentManager fragmentManager) {
super(ctx, BlogPostItem.class);
super(new DiffUtil.ItemCallback<BlogPostItem>() {
@Override
public boolean areItemsTheSame(BlogPostItem a, BlogPostItem b) {
return a.getId().equals(b.getId());
}
@Override
public boolean areContentsTheSame(BlogPostItem a, BlogPostItem b) {
return a.isRead() == b.isRead();
}
});
this.listener = listener;
this.fragmentManager = fragmentManager;
}
@@ -31,29 +41,14 @@ class BlogPostAdapter extends BriarAdapter<BlogPostItem, BlogPostViewHolder> {
@Override
public BlogPostViewHolder onCreateViewHolder(ViewGroup parent,
int viewType) {
View v = LayoutInflater.from(ctx).inflate(
View v = LayoutInflater.from(parent.getContext()).inflate(
R.layout.list_item_blog_post, parent, false);
return new BlogPostViewHolder(v, false, listener, fragmentManager);
}
@Override
public void onBindViewHolder(BlogPostViewHolder ui, int position) {
ui.bindItem(getItemAt(position));
}
@Override
public int compare(BlogPostItem a, BlogPostItem b) {
return a.compareTo(b);
}
@Override
public boolean areContentsTheSame(BlogPostItem a, BlogPostItem b) {
return a.isRead() == b.isRead();
}
@Override
public boolean areItemsTheSame(BlogPostItem a, BlogPostItem b) {
return a.getId().equals(b.getId());
ui.bindItem(getItem(position));
}
}

View File

@@ -74,9 +74,6 @@ public class BlogPostItem implements Comparable<BlogPostItem> {
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());
}
}

View File

@@ -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<Collection<BlogPostItem>, DbException> handler);
void loadPersonalBlog(ResultExceptionHandler<Blog, DbException> handler);
@UiThread
void setFeedListener(FeedListener listener);
@UiThread
void unsetFeedListener(FeedListener listener);
@NotNullByDefault
interface FeedListener extends BlogListener {
@UiThread
void onBlogAdded();
}
}

View File

@@ -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<Collection<BlogPostItem>, DbException> handler) {
runOnDbThread(() -> {
try {
long start = now();
Collection<BlogPostItem> 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<Blog, DbException> 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);
}
});
}
}

View File

@@ -10,52 +10,44 @@ import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.blog.FeedController.FeedListener;
import org.briarproject.briar.android.controller.handler.UiResultExceptionHandler;
import org.briarproject.briar.android.fragment.BaseFragment;
import org.briarproject.briar.android.util.BriarSnackbarBuilder;
import org.briarproject.briar.android.view.BriarRecyclerView;
import org.briarproject.briar.api.blog.Blog;
import org.briarproject.briar.api.blog.BlogPostHeader;
import java.util.Collection;
import java.util.logging.Logger;
import java.util.List;
import javax.inject.Inject;
import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.LinearLayoutManager;
import static android.app.Activity.RESULT_OK;
import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP;
import static com.google.android.material.snackbar.Snackbar.LENGTH_LONG;
import static org.briarproject.briar.android.activity.BriarActivity.GROUP_ID;
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_WRITE_BLOG_POST;
@UiThread
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class FeedFragment extends BaseFragment implements
OnBlogPostClickListener, FeedListener {
public class FeedFragment extends BaseFragment
implements OnBlogPostClickListener {
public final static String TAG = FeedFragment.class.getName();
private static final Logger LOG = Logger.getLogger(TAG);
@Inject
FeedController feedController;
ViewModelProvider.Factory viewModelFactory;
private FeedViewModel viewModel;
private BlogPostAdapter adapter;
private LinearLayoutManager layoutManager;
private BriarRecyclerView list;
@Nullable
private Blog personalBlog;
@Nullable
private Parcelable layoutManagerState;
public static FeedFragment newInstance() {
@@ -70,7 +62,12 @@ public class FeedFragment extends BaseFragment implements
@Override
public void injectFragment(ActivityComponent component) {
component.inject(this);
feedController.setFeedListener(this);
// TODO don't use NavDrawerActivity scope here
viewModel = new ViewModelProvider(requireActivity(), viewModelFactory)
.get(FeedViewModel.class);
// TODO ideally we only do this once when the ViewModel gets created
viewModel.loadPersonalBlog();
viewModel.loadAllBlogPosts();
}
@Nullable
@@ -82,8 +79,7 @@ public class FeedFragment extends BaseFragment implements
View v = inflater.inflate(R.layout.fragment_blog, container, false);
adapter =
new BlogPostAdapter(getActivity(), this, getFragmentManager());
adapter = new BlogPostAdapter(this, getParentFragmentManager());
layoutManager = new LinearLayoutManager(getActivity());
list = v.findViewById(R.id.postList);
@@ -93,6 +89,12 @@ public class FeedFragment extends BaseFragment implements
list.setEmptyText(R.string.blogs_feed_empty_state);
list.setEmptyAction(R.string.blogs_feed_empty_state_action);
viewModel.getAllBlogPosts().observe(getViewLifecycleOwner(), result ->
result
.onError(this::handleException)
.onSuccess(this::onBlogPostsLoaded)
);
if (savedInstanceState != null) {
layoutManagerState =
savedInstanceState.getParcelable("layoutManager");
@@ -101,40 +103,19 @@ public class FeedFragment extends BaseFragment implements
return v;
}
@Override
public void onActivityResult(int requestCode, int resultCode,
@Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
// The BlogPostAddedEvent arrives when the controller is not listening
if (requestCode == REQUEST_WRITE_BLOG_POST && resultCode == RESULT_OK) {
showSnackBar(R.string.blogs_blog_post_created);
}
}
@Override
public void onStart() {
super.onStart();
feedController.onStart();
viewModel.blockAllBlogPostNotifications();
viewModel.clearAllBlogPostNotifications();
list.startPeriodicUpdate();
loadPersonalBlog();
loadBlogPosts(false);
}
@Override
public void onStop() {
super.onStop();
feedController.onStop();
adapter.clear();
list.showProgressBar();
viewModel.unblockAllBlogPostNotifications();
list.stopPeriodicUpdate();
// TODO save list position in database/preferences?
}
@Override
public void onDestroy() {
super.onDestroy();
feedController.unsetFeedListener(this);
}
@Override
@@ -146,50 +127,16 @@ public class FeedFragment extends BaseFragment implements
}
}
private void loadPersonalBlog() {
feedController.loadPersonalBlog(
new UiResultExceptionHandler<Blog, DbException>(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<Collection<BlogPostItem>, DbException>(
this) {
@Override
public void onResultUi(Collection<BlogPostItem> posts) {
if (revision == adapter.getRevision()) {
adapter.incrementRevision();
if (clear) adapter.setItems(posts);
else adapter.addAll(posts);
if (posts.isEmpty()) list.showData();
if (layoutManagerState == null) {
list.scrollToPosition(0); // Scroll to the top
} else {
layoutManager.onRestoreInstanceState(
layoutManagerState);
}
} else {
LOG.info("Concurrent update, reloading");
loadBlogPosts(clear);
}
}
@Override
public void onExceptionUi(DbException exception) {
handleException(exception);
}
});
private void onBlogPostsLoaded(List<BlogPostItem> items) {
if (items.isEmpty()) list.showData();
else adapter.submitList(items, () -> {
Boolean wasLocal = viewModel.getPostAddedWasLocalAndReset();
if (wasLocal != null && wasLocal) {
showSnackBar(R.string.blogs_blog_post_created);
} else if (wasLocal != null) {
showSnackBar(R.string.blogs_blog_post_received);
}
});
}
@Override
@@ -200,52 +147,27 @@ public class FeedFragment extends BaseFragment implements
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (personalBlog == null) return false;
switch (item.getItemId()) {
case R.id.action_write_blog_post:
Intent i1 =
new Intent(getActivity(), WriteBlogPostActivity.class);
i1.putExtra(GROUP_ID, personalBlog.getId().getBytes());
startActivityForResult(i1, REQUEST_WRITE_BLOG_POST);
return true;
case R.id.action_rss_feeds_import:
Intent i2 =
new Intent(getActivity(), RssFeedImportActivity.class);
startActivity(i2);
return true;
case R.id.action_rss_feeds_manage:
Intent i3 =
new Intent(getActivity(), RssFeedManageActivity.class);
i3.putExtra(GROUP_ID, personalBlog.getId().getBytes());
startActivity(i3);
return true;
default:
return super.onOptionsItemSelected(item);
int itemId = item.getItemId();
if (itemId == R.id.action_write_blog_post) {
Blog personalBlog = viewModel.getPersonalBlog().getValue();
if (personalBlog == null) return false;
Intent i = new Intent(getActivity(), WriteBlogPostActivity.class);
i.putExtra(GROUP_ID, personalBlog.getId().getBytes());
startActivity(i);
return true;
} else if (itemId == R.id.action_rss_feeds_import) {
Intent i = new Intent(getActivity(), RssFeedImportActivity.class);
startActivity(i);
return true;
} else if (itemId == R.id.action_rss_feeds_manage) {
Blog personalBlog = viewModel.getPersonalBlog().getValue();
if (personalBlog == null) return false;
Intent i = new Intent(getActivity(), RssFeedManageActivity.class);
i.putExtra(GROUP_ID, personalBlog.getId().getBytes());
startActivity(i);
return true;
}
}
@Override
public void onBlogPostAdded(BlogPostHeader header, boolean local) {
feedController.loadBlogPost(header,
new UiResultExceptionHandler<BlogPostItem, DbException>(
this) {
@Override
public void onResultUi(BlogPostItem post) {
adapter.incrementRevision();
adapter.add(post);
if (local) {
showSnackBar(R.string.blogs_blog_post_created);
} else {
showSnackBar(R.string.blogs_blog_post_received);
}
}
@Override
public void onExceptionUi(DbException exception) {
handleException(exception);
}
}
);
return super.onOptionsItemSelected(item);
}
@Override
@@ -257,10 +179,10 @@ public class FeedFragment extends BaseFragment implements
@Override
public void onAuthorClick(BlogPostItem post) {
Intent i = new Intent(getContext(), BlogActivity.class);
Intent i = new Intent(requireContext(), BlogActivity.class);
i.putExtra(GROUP_ID, post.getGroupId().getBytes());
i.setFlags(FLAG_ACTIVITY_CLEAR_TOP);
getContext().startActivity(i);
requireContext().startActivity(i);
}
@Override
@@ -283,14 +205,4 @@ public class FeedFragment extends BaseFragment implements
sb.make(list, stringRes, LENGTH_LONG).show();
}
@Override
public void onBlogAdded() {
loadBlogPosts(false);
}
@Override
public void onBlogRemoved() {
loadBlogPosts(true);
}
}

View File

@@ -5,19 +5,19 @@ import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.controller.handler.UiResultExceptionHandler;
import javax.annotation.Nullable;
import javax.inject.Inject;
import androidx.annotation.UiThread;
import androidx.lifecycle.ViewModelProvider;
import static java.util.Objects.requireNonNull;
import static org.briarproject.briar.android.activity.BriarActivity.GROUP_ID;
@UiThread
@@ -27,18 +27,16 @@ public class FeedPostFragment extends BasePostFragment {
private static final String TAG = FeedPostFragment.class.getName();
private GroupId blogId;
@Inject
FeedController feedController;
ViewModelProvider.Factory viewModelFactory;
private FeedViewModel viewModel;
static FeedPostFragment newInstance(GroupId blogId, MessageId postId) {
FeedPostFragment f = new FeedPostFragment();
Bundle bundle = new Bundle();
bundle.putByteArray(GROUP_ID, blogId.getBytes());
bundle.putByteArray(POST_ID, postId.getBytes());
f.setArguments(bundle);
return f;
}
@@ -46,6 +44,8 @@ public class FeedPostFragment extends BasePostFragment {
@Override
public void injectFragment(ActivityComponent component) {
component.inject(this);
viewModel = new ViewModelProvider(requireActivity(), viewModelFactory)
.get(FeedViewModel.class);
}
@Nullable
@@ -54,10 +54,14 @@ public class FeedPostFragment extends BasePostFragment {
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
Bundle args = requireArguments();
byte[] b = args.getByteArray(GROUP_ID);
if (b == null) throw new IllegalStateException("No group ID in args");
blogId = new GroupId(b);
GroupId groupId =
new GroupId(requireNonNull(args.getByteArray(GROUP_ID)));
MessageId postId =
new MessageId(requireNonNull(args.getByteArray(POST_ID)));
viewModel.loadBlogPost(groupId, postId).observe(getViewLifecycleOwner(),
result -> result.onError(this::handleException)
.onSuccess(this::onBlogPostLoaded)
);
return super.onCreateView(inflater, container, savedInstanceState);
}
@@ -66,22 +70,4 @@ public class FeedPostFragment extends BasePostFragment {
return TAG;
}
@Override
public void onStart() {
super.onStart();
feedController.loadBlogPost(blogId, postId,
new UiResultExceptionHandler<BlogPostItem, DbException>(
this) {
@Override
public void onResultUi(BlogPostItem post) {
onBlogPostLoaded(post);
}
@Override
public void onExceptionUi(DbException exception) {
handleException(exception);
}
});
}
}

View File

@@ -0,0 +1,174 @@
package org.briarproject.briar.android.blog;
import android.app.Application;
import org.briarproject.bramble.api.db.DatabaseExecutor;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.db.NoSuchMessageException;
import org.briarproject.bramble.api.db.Transaction;
import org.briarproject.bramble.api.db.TransactionManager;
import org.briarproject.bramble.api.event.Event;
import org.briarproject.bramble.api.event.EventBus;
import org.briarproject.bramble.api.identity.Author;
import org.briarproject.bramble.api.identity.IdentityManager;
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.sync.event.GroupAddedEvent;
import org.briarproject.bramble.api.sync.event.GroupRemovedEvent;
import org.briarproject.bramble.api.system.AndroidExecutor;
import org.briarproject.briar.android.viewmodel.LiveResult;
import org.briarproject.briar.api.android.AndroidNotificationManager;
import org.briarproject.briar.api.blog.Blog;
import org.briarproject.briar.api.blog.BlogManager;
import org.briarproject.briar.api.blog.BlogPostHeader;
import org.briarproject.briar.api.blog.event.BlogPostAddedEvent;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import javax.inject.Inject;
import androidx.annotation.UiThread;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.LogUtils.logDuration;
import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.bramble.util.LogUtils.now;
import static org.briarproject.briar.api.blog.BlogManager.CLIENT_ID;
@NotNullByDefault
class FeedViewModel extends BaseViewModel {
private static final Logger LOG = getLogger(FeedViewModel.class.getName());
protected final AndroidNotificationManager notificationManager;
private final MutableLiveData<Blog> personalBlog = new MutableLiveData<>();
@Nullable
private Boolean postAddedWasLocal = null;
@Inject
FeedViewModel(Application application,
@DatabaseExecutor Executor dbExecutor,
LifecycleManager lifecycleManager,
TransactionManager db,
AndroidExecutor androidExecutor,
EventBus eventBus,
IdentityManager identityManager,
BlogManager blogManager,
AndroidNotificationManager notificationManager) {
super(application, dbExecutor, lifecycleManager, db, androidExecutor,
eventBus, identityManager, blogManager);
this.notificationManager = notificationManager;
}
@Override
public void eventOccurred(Event e) {
if (e instanceof BlogPostAddedEvent) {
BlogPostAddedEvent b = (BlogPostAddedEvent) e;
LOG.info("Blog post added");
onBlogPostAdded(b.getHeader(), b.isLocal());
} else if (e instanceof GroupAddedEvent) {
GroupAddedEvent g = (GroupAddedEvent) e;
if (g.getGroup().getClientId().equals(CLIENT_ID)) {
LOG.info("Blog added");
loadAllBlogPosts();
}
} else if (e instanceof GroupRemovedEvent) {
GroupRemovedEvent g = (GroupRemovedEvent) e;
if (g.getGroup().getClientId().equals(CLIENT_ID)) {
LOG.info("Blog removed");
loadAllBlogPosts();
}
}
}
void blockAllBlogPostNotifications() {
notificationManager.blockAllBlogPostNotifications();
}
void clearAllBlogPostNotifications() {
notificationManager.clearAllBlogPostNotifications();
}
void unblockAllBlogPostNotifications() {
notificationManager.unblockAllBlogPostNotifications();
}
void loadPersonalBlog() {
runOnDbThread(() -> {
try {
long start = now();
Author a = identityManager.getLocalAuthor();
Blog b = blogManager.getPersonalBlog(a);
logDuration(LOG, "Loading personal blog", start);
personalBlog.postValue(b);
} catch (DbException e) {
logException(LOG, WARNING, e);
}
});
}
LiveData<Blog> getPersonalBlog() {
return personalBlog;
}
void loadAllBlogPosts() {
loadList(this::loadAllBlogPosts, blogPosts::setValue);
}
@DatabaseExecutor
private List<BlogPostItem> loadAllBlogPosts(Transaction txn)
throws DbException {
long start = now();
List<BlogPostItem> posts = new ArrayList<>();
for (GroupId g : blogManager.getBlogIds(txn)) {
try {
posts.addAll(loadBlogPosts(txn, g));
} catch (NoSuchMessageException e) {
logException(LOG, WARNING, e);
}
}
Collections.sort(posts);
logDuration(LOG, "Loading all posts", start);
return posts;
}
private void onBlogPostAdded(BlogPostHeader header, boolean local) {
postAddedWasLocal = local;
runOnDbThread(() -> {
try {
db.transaction(true, txn -> {
BlogPostItem item = getItem(txn, header);
txn.attach(() -> {
List<BlogPostItem> items = addListItem(blogPosts, item);
if (items != null) {
Collections.sort(items);
blogPosts.setValue(new LiveResult<>(items));
}
});
});
} catch (DbException e) {
logException(LOG, WARNING, e);
}
});
}
@UiThread
@Nullable
Boolean getPostAddedWasLocalAndReset() {
if (postAddedWasLocal == null) return null;
boolean wasLocal = postAddedWasLocal;
postAddedWasLocal = null;
return wasLocal;
}
}

View File

@@ -7,15 +7,12 @@ import android.view.ViewGroup;
import android.widget.ProgressBar;
import android.widget.ScrollView;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.controller.handler.UiExceptionHandler;
import org.briarproject.briar.android.controller.handler.UiResultExceptionHandler;
import org.briarproject.briar.android.fragment.BaseFragment;
import org.briarproject.briar.android.view.TextInputView;
import org.briarproject.briar.android.view.TextSendController;
@@ -27,6 +24,8 @@ import java.util.List;
import javax.annotation.Nullable;
import javax.inject.Inject;
import androidx.lifecycle.ViewModelProvider;
import static android.view.View.FOCUS_DOWN;
import static android.view.View.GONE;
import static android.view.View.INVISIBLE;
@@ -42,12 +41,13 @@ public class ReblogFragment extends BaseFragment implements SendListener {
public static final String TAG = ReblogFragment.class.getName();
@Inject
ViewModelProvider.Factory viewModelFactory;
private FeedViewModel viewModel;
private ViewHolder ui;
private BlogPostItem item;
@Inject
FeedController feedController;
static ReblogFragment newInstance(GroupId groupId, MessageId messageId) {
ReblogFragment f = new ReblogFragment();
@@ -67,6 +67,8 @@ public class ReblogFragment extends BaseFragment implements SendListener {
@Override
public void injectFragment(ActivityComponent component) {
component.inject(this);
viewModel = new ViewModelProvider(requireActivity(), viewModelFactory)
.get(FeedViewModel.class);
}
@Override
@@ -90,30 +92,20 @@ public class ReblogFragment extends BaseFragment implements SendListener {
ui.input.setMaxTextLength(MAX_BLOG_POST_TEXT_LENGTH);
showProgressBar();
feedController.loadBlogPost(blogId, postId,
new UiResultExceptionHandler<BlogPostItem, DbException>(
this) {
@Override
public void onResultUi(BlogPostItem result) {
item = result;
bindViewHolder();
}
@Override
public void onExceptionUi(DbException exception) {
handleException(exception);
}
});
viewModel.loadBlogPost(blogId, postId).observe(getViewLifecycleOwner(),
result -> result.onError(this::handleException)
.onSuccess(this::bindViewHolder)
);
return v;
}
private void bindViewHolder() {
if (item == null) return;
private void bindViewHolder(BlogPostItem item) {
this.item = item;
hideProgressBar();
ui.post.bindItem(item);
ui.post.bindItem(this.item);
ui.post.hideReblogButton();
ui.input.setReady(true);
@@ -124,13 +116,7 @@ public class ReblogFragment extends BaseFragment implements SendListener {
public void onSendClick(@Nullable String text,
List<AttachmentHeader> headers) {
ui.input.hideSoftKeyboard();
feedController.repeatPost(item, text,
new UiExceptionHandler<DbException>(this) {
@Override
public void onExceptionUi(DbException exception) {
handleException(exception);
}
});
viewModel.repeatPost(item, text);
finish();
}
@@ -165,7 +151,7 @@ public class ReblogFragment extends BaseFragment implements SendListener {
public void onAuthorClick(BlogPostItem post) {
// probably don't want to allow author clicks here
}
}, getFragmentManager());
}, getParentFragmentManager());
input = v.findViewById(R.id.inputText);
}
}

View File

@@ -142,6 +142,24 @@ public abstract class DbViewModel extends AndroidViewModel {
void accept(T t);
}
/**
* Creates a copy of the list available in the given LiveData
* and adds the given item to the copy.
*
* @return a copy of the list in the LiveData with item added or null when
* <ul>
* <li> LiveData does not have a value
* <li> LiveResult in the LiveData has an error
* </ul>
*/
@Nullable
protected <T> List<T> addListItem(LiveData<LiveResult<List<T>>> liveData, T item) {
List<T> items = getListCopy(liveData);
if (items == null) return null;
items.add(item);
return items;
}
/**
* Creates a copy of the list available in the given LiveData
* and replaces items where the given test function returns true.