Migrate BlogController to BlogViewModel

This commit is contained in:
Torsten Grote
2021-01-12 09:43:08 -03:00
parent b678de7529
commit 1fa4b78474
12 changed files with 344 additions and 646 deletions

View File

@@ -1,48 +0,0 @@
package org.briarproject.briar.android.blog;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.briar.android.controller.handler.ExceptionHandler;
import org.briarproject.briar.android.controller.handler.ResultExceptionHandler;
import org.briarproject.briar.api.blog.BlogPostHeader;
import java.util.List;
import javax.annotation.Nullable;
import androidx.annotation.UiThread;
@NotNullByDefault
interface BaseController {
@UiThread
void onStart();
@UiThread
void onStop();
void loadBlogPosts(GroupId g,
ResultExceptionHandler<List<BlogPostItem>, DbException> handler);
void loadBlogPost(BlogPostHeader header,
ResultExceptionHandler<BlogPostItem, DbException> handler);
void loadBlogPost(GroupId g, MessageId m,
ResultExceptionHandler<BlogPostItem, DbException> handler);
void repeatPost(BlogPostItem item, @Nullable String comment,
ExceptionHandler<DbException> handler);
@NotNullByDefault
interface BlogListener {
@UiThread
void onBlogPostAdded(BlogPostHeader header, boolean local);
@UiThread
void onBlogRemoved();
}
}

View File

@@ -1,61 +1,28 @@
package org.briarproject.briar.android.blog;
import org.briarproject.bramble.api.db.DatabaseExecutor;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.event.EventBus;
import org.briarproject.bramble.api.event.EventListener;
import org.briarproject.bramble.api.identity.IdentityManager;
import org.briarproject.bramble.api.identity.LocalAuthor;
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.briar.android.controller.DbControllerImpl;
import org.briarproject.briar.android.controller.handler.ExceptionHandler;
import org.briarproject.briar.android.controller.handler.ResultExceptionHandler;
import org.briarproject.briar.api.android.AndroidNotificationManager;
import org.briarproject.briar.api.blog.Blog;
import org.briarproject.briar.api.blog.BlogCommentHeader;
import org.briarproject.briar.api.blog.BlogManager;
import org.briarproject.briar.api.blog.BlogPostHeader;
import org.briarproject.briar.util.HtmlUtils;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import androidx.annotation.CallSuper;
import static java.util.logging.Level.WARNING;
import static org.briarproject.bramble.util.LogUtils.logDuration;
import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.bramble.util.LogUtils.now;
import static org.briarproject.briar.util.HtmlUtils.ARTICLE;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
abstract class BaseControllerImpl extends DbControllerImpl
implements BaseController, EventListener {
private static final Logger LOG =
Logger.getLogger(BaseControllerImpl.class.getName());
implements EventListener {
protected final EventBus eventBus;
protected final AndroidNotificationManager notificationManager;
protected final IdentityManager identityManager;
protected final BlogManager blogManager;
private final Map<MessageId, String> textCache = new ConcurrentHashMap<>();
private final Map<MessageId, BlogPostHeader> headerCache =
new ConcurrentHashMap<>();
BaseControllerImpl(@DatabaseExecutor Executor dbExecutor,
LifecycleManager lifecycleManager, EventBus eventBus,
AndroidNotificationManager notificationManager,
@@ -67,144 +34,4 @@ abstract class BaseControllerImpl extends DbControllerImpl
this.blogManager = blogManager;
}
@Override
@CallSuper
public void onStart() {
eventBus.addListener(this);
}
@Override
@CallSuper
public void onStop() {
eventBus.removeListener(this);
}
@Override
public void loadBlogPosts(GroupId groupId,
ResultExceptionHandler<List<BlogPostItem>, DbException> handler) {
runOnDbThread(() -> {
try {
List<BlogPostItem> items = loadItems(groupId);
handler.onResult(items);
} catch (DbException e) {
logException(LOG, WARNING, e);
handler.onException(e);
}
});
}
List<BlogPostItem> loadItems(GroupId groupId) throws DbException {
long start = now();
Collection<BlogPostHeader> headers =
blogManager.getPostHeaders(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(h);
items.add(item);
}
logDuration(LOG, "Loading bodies", start);
return items;
}
@Override
public void loadBlogPost(BlogPostHeader header,
ResultExceptionHandler<BlogPostItem, DbException> 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<BlogPostItem, DbException> 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<DbException> 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;
}
}

View File

@@ -4,7 +4,6 @@ import android.app.Application;
import org.briarproject.bramble.api.db.DatabaseExecutor;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.db.NoSuchMessageException;
import org.briarproject.bramble.api.db.Transaction;
import org.briarproject.bramble.api.db.TransactionManager;
import org.briarproject.bramble.api.event.Event;
@@ -19,6 +18,7 @@ import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.bramble.api.system.AndroidExecutor;
import org.briarproject.briar.android.viewmodel.DbViewModel;
import org.briarproject.briar.android.viewmodel.LiveResult;
import org.briarproject.briar.api.android.AndroidNotificationManager;
import org.briarproject.briar.api.blog.Blog;
import org.briarproject.briar.api.blog.BlogCommentHeader;
import org.briarproject.briar.api.blog.BlogManager;
@@ -26,6 +26,7 @@ import org.briarproject.briar.api.blog.BlogPostHeader;
import org.briarproject.briar.util.HtmlUtils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@@ -35,11 +36,10 @@ import java.util.logging.Logger;
import javax.annotation.Nullable;
import javax.inject.Inject;
import androidx.annotation.UiThread;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.Transformations;
import static java.util.Objects.requireNonNull;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.LogUtils.logDuration;
@@ -50,20 +50,23 @@ import static org.briarproject.briar.util.HtmlUtils.ARTICLE;
@NotNullByDefault
public class BaseViewModel extends DbViewModel implements EventListener {
private static Logger LOG = getLogger(BaseViewModel.class.getName());
private static final Logger LOG = getLogger(BaseViewModel.class.getName());
protected final TransactionManager db;
private final EventBus eventBus;
protected final IdentityManager identityManager;
protected final AndroidNotificationManager notificationManager;
protected final BlogManager blogManager;
protected final MutableLiveData<LiveResult<List<BlogPostItem>>> blogPosts =
private 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<>();
@Nullable
private Boolean postAddedWasLocal = null;
@Inject
BaseViewModel(Application application,
@@ -73,11 +76,13 @@ public class BaseViewModel extends DbViewModel implements EventListener {
AndroidExecutor androidExecutor,
EventBus eventBus,
IdentityManager identityManager,
AndroidNotificationManager notificationManager,
BlogManager blogManager) {
super(application, dbExecutor, lifecycleManager, db, androidExecutor);
this.db = db;
this.eventBus = eventBus;
this.identityManager = identityManager;
this.notificationManager = notificationManager;
this.blogManager = blogManager;
eventBus.addListener(this);
@@ -93,10 +98,6 @@ public class BaseViewModel extends DbViewModel implements EventListener {
public void eventOccurred(Event e) {
}
void loadItems(GroupId groupId) {
loadList(txn -> loadBlogPosts(txn, groupId), blogPosts::setValue);
}
@DatabaseExecutor
protected List<BlogPostItem> loadBlogPosts(Transaction txn, GroupId groupId)
throws DbException {
@@ -148,9 +149,9 @@ public class BaseViewModel extends DbViewModel implements EventListener {
runOnDbThread(() -> {
try {
long start = now();
BlogPostHeader header1 = getPostHeader(g, m);
BlogPostHeader header = getPostHeader(g, m);
BlogPostItem item = db.transactionWithResult(true, txn ->
getItem(txn, header1)
getItem(txn, header)
);
logDuration(LOG, "Loading post", start);
result.postValue(new LiveResult<>(item));
@@ -173,6 +174,31 @@ public class BaseViewModel extends DbViewModel implements EventListener {
return header;
}
@UiThread
protected void updateBlogPosts(LiveResult<List<BlogPostItem>> posts) {
blogPosts.setValue(posts);
}
protected void onBlogPostAdded(BlogPostHeader header, boolean local) {
postAddedWasLocal = local;
runOnDbThread(() -> {
try {
db.transaction(true, txn -> {
BlogPostItem item = getItem(txn, header);
txn.attach(() -> {
List<BlogPostItem> items = addListItem(blogPosts, item);
if (items != null) {
Collections.sort(items);
blogPosts.setValue(new LiveResult<>(items));
}
});
});
} catch (DbException e) {
logException(LOG, WARNING, e);
}
});
}
void repeatPost(BlogPostItem item, @Nullable String comment) {
runOnDbThread(() -> {
try {
@@ -186,34 +212,17 @@ public class BaseViewModel extends DbViewModel implements EventListener {
});
}
LiveData<LiveResult<List<BlogPostItem>>> getAllBlogPosts() {
LiveData<LiveResult<List<BlogPostItem>>> getBlogPosts() {
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());
});
@UiThread
@Nullable
Boolean getPostAddedWasLocalAndReset() {
if (postAddedWasLocal == null) return null;
boolean wasLocal = postAddedWasLocal;
postAddedWasLocal = null;
return wasLocal;
}
}

View File

@@ -16,15 +16,27 @@ import javax.annotation.Nullable;
import javax.inject.Inject;
import androidx.appcompat.widget.Toolbar;
import androidx.lifecycle.ViewModelProvider;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class BlogActivity extends BriarActivity
implements BaseFragmentListener {
@Inject
ViewModelProvider.Factory viewModelFactory;
@Inject
BlogController blogController;
private BlogViewModel viewModel;
@Override
public void injectActivity(ActivityComponent component) {
component.inject(this);
viewModel = new ViewModelProvider(this, viewModelFactory)
.get(BlogViewModel.class);
}
@Override
public void onCreate(@Nullable Bundle state) {
super.onCreate(state);
@@ -35,6 +47,7 @@ public class BlogActivity extends BriarActivity
if (b == null) throw new IllegalStateException("No group ID in intent");
GroupId groupId = new GroupId(b);
blogController.setGroupId(groupId);
viewModel.setGroupId(groupId);
setContentView(R.layout.activity_fragment_container_toolbar);
Toolbar toolbar = setUpCustomToolbar(false);
@@ -54,9 +67,4 @@ public class BlogActivity extends BriarActivity
}
}
@Override
public void injectActivity(ActivityComponent component) {
component.inject(this);
}
}

View File

@@ -4,44 +4,16 @@ import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.briar.android.controller.handler.ResultExceptionHandler;
import java.util.Collection;
import java.util.List;
import androidx.annotation.UiThread;
@NotNullByDefault
public interface BlogController extends BaseController {
public interface BlogController {
void setGroupId(GroupId g);
@UiThread
void setBlogSharingListener(BlogSharingListener listener);
@UiThread
void unsetBlogSharingListener(BlogSharingListener listener);
void loadBlogPosts(
ResultExceptionHandler<List<BlogPostItem>, DbException> handler);
void loadBlogPost(MessageId m,
ResultExceptionHandler<BlogPostItem, DbException> handler);
void loadBlog(ResultExceptionHandler<BlogItem, DbException> handler);
void deleteBlog(ResultExceptionHandler<Void, DbException> handler);
void loadSharingContacts(
ResultExceptionHandler<Collection<ContactId>, DbException> handler);
interface BlogSharingListener extends BlogListener {
@UiThread
void onBlogInvitationAccepted(ContactId c);
@UiThread
void onBlogLeft(ContactId c);
}
}

View File

@@ -10,38 +10,25 @@ import org.briarproject.bramble.api.event.Event;
import org.briarproject.bramble.api.event.EventBus;
import org.briarproject.bramble.api.event.EventListener;
import org.briarproject.bramble.api.identity.IdentityManager;
import org.briarproject.bramble.api.identity.LocalAuthor;
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.bramble.api.sync.event.GroupRemovedEvent;
import org.briarproject.briar.android.controller.ActivityLifecycleController;
import org.briarproject.briar.android.controller.handler.ResultExceptionHandler;
import org.briarproject.briar.api.android.AndroidNotificationManager;
import org.briarproject.briar.api.blog.Blog;
import org.briarproject.briar.api.blog.BlogInvitationResponse;
import org.briarproject.briar.api.blog.BlogManager;
import org.briarproject.briar.api.blog.BlogSharingManager;
import org.briarproject.briar.api.blog.event.BlogInvitationResponseReceivedEvent;
import org.briarproject.briar.api.blog.event.BlogPostAddedEvent;
import org.briarproject.briar.api.sharing.event.ContactLeftShareableEvent;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.logging.Logger;
import javax.inject.Inject;
import androidx.annotation.Nullable;
import static java.util.logging.Level.WARNING;
import static org.briarproject.bramble.util.LogUtils.logDuration;
import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.bramble.util.LogUtils.now;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
@@ -53,10 +40,6 @@ class BlogControllerImpl extends BaseControllerImpl
private final BlogSharingManager blogSharingManager;
// UI thread
@Nullable
private BlogSharingListener listener;
private volatile GroupId groupId = null;
@Inject
@@ -76,15 +59,10 @@ class BlogControllerImpl extends BaseControllerImpl
@Override
public void onActivityStart() {
super.onStart();
notificationManager.blockNotification(groupId);
notificationManager.clearBlogPostNotification(groupId);
}
@Override
public void onActivityStop() {
super.onStop();
notificationManager.unblockNotification(groupId);
}
@Override
@@ -96,99 +74,9 @@ class BlogControllerImpl extends BaseControllerImpl
groupId = g;
}
@Override
public void setBlogSharingListener(BlogSharingListener listener) {
this.listener = listener;
}
@Override
public void unsetBlogSharingListener(BlogSharingListener listener) {
if (this.listener == listener) this.listener = null;
}
@Override
public void eventOccurred(Event e) {
if (groupId == null || listener == null)
throw new IllegalStateException();
if (e instanceof BlogPostAddedEvent) {
BlogPostAddedEvent b = (BlogPostAddedEvent) e;
if (b.getGroupId().equals(groupId)) {
LOG.info("Blog post added");
listener.onBlogPostAdded(b.getHeader(), b.isLocal());
}
} else if (e instanceof BlogInvitationResponseReceivedEvent) {
BlogInvitationResponseReceivedEvent b =
(BlogInvitationResponseReceivedEvent) e;
BlogInvitationResponse r = b.getMessageHeader();
if (r.getShareableId().equals(groupId) && r.wasAccepted()) {
LOG.info("Blog invitation accepted");
listener.onBlogInvitationAccepted(b.getContactId());
}
} else if (e instanceof ContactLeftShareableEvent) {
ContactLeftShareableEvent s = (ContactLeftShareableEvent) e;
if (s.getGroupId().equals(groupId)) {
LOG.info("Blog left by contact");
listener.onBlogLeft(s.getContactId());
}
} else if (e instanceof GroupRemovedEvent) {
GroupRemovedEvent g = (GroupRemovedEvent) e;
if (g.getGroup().getId().equals(groupId)) {
LOG.info("Blog removed");
listener.onBlogRemoved();
}
}
}
@Override
public void loadBlogPosts(
ResultExceptionHandler<List<BlogPostItem>, DbException> handler) {
if (groupId == null) throw new IllegalStateException();
loadBlogPosts(groupId, handler);
}
@Override
public void loadBlogPost(MessageId m,
ResultExceptionHandler<BlogPostItem, DbException> handler) {
if (groupId == null) throw new IllegalStateException();
loadBlogPost(groupId, m, handler);
}
@Override
public void loadBlog(
ResultExceptionHandler<BlogItem, DbException> handler) {
if (groupId == null) throw new IllegalStateException();
runOnDbThread(() -> {
try {
long start = now();
LocalAuthor a = identityManager.getLocalAuthor();
Blog b = blogManager.getBlog(groupId);
boolean ours = a.getId().equals(b.getAuthor().getId());
boolean removable = blogManager.canBeRemoved(b);
BlogItem blog = new BlogItem(b, ours, removable);
logDuration(LOG, "Loading blog", start);
handler.onResult(blog);
} catch (DbException e) {
logException(LOG, WARNING, e);
handler.onException(e);
}
});
}
@Override
public void deleteBlog(ResultExceptionHandler<Void, DbException> handler) {
if (groupId == null) throw new IllegalStateException();
runOnDbThread(() -> {
try {
long start = now();
Blog b = blogManager.getBlog(groupId);
blogManager.removeBlog(b);
logDuration(LOG, "Removing blog", start);
handler.onResult(null);
} catch (DbException e) {
logException(LOG, WARNING, e);
handler.onException(e);
}
});
}
@Override

View File

@@ -1,6 +1,5 @@
package org.briarproject.briar.android.blog;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
import android.os.Parcelable;
@@ -21,7 +20,6 @@ import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.activity.BriarActivity;
import org.briarproject.briar.android.blog.BlogController.BlogSharingListener;
import org.briarproject.briar.android.controller.SharingController;
import org.briarproject.briar.android.controller.handler.UiResultExceptionHandler;
import org.briarproject.briar.android.fragment.BaseFragment;
@@ -30,7 +28,6 @@ import org.briarproject.briar.android.sharing.ShareBlogActivity;
import org.briarproject.briar.android.util.BriarSnackbarBuilder;
import org.briarproject.briar.android.view.BriarRecyclerView;
import org.briarproject.briar.android.widget.LinkDialogFragment;
import org.briarproject.briar.api.blog.BlogPostHeader;
import java.util.Collection;
import java.util.List;
@@ -41,6 +38,7 @@ import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AlertDialog;
import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView.LayoutManager;
@@ -57,11 +55,12 @@ import static org.briarproject.briar.android.controller.SharingController.Sharin
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class BlogFragment extends BaseFragment
implements BlogSharingListener, SharingListener,
OnBlogPostClickListener {
implements SharingListener, OnBlogPostClickListener {
private final static String TAG = BlogFragment.class.getName();
@Inject
ViewModelProvider.Factory viewModelFactory;
@Inject
BlogController blogController;
@Inject
@@ -70,11 +69,10 @@ public class BlogFragment extends BaseFragment
private Parcelable layoutManagerState;
private GroupId groupId;
private BlogViewModel viewModel;
private final BlogPostAdapter adapter = new BlogPostAdapter(this);
private LayoutManager layoutManager;
private BriarRecyclerView list;
private MenuItem writeButton, deleteButton;
private boolean isMyBlog = false, canDeleteBlog = false;
static BlogFragment newInstance(GroupId groupId) {
BlogFragment f = new BlogFragment();
@@ -89,7 +87,8 @@ public class BlogFragment extends BaseFragment
@Override
public void injectFragment(ActivityComponent component) {
component.inject(this);
blogController.setBlogSharingListener(this);
viewModel = new ViewModelProvider(requireActivity(), viewModelFactory)
.get(BlogViewModel.class);
sharingController.setSharingListener(this);
}
@@ -112,6 +111,14 @@ public class BlogFragment extends BaseFragment
list.showProgressBar();
list.setEmptyText(getString(R.string.blogs_other_blog_empty_state));
viewModel.getBlogPosts().observe(getViewLifecycleOwner(), result ->
result.onError(this::handleException)
.onSuccess(this::onBlogPostsLoaded)
);
viewModel.getBlogRemoved().observe(getViewLifecycleOwner(), removed -> {
if (removed) finish();
});
if (savedInstanceState != null) {
layoutManagerState =
savedInstanceState.getParcelable("layoutManager");
@@ -123,16 +130,17 @@ public class BlogFragment extends BaseFragment
@Override
public void onStart() {
super.onStart();
viewModel.blockNotifications();
viewModel.clearBlogPostNotifications();
sharingController.onStart();
loadBlog();
loadSharedContacts();
loadBlogPosts(false);
list.startPeriodicUpdate();
}
@Override
public void onStop() {
super.onStop();
viewModel.unblockNotifications();
sharingController.onStop();
list.stopPeriodicUpdate();
}
@@ -140,7 +148,6 @@ public class BlogFragment extends BaseFragment
@Override
public void onDestroy() {
super.onDestroy();
blogController.unsetBlogSharingListener(this);
sharingController.unsetSharingListener(this);
}
@@ -156,42 +163,43 @@ public class BlogFragment extends BaseFragment
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.blogs_blog_actions, menu);
writeButton = menu.findItem(R.id.action_write_blog_post);
if (isMyBlog) writeButton.setVisible(true);
deleteButton = menu.findItem(R.id.action_blog_delete);
if (canDeleteBlog) deleteButton.setEnabled(true);
MenuItem writeButton = menu.findItem(R.id.action_write_blog_post);
MenuItem deleteButton = menu.findItem(R.id.action_blog_delete);
viewModel.getBlog().observe(getViewLifecycleOwner(), blog -> {
setToolbarTitle(blog.getBlog().getAuthor());
if (blog.isOurs()) writeButton.setVisible(true);
if (blog.canBeRemoved()) deleteButton.setEnabled(true);
});
super.onCreateOptionsMenu(menu, inflater);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.action_write_blog_post:
Intent i = new Intent(getActivity(),
WriteBlogPostActivity.class);
i.putExtra(GROUP_ID, groupId.getBytes());
startActivityForResult(i, REQUEST_WRITE_BLOG_POST);
return true;
case R.id.action_blog_share:
Intent i2 = new Intent(getActivity(), ShareBlogActivity.class);
i2.setFlags(FLAG_ACTIVITY_CLEAR_TOP);
i2.putExtra(GROUP_ID, groupId.getBytes());
startActivityForResult(i2, REQUEST_SHARE_BLOG);
return true;
case R.id.action_blog_sharing_status:
Intent i3 = new Intent(getActivity(),
BlogSharingStatusActivity.class);
i3.setFlags(FLAG_ACTIVITY_CLEAR_TOP);
i3.putExtra(GROUP_ID, groupId.getBytes());
startActivity(i3);
return true;
case R.id.action_blog_delete:
showDeleteDialog();
return true;
default:
return super.onOptionsItemSelected(item);
int itemId = item.getItemId();
if (itemId == R.id.action_write_blog_post) {
Intent i = new Intent(getActivity(),
WriteBlogPostActivity.class);
i.putExtra(GROUP_ID, groupId.getBytes());
startActivityForResult(i, REQUEST_WRITE_BLOG_POST);
return true;
} else if (itemId == R.id.action_blog_share) {
Intent i2 = new Intent(getActivity(), ShareBlogActivity.class);
i2.setFlags(FLAG_ACTIVITY_CLEAR_TOP);
i2.putExtra(GROUP_ID, groupId.getBytes());
startActivityForResult(i2, REQUEST_SHARE_BLOG);
return true;
} else if (itemId == R.id.action_blog_sharing_status) {
Intent i3 = new Intent(getActivity(),
BlogSharingStatusActivity.class);
i3.setFlags(FLAG_ACTIVITY_CLEAR_TOP);
i3.putExtra(GROUP_ID, groupId.getBytes());
startActivity(i3);
return true;
} else if (itemId == R.id.action_blog_delete) {
showDeleteDialog();
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
@@ -201,7 +209,7 @@ public class BlogFragment extends BaseFragment
if (request == REQUEST_WRITE_BLOG_POST && result == RESULT_OK) {
displaySnackbar(R.string.blogs_blog_post_created, true);
loadBlogPosts(true);
viewModel.loadBlogPosts(groupId);
} else if (request == REQUEST_SHARE_BLOG && result == RESULT_OK) {
displaySnackbar(R.string.blogs_sharing_snackbar, false);
}
@@ -212,30 +220,25 @@ public class BlogFragment extends BaseFragment
return TAG;
}
@Override
public void onBlogPostAdded(BlogPostHeader header, boolean local) {
blogController.loadBlogPost(header,
new UiResultExceptionHandler<BlogPostItem, DbException>(
this) {
@Override
public void onResultUi(BlogPostItem post) {
// adapter.add(post);
if (local) {
list.scrollToPosition(0);
displaySnackbar(R.string.blogs_blog_post_created,
false);
} else {
displaySnackbar(R.string.blogs_blog_post_received,
true);
}
}
@Override
public void onExceptionUi(DbException exception) {
handleException(exception);
}
}
);
private void onBlogPostsLoaded(List<BlogPostItem> items) {
adapter.submitList(items, () -> {
Boolean wasLocal = viewModel.getPostAddedWasLocalAndReset();
if (wasLocal != null && wasLocal) {
list.scrollToPosition(0);
displaySnackbar(R.string.blogs_blog_post_created,
false);
} else if (wasLocal != null) {
displaySnackbar(R.string.blogs_blog_post_received,
true);
}
list.showData();
if (layoutManagerState == null) {
list.scrollToPosition(0);
} else {
layoutManager.onRestoreInstanceState(
layoutManagerState);
}
});
}
@Override
@@ -262,53 +265,8 @@ public class BlogFragment extends BaseFragment
f.show(getParentFragmentManager(), f.getUniqueTag());
}
private void loadBlogPosts(boolean reload) {
blogController.loadBlogPosts(
new UiResultExceptionHandler<List<BlogPostItem>,
DbException>(this) {
@Override
public void onResultUi(List<BlogPostItem> posts) {
if (posts.isEmpty()) {
list.showData();
} else {
adapter.submitList(posts);
if (reload || layoutManagerState == null) {
list.scrollToPosition(0);
} else {
layoutManager.onRestoreInstanceState(
layoutManagerState);
}
}
}
@Override
public void onExceptionUi(DbException exception) {
handleException(exception);
}
});
}
private void loadBlog() {
blogController.loadBlog(
new UiResultExceptionHandler<BlogItem, DbException>(this) {
@Override
public void onResultUi(BlogItem blog) {
setToolbarTitle(blog.getBlog().getAuthor());
if (blog.isOurs())
showWriteButton();
if (blog.canBeRemoved())
enableDeleteButton();
}
@Override
public void onExceptionUi(DbException exception) {
handleException(exception);
}
});
}
private void setToolbarTitle(Author a) {
getActivity().setTitle(a.getName());
requireActivity().setTitle(a.getName());
}
private void loadSharedContacts() {
@@ -329,20 +287,6 @@ public class BlogFragment extends BaseFragment
});
}
@Override
public void onBlogInvitationAccepted(ContactId c) {
sharingController.add(c);
setToolbarSubTitle(sharingController.getTotalCount(),
sharingController.getOnlineCount());
}
@Override
public void onBlogLeft(ContactId c) {
sharingController.remove(c);
setToolbarSubTitle(sharingController.getTotalCount(),
sharingController.getOnlineCount());
}
@Override
public void onSharingInfoUpdated(int total, int online) {
setToolbarSubTitle(total, online);
@@ -350,25 +294,13 @@ public class BlogFragment extends BaseFragment
private void setToolbarSubTitle(int total, int online) {
ActionBar actionBar =
((BriarActivity) getActivity()).getSupportActionBar();
((BriarActivity) requireActivity()).getSupportActionBar();
if (actionBar != null) {
actionBar.setSubtitle(
getString(R.string.shared_with, total, online));
}
}
private void showWriteButton() {
isMyBlog = true;
if (writeButton != null)
writeButton.setVisible(true);
}
private void enableDeleteButton() {
canDeleteBlog = true;
if (deleteButton != null)
deleteButton.setEnabled(true);
}
private void displaySnackbar(int stringId, boolean scroll) {
BriarSnackbarBuilder sb = new BriarSnackbarBuilder();
if (scroll) {
@@ -379,38 +311,21 @@ public class BlogFragment extends BaseFragment
}
private void showDeleteDialog() {
DialogInterface.OnClickListener okListener =
(dialog, which) -> deleteBlog();
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity(),
AlertDialog.Builder builder = new AlertDialog.Builder(requireContext(),
R.style.BriarDialogTheme);
builder.setTitle(getString(R.string.blogs_remove_blog));
builder.setMessage(
getString(R.string.blogs_remove_blog_dialog_message));
builder.setPositiveButton(R.string.cancel, null);
builder.setNegativeButton(R.string.blogs_remove_blog_ok, okListener);
builder.setNegativeButton(R.string.blogs_remove_blog_ok,
(dialog, which) -> deleteBlog());
builder.show();
}
private void deleteBlog() {
blogController.deleteBlog(
new UiResultExceptionHandler<Void, DbException>(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();
}

View File

@@ -21,6 +21,11 @@ public class BlogModule {
@IntoMap
@ViewModelKey(FeedViewModel.class)
abstract ViewModel bindFeedViewModel(FeedViewModel feedViewModel);
@Binds
@IntoMap
@ViewModelKey(BlogViewModel.class)
abstract ViewModel bindBlogViewModel(BlogViewModel blogViewModel);
}
@ActivityScope

View File

@@ -1,30 +1,35 @@
package org.briarproject.briar.android.blog;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.blog.BaseController.BlogListener;
import org.briarproject.briar.android.controller.handler.UiResultExceptionHandler;
import org.briarproject.briar.api.blog.BlogPostHeader;
import javax.annotation.Nullable;
import javax.inject.Inject;
import androidx.annotation.UiThread;
import androidx.lifecycle.ViewModelProvider;
@UiThread
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class BlogPostFragment extends BasePostFragment implements BlogListener {
public class BlogPostFragment extends BasePostFragment {
private static final String TAG = BlogPostFragment.class.getName();
@Inject
ViewModelProvider.Factory viewModelFactory;
@Inject
BlogController blogController;
private BlogViewModel viewModel;
static BlogPostFragment newInstance(MessageId postId) {
BlogPostFragment f = new BlogPostFragment();
@@ -35,42 +40,29 @@ public class BlogPostFragment extends BasePostFragment implements BlogListener {
return f;
}
@Override
public void injectFragment(ActivityComponent component) {
component.inject(this);
viewModel = new ViewModelProvider(requireActivity(), viewModelFactory)
.get(BlogViewModel.class);
}
@Override
public String getUniqueTag() {
return TAG;
}
@Nullable
@Override
public void injectFragment(ActivityComponent component) {
component.inject(this);
}
@Override
public void onStart() {
super.onStart();
blogController.loadBlogPost(postId,
new UiResultExceptionHandler<BlogPostItem, DbException>(
this) {
@Override
public void onResultUi(BlogPostItem post) {
onBlogPostLoaded(post);
}
@Override
public void onExceptionUi(DbException exception) {
handleException(exception);
}
});
}
@Override
public void onBlogPostAdded(BlogPostHeader header, boolean local) {
// doesn't matter here
}
@Override
public void onBlogRemoved() {
finish();
public View onCreateView(LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
View v = super.onCreateView(inflater, container, savedInstanceState);
viewModel.loadBlogPost(postId).observe(getViewLifecycleOwner(), res ->
res.onError(this::handleException)
.onSuccess(this::onBlogPostLoaded)
);
return v;
}
}

View File

@@ -0,0 +1,168 @@
package org.briarproject.briar.android.blog;
import android.app.Application;
import org.briarproject.bramble.api.db.DatabaseExecutor;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.db.TransactionManager;
import org.briarproject.bramble.api.event.Event;
import org.briarproject.bramble.api.event.EventBus;
import org.briarproject.bramble.api.identity.IdentityManager;
import org.briarproject.bramble.api.identity.LocalAuthor;
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.bramble.api.sync.event.GroupRemovedEvent;
import org.briarproject.bramble.api.system.AndroidExecutor;
import org.briarproject.briar.android.viewmodel.LiveResult;
import org.briarproject.briar.api.android.AndroidNotificationManager;
import org.briarproject.briar.api.blog.Blog;
import org.briarproject.briar.api.blog.BlogInvitationResponse;
import org.briarproject.briar.api.blog.BlogManager;
import org.briarproject.briar.api.blog.event.BlogInvitationResponseReceivedEvent;
import org.briarproject.briar.api.blog.event.BlogPostAddedEvent;
import org.briarproject.briar.api.sharing.event.ContactLeftShareableEvent;
import java.util.concurrent.Executor;
import java.util.logging.Logger;
import javax.inject.Inject;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.LogUtils.logDuration;
import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.bramble.util.LogUtils.now;
@NotNullByDefault
class BlogViewModel extends BaseViewModel {
private static final Logger LOG = getLogger(BlogViewModel.class.getName());
// implicitly non-null
private volatile GroupId groupId = null;
private final MutableLiveData<BlogItem> blog = new MutableLiveData<>();
private final MutableLiveData<Boolean> blogRemoved =
new MutableLiveData<>();
@Inject
BlogViewModel(Application application,
@DatabaseExecutor Executor dbExecutor,
LifecycleManager lifecycleManager,
TransactionManager db,
AndroidExecutor androidExecutor,
EventBus eventBus,
IdentityManager identityManager,
AndroidNotificationManager notificationManager,
BlogManager blogManager) {
super(application, dbExecutor, lifecycleManager, db, androidExecutor,
eventBus, identityManager, notificationManager, blogManager);
}
@Override
public void eventOccurred(Event e) {
if (e instanceof BlogPostAddedEvent) {
BlogPostAddedEvent b = (BlogPostAddedEvent) e;
if (b.getGroupId().equals(groupId)) {
LOG.info("Blog post added");
onBlogPostAdded(b.getHeader(), b.isLocal());
}
} else if (e instanceof BlogInvitationResponseReceivedEvent) {
BlogInvitationResponseReceivedEvent b =
(BlogInvitationResponseReceivedEvent) e;
BlogInvitationResponse r = b.getMessageHeader();
if (r.getShareableId().equals(groupId) && r.wasAccepted()) {
LOG.info("Blog invitation accepted");
// TODO
// onBlogInvitationAccepted(b.getContactId());
// sharingController.add(c);
}
} else if (e instanceof ContactLeftShareableEvent) {
ContactLeftShareableEvent s = (ContactLeftShareableEvent) e;
if (s.getGroupId().equals(groupId)) {
LOG.info("Blog left by contact");
// TODO
// onBlogLeft(s.getContactId());
// sharingController.remove(c);
}
} else if (e instanceof GroupRemovedEvent) {
GroupRemovedEvent g = (GroupRemovedEvent) e;
if (g.getGroup().getId().equals(groupId)) {
LOG.info("Blog removed");
blogRemoved.setValue(true);
}
}
}
/**
* Set this before calling any other methods.
*/
public void setGroupId(GroupId groupId) {
this.groupId = groupId;
loadBlog(groupId);
loadBlogPosts(groupId);
}
private void loadBlog(GroupId groupId) {
runOnDbThread(() -> {
try {
long start = now();
LocalAuthor a = identityManager.getLocalAuthor();
Blog b = blogManager.getBlog(groupId);
boolean ours = a.getId().equals(b.getAuthor().getId());
boolean removable = blogManager.canBeRemoved(b);
blog.postValue(new BlogItem(b, ours, removable));
logDuration(LOG, "Loading blog", start);
} catch (DbException e) {
logException(LOG, WARNING, e);
}
});
}
void blockNotifications() {
notificationManager.blockNotification(groupId);
}
void clearBlogPostNotifications() {
notificationManager.clearBlogPostNotification(groupId);
}
void unblockNotifications() {
notificationManager.unblockNotification(groupId);
}
void loadBlogPosts(GroupId groupId) {
loadList(txn -> loadBlogPosts(txn, groupId), this::updateBlogPosts);
}
void deleteBlog() {
runOnDbThread(() -> {
try {
long start = now();
Blog b = blogManager.getBlog(groupId);
blogManager.removeBlog(b);
logDuration(LOG, "Removing blog", start);
} catch (DbException e) {
logException(LOG, WARNING, e);
}
});
}
LiveData<LiveResult<BlogPostItem>> loadBlogPost(MessageId m) {
return loadBlogPost(groupId, m);
}
LiveData<BlogItem> getBlog() {
return blog;
}
LiveData<Boolean> getBlogRemoved() {
return blogRemoved;
}
}

View File

@@ -63,8 +63,7 @@ public class FeedFragment extends BaseFragment
@Override
public void injectFragment(ActivityComponent component) {
component.inject(this);
// TODO don't use NavDrawerActivity scope here
viewModel = new ViewModelProvider(requireActivity(), viewModelFactory)
viewModel = new ViewModelProvider(this, viewModelFactory)
.get(FeedViewModel.class);
// TODO ideally we only do this once when the ViewModel gets created
viewModel.loadPersonalBlog();
@@ -88,7 +87,7 @@ public class FeedFragment extends BaseFragment
list.setEmptyText(R.string.blogs_feed_empty_state);
list.setEmptyAction(R.string.blogs_feed_empty_state_action);
viewModel.getAllBlogPosts().observe(getViewLifecycleOwner(), result ->
viewModel.getBlogPosts().observe(getViewLifecycleOwner(), result ->
result
.onError(this::handleException)
.onSuccess(this::onBlogPostsLoaded)
@@ -135,6 +134,7 @@ public class FeedFragment extends BaseFragment
} else if (wasLocal != null) {
showSnackBar(R.string.blogs_blog_post_received);
}
list.showData();
});
}

View File

@@ -17,11 +17,9 @@ import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.sync.event.GroupAddedEvent;
import org.briarproject.bramble.api.sync.event.GroupRemovedEvent;
import org.briarproject.bramble.api.system.AndroidExecutor;
import org.briarproject.briar.android.viewmodel.LiveResult;
import org.briarproject.briar.api.android.AndroidNotificationManager;
import org.briarproject.briar.api.blog.Blog;
import org.briarproject.briar.api.blog.BlogManager;
import org.briarproject.briar.api.blog.BlogPostHeader;
import org.briarproject.briar.api.blog.event.BlogPostAddedEvent;
import java.util.ArrayList;
@@ -30,10 +28,8 @@ import java.util.List;
import java.util.concurrent.Executor;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import javax.inject.Inject;
import androidx.annotation.UiThread;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
@@ -49,11 +45,7 @@ class FeedViewModel extends BaseViewModel {
private static final Logger LOG = getLogger(FeedViewModel.class.getName());
protected final AndroidNotificationManager notificationManager;
private final MutableLiveData<Blog> personalBlog = new MutableLiveData<>();
@Nullable
private Boolean postAddedWasLocal = null;
@Inject
FeedViewModel(Application application,
@@ -63,11 +55,10 @@ class FeedViewModel extends BaseViewModel {
AndroidExecutor androidExecutor,
EventBus eventBus,
IdentityManager identityManager,
BlogManager blogManager,
AndroidNotificationManager notificationManager) {
AndroidNotificationManager notificationManager,
BlogManager blogManager) {
super(application, dbExecutor, lifecycleManager, db, androidExecutor,
eventBus, identityManager, blogManager);
this.notificationManager = notificationManager;
eventBus, identityManager, notificationManager, blogManager);
}
@Override
@@ -122,7 +113,7 @@ class FeedViewModel extends BaseViewModel {
}
void loadAllBlogPosts() {
loadList(this::loadAllBlogPosts, blogPosts::setValue);
loadList(this::loadAllBlogPosts, this::updateBlogPosts);
}
@DatabaseExecutor
@@ -142,33 +133,4 @@ class FeedViewModel extends BaseViewModel {
return posts;
}
private void onBlogPostAdded(BlogPostHeader header, boolean local) {
postAddedWasLocal = local;
runOnDbThread(() -> {
try {
db.transaction(true, txn -> {
BlogPostItem item = getItem(txn, header);
txn.attach(() -> {
List<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;
}
}