Compare commits

...

13 Commits

Author SHA1 Message Date
Torsten Grote
b4f38e81ea Allow DbViewModel work on things other than lists. 2021-03-23 14:15:14 +00:00
Torsten Grote
4074ac8578 Add handleException() to DbViewModel
and use it for blogs
2021-03-22 15:17:30 -03:00
Torsten Grote
e97478a21a Don't reload blog data when configuration changes 2021-03-17 14:16:02 -03:00
Torsten Grote
726ebcea3f Make blog post author clickable when not already in their blog 2021-03-17 14:16:02 -03:00
Torsten Grote
2f969775d8 Remove TransactionManager from blog's BaseViewModel 2021-03-17 14:16:02 -03:00
Torsten Grote
d3b855318c Anticipate review feedback for blog view models after re-basing 2021-03-17 14:16:01 -03:00
Torsten Grote
95104d3383 Clean up after migrating blog controllers to view model 2021-03-17 14:16:01 -03:00
Torsten Grote
6860a04e8b Don't use layoutManager hack to restore scrolling position of blogs
not needed anymore when posts are cached in viewmodels
2021-03-17 14:16:01 -03:00
Torsten Grote
33c24f8655 Migrate blogs to new SharingController
and get rid of the deprecated one
2021-03-17 14:16:00 -03:00
Torsten Grote
1fa4b78474 Migrate BlogController to BlogViewModel 2021-03-17 14:16:00 -03:00
Torsten Grote
b678de7529 Make BlogAdapter final and don't pass in a FragmentManager 2021-03-17 14:16:00 -03:00
Torsten Grote
ab1ed0ff5a Turn FeedController into FeedViewModel 2021-03-17 14:15:59 -03:00
Torsten Grote
ad20e5230a Allow blog posts to be loaded within one transaction 2021-03-17 14:15:59 -03:00
36 changed files with 996 additions and 1663 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.DozeHelperModule;
import org.briarproject.briar.android.account.LockManagerImpl; import org.briarproject.briar.android.account.LockManagerImpl;
import org.briarproject.briar.android.account.SetupModule; 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.contact.ContactListModule;
import org.briarproject.briar.android.forum.ForumModule; import org.briarproject.briar.android.forum.ForumModule;
import org.briarproject.briar.android.introduction.IntroductionModule; import org.briarproject.briar.android.introduction.IntroductionModule;
@@ -85,6 +86,7 @@ import static org.briarproject.briar.android.TestingConstants.IS_DEBUG_BUILD;
ContactListModule.class, ContactListModule.class,
IntroductionModule.class, IntroductionModule.class,
// below need to be within same scope as ViewModelProvider.Factory // below need to be within same scope as ViewModelProvider.Factory
BlogModule.class,
ForumModule.class, ForumModule.class,
GroupListModule.class, GroupListModule.class,
GroupConversationModule.class, GroupConversationModule.class,

View File

@@ -11,10 +11,8 @@ import org.briarproject.briar.android.account.SetupActivity;
import org.briarproject.briar.android.account.UnlockActivity; import org.briarproject.briar.android.account.UnlockActivity;
import org.briarproject.briar.android.blog.BlogActivity; import org.briarproject.briar.android.blog.BlogActivity;
import org.briarproject.briar.android.blog.BlogFragment; 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.BlogPostFragment;
import org.briarproject.briar.android.blog.FeedFragment; 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.ReblogActivity;
import org.briarproject.briar.android.blog.ReblogFragment; import org.briarproject.briar.android.blog.ReblogFragment;
import org.briarproject.briar.android.blog.RssFeedImportActivity; import org.briarproject.briar.android.blog.RssFeedImportActivity;
@@ -85,7 +83,6 @@ import dagger.Component;
@ActivityScope @ActivityScope
@Component(modules = { @Component(modules = {
ActivityModule.class, ActivityModule.class,
BlogModule.class,
CreateGroupModule.class, CreateGroupModule.class,
GroupInvitationModule.class, GroupInvitationModule.class,
GroupMemberModule.class, GroupMemberModule.class,
@@ -152,8 +149,6 @@ public interface ActivityComponent {
void inject(BlogPostFragment fragment); void inject(BlogPostFragment fragment);
void inject(FeedPostFragment fragment);
void inject(ReblogFragment fragment); void inject(ReblogFragment fragment);
void inject(ReblogActivity activity); void inject(ReblogActivity activity);

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.Collection;
import javax.annotation.Nullable;
import androidx.annotation.UiThread;
@NotNullByDefault
interface BaseController {
@UiThread
void onStart();
@UiThread
void onStop();
void loadBlogPosts(GroupId g,
ResultExceptionHandler<Collection<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,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<MessageId, String> textCache = new ConcurrentHashMap<>();
private final Map<MessageId, BlogPostHeader> 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<Collection<BlogPostItem>, DbException> handler) {
runOnDbThread(() -> {
try {
Collection<BlogPostItem> items = loadItems(groupId);
handler.onResult(items);
} catch (DbException e) {
logException(LOG, WARNING, e);
handler.onException(e);
}
});
}
Collection<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());
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

@@ -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);
}
}
}

View File

@@ -0,0 +1,199 @@
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<LiveResult<ListUpdate>> 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<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) {
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<LiveResult<BlogPostItem>> loadBlogPost(GroupId g, MessageId m) {
MutableLiveData<LiveResult<BlogPostItem>> 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<BlogPostItem> 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<LiveResult<ListUpdate>> getBlogPosts() {
return blogPosts;
}
@UiThread
protected List<BlogPostItem> getBlogPostItems() {
LiveResult<ListUpdate> 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<BlogPostItem> items;
ListUpdate(@Nullable Boolean postAddedWasLocal,
List<BlogPostItem> items) {
this.postAddedWasLocal = postAddedWasLocal;
this.items = items;
}
@Nullable
public Boolean getPostAddedWasLocal() {
return postAddedWasLocal;
}
public List<BlogPostItem> getItems() {
return items;
}
}
}

View File

@@ -16,6 +16,9 @@ import javax.annotation.Nullable;
import javax.inject.Inject; import javax.inject.Inject;
import androidx.appcompat.widget.Toolbar; import androidx.appcompat.widget.Toolbar;
import androidx.lifecycle.ViewModelProvider;
import static java.util.Objects.requireNonNull;
@MethodsNotNullByDefault @MethodsNotNullByDefault
@ParametersNotNullByDefault @ParametersNotNullByDefault
@@ -23,7 +26,16 @@ public class BlogActivity extends BriarActivity
implements BaseFragmentListener { implements BaseFragmentListener {
@Inject @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 @Override
public void onCreate(@Nullable Bundle state) { public void onCreate(@Nullable Bundle state) {
@@ -31,32 +43,36 @@ public class BlogActivity extends BriarActivity
// GroupId from Intent // GroupId from Intent
Intent i = getIntent(); Intent i = getIntent();
byte[] b = i.getByteArrayExtra(GROUP_ID); GroupId groupId =
if (b == null) throw new IllegalStateException("No group ID in intent"); new GroupId(requireNonNull(i.getByteArrayExtra(GROUP_ID)));
GroupId groupId = new GroupId(b); viewModel.setGroupId(groupId);
blogController.setGroupId(groupId);
setContentView(R.layout.activity_fragment_container_toolbar); setContentView(R.layout.activity_fragment_container_toolbar);
Toolbar toolbar = setUpCustomToolbar(false); Toolbar toolbar = setUpCustomToolbar(false);
// Open Sharing Status on Toolbar click // Open Sharing Status on Toolbar click
if (toolbar != null) { toolbar.setOnClickListener(v -> {
toolbar.setOnClickListener(v -> { Intent i1 = new Intent(BlogActivity.this,
Intent i1 = new Intent(BlogActivity.this, BlogSharingStatusActivity.class);
BlogSharingStatusActivity.class); i1.putExtra(GROUP_ID, groupId.getBytes());
i1.putExtra(GROUP_ID, groupId.getBytes()); startActivity(i1);
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) { if (state == null) {
showInitialFragment(BlogFragment.newInstance(groupId)); showInitialFragment(BlogFragment.newInstance(groupId));
} }
} }
@Override private void setToolbarSubTitle(int total, int online) {
public void injectActivity(ActivityComponent component) { requireNonNull(getSupportActionBar())
component.inject(this); .setSubtitle(getString(R.string.shared_with, total, online));
} }
} }

View File

@@ -8,7 +8,9 @@ import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
import java.util.List; import java.util.List;
// This class is not thread-safe import javax.annotation.concurrent.NotThreadSafe;
@NotThreadSafe
class BlogCommentItem extends BlogPostItem { class BlogCommentItem extends BlogPostItem {
private static final BlogCommentComparator COMPARATOR = private static final BlogCommentComparator COMPARATOR =

View File

@@ -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<Collection<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

@@ -1,9 +1,7 @@
package org.briarproject.briar.android.blog; package org.briarproject.briar.android.blog;
import android.content.DialogInterface;
import android.content.Intent; import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
import android.os.Parcelable;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.Menu; import android.view.Menu;
import android.view.MenuInflater; import android.view.MenuInflater;
@@ -12,33 +10,25 @@ import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.Toast; 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.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.bramble.api.sync.GroupId; import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.briar.R; import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent; import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.activity.BriarActivity; import org.briarproject.briar.android.blog.BaseViewModel.ListUpdate;
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; import org.briarproject.briar.android.fragment.BaseFragment;
import org.briarproject.briar.android.sharing.BlogSharingStatusActivity; import org.briarproject.briar.android.sharing.BlogSharingStatusActivity;
import org.briarproject.briar.android.sharing.ShareBlogActivity; import org.briarproject.briar.android.sharing.ShareBlogActivity;
import org.briarproject.briar.android.util.BriarSnackbarBuilder; import org.briarproject.briar.android.util.BriarSnackbarBuilder;
import org.briarproject.briar.android.view.BriarRecyclerView; import org.briarproject.briar.android.view.BriarRecyclerView;
import org.briarproject.briar.api.blog.BlogPostHeader; import org.briarproject.briar.android.widget.LinkDialogFragment;
import java.util.Collection;
import javax.inject.Inject; import javax.inject.Inject;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.UiThread; import androidx.annotation.UiThread;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AlertDialog;
import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView.LayoutManager; 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.BriarActivity.GROUP_ID;
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_SHARE_BLOG; 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.activity.RequestCodes.REQUEST_WRITE_BLOG_POST;
import static org.briarproject.briar.android.controller.SharingController.SharingListener;
@UiThread @UiThread
@MethodsNotNullByDefault @MethodsNotNullByDefault
@ParametersNotNullByDefault @ParametersNotNullByDefault
public class BlogFragment extends BaseFragment public class BlogFragment extends BaseFragment
implements BlogSharingListener, SharingListener, implements OnBlogPostClickListener {
OnBlogPostClickListener {
private final static String TAG = BlogFragment.class.getName(); private final static String TAG = BlogFragment.class.getName();
@Inject @Inject
BlogController blogController; ViewModelProvider.Factory viewModelFactory;
@Inject
SharingController sharingController;
@Nullable
private Parcelable layoutManagerState;
private GroupId groupId; private GroupId groupId;
private BlogPostAdapter adapter; private BlogViewModel viewModel;
private LayoutManager layoutManager; private final BlogPostAdapter adapter = new BlogPostAdapter(false, this);
private BriarRecyclerView list; private BriarRecyclerView list;
private MenuItem writeButton, deleteButton;
private boolean isMyBlog = false, canDeleteBlog = false;
static BlogFragment newInstance(GroupId groupId) { static BlogFragment newInstance(GroupId groupId) {
BlogFragment f = new BlogFragment(); BlogFragment f = new BlogFragment();
@@ -87,8 +69,8 @@ public class BlogFragment extends BaseFragment
@Override @Override
public void injectFragment(ActivityComponent component) { public void injectFragment(ActivityComponent component) {
component.inject(this); component.inject(this);
blogController.setBlogSharingListener(this); viewModel = new ViewModelProvider(requireActivity(), viewModelFactory)
sharingController.setSharingListener(this); .get(BlogViewModel.class);
} }
@Nullable @Nullable
@@ -103,95 +85,75 @@ public class BlogFragment extends BaseFragment
View v = inflater.inflate(R.layout.fragment_blog, container, false); View v = inflater.inflate(R.layout.fragment_blog, container, false);
adapter = new BlogPostAdapter(requireActivity(), this,
getFragmentManager());
list = v.findViewById(R.id.postList); list = v.findViewById(R.id.postList);
layoutManager = new LinearLayoutManager(getActivity()); LayoutManager layoutManager = new LinearLayoutManager(getActivity());
list.setLayoutManager(layoutManager); list.setLayoutManager(layoutManager);
list.setAdapter(adapter); list.setAdapter(adapter);
list.showProgressBar(); list.showProgressBar();
list.setEmptyText(getString(R.string.blogs_other_blog_empty_state)); list.setEmptyText(getString(R.string.blogs_other_blog_empty_state));
if (savedInstanceState != null) { viewModel.getBlogPosts().observe(getViewLifecycleOwner(), result ->
layoutManagerState = result.onError(this::handleException)
savedInstanceState.getParcelable("layoutManager"); .onSuccess(this::onBlogPostsLoaded)
} );
viewModel.getBlogRemoved().observe(getViewLifecycleOwner(), removed -> {
if (removed) finish();
});
return v; return v;
} }
@Override @Override
public void onStart() { public void onStart() {
super.onStart(); super.onStart();
sharingController.onStart(); viewModel.blockAndClearNotifications();
loadBlog();
loadSharedContacts();
loadBlogPosts(false);
list.startPeriodicUpdate(); list.startPeriodicUpdate();
} }
@Override @Override
public void onStop() { public void onStop() {
super.onStop(); super.onStop();
sharingController.onStop(); viewModel.unblockNotifications();
list.stopPeriodicUpdate(); 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 @Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.blogs_blog_actions, menu); inflater.inflate(R.menu.blogs_blog_actions, menu);
writeButton = menu.findItem(R.id.action_write_blog_post); MenuItem writeButton = menu.findItem(R.id.action_write_blog_post);
if (isMyBlog) writeButton.setVisible(true); MenuItem deleteButton = menu.findItem(R.id.action_blog_delete);
deleteButton = menu.findItem(R.id.action_blog_delete); viewModel.getBlog().observe(getViewLifecycleOwner(), blog -> {
if (canDeleteBlog) deleteButton.setEnabled(true); if (blog.isOurs()) writeButton.setVisible(true);
if (blog.canBeRemoved()) deleteButton.setEnabled(true);
});
super.onCreateOptionsMenu(menu, inflater); super.onCreateOptionsMenu(menu, inflater);
} }
@Override @Override
public boolean onOptionsItemSelected(MenuItem item) { public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) { int itemId = item.getItemId();
case R.id.action_write_blog_post: if (itemId == R.id.action_write_blog_post) {
Intent i = new Intent(getActivity(), Intent i = new Intent(getActivity(), WriteBlogPostActivity.class);
WriteBlogPostActivity.class); i.putExtra(GROUP_ID, groupId.getBytes());
i.putExtra(GROUP_ID, groupId.getBytes()); startActivityForResult(i, REQUEST_WRITE_BLOG_POST);
startActivityForResult(i, REQUEST_WRITE_BLOG_POST); return true;
return true; } else if (itemId == R.id.action_blog_share) {
case R.id.action_blog_share: Intent i = new Intent(getActivity(), ShareBlogActivity.class);
Intent i2 = new Intent(getActivity(), ShareBlogActivity.class); i.setFlags(FLAG_ACTIVITY_CLEAR_TOP);
i2.setFlags(FLAG_ACTIVITY_CLEAR_TOP); i.putExtra(GROUP_ID, groupId.getBytes());
i2.putExtra(GROUP_ID, groupId.getBytes()); startActivityForResult(i, REQUEST_SHARE_BLOG);
startActivityForResult(i2, REQUEST_SHARE_BLOG); return true;
return true; } else if (itemId == R.id.action_blog_sharing_status) {
case R.id.action_blog_sharing_status: Intent i =
Intent i3 = new Intent(getActivity(), new Intent(getActivity(), BlogSharingStatusActivity.class);
BlogSharingStatusActivity.class); i.setFlags(FLAG_ACTIVITY_CLEAR_TOP);
i3.setFlags(FLAG_ACTIVITY_CLEAR_TOP); i.putExtra(GROUP_ID, groupId.getBytes());
i3.putExtra(GROUP_ID, groupId.getBytes()); startActivity(i);
startActivity(i3); return true;
return true; } else if (itemId == R.id.action_blog_delete) {
case R.id.action_blog_delete: showDeleteDialog();
showDeleteDialog(); return true;
return true;
default:
return super.onOptionsItemSelected(item);
} }
return super.onOptionsItemSelected(item);
} }
@Override @Override
@@ -201,7 +163,6 @@ public class BlogFragment extends BaseFragment
if (request == REQUEST_WRITE_BLOG_POST && result == RESULT_OK) { if (request == REQUEST_WRITE_BLOG_POST && result == RESULT_OK) {
displaySnackbar(R.string.blogs_blog_post_created, true); displaySnackbar(R.string.blogs_blog_post_created, true);
loadBlogPosts(true);
} else if (request == REQUEST_SHARE_BLOG && result == RESULT_OK) { } else if (request == REQUEST_SHARE_BLOG && result == RESULT_OK) {
displaySnackbar(R.string.blogs_sharing_snackbar, false); displaySnackbar(R.string.blogs_sharing_snackbar, false);
} }
@@ -212,35 +173,25 @@ public class BlogFragment extends BaseFragment
return TAG; return TAG;
} }
@Override private void onBlogPostsLoaded(ListUpdate update) {
public void onBlogPostAdded(BlogPostHeader header, boolean local) { adapter.submitList(update.getItems(), () -> {
blogController.loadBlogPost(header, Boolean wasLocal = update.getPostAddedWasLocal();
new UiResultExceptionHandler<BlogPostItem, DbException>( if (wasLocal != null && wasLocal) {
this) { list.scrollToPosition(0);
@Override displaySnackbar(R.string.blogs_blog_post_created,
public void onResultUi(BlogPostItem post) { false);
adapter.add(post); } else if (wasLocal != null) {
if (local) { displaySnackbar(R.string.blogs_blog_post_received,
list.scrollToPosition(0); true);
displaySnackbar(R.string.blogs_blog_post_created, }
false); list.showData();
} else { });
displaySnackbar(R.string.blogs_blog_post_received,
true);
}
}
@Override
public void onExceptionUi(DbException exception) {
handleException(exception);
}
}
);
} }
@Override @Override
public void onBlogPostClick(BlogPostItem post) { public void onBlogPostClick(BlogPostItem post) {
BlogPostFragment f = BlogPostFragment.newInstance(post.getId()); BlogPostFragment f =
BlogPostFragment.newInstance(groupId, post.getId(), false);
showNextFragment(f); showNextFragment(f);
} }
@@ -256,111 +207,10 @@ public class BlogFragment extends BaseFragment
getContext().startActivity(i); getContext().startActivity(i);
} }
private void loadBlogPosts(boolean reload) {
blogController.loadBlogPosts(
new UiResultExceptionHandler<Collection<BlogPostItem>,
DbException>(this) {
@Override
public void onResultUi(Collection<BlogPostItem> 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<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());
}
private void loadSharedContacts() {
blogController.loadSharingContacts(
new UiResultExceptionHandler<Collection<ContactId>,
DbException>(this) {
@Override
public void onResultUi(Collection<ContactId> contacts) {
sharingController.addAll(contacts);
int online = sharingController.getOnlineCount();
setToolbarSubTitle(contacts.size(), online);
}
@Override
public void onExceptionUi(DbException exception) {
handleException(exception);
}
});
}
@Override @Override
public void onBlogInvitationAccepted(ContactId c) { public void onLinkClick(String url) {
sharingController.add(c); LinkDialogFragment f = LinkDialogFragment.newInstance(url);
setToolbarSubTitle(sharingController.getTotalCount(), f.show(getParentFragmentManager(), f.getUniqueTag());
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);
} }
private void displaySnackbar(int stringId, boolean scroll) { private void displaySnackbar(int stringId, boolean scroll) {
@@ -373,38 +223,21 @@ public class BlogFragment extends BaseFragment
} }
private void showDeleteDialog() { private void showDeleteDialog() {
DialogInterface.OnClickListener okListener = AlertDialog.Builder builder = new AlertDialog.Builder(requireContext(),
(dialog, which) -> deleteBlog();
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity(),
R.style.BriarDialogTheme); R.style.BriarDialogTheme);
builder.setTitle(getString(R.string.blogs_remove_blog)); builder.setTitle(getString(R.string.blogs_remove_blog));
builder.setMessage( builder.setMessage(
getString(R.string.blogs_remove_blog_dialog_message)); getString(R.string.blogs_remove_blog_dialog_message));
builder.setPositiveButton(R.string.cancel, null); 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(); builder.show();
} }
private void deleteBlog() { private void deleteBlog() {
blogController.deleteBlog( viewModel.deleteBlog();
new UiResultExceptionHandler<Void, DbException>(this) { Toast.makeText(getActivity(), R.string.blogs_blog_removed, LENGTH_SHORT)
@Override .show();
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() {
finish(); finish();
} }

View File

@@ -1,35 +1,23 @@
package org.briarproject.briar.android.blog; package org.briarproject.briar.android.blog;
import org.briarproject.briar.android.activity.ActivityScope; import org.briarproject.briar.android.viewmodel.ViewModelKey;
import org.briarproject.briar.android.activity.BaseActivity;
import org.briarproject.briar.android.controller.SharingController;
import org.briarproject.briar.android.controller.SharingControllerImpl;
import androidx.lifecycle.ViewModel;
import dagger.Binds;
import dagger.Module; import dagger.Module;
import dagger.Provides; import dagger.multibindings.IntoMap;
@Module @Module
public class BlogModule { public interface BlogModule {
@ActivityScope @Binds
@Provides @IntoMap
BlogController provideBlogController(BaseActivity activity, @ViewModelKey(FeedViewModel.class)
BlogControllerImpl blogController) { ViewModel bindFeedViewModel(FeedViewModel feedViewModel);
activity.addLifecycleController(blogController);
return blogController;
}
@ActivityScope @Binds
@Provides @IntoMap
FeedController provideFeedController(FeedControllerImpl feedController) { @ViewModelKey(BlogViewModel.class)
return feedController; ViewModel bindBlogViewModel(BlogViewModel blogViewModel);
}
@ActivityScope
@Provides
SharingController provideSharingController(
SharingControllerImpl sharingController) {
return sharingController;
}
} }

View File

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

View File

@@ -1,76 +1,163 @@
package org.briarproject.briar.android.blog; package org.briarproject.briar.android.blog;
import android.content.Intent;
import android.os.Bundle; 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.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; 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.MessageId;
import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent; import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.blog.BaseController.BlogListener; import org.briarproject.briar.android.fragment.BaseFragment;
import org.briarproject.briar.android.controller.handler.UiResultExceptionHandler; import org.briarproject.briar.android.widget.LinkDialogFragment;
import org.briarproject.briar.api.blog.BlogPostHeader;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import javax.inject.Inject; import javax.inject.Inject;
import androidx.annotation.UiThread; 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 @MethodsNotNullByDefault
@ParametersNotNullByDefault @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 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 @Inject
BlogController blogController; ViewModelProvider.Factory viewModelFactory;
static BlogPostFragment newInstance(MessageId postId) { static BlogPostFragment newInstance(GroupId blogId, MessageId postId,
boolean isFeed) {
BlogPostFragment f = new BlogPostFragment(); BlogPostFragment f = new BlogPostFragment();
Bundle bundle = new Bundle(); Bundle bundle = new Bundle();
bundle.putByteArray(GROUP_ID, blogId.getBytes());
bundle.putByteArray(POST_ID, postId.getBytes()); bundle.putByteArray(POST_ID, postId.getBytes());
bundle.putBoolean(IS_FEED, isFeed);
f.setArguments(bundle); f.setArguments(bundle);
return f; 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 @Override
public String getUniqueTag() { public String getUniqueTag() {
return TAG; return TAG;
} }
@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();
}
} }

View File

@@ -17,7 +17,7 @@ public class BlogPostItem implements Comparable<BlogPostItem> {
private final BlogPostHeader header; private final BlogPostHeader header;
@Nullable @Nullable
protected String text; protected String text;
private boolean read; private final boolean read;
BlogPostItem(BlogPostHeader header, @Nullable String text) { BlogPostItem(BlogPostHeader header, @Nullable String text) {
this.header = header; this.header = header;
@@ -74,9 +74,6 @@ public class BlogPostItem implements Comparable<BlogPostItem> {
protected static int compare(BlogPostHeader h1, BlogPostHeader h2) { protected static int compare(BlogPostHeader h1, BlogPostHeader h2) {
// The newest post comes first // The newest post comes first
long aTime = h1.getTimeReceived(), bTime = h2.getTimeReceived(); return Long.compare(h2.getTimeReceived(), h1.getTimeReceived());
if (aTime > bTime) return -1;
if (aTime < bTime) return 1;
return 0;
} }
} }

View File

@@ -9,6 +9,7 @@ import android.view.ViewGroup;
import android.widget.ImageButton; import android.widget.ImageButton;
import android.widget.TextView; import android.widget.TextView;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.MessageId; import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.briar.R; import org.briarproject.briar.R;
import org.briarproject.briar.android.view.AuthorView; import org.briarproject.briar.android.view.AuthorView;
@@ -17,23 +18,24 @@ import org.briarproject.briar.api.blog.BlogPostHeader;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import androidx.annotation.NonNull;
import androidx.annotation.UiThread; import androidx.annotation.UiThread;
import androidx.core.view.ViewCompat; import androidx.core.view.ViewCompat;
import androidx.fragment.app.FragmentManager;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import static android.view.View.GONE; import static android.view.View.GONE;
import static android.view.View.VISIBLE; import static android.view.View.VISIBLE;
import static org.briarproject.briar.android.activity.BriarActivity.GROUP_ID; 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.TEASER_LENGTH;
import static org.briarproject.briar.android.util.UiUtils.getSpanned; 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.getTeaser;
import static org.briarproject.briar.android.util.UiUtils.makeLinksClickable; 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 @UiThread
@NotNullByDefault
class BlogPostViewHolder extends RecyclerView.ViewHolder { class BlogPostViewHolder extends RecyclerView.ViewHolder {
private final Context ctx; private final Context ctx;
@@ -43,20 +45,16 @@ class BlogPostViewHolder extends RecyclerView.ViewHolder {
private final ImageButton reblogButton; private final ImageButton reblogButton;
private final TextView text; private final TextView text;
private final ViewGroup commentContainer; private final ViewGroup commentContainer;
private final boolean fullText; private final boolean fullText, authorClickable;
@NonNull
private final OnBlogPostClickListener listener; private final OnBlogPostClickListener listener;
@Nullable
private final FragmentManager fragmentManager;
BlogPostViewHolder(View v, boolean fullText, BlogPostViewHolder(View v, boolean fullText,
@NonNull OnBlogPostClickListener listener, OnBlogPostClickListener listener, boolean authorClickable) {
@Nullable FragmentManager fragmentManager) {
super(v); super(v);
this.fullText = fullText; this.fullText = fullText;
this.listener = listener; this.listener = listener;
this.fragmentManager = fragmentManager; this.authorClickable = authorClickable;
ctx = v.getContext(); ctx = v.getContext();
layout = v.findViewById(R.id.postLayout); layout = v.findViewById(R.id.postLayout);
@@ -67,10 +65,6 @@ class BlogPostViewHolder extends RecyclerView.ViewHolder {
commentContainer = v.findViewById(R.id.commentContainer); commentContainer = v.findViewById(R.id.commentContainer);
} }
void setVisibility(int visibility) {
layout.setVisibility(visibility);
}
void hideReblogButton() { void hideReblogButton() {
reblogButton.setVisibility(GONE); reblogButton.setVisibility(GONE);
} }
@@ -103,7 +97,7 @@ class BlogPostViewHolder extends RecyclerView.ViewHolder {
author.setPersona( author.setPersona(
item.isRssFeed() ? AuthorView.RSS_FEED : AuthorView.NORMAL); item.isRssFeed() ? AuthorView.RSS_FEED : AuthorView.NORMAL);
// TODO make author clickable more often #624 // TODO make author clickable more often #624
if (!fullText && item.getHeader().getType() == POST) { if (authorClickable) {
author.setAuthorClickable(v -> listener.onAuthorClick(item)); author.setAuthorClickable(v -> listener.onAuthorClick(item));
} else { } else {
author.setAuthorNotClickable(); author.setAuthorNotClickable();
@@ -114,7 +108,7 @@ class BlogPostViewHolder extends RecyclerView.ViewHolder {
if (fullText) { if (fullText) {
text.setText(postText); text.setText(postText);
text.setTextIsSelectable(true); text.setTextIsSelectable(true);
makeLinksClickable(text, fragmentManager); makeLinksClickable(text, listener::onLinkClick);
} else { } else {
text.setTextIsSelectable(false); text.setTextIsSelectable(false);
if (postText.length() > TEASER_LENGTH) if (postText.length() > TEASER_LENGTH)
@@ -147,17 +141,16 @@ class BlogPostViewHolder extends RecyclerView.ViewHolder {
reblogger.setAuthorClickable(v -> listener.onAuthorClick(item)); reblogger.setAuthorClickable(v -> listener.onAuthorClick(item));
} }
reblogger.setVisibility(VISIBLE); reblogger.setVisibility(VISIBLE);
reblogger.setPersona(AuthorView.REBLOGGER); reblogger.setPersona(REBLOGGER);
author.setPersona(item.getHeader().getRootPost().isRssFeed() ? author.setPersona(item.getHeader().getRootPost().isRssFeed() ?
AuthorView.RSS_FEED_REBLOGGED : RSS_FEED_REBLOGGED : COMMENTER);
AuthorView.COMMENTER);
// comments // comments
// TODO use nested RecyclerView instead like we do for Image Attachments
for (BlogCommentHeader c : item.getComments()) { for (BlogCommentHeader c : item.getComments()) {
View v = LayoutInflater.from(ctx) View v = LayoutInflater.from(ctx).inflate(
.inflate(R.layout.list_item_blog_comment, R.layout.list_item_blog_comment, commentContainer, false);
commentContainer, false);
AuthorView author = v.findViewById(R.id.authorView); AuthorView author = v.findViewById(R.id.authorView);
TextView text = v.findViewById(R.id.textView); TextView text = v.findViewById(R.id.textView);

View File

@@ -1,24 +1,24 @@
package org.briarproject.briar.android.blog; 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.Contact;
import org.briarproject.bramble.api.contact.ContactId; import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.db.DatabaseExecutor; import org.briarproject.bramble.api.db.DatabaseExecutor;
import org.briarproject.bramble.api.db.DbException; 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.Event;
import org.briarproject.bramble.api.event.EventBus; 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.IdentityManager;
import org.briarproject.bramble.api.identity.LocalAuthor; import org.briarproject.bramble.api.identity.LocalAuthor;
import org.briarproject.bramble.api.lifecycle.LifecycleManager; import org.briarproject.bramble.api.lifecycle.LifecycleManager;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault; import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.bramble.api.sync.GroupId; 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.sync.event.GroupRemovedEvent;
import org.briarproject.briar.android.controller.ActivityLifecycleController; import org.briarproject.bramble.api.system.AndroidExecutor;
import org.briarproject.briar.android.controller.handler.ResultExceptionHandler; 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.android.AndroidNotificationManager;
import org.briarproject.briar.api.blog.Blog; import org.briarproject.briar.api.blog.Blog;
import org.briarproject.briar.api.blog.BlogInvitationResponse; import org.briarproject.briar.api.blog.BlogInvitationResponse;
@@ -35,85 +35,54 @@ import java.util.logging.Logger;
import javax.inject.Inject; 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.logDuration;
import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.bramble.util.LogUtils.now; import static org.briarproject.bramble.util.LogUtils.now;
@MethodsNotNullByDefault @MethodsNotNullByDefault
@ParametersNotNullByDefault @ParametersNotNullByDefault
class BlogControllerImpl extends BaseControllerImpl class BlogViewModel extends BaseViewModel {
implements ActivityLifecycleController, BlogController, EventListener {
private static final Logger LOG = private static final Logger LOG = getLogger(BlogViewModel.class.getName());
Logger.getLogger(BlogControllerImpl.class.getName());
private final BlogSharingManager blogSharingManager; private final BlogSharingManager blogSharingManager;
private final SharingController sharingController;
// UI thread private volatile GroupId groupId;
@Nullable
private BlogSharingListener listener;
private volatile GroupId groupId = null; private final MutableLiveData<BlogItem> blog = new MutableLiveData<>();
private final MutableLiveData<Boolean> blogRemoved =
new MutableLiveData<>();
@Inject @Inject
BlogControllerImpl(@DatabaseExecutor Executor dbExecutor, BlogViewModel(Application application,
LifecycleManager lifecycleManager, EventBus eventBus, @DatabaseExecutor Executor dbExecutor,
LifecycleManager lifecycleManager,
TransactionManager db,
AndroidExecutor androidExecutor,
EventBus eventBus,
IdentityManager identityManager,
AndroidNotificationManager notificationManager, AndroidNotificationManager notificationManager,
IdentityManager identityManager, BlogManager blogManager, BlogManager blogManager,
BlogSharingManager blogSharingManager) { BlogSharingManager blogSharingManager,
super(dbExecutor, lifecycleManager, eventBus, notificationManager, SharingController sharingController) {
identityManager, blogManager); super(application, dbExecutor, lifecycleManager, db, androidExecutor,
eventBus, identityManager, notificationManager, blogManager);
this.blogSharingManager = blogSharingManager; this.blogSharingManager = blogSharingManager;
} this.sharingController = sharingController;
@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;
} }
@Override @Override
public void eventOccurred(Event e) { public void eventOccurred(Event e) {
if (groupId == null || listener == null)
throw new IllegalStateException();
if (e instanceof BlogPostAddedEvent) { if (e instanceof BlogPostAddedEvent) {
BlogPostAddedEvent b = (BlogPostAddedEvent) e; BlogPostAddedEvent b = (BlogPostAddedEvent) e;
if (b.getGroupId().equals(groupId)) { if (b.getGroupId().equals(groupId)) {
LOG.info("Blog post added"); LOG.info("Blog post added");
listener.onBlogPostAdded(b.getHeader(), b.isLocal()); onBlogPostAdded(b.getHeader(), b.isLocal());
} }
} else if (e instanceof BlogInvitationResponseReceivedEvent) { } else if (e instanceof BlogInvitationResponseReceivedEvent) {
BlogInvitationResponseReceivedEvent b = BlogInvitationResponseReceivedEvent b =
@@ -121,41 +90,36 @@ class BlogControllerImpl extends BaseControllerImpl
BlogInvitationResponse r = b.getMessageHeader(); BlogInvitationResponse r = b.getMessageHeader();
if (r.getShareableId().equals(groupId) && r.wasAccepted()) { if (r.getShareableId().equals(groupId) && r.wasAccepted()) {
LOG.info("Blog invitation accepted"); LOG.info("Blog invitation accepted");
listener.onBlogInvitationAccepted(b.getContactId()); sharingController.add(b.getContactId());
} }
} else if (e instanceof ContactLeftShareableEvent) { } else if (e instanceof ContactLeftShareableEvent) {
ContactLeftShareableEvent s = (ContactLeftShareableEvent) e; ContactLeftShareableEvent s = (ContactLeftShareableEvent) e;
if (s.getGroupId().equals(groupId)) { if (s.getGroupId().equals(groupId)) {
LOG.info("Blog left by contact"); LOG.info("Blog left by contact");
listener.onBlogLeft(s.getContactId()); sharingController.remove(s.getContactId());
} }
} else if (e instanceof GroupRemovedEvent) { } else if (e instanceof GroupRemovedEvent) {
GroupRemovedEvent g = (GroupRemovedEvent) e; GroupRemovedEvent g = (GroupRemovedEvent) e;
if (g.getGroup().getId().equals(groupId)) { if (g.getGroup().getId().equals(groupId)) {
LOG.info("Blog removed"); LOG.info("Blog removed");
listener.onBlogRemoved(); blogRemoved.setValue(true);
} }
} }
} }
@Override /**
public void loadBlogPosts( * Set this before calling any other methods.
ResultExceptionHandler<Collection<BlogPostItem>, DbException> handler) { */
if (groupId == null) throw new IllegalStateException(); @UiThread
loadBlogPosts(groupId, handler); public void setGroupId(GroupId groupId) {
if (this.groupId == groupId) return; // configuration change
this.groupId = groupId;
loadBlog(groupId);
loadBlogPosts(groupId);
loadSharingContacts(groupId);
} }
@Override private void loadBlog(GroupId groupId) {
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(() -> { runOnDbThread(() -> {
try { try {
long start = now(); long start = now();
@@ -163,50 +127,65 @@ class BlogControllerImpl extends BaseControllerImpl
Blog b = blogManager.getBlog(groupId); Blog b = blogManager.getBlog(groupId);
boolean ours = a.getId().equals(b.getAuthor().getId()); boolean ours = a.getId().equals(b.getAuthor().getId());
boolean removable = blogManager.canBeRemoved(b); boolean removable = blogManager.canBeRemoved(b);
BlogItem blog = new BlogItem(b, ours, removable); blog.postValue(new BlogItem(b, ours, removable));
logDuration(LOG, "Loading blog", start); logDuration(LOG, "Loading blog", start);
handler.onResult(blog);
} catch (DbException e) { } catch (DbException e) {
logException(LOG, WARNING, e); handleException(e);
handler.onException(e);
} }
}); });
} }
@Override void blockAndClearNotifications() {
public void deleteBlog(ResultExceptionHandler<Void, DbException> handler) { notificationManager.blockNotification(groupId);
if (groupId == null) throw new IllegalStateException(); notificationManager.clearBlogPostNotification(groupId);
}
void unblockNotifications() {
notificationManager.unblockNotification(groupId);
}
private void loadBlogPosts(GroupId groupId) {
loadList(txn -> new ListUpdate(null, loadBlogPosts(txn, groupId)),
blogPosts::setValue);
}
private void loadSharingContacts(GroupId groupId) {
runOnDbThread(true, txn -> {
Collection<Contact> contacts =
blogSharingManager.getSharedWith(txn, groupId);
txn.attach(() -> onSharingContactsLoaded(contacts));
}, this::handleException);
}
@UiThread
private void onSharingContactsLoaded(Collection<Contact> contacts) {
Collection<ContactId> contactIds = new ArrayList<>(contacts.size());
for (Contact c : contacts) contactIds.add(c.getId());
sharingController.addAll(contactIds);
}
void deleteBlog() {
runOnDbThread(() -> { runOnDbThread(() -> {
try { try {
long start = now(); long start = now();
Blog b = blogManager.getBlog(groupId); Blog b = blogManager.getBlog(groupId);
blogManager.removeBlog(b); blogManager.removeBlog(b);
logDuration(LOG, "Removing blog", start); logDuration(LOG, "Removing blog", start);
handler.onResult(null);
} catch (DbException e) { } catch (DbException e) {
logException(LOG, WARNING, e); handleException(e);
handler.onException(e);
} }
}); });
} }
@Override LiveData<BlogItem> getBlog() {
public void loadSharingContacts( return blog;
ResultExceptionHandler<Collection<ContactId>, DbException> handler) {
if (groupId == null) throw new IllegalStateException();
runOnDbThread(() -> {
try {
Collection<Contact> contacts =
blogSharingManager.getSharedWith(groupId);
Collection<ContactId> 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<Boolean> getBlogRemoved() {
return blogRemoved;
}
LiveData<SharingInfo> getSharingInfo() {
return sharingController.getSharingInfo();
}
} }

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

@@ -2,7 +2,6 @@ package org.briarproject.briar.android.blog;
import android.content.Intent; import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
import android.os.Parcelable;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.Menu; import android.view.Menu;
import android.view.MenuInflater; import android.view.MenuInflater;
@@ -10,53 +9,41 @@ import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault; import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.briar.R; import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent; import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.blog.FeedController.FeedListener; import org.briarproject.briar.android.blog.BaseViewModel.ListUpdate;
import org.briarproject.briar.android.controller.handler.UiResultExceptionHandler;
import org.briarproject.briar.android.fragment.BaseFragment; import org.briarproject.briar.android.fragment.BaseFragment;
import org.briarproject.briar.android.util.BriarSnackbarBuilder; import org.briarproject.briar.android.util.BriarSnackbarBuilder;
import org.briarproject.briar.android.view.BriarRecyclerView; 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.Blog;
import org.briarproject.briar.api.blog.BlogPostHeader;
import java.util.Collection;
import java.util.logging.Logger;
import javax.inject.Inject; import javax.inject.Inject;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.UiThread; import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager;
import static android.app.Activity.RESULT_OK;
import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP; import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP;
import static com.google.android.material.snackbar.Snackbar.LENGTH_LONG; 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.BriarActivity.GROUP_ID;
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_WRITE_BLOG_POST;
@UiThread
@MethodsNotNullByDefault @MethodsNotNullByDefault
@ParametersNotNullByDefault @ParametersNotNullByDefault
public class FeedFragment extends BaseFragment implements public class FeedFragment extends BaseFragment
OnBlogPostClickListener, FeedListener { implements OnBlogPostClickListener {
public final static String TAG = FeedFragment.class.getName(); public final static String TAG = FeedFragment.class.getName();
private static final Logger LOG = Logger.getLogger(TAG);
@Inject @Inject
FeedController feedController; ViewModelProvider.Factory viewModelFactory;
private BlogPostAdapter adapter; private FeedViewModel viewModel;
private final BlogPostAdapter adapter = new BlogPostAdapter(true, this);
private LinearLayoutManager layoutManager; private LinearLayoutManager layoutManager;
private BriarRecyclerView list; private BriarRecyclerView list;
@Nullable
private Blog personalBlog;
@Nullable
private Parcelable layoutManagerState;
public static FeedFragment newInstance() { public static FeedFragment newInstance() {
FeedFragment f = new FeedFragment(); FeedFragment f = new FeedFragment();
@@ -70,7 +57,8 @@ public class FeedFragment extends BaseFragment implements
@Override @Override
public void injectFragment(ActivityComponent component) { public void injectFragment(ActivityComponent component) {
component.inject(this); component.inject(this);
feedController.setFeedListener(this); viewModel = new ViewModelProvider(this, viewModelFactory)
.get(FeedViewModel.class);
} }
@Nullable @Nullable
@@ -82,9 +70,6 @@ public class FeedFragment extends BaseFragment implements
View v = inflater.inflate(R.layout.fragment_blog, container, false); View v = inflater.inflate(R.layout.fragment_blog, container, false);
adapter =
new BlogPostAdapter(getActivity(), this, getFragmentManager());
layoutManager = new LinearLayoutManager(getActivity()); layoutManager = new LinearLayoutManager(getActivity());
list = v.findViewById(R.id.postList); list = v.findViewById(R.id.postList);
list.setLayoutManager(layoutManager); list.setLayoutManager(layoutManager);
@@ -93,103 +78,38 @@ public class FeedFragment extends BaseFragment implements
list.setEmptyText(R.string.blogs_feed_empty_state); list.setEmptyText(R.string.blogs_feed_empty_state);
list.setEmptyAction(R.string.blogs_feed_empty_state_action); list.setEmptyAction(R.string.blogs_feed_empty_state_action);
if (savedInstanceState != null) { viewModel.getBlogPosts().observe(getViewLifecycleOwner(), result ->
layoutManagerState = result.onError(this::handleException)
savedInstanceState.getParcelable("layoutManager"); .onSuccess(this::onBlogPostsLoaded)
} );
return v; 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 @Override
public void onStart() { public void onStart() {
super.onStart(); super.onStart();
feedController.onStart(); viewModel.blockAndClearAllBlogPostNotifications();
list.startPeriodicUpdate(); list.startPeriodicUpdate();
loadPersonalBlog();
loadBlogPosts(false);
} }
@Override @Override
public void onStop() { public void onStop() {
super.onStop(); super.onStop();
feedController.onStop(); viewModel.unblockAllBlogPostNotifications();
adapter.clear();
list.showProgressBar();
list.stopPeriodicUpdate(); list.stopPeriodicUpdate();
// TODO save list position in database/preferences?
} }
@Override private void onBlogPostsLoaded(ListUpdate update) {
public void onDestroy() { adapter.submitList(update.getItems(), () -> {
super.onDestroy(); Boolean wasLocal = update.getPostAddedWasLocal();
feedController.unsetFeedListener(this); if (wasLocal != null && wasLocal) {
} showSnackBar(R.string.blogs_blog_post_created);
} else if (wasLocal != null) {
@Override showSnackBar(R.string.blogs_blog_post_received);
public void onSaveInstanceState(Bundle outState) { }
super.onSaveInstanceState(outState); list.showData();
if (layoutManager != null) { });
layoutManagerState = layoutManager.onSaveInstanceState();
outState.putParcelable("layoutManager", layoutManagerState);
}
}
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);
}
});
} }
@Override @Override
@@ -200,67 +120,48 @@ public class FeedFragment extends BaseFragment implements
@Override @Override
public boolean onOptionsItemSelected(MenuItem item) { public boolean onOptionsItemSelected(MenuItem item) {
if (personalBlog == null) return false; int itemId = item.getItemId();
switch (item.getItemId()) { if (itemId == R.id.action_write_blog_post) {
case R.id.action_write_blog_post: Blog personalBlog = viewModel.getPersonalBlog().getValue();
Intent i1 = if (personalBlog == null) return false;
new Intent(getActivity(), WriteBlogPostActivity.class); Intent i = new Intent(getActivity(), WriteBlogPostActivity.class);
i1.putExtra(GROUP_ID, personalBlog.getId().getBytes()); i.putExtra(GROUP_ID, personalBlog.getId().getBytes());
startActivityForResult(i1, REQUEST_WRITE_BLOG_POST); startActivity(i);
return true; return true;
case R.id.action_rss_feeds_import: } else if (itemId == R.id.action_rss_feeds_import) {
Intent i2 = Intent i = new Intent(getActivity(), RssFeedImportActivity.class);
new Intent(getActivity(), RssFeedImportActivity.class); startActivity(i);
startActivity(i2); return true;
return true; } else if (itemId == R.id.action_rss_feeds_manage) {
case R.id.action_rss_feeds_manage: Blog personalBlog = viewModel.getPersonalBlog().getValue();
Intent i3 = if (personalBlog == null) return false;
new Intent(getActivity(), RssFeedManageActivity.class); Intent i = new Intent(getActivity(), RssFeedManageActivity.class);
i3.putExtra(GROUP_ID, personalBlog.getId().getBytes()); i.putExtra(GROUP_ID, personalBlog.getId().getBytes());
startActivity(i3); startActivity(i);
return true; return true;
default:
return super.onOptionsItemSelected(item);
} }
} return super.onOptionsItemSelected(item);
@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);
}
}
);
} }
@Override @Override
public void onBlogPostClick(BlogPostItem post) { public void onBlogPostClick(BlogPostItem post) {
FeedPostFragment f = BaseFragment f = BlogPostFragment
FeedPostFragment.newInstance(post.getGroupId(), post.getId()); .newInstance(post.getGroupId(), post.getId(), true);
showNextFragment(f); showNextFragment(f);
} }
@Override @Override
public void onAuthorClick(BlogPostItem post) { 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.putExtra(GROUP_ID, post.getGroupId().getBytes());
i.setFlags(FLAG_ACTIVITY_CLEAR_TOP); 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 @Override
@@ -283,14 +184,4 @@ public class FeedFragment extends BaseFragment implements
sb.make(list, stringRes, LENGTH_LONG).show(); sb.make(list, stringRes, LENGTH_LONG).show();
} }
@Override
public void onBlogAdded() {
loadBlogPosts(false);
}
@Override
public void onBlogRemoved() {
loadBlogPosts(true);
}
} }

View File

@@ -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<BlogPostItem, DbException>(
this) {
@Override
public void onResultUi(BlogPostItem post) {
onBlogPostLoaded(post);
}
@Override
public void onExceptionUi(DbException exception) {
handleException(exception);
}
});
}
}

View File

@@ -0,0 +1,134 @@
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<Blog> 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());
}
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<Blog> getPersonalBlog() {
return personalBlog;
}
private void loadAllBlogPosts() {
loadList(this::loadAllBlogPosts, blogPosts::setValue);
}
@DatabaseExecutor
private ListUpdate loadAllBlogPosts(Transaction txn)
throws DbException {
long start = now();
List<BlogPostItem> 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<BlogPostItem> items = removeListItems(getBlogPostItems(),
item -> item.getGroupId().equals(g)
);
if (items != null) {
blogPosts.setValue(new LiveResult<>(new ListUpdate(null, items)));
}
}
}

View File

@@ -5,4 +5,6 @@ interface OnBlogPostClickListener {
void onBlogPostClick(BlogPostItem post); void onBlogPostClick(BlogPostItem post);
void onAuthorClick(BlogPostItem post); void onAuthorClick(BlogPostItem post);
void onLinkClick(String url);
} }

View File

@@ -11,7 +11,7 @@ import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.activity.BriarActivity; import org.briarproject.briar.android.activity.BriarActivity;
import org.briarproject.briar.android.fragment.BaseFragment.BaseFragmentListener; 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 public class ReblogActivity extends BriarActivity implements
BaseFragmentListener { BaseFragmentListener {
@@ -39,13 +39,11 @@ public class ReblogActivity extends BriarActivity implements
@Override @Override
public boolean onOptionsItemSelected(MenuItem item) { public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) { if (item.getItemId() == android.R.id.home) {
case android.R.id.home: onBackPressed();
onBackPressed(); return true;
return true;
default:
return super.onOptionsItemSelected(item);
} }
return super.onOptionsItemSelected(item);
} }
@Override @Override

View File

@@ -7,19 +7,17 @@ import android.view.ViewGroup;
import android.widget.ProgressBar; import android.widget.ProgressBar;
import android.widget.ScrollView; import android.widget.ScrollView;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault; import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.bramble.api.sync.GroupId; import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.sync.MessageId; import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.briar.R; import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent; 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.fragment.BaseFragment;
import org.briarproject.briar.android.view.TextInputView; import org.briarproject.briar.android.view.TextInputView;
import org.briarproject.briar.android.view.TextSendController; import org.briarproject.briar.android.view.TextSendController;
import org.briarproject.briar.android.view.TextSendController.SendListener; import org.briarproject.briar.android.view.TextSendController.SendListener;
import org.briarproject.briar.android.widget.LinkDialogFragment;
import org.briarproject.briar.api.attachment.AttachmentHeader; import org.briarproject.briar.api.attachment.AttachmentHeader;
import java.util.List; import java.util.List;
@@ -27,13 +25,15 @@ import java.util.List;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import javax.inject.Inject; import javax.inject.Inject;
import androidx.lifecycle.ViewModelProvider;
import static android.view.View.FOCUS_DOWN; import static android.view.View.FOCUS_DOWN;
import static android.view.View.GONE; import static android.view.View.GONE;
import static android.view.View.INVISIBLE; import static android.view.View.INVISIBLE;
import static android.view.View.VISIBLE; import static android.view.View.VISIBLE;
import static java.util.Objects.requireNonNull; import static java.util.Objects.requireNonNull;
import static org.briarproject.briar.android.activity.BriarActivity.GROUP_ID; 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; import static org.briarproject.briar.api.blog.BlogConstants.MAX_BLOG_POST_TEXT_LENGTH;
@MethodsNotNullByDefault @MethodsNotNullByDefault
@@ -42,12 +42,13 @@ public class ReblogFragment extends BaseFragment implements SendListener {
public static final String TAG = ReblogFragment.class.getName(); public static final String TAG = ReblogFragment.class.getName();
@Inject
ViewModelProvider.Factory viewModelFactory;
private BlogViewModel viewModel;
private ViewHolder ui; private ViewHolder ui;
private BlogPostItem item; private BlogPostItem item;
@Inject
FeedController feedController;
static ReblogFragment newInstance(GroupId groupId, MessageId messageId) { static ReblogFragment newInstance(GroupId groupId, MessageId messageId) {
ReblogFragment f = new ReblogFragment(); ReblogFragment f = new ReblogFragment();
@@ -67,6 +68,8 @@ public class ReblogFragment extends BaseFragment implements SendListener {
@Override @Override
public void injectFragment(ActivityComponent component) { public void injectFragment(ActivityComponent component) {
component.inject(this); component.inject(this);
viewModel = new ViewModelProvider(requireActivity(), viewModelFactory)
.get(BlogViewModel.class);
} }
@Override @Override
@@ -90,30 +93,20 @@ public class ReblogFragment extends BaseFragment implements SendListener {
ui.input.setMaxTextLength(MAX_BLOG_POST_TEXT_LENGTH); ui.input.setMaxTextLength(MAX_BLOG_POST_TEXT_LENGTH);
showProgressBar(); showProgressBar();
feedController.loadBlogPost(blogId, postId, viewModel.loadBlogPost(blogId, postId).observe(getViewLifecycleOwner(),
new UiResultExceptionHandler<BlogPostItem, DbException>( result -> result.onError(this::handleException)
this) { .onSuccess(this::bindViewHolder)
@Override );
public void onResultUi(BlogPostItem result) {
item = result;
bindViewHolder();
}
@Override
public void onExceptionUi(DbException exception) {
handleException(exception);
}
});
return v; return v;
} }
private void bindViewHolder() { private void bindViewHolder(BlogPostItem item) {
if (item == null) return; this.item = item;
hideProgressBar(); hideProgressBar();
ui.post.bindItem(item); ui.post.bindItem(this.item);
ui.post.hideReblogButton(); ui.post.hideReblogButton();
ui.input.setReady(true); ui.input.setReady(true);
@@ -124,13 +117,7 @@ public class ReblogFragment extends BaseFragment implements SendListener {
public void onSendClick(@Nullable String text, public void onSendClick(@Nullable String text,
List<AttachmentHeader> headers) { List<AttachmentHeader> headers) {
ui.input.hideSoftKeyboard(); ui.input.hideSoftKeyboard();
feedController.repeatPost(item, text, viewModel.repeatPost(item, text);
new UiExceptionHandler<DbException>(this) {
@Override
public void onExceptionUi(DbException exception) {
handleException(exception);
}
});
finish(); finish();
} }
@@ -144,7 +131,7 @@ public class ReblogFragment extends BaseFragment implements SendListener {
ui.input.setVisibility(VISIBLE); ui.input.setVisibility(VISIBLE);
} }
private class ViewHolder { private class ViewHolder implements OnBlogPostClickListener {
private final ScrollView scrollView; private final ScrollView scrollView;
private final ProgressBar progressBar; private final ProgressBar progressBar;
@@ -155,18 +142,25 @@ public class ReblogFragment extends BaseFragment implements SendListener {
scrollView = v.findViewById(R.id.scrollView); scrollView = v.findViewById(R.id.scrollView);
progressBar = v.findViewById(R.id.progressBar); progressBar = v.findViewById(R.id.progressBar);
post = new BlogPostViewHolder(v.findViewById(R.id.postLayout), post = new BlogPostViewHolder(v.findViewById(R.id.postLayout),
true, new OnBlogPostClickListener() { true, this, false);
@Override
public void onBlogPostClick(BlogPostItem post) {
// do nothing
}
@Override
public void onAuthorClick(BlogPostItem post) {
// probably don't want to allow author clicks here
}
}, getFragmentManager());
input = v.findViewById(R.id.inputText); 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());
}
} }
} }

View File

@@ -151,7 +151,7 @@ public class ContactsViewModel extends DbViewModel implements EventListener {
@UiThread @UiThread
private void updateItem(ContactId c, private void updateItem(ContactId c,
Function<ContactListItem, ContactListItem> replacer, boolean sort) { Function<ContactListItem, ContactListItem> replacer, boolean sort) {
List<ContactListItem> list = updateListItems(contactListItems, List<ContactListItem> list = updateListItems(getList(contactListItems),
itemToTest -> itemToTest.getContact().getId().equals(c), itemToTest -> itemToTest.getContact().getId().equals(c),
replacer); replacer);
if (list == null) return; if (list == null) return;
@@ -161,10 +161,8 @@ public class ContactsViewModel extends DbViewModel implements EventListener {
@UiThread @UiThread
private void removeItem(ContactId c) { private void removeItem(ContactId c) {
List<ContactListItem> list = removeListItems(contactListItems, removeAndUpdateListItems(contactListItems,
itemToTest -> itemToTest.getContact().getId().equals(c)); itemToTest -> itemToTest.getContact().getId().equals(c));
if (list == null) return;
contactListItems.setValue(new LiveResult<>(list));
} }
} }

View File

@@ -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<ContactId> 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);
}
}

View File

@@ -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<ContactId> 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<ContactId> 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();
}
}

View File

@@ -145,7 +145,7 @@ class ForumListViewModel extends DbViewModel implements EventListener {
@UiThread @UiThread
private void onForumPostReceived(GroupId g, ForumPostHeader header) { private void onForumPostReceived(GroupId g, ForumPostHeader header) {
List<ForumListItem> list = updateListItems(forumItems, List<ForumListItem> list = updateListItems(getList(forumItems),
itemToTest -> itemToTest.getForum().getId().equals(g), itemToTest -> itemToTest.getForum().getId().equals(g),
itemToUpdate -> new ForumListItem(itemToUpdate, header)); itemToUpdate -> new ForumListItem(itemToUpdate, header));
if (list == null) return; if (list == null) return;
@@ -156,11 +156,9 @@ class ForumListViewModel extends DbViewModel implements EventListener {
@UiThread @UiThread
private void onGroupRemoved(GroupId groupId) { private void onGroupRemoved(GroupId groupId) {
List<ForumListItem> list = removeListItems(forumItems, i -> removeAndUpdateListItems(forumItems, i ->
i.getForum().getId().equals(groupId) i.getForum().getId().equals(groupId)
); );
if (list == null) return;
forumItems.setValue(new LiveResult<>(list));
} }
void loadForumInvitations() { void loadForumInvitations() {

View File

@@ -2,7 +2,6 @@ package org.briarproject.briar.android.privategroup.list;
import android.app.Application; import android.app.Application;
import org.briarproject.bramble.api.contact.ContactManager;
import org.briarproject.bramble.api.db.DatabaseExecutor; import org.briarproject.bramble.api.db.DatabaseExecutor;
import org.briarproject.bramble.api.db.DbException; import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.db.Transaction; import org.briarproject.bramble.api.db.Transaction;
@@ -173,7 +172,7 @@ class GroupListViewModel extends DbViewModel implements EventListener {
@UiThread @UiThread
private void onGroupMessageAdded(GroupMessageHeader header) { private void onGroupMessageAdded(GroupMessageHeader header) {
GroupId g = header.getGroupId(); GroupId g = header.getGroupId();
List<GroupItem> list = updateListItems(groupItems, List<GroupItem> list = updateListItems(getList(groupItems),
itemToTest -> itemToTest.getId().equals(g), itemToTest -> itemToTest.getId().equals(g),
itemToUpdate -> new GroupItem(itemToUpdate, header)); itemToUpdate -> new GroupItem(itemToUpdate, header));
if (list == null) return; if (list == null) return;
@@ -184,7 +183,7 @@ class GroupListViewModel extends DbViewModel implements EventListener {
@UiThread @UiThread
private void onGroupDissolved(GroupId groupId) { private void onGroupDissolved(GroupId groupId) {
List<GroupItem> list = updateListItems(groupItems, List<GroupItem> list = updateListItems(getList(groupItems),
itemToTest -> itemToTest.getId().equals(groupId), itemToTest -> itemToTest.getId().equals(groupId),
itemToUpdate -> new GroupItem(itemToUpdate, true)); itemToUpdate -> new GroupItem(itemToUpdate, true));
if (list == null) return; if (list == null) return;
@@ -193,10 +192,7 @@ class GroupListViewModel extends DbViewModel implements EventListener {
@UiThread @UiThread
private void onGroupRemoved(GroupId groupId) { private void onGroupRemoved(GroupId groupId) {
List<GroupItem> list = removeAndUpdateListItems(groupItems, i -> i.getId().equals(groupId));
removeListItems(groupItems, i -> i.getId().equals(groupId));
if (list == null) return;
groupItems.setValue(new LiveResult<>(list));
} }
void removeGroup(GroupId g) { void removeGroup(GroupId g) {

View File

@@ -34,7 +34,6 @@ import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.briar.R; import org.briarproject.briar.R;
import org.briarproject.briar.android.reporting.FeedbackActivity; import org.briarproject.briar.android.reporting.FeedbackActivity;
import org.briarproject.briar.android.view.ArticleMovementMethod; import org.briarproject.briar.android.view.ArticleMovementMethod;
import org.briarproject.briar.android.widget.LinkDialogFragment;
import java.util.Locale; import java.util.Locale;
@@ -49,8 +48,8 @@ import androidx.appcompat.app.AlertDialog;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
import androidx.core.hardware.fingerprint.FingerprintManagerCompat; import androidx.core.hardware.fingerprint.FingerprintManagerCompat;
import androidx.core.text.HtmlCompat; import androidx.core.text.HtmlCompat;
import androidx.core.util.Consumer;
import androidx.fragment.app.FragmentActivity; import androidx.fragment.app.FragmentActivity;
import androidx.fragment.app.FragmentManager;
import androidx.lifecycle.LifecycleOwner; import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.LiveData; import androidx.lifecycle.LiveData;
import androidx.lifecycle.Observer; import androidx.lifecycle.Observer;
@@ -198,8 +197,7 @@ public class UiUtils {
} }
public static void makeLinksClickable(TextView v, public static void makeLinksClickable(TextView v,
@Nullable FragmentManager fm) { Consumer<String> onLinkClicked) {
if (fm == null) return;
SpannableStringBuilder ssb = new SpannableStringBuilder(v.getText()); SpannableStringBuilder ssb = new SpannableStringBuilder(v.getText());
URLSpan[] spans = ssb.getSpans(0, ssb.length(), URLSpan.class); URLSpan[] spans = ssb.getSpans(0, ssb.length(), URLSpan.class);
for (URLSpan span : spans) { for (URLSpan span : spans) {
@@ -210,8 +208,7 @@ public class UiUtils {
ClickableSpan cSpan = new ClickableSpan() { ClickableSpan cSpan = new ClickableSpan() {
@Override @Override
public void onClick(View v2) { public void onClick(View v2) {
LinkDialogFragment f = LinkDialogFragment.newInstance(url); onLinkClicked.accept(url);
f.show(fm, f.getUniqueTag());
} }
}; };
ssb.setSpan(cSpan, start, end, 0); ssb.setSpan(cSpan, start, end, 0);

View File

@@ -1,6 +1,7 @@
package org.briarproject.briar.android.viewmodel; package org.briarproject.briar.android.viewmodel;
import android.app.Application; import android.app.Application;
import android.widget.Toast;
import org.briarproject.bramble.api.db.DatabaseExecutor; import org.briarproject.bramble.api.db.DatabaseExecutor;
import org.briarproject.bramble.api.db.DbCallable; 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.lifecycle.LifecycleManager;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.system.AndroidExecutor; import org.briarproject.bramble.api.system.AndroidExecutor;
import org.briarproject.bramble.util.StringUtils;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.ListIterator; import java.util.ListIterator;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
@@ -20,6 +23,7 @@ import java.util.logging.Logger;
import javax.annotation.concurrent.Immutable; import javax.annotation.concurrent.Immutable;
import androidx.annotation.AnyThread;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.UiThread; import androidx.annotation.UiThread;
@@ -27,8 +31,10 @@ import androidx.arch.core.util.Function;
import androidx.core.util.Consumer; import androidx.core.util.Consumer;
import androidx.lifecycle.AndroidViewModel; import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData; import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import static android.widget.Toast.LENGTH_LONG;
import static java.util.logging.Level.WARNING; import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger; import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.LogUtils.logException; import static org.briarproject.bramble.util.LogUtils.logException;
@@ -114,9 +120,11 @@ public abstract class DbViewModel extends AndroidViewModel {
* This method ensures that those operations can be processed on the * This method ensures that those operations can be processed on the
* UiThread in the correct order so that the removed message will not be * UiThread in the correct order so that the removed message will not be
* re-added when the re-load completes. * re-added when the re-load completes.
*
* TODO: Rename this method and update javadoc, as it's not restricted to
* lists
*/ */
protected <T extends List<?>> void loadList( protected <T> void loadList(DbCallable<T, DbException> task,
DbCallable<T, DbException> task,
UiConsumer<LiveResult<T>> uiConsumer) { UiConsumer<LiveResult<T>> uiConsumer) {
dbExecutor.execute(() -> { dbExecutor.execute(() -> {
try { try {
@@ -143,25 +151,46 @@ public abstract class DbViewModel extends AndroidViewModel {
} }
/** /**
* Creates a copy of the list available in the given LiveData * Creates a copy of the given list and adds the given item to the copy.
* and replaces items where the given test function returns true.
* *
* @return a copy of the list in the LiveData with item(s) replaced * @return an updated copy of the list, or null if the list is null
* or null when the
* <ul>
* <li> LiveData does not have a value
* <li> LiveResult in the LiveData has an error
* <li> test function did return false for all items in the list
* </ul>
*/ */
@Nullable @Nullable
protected <T> List<T> updateListItems( protected <T> List<T> addListItem(@Nullable List<T> list, T item) {
LiveData<LiveResult<List<T>>> liveData, Function<T, Boolean> test, if (list == null) return null;
Function<T, T> replacer) { List<T> copy = new ArrayList<>(list);
List<T> items = getListCopy(liveData); copy.add(item);
if (items == null) return null; return copy;
}
ListIterator<T> 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 <T> List<T> addListItems(@Nullable List<T> list,
Collection<T> items) {
if (list == null) return null;
List<T> 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 <T> List<T> updateListItems(@Nullable List<T> list,
Function<T, Boolean> test, Function<T, T> replacer) {
if (list == null) return null;
List<T> copy = new ArrayList<>(list);
ListIterator<T> iterator = copy.listIterator();
boolean changed = false; boolean changed = false;
while (iterator.hasNext()) { while (iterator.hasNext()) {
T item = iterator.next(); T item = iterator.next();
@@ -170,28 +199,23 @@ public abstract class DbViewModel extends AndroidViewModel {
iterator.set(replacer.apply(item)); iterator.set(replacer.apply(item));
} }
} }
return changed ? items : null; return changed ? copy : null;
} }
/** /**
* Creates a copy of the list available in the given LiveData * Creates a copy of the given list, removing items from it where the given
* and removes the items from it where the given test function returns true. * test function returns true.
* *
* @return a copy of the list in the LiveData with item(s) removed * @return an updated copy of the list, or null if either the list is null
* or null when the * or the test function returns false for all items
* <ul>
* <li> LiveData does not have a value
* <li> LiveResult in the LiveData has an error
* <li> test function did return false for all items in the list
* </ul>
*/ */
@Nullable @Nullable
protected <T> List<T> removeListItems( protected <T> List<T> removeListItems(@Nullable List<T> list,
LiveData<LiveResult<List<T>>> liveData, Function<T, Boolean> test) { Function<T, Boolean> test) {
List<T> items = getListCopy(liveData); if (list == null) return null;
if (items == null) return null; List<T> copy = new ArrayList<>(list);
ListIterator<T> iterator = items.listIterator(); ListIterator<T> iterator = copy.listIterator();
boolean changed = false; boolean changed = false;
while (iterator.hasNext()) { while (iterator.hasNext()) {
T item = iterator.next(); T item = iterator.next();
@@ -200,21 +224,58 @@ public abstract class DbViewModel extends AndroidViewModel {
iterator.remove(); iterator.remove();
} }
} }
return changed ? items : null; return changed ? copy : null;
} }
/** /**
* Retrieves a copy of the list of items from the given LiveData * Updates the given LiveData with a copy of its list
* or null if it is not available. * with the items removed where the given test function returns true.
* The list copy can be safely mutated. * <p>
* Nothing is updated, if the
* <ul>
* <li> LiveData does not have a value
* <li> LiveResult in the LiveData has an error
* <li> test function returned false for all items in the list
* </ul>
*/
@UiThread
protected <T> void removeAndUpdateListItems(
MutableLiveData<LiveResult<List<T>>> liveData,
Function<T, Boolean> test) {
List<T> 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 @Nullable
private <T> List<T> getListCopy(LiveData<LiveResult<List<T>>> liveData) { protected <T> List<T> getList(LiveData<LiveResult<List<T>>> liveData) {
LiveResult<List<T>> value = liveData.getValue(); LiveResult<List<T>> value = liveData.getValue();
if (value == null) return null; if (value == null) return null;
List<T> list = value.getResultOrNull(); return value.getResultOrNull();
if (list == null) return null; }
return new ArrayList<>(list);
/**
* Logs the exception and shows a Toast to the user.
* <p>
* 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();
});
} }
} }

View File

@@ -10,6 +10,7 @@ import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.sync.MessageId; import org.briarproject.bramble.api.sync.MessageId;
import java.util.Collection; import java.util.Collection;
import java.util.List;
import javax.annotation.Nullable; import javax.annotation.Nullable;
@@ -98,21 +99,38 @@ public interface BlogManager {
*/ */
Collection<Blog> getBlogs() throws DbException; Collection<Blog> getBlogs() throws DbException;
/**
* Returns the group IDs of all blogs to which the user subscribes.
*/
Collection<GroupId> getBlogIds(Transaction txn) throws DbException;
/** /**
* Returns the header of the blog post with the given ID. * 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. * Returns the text of the blog post with the given ID.
*/ */
String getPostText(MessageId m) throws DbException; 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. * Returns the headers of all posts in the given blog.
*/ */
Collection<BlogPostHeader> getPostHeaders(GroupId g) throws DbException; Collection<BlogPostHeader> getPostHeaders(GroupId g) throws DbException;
/**
* Returns the headers of all posts in the given blog.
*/
List<BlogPostHeader> getPostHeaders(Transaction txn, GroupId g)
throws DbException;
/** /**
* Marks a blog post as read or unread. * Marks a blog post as read or unread.
*/ */

View File

@@ -446,19 +446,22 @@ class BlogManagerImpl extends BdfIncomingMessageHook implements BlogManager,
} }
@Override @Override
public BlogPostHeader getPostHeader(GroupId g, MessageId m) public Collection<GroupId> getBlogIds(Transaction txn) throws DbException {
List<GroupId> groupIds = new ArrayList<>();
Collection<Group> 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 { throws DbException {
Transaction txn = db.startTransaction(true);
try { try {
BdfDictionary meta = BdfDictionary meta =
clientHelper.getMessageMetadataAsDictionary(txn, m); clientHelper.getMessageMetadataAsDictionary(txn, m);
BlogPostHeader h = getPostHeaderFromMetadata(txn, g, m, meta); return getPostHeaderFromMetadata(txn, g, m, meta);
db.commitTransaction(txn);
return h;
} catch (FormatException e) { } catch (FormatException e) {
throw new DbException(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 { private String getPostText(BdfList message) throws FormatException {
MessageType type = MessageType.valueOf(message.getLong(0).intValue()); MessageType type = MessageType.valueOf(message.getLong(0).intValue());
if (type == POST) { if (type == POST) {
@@ -488,7 +500,12 @@ class BlogManagerImpl extends BdfIncomingMessageHook implements BlogManager,
@Override @Override
public Collection<BlogPostHeader> getPostHeaders(GroupId g) public Collection<BlogPostHeader> getPostHeaders(GroupId g)
throws DbException { throws DbException {
return db.transactionWithResult(true, txn -> getPostHeaders(txn, g));
}
@Override
public List<BlogPostHeader> getPostHeaders(Transaction txn, GroupId g)
throws DbException {
// Query for posts and comments only // Query for posts and comments only
BdfDictionary query1 = BdfDictionary.of( BdfDictionary query1 = BdfDictionary.of(
new BdfEntry(KEY_TYPE, POST.getInt()) new BdfEntry(KEY_TYPE, POST.getInt())
@@ -497,8 +514,7 @@ class BlogManagerImpl extends BdfIncomingMessageHook implements BlogManager,
new BdfEntry(KEY_TYPE, COMMENT.getInt()) new BdfEntry(KEY_TYPE, COMMENT.getInt())
); );
Collection<BlogPostHeader> headers = new ArrayList<>(); List<BlogPostHeader> headers = new ArrayList<>();
Transaction txn = db.startTransaction(true);
try { try {
Map<MessageId, BdfDictionary> metadata1 = Map<MessageId, BdfDictionary> metadata1 =
clientHelper.getMessageMetadataAsDictionary(txn, g, query1); clientHelper.getMessageMetadataAsDictionary(txn, g, query1);
@@ -528,13 +544,10 @@ class BlogManagerImpl extends BdfIncomingMessageHook implements BlogManager,
entry.getKey(), meta, authorInfos); entry.getKey(), meta, authorInfos);
headers.add(h); headers.add(h);
} }
db.commitTransaction(txn);
return headers;
} catch (FormatException e) { } catch (FormatException e) {
throw new DbException(e); throw new DbException(e);
} finally {
db.endTransaction(txn);
} }
return headers;
} }
@Override @Override

View File

@@ -3,12 +3,15 @@ package org.briarproject.briar.headless.blogs
import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.ObjectMapper
import io.javalin.http.BadRequestResponse import io.javalin.http.BadRequestResponse
import io.javalin.http.Context 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.identity.IdentityManager
import org.briarproject.bramble.api.system.Clock import org.briarproject.bramble.api.system.Clock
import org.briarproject.bramble.util.StringUtils.utf8IsTooLong 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.BlogConstants.MAX_BLOG_POST_TEXT_LENGTH
import org.briarproject.briar.api.blog.BlogManager import org.briarproject.briar.api.blog.BlogManager
import org.briarproject.briar.api.blog.BlogPostFactory import org.briarproject.briar.api.blog.BlogPostFactory
import org.briarproject.briar.api.blog.BlogPostHeader
import org.briarproject.briar.headless.getFromJson import org.briarproject.briar.headless.getFromJson
import javax.annotation.concurrent.Immutable import javax.annotation.concurrent.Immutable
import javax.inject.Inject import javax.inject.Inject
@@ -21,6 +24,7 @@ internal class BlogControllerImpl
constructor( constructor(
private val blogManager: BlogManager, private val blogManager: BlogManager,
private val blogPostFactory: BlogPostFactory, private val blogPostFactory: BlogPostFactory,
private val db: TransactionManager,
private val identityManager: IdentityManager, private val identityManager: IdentityManager,
private val objectMapper: ObjectMapper, private val objectMapper: ObjectMapper,
private val clock: Clock private val clock: Clock
@@ -45,8 +49,10 @@ constructor(
val blog = blogManager.getPersonalBlog(author) val blog = blogManager.getPersonalBlog(author)
val now = clock.currentTimeMillis() val now = clock.currentTimeMillis()
val post = blogPostFactory.createBlogPost(blog.id, now, null, author, text) val post = blogPostFactory.createBlogPost(blog.id, now, null, author, text)
blogManager.addLocalPost(post) val header = db.transactionWithResult<BlogPostHeader, DbException>(true) { txn ->
val header = blogManager.getPostHeader(blog.id, post.message.id) blogManager.addLocalPost(txn, post)
return@transactionWithResult blogManager.getPostHeader(txn, blog.id, post.message.id)
}
return ctx.json(header.output(text)) return ctx.json(header.output(text))
} }

View File

@@ -7,6 +7,7 @@ import io.mockk.mockk
import org.briarproject.bramble.api.connection.ConnectionRegistry import org.briarproject.bramble.api.connection.ConnectionRegistry
import org.briarproject.bramble.api.contact.Contact import org.briarproject.bramble.api.contact.Contact
import org.briarproject.bramble.api.contact.ContactManager 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.Author
import org.briarproject.bramble.api.identity.IdentityManager import org.briarproject.bramble.api.identity.IdentityManager
import org.briarproject.bramble.api.identity.LocalAuthor import org.briarproject.bramble.api.identity.LocalAuthor
@@ -24,6 +25,7 @@ import javax.servlet.http.HttpServletResponse
abstract class ControllerTest { abstract class ControllerTest {
protected val db = mockk<TransactionManager>()
protected val contactManager = mockk<ContactManager>() protected val contactManager = mockk<ContactManager>()
protected val conversationManager = mockk<ConversationManager>() protected val conversationManager = mockk<ConversationManager>()
protected val identityManager = mockk<IdentityManager>() protected val identityManager = mockk<IdentityManager>()

View File

@@ -6,14 +6,22 @@ import io.mockk.Runs
import io.mockk.every import io.mockk.every
import io.mockk.just import io.mockk.just
import io.mockk.mockk import io.mockk.mockk
import org.briarproject.briar.api.identity.AuthorInfo import io.mockk.slot
import org.briarproject.briar.api.identity.AuthorInfo.Status.OURSELVES 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.api.sync.MessageId
import org.briarproject.bramble.identity.output import org.briarproject.bramble.identity.output
import org.briarproject.bramble.util.StringUtils.getRandomString 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.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.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.briarproject.briar.headless.ControllerTest
import org.junit.jupiter.api.Assertions.assertThrows import org.junit.jupiter.api.Assertions.assertThrows
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
@@ -24,7 +32,7 @@ internal class BlogControllerTest : ControllerTest() {
private val blogPostFactory = mockk<BlogPostFactory>() private val blogPostFactory = mockk<BlogPostFactory>()
private val controller = 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 blog = Blog(group, author, false)
private val parentId: MessageId? = null private val parentId: MessageId? = null
@@ -46,6 +54,8 @@ internal class BlogControllerTest : ControllerTest() {
@Test @Test
fun testCreate() { fun testCreate() {
val post = BlogPost(message, null, localAuthor) val post = BlogPost(message, null, localAuthor)
val dbSlot = slot<DbCallable<BlogPostHeader, DbException>>()
val txn = Transaction(Object(), true)
every { ctx.body() } returns """{"text": "$text"}""" every { ctx.body() } returns """{"text": "$text"}"""
every { identityManager.localAuthor } returns localAuthor every { identityManager.localAuthor } returns localAuthor
@@ -60,8 +70,13 @@ internal class BlogControllerTest : ControllerTest() {
text text
) )
} returns post } returns post
every { blogManager.addLocalPost(post) } just Runs every { db.transactionWithResult(true, capture(dbSlot)) } answers {
every { blogManager.getPostHeader(post.message.groupId, post.message.id) } returns header 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 every { ctx.json(header.output(text)) } returns ctx
controller.createPost(ctx) controller.createPost(ctx)