diff --git a/briar-android/src/org/briarproject/android/blogs/BlogActivity.java b/briar-android/src/org/briarproject/android/blogs/BlogActivity.java index 866b191d96d38d6359aca975ae42196218f097df..7c3309010e1c85ccfabdcedab718e80637d61816 100644 --- a/briar-android/src/org/briarproject/android/blogs/BlogActivity.java +++ b/briar-android/src/org/briarproject/android/blogs/BlogActivity.java @@ -2,31 +2,36 @@ package org.briarproject.android.blogs; import android.content.Intent; import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.annotation.UiThread; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentStatePagerAdapter; import android.support.v4.view.ViewPager; import android.view.ViewGroup; import android.widget.ProgressBar; -import android.widget.Toast; import org.briarproject.R; import org.briarproject.android.ActivityComponent; import org.briarproject.android.BriarActivity; import org.briarproject.android.blogs.BlogController.BlogPostListener; import org.briarproject.android.blogs.BlogPostAdapter.OnBlogPostClickListener; -import org.briarproject.android.controller.handler.UiResultHandler; +import org.briarproject.android.controller.handler.UiResultExceptionHandler; import org.briarproject.android.fragment.BaseFragment.BaseFragmentListener; +import org.briarproject.api.blogs.BlogPostHeader; +import org.briarproject.api.db.DbException; import org.briarproject.api.sync.GroupId; import org.briarproject.api.sync.MessageId; +import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; +import java.util.List; import javax.inject.Inject; import static android.view.View.GONE; import static android.view.View.VISIBLE; -import static android.widget.Toast.LENGTH_SHORT; public class BlogActivity extends BriarActivity implements BlogPostListener, OnBlogPostClickListener, BaseFragmentListener { @@ -37,17 +42,17 @@ public class BlogActivity extends BriarActivity implements BlogPostListener, static final String IS_MY_BLOG = "briar.IS_MY_BLOG"; static final String IS_NEW_BLOG = "briar.IS_NEW_BLOG"; - private static final String BLOG_PAGER_ADAPTER = "briar.BLOG_PAGER_ADAPTER"; + private static final String POST_ID = "briar.POST_ID"; + private GroupId groupId; private ProgressBar progressBar; private ViewPager pager; private BlogPagerAdapter blogPagerAdapter; private BlogPostPagerAdapter postPagerAdapter; private String blogName; private boolean myBlog, isNew; + private MessageId savedPostId; - // Fields that are accessed from background threads must be volatile - private volatile GroupId groupId = null; @Inject BlogController blogController; @@ -58,8 +63,9 @@ public class BlogActivity extends BriarActivity implements BlogPostListener, // GroupId from Intent Intent i = getIntent(); byte[] b = i.getByteArrayExtra(GROUP_ID); - if (b == null) throw new IllegalStateException("No Group in intent."); + if (b == null) throw new IllegalStateException("No group ID in intent"); groupId = new GroupId(b); + blogController.setGroupId(groupId); // Name of the Blog from Intent blogName = i.getStringExtra(BLOG_NAME); @@ -73,30 +79,46 @@ public class BlogActivity extends BriarActivity implements BlogPostListener, pager = (ViewPager) findViewById(R.id.pager); progressBar = (ProgressBar) findViewById(R.id.progressBar); - hideLoadingScreen(); blogPagerAdapter = new BlogPagerAdapter(getSupportFragmentManager()); - if (state == null || state.getBoolean(BLOG_PAGER_ADAPTER, true)) { + postPagerAdapter = new BlogPostPagerAdapter( + getSupportFragmentManager()); + + if (state == null || state.getByteArray(POST_ID) == null) { + // The blog fragment has its own progress bar + hideLoadingScreen(); pager.setAdapter(blogPagerAdapter); + savedPostId = null; } else { - // this initializes and restores the postPagerAdapter - loadBlogPosts(); + // Adapter will be set in selectPostInPostPager() + savedPostId = new MessageId(state.getByteArray(POST_ID)); + } + } + + @Override + public void onResume() { + super.onResume(); + if (savedPostId == null) { + MessageId selected = getSelectedPostInPostPager(); + if (selected != null) loadBlogPosts(selected); + } else { + loadBlogPosts(savedPostId); } } @Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); - - // remember which adapter we had active - outState.putBoolean(BLOG_PAGER_ADAPTER, - pager.getAdapter() == blogPagerAdapter); + MessageId selected = getSelectedPostInPostPager(); + if (selected != null) + outState.putByteArray(POST_ID, selected.getBytes()); } @Override public void onBackPressed() { if (pager.getAdapter() == postPagerAdapter) { pager.setAdapter(blogPagerAdapter); + savedPostId = null; } else { super.onBackPressed(); } @@ -112,10 +134,6 @@ public class BlogActivity extends BriarActivity implements BlogPostListener, progressBar.setVisibility(VISIBLE); } - private void showLoadingScreen() { - showLoadingScreen(false, 0); - } - @Override public void hideLoadingScreen() { progressBar.setVisibility(GONE); @@ -127,60 +145,79 @@ public class BlogActivity extends BriarActivity implements BlogPostListener, } @Override - public void onBlogPostClick(int position, BlogPostItem post) { - loadBlogPosts(position, true); + public void onBlogPostClick(BlogPostItem post) { + loadBlogPosts(post.getId()); } - private void loadBlogPosts() { - loadBlogPosts(0, false); - } + private void loadBlogPosts(final MessageId select) { + blogController.loadBlogPosts( + new UiResultExceptionHandler<Collection<BlogPostItem>, DbException>( + this) { + @Override + public void onResultUi(Collection<BlogPostItem> posts) { + hideLoadingScreen(); + savedPostId = null; + postPagerAdapter.setPosts(posts); + selectPostInPostPager(select); + } - private void loadBlogPosts(final int position, final boolean setItem) { - showLoadingScreen(); - blogController.loadBlog(groupId, false, - new UiResultHandler<Boolean>(this) { @Override - public void onResultUi(Boolean result) { - if (result) { - Collection<BlogPostItem> posts = - blogController.getBlogPosts(); - - if (postPagerAdapter == null) { - postPagerAdapter = new BlogPostPagerAdapter( - getSupportFragmentManager(), - posts.size()); - } else { - postPagerAdapter.setSize(posts.size()); - } - pager.setAdapter(postPagerAdapter); - if (setItem) pager.setCurrentItem(position); - } else { - Toast.makeText(BlogActivity.this, - R.string.blogs_blog_post_failed_to_load, - LENGTH_SHORT).show(); - } + public void onExceptionUi(DbException exception) { + // TODO: Decide how to handle errors in the UI + finish(); } }); } @Override - public void onBlogPostAdded(final BlogPostItem post, final boolean local) { - runOnUiThread(new Runnable() { - @Override - public void run() { - if (blogPagerAdapter != null) { - BlogFragment f = blogPagerAdapter.getFragment(); - if (f != null && f.isVisible()) { - f.onBlogPostAdded(post, local); + public void onBlogPostAdded(BlogPostHeader header, boolean local) { + if (pager.getAdapter() == postPagerAdapter) { + loadBlogPost(header); + } else { + BlogFragment f = blogPagerAdapter.getFragment(); + if (f != null && f.isVisible()) f.onBlogPostAdded(header, local); + } + } + + private void loadBlogPost(BlogPostHeader header) { + blogController.loadBlogPost(header, + new UiResultExceptionHandler<BlogPostItem, DbException>(this) { + @Override + public void onResultUi(BlogPostItem post) { + addPostToPostPager(post); } - } - if (postPagerAdapter != null) { - postPagerAdapter.onBlogPostAdded(); - postPagerAdapter.notifyDataSetChanged(); - } + @Override + public void onExceptionUi(DbException exception) { + // TODO: Decide how to handle errors in the UI + finish(); + } + }); + } + + @Nullable + private MessageId getSelectedPostInPostPager() { + if (pager.getAdapter() != postPagerAdapter) return null; + if (postPagerAdapter.getCount() == 0) return null; + int position = pager.getCurrentItem(); + return postPagerAdapter.getPost(position).getId(); + } + + private void selectPostInPostPager(MessageId m) { + int count = postPagerAdapter.getCount(); + for (int i = 0; i < count; i++) { + if (postPagerAdapter.getPost(i).getId().equals(m)) { + pager.setAdapter(postPagerAdapter); + pager.setCurrentItem(i); + return; } - }); + } + } + + private void addPostToPostPager(BlogPostItem post) { + MessageId selected = getSelectedPostInPostPager(); + postPagerAdapter.addPost(post); + if (selected != null) selectPostInPostPager(selected); } @Override @@ -191,18 +228,22 @@ public class BlogActivity extends BriarActivity implements BlogPostListener, // The BlogPostAddedEvent arrives when the controller is not listening, // so we need to manually reload the blog posts :( if (requestCode == REQUEST_WRITE_POST && resultCode == RESULT_OK) { - BlogFragment f = blogPagerAdapter.getFragment(); - if (f != null && f.isVisible()) { - f.reload(); + if (pager.getAdapter() == postPagerAdapter) { + MessageId selected = getSelectedPostInPostPager(); + if (selected != null) loadBlogPosts(selected); + } else { + BlogFragment f = blogPagerAdapter.getFragment(); + if (f != null && f.isVisible()) f.loadBlogPosts(true); } } } - + @UiThread private class BlogPagerAdapter extends FragmentStatePagerAdapter { + private BlogFragment fragment = null; - BlogPagerAdapter(FragmentManager fm) { + private BlogPagerAdapter(FragmentManager fm) { super(fm); } @@ -224,36 +265,46 @@ public class BlogActivity extends BriarActivity implements BlogPostListener, return fragment; } - BlogFragment getFragment() { + private BlogFragment getFragment() { return fragment; } } - private class BlogPostPagerAdapter extends FragmentStatePagerAdapter { - private int size; + @UiThread + private static class BlogPostPagerAdapter + extends FragmentStatePagerAdapter { + + private final List<BlogPostItem> posts = new ArrayList<>(); - BlogPostPagerAdapter(FragmentManager fm, int size) { + private BlogPostPagerAdapter(FragmentManager fm) { super(fm); - this.size = size; } @Override public int getCount() { - return size; + return posts.size(); } @Override public Fragment getItem(int position) { - MessageId postIdOfPos = blogController.getBlogPostId(position); - return BlogPostFragment.newInstance(groupId, postIdOfPos); + return BlogPostFragment.newInstance(posts.get(position).getId()); + } + + private BlogPostItem getPost(int position) { + return posts.get(position); } - void onBlogPostAdded() { - size++; + private void setPosts(Collection<BlogPostItem> posts) { + this.posts.clear(); + this.posts.addAll(posts); + Collections.sort(this.posts); + notifyDataSetChanged(); } - void setSize(int size) { - this.size = size; + private void addPost(BlogPostItem post) { + posts.add(post); + Collections.sort(posts); + notifyDataSetChanged(); } } diff --git a/briar-android/src/org/briarproject/android/blogs/BlogController.java b/briar-android/src/org/briarproject/android/blogs/BlogController.java index 8436813de64e2505bd8fa27eea954199282ba867..90fbb61cf1ae4ab2428674fadca842608a92da97 100644 --- a/briar-android/src/org/briarproject/android/blogs/BlogController.java +++ b/briar-android/src/org/briarproject/android/blogs/BlogController.java @@ -1,36 +1,36 @@ package org.briarproject.android.blogs; -import android.support.annotation.Nullable; +import android.support.annotation.UiThread; import org.briarproject.android.controller.ActivityLifecycleController; import org.briarproject.android.controller.handler.ResultExceptionHandler; -import org.briarproject.android.controller.handler.ResultHandler; +import org.briarproject.api.blogs.BlogPostHeader; import org.briarproject.api.db.DbException; import org.briarproject.api.sync.GroupId; import org.briarproject.api.sync.MessageId; -import java.util.SortedSet; +import java.util.Collection; public interface BlogController extends ActivityLifecycleController { - void loadBlog(GroupId groupId, boolean reload, - ResultHandler<Boolean> resultHandler); + void setGroupId(GroupId g); - SortedSet<BlogPostItem> getBlogPosts(); + void loadBlogPosts( + ResultExceptionHandler<Collection<BlogPostItem>, DbException> handler); - @Nullable - BlogPostItem getBlogPost(MessageId postId); + void loadBlogPost(BlogPostHeader header, + ResultExceptionHandler<BlogPostItem, DbException> handler); - @Nullable - MessageId getBlogPostId(int position); + void loadBlogPost(MessageId m, + ResultExceptionHandler<BlogPostItem, DbException> handler); - void canDeleteBlog(GroupId groupId, - ResultExceptionHandler<Boolean, DbException> resultHandler); + void canDeleteBlog(ResultExceptionHandler<Boolean, DbException> handler); - void deleteBlog(ResultHandler<Boolean> resultHandler); + void deleteBlog(ResultExceptionHandler<Void, DbException> handler); interface BlogPostListener { - void onBlogPostAdded(BlogPostItem post, boolean local); + @UiThread + void onBlogPostAdded(BlogPostHeader header, boolean local); } } diff --git a/briar-android/src/org/briarproject/android/blogs/BlogControllerImpl.java b/briar-android/src/org/briarproject/android/blogs/BlogControllerImpl.java index 0095b4beeca507d5e02e055a2cff57603e921f89..fbed89b0c9943656fa845cb1856c410dde1d35c9 100644 --- a/briar-android/src/org/briarproject/android/blogs/BlogControllerImpl.java +++ b/briar-android/src/org/briarproject/android/blogs/BlogControllerImpl.java @@ -1,11 +1,9 @@ package org.briarproject.android.blogs; import android.app.Activity; -import android.support.annotation.Nullable; import org.briarproject.android.controller.DbControllerImpl; import org.briarproject.android.controller.handler.ResultExceptionHandler; -import org.briarproject.android.controller.handler.ResultHandler; import org.briarproject.api.blogs.Blog; import org.briarproject.api.blogs.BlogManager; import org.briarproject.api.blogs.BlogPostHeader; @@ -20,8 +18,9 @@ import org.briarproject.api.sync.MessageId; import java.util.ArrayList; import java.util.Collection; -import java.util.SortedSet; -import java.util.TreeSet; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import java.util.logging.Logger; import javax.inject.Inject; @@ -38,19 +37,27 @@ public class BlogControllerImpl extends DbControllerImpl @Inject protected Activity activity; @Inject - protected volatile BlogManager blogManager; + protected EventBus eventBus; + @Inject - protected volatile EventBus eventBus; + protected volatile BlogManager blogManager; + + private final Map<MessageId, byte[]> bodyCache = new ConcurrentHashMap<>(); + private final Map<MessageId, BlogPostHeader> headerCache = + new ConcurrentHashMap<>(); private volatile BlogPostListener listener; private volatile GroupId groupId = null; - // FIXME: This collection isn't thread-safe, isn't updated atomically - private volatile TreeSet<BlogPostItem> posts = null; @Inject BlogControllerImpl() { } + @Override + public void setGroupId(GroupId g) { + groupId = g; + } + @Override public void onActivityCreate() { if (activity instanceof BlogPostListener) { @@ -78,26 +85,17 @@ public class BlogControllerImpl extends DbControllerImpl @Override public void eventOccurred(Event e) { + if (groupId == null) throw new IllegalStateException(); if (e instanceof BlogPostAddedEvent) { - BlogPostAddedEvent m = (BlogPostAddedEvent) e; + final BlogPostAddedEvent m = (BlogPostAddedEvent) e; if (m.getGroupId().equals(groupId)) { LOG.info("New blog post added"); - if (posts == null) { - LOG.info("Posts have not loaded, yet"); - // FIXME: Race condition, new post may not get loaded - return; - } - final BlogPostHeader header = m.getHeader(); - // FIXME: Don't make blocking calls in event handlers - try { - byte[] body = blogManager.getPostBody(header.getId()); - BlogPostItem post = new BlogPostItem(groupId, header, body); - posts.add(post); - listener.onBlogPostAdded(post, m.isLocal()); - } catch (DbException ex) { - if (LOG.isLoggable(WARNING)) - LOG.log(WARNING, ex.toString(), ex); - } + activity.runOnUiThread(new Runnable() { + @Override + public void run() { + listener.onBlogPostAdded(m.getHeader(), m.isLocal()); + } + }); } } else if (e instanceof GroupRemovedEvent) { GroupRemovedEvent s = (GroupRemovedEvent) e; @@ -106,6 +104,7 @@ public class BlogControllerImpl extends DbControllerImpl activity.runOnUiThread(new Runnable() { @Override public void run() { + // TODO: Not the controller's job, add a listener method activity.finish(); } }); @@ -114,106 +113,149 @@ public class BlogControllerImpl extends DbControllerImpl } @Override - public void loadBlog(final GroupId g, final boolean reload, - final ResultHandler<Boolean> resultHandler) { - + public void loadBlogPosts( + final ResultExceptionHandler<Collection<BlogPostItem>, DbException> handler) { + if (groupId == null) throw new IllegalStateException(); runOnDbThread(new Runnable() { @Override public void run() { try { - if (reload || posts == null) { - groupId = g; - posts = new TreeSet<>(); - // load blog posts - long now = System.currentTimeMillis(); - Collection<BlogPostItem> newPosts = new ArrayList<>(); - Collection<BlogPostHeader> header = - blogManager.getPostHeaders(g); - for (BlogPostHeader h : header) { - byte[] body = blogManager.getPostBody(h.getId()); - newPosts.add(new BlogPostItem(g, h, body)); - } - posts.addAll(newPosts); - long duration = System.currentTimeMillis() - now; - if (LOG.isLoggable(INFO)) - LOG.info("Loading blog took " + duration + " ms"); + long now = System.currentTimeMillis(); + Collection<BlogPostHeader> headers = + blogManager.getPostHeaders(groupId); + long duration = System.currentTimeMillis() - now; + if (LOG.isLoggable(INFO)) + LOG.info("Loading headers took " + duration + " ms"); + List<BlogPostItem> items = new ArrayList<>(headers.size()); + now = System.currentTimeMillis(); + for (BlogPostHeader h : headers) { + headerCache.put(h.getId(), h); + byte[] body = getPostBody(h.getId()); + items.add(new BlogPostItem(groupId, h, body)); } - resultHandler.onResult(true); + duration = System.currentTimeMillis() - now; + if (LOG.isLoggable(INFO)) + LOG.info("Loading bodies took " + duration + " ms"); + handler.onResult(items); } catch (DbException e) { if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); - resultHandler.onResult(false); + handler.onException(e); } } }); } @Override - @Nullable - public SortedSet<BlogPostItem> getBlogPosts() { - return posts; + public void loadBlogPost(final BlogPostHeader header, + final ResultExceptionHandler<BlogPostItem, DbException> handler) { + if (groupId == null) throw new IllegalStateException(); + byte[] body = bodyCache.get(header.getId()); + if (body != null) { + LOG.info("Loaded body from cache"); + handler.onResult(new BlogPostItem(groupId, header, body)); + return; + } + runOnDbThread(new Runnable() { + @Override + public void run() { + try { + long now = System.currentTimeMillis(); + byte[] body = getPostBody(header.getId()); + long duration = System.currentTimeMillis() - now; + if (LOG.isLoggable(INFO)) + LOG.info("Loading body took " + duration + " ms"); + handler.onResult(new BlogPostItem(groupId, header, body)); + } catch (DbException e) { + if (LOG.isLoggable(WARNING)) + LOG.log(WARNING, e.toString(), e); + handler.onException(e); + } + } + }); } @Override - @Nullable - public BlogPostItem getBlogPost(MessageId id) { - if (posts == null) return null; - for (BlogPostItem item : posts) { - if (item.getId().equals(id)) return item; + public void loadBlogPost(final MessageId m, + final ResultExceptionHandler<BlogPostItem, DbException> handler) { + if (groupId == null) throw new IllegalStateException(); + BlogPostHeader header = headerCache.get(m); + if (header != null) { + LOG.info("Loaded header from cache"); + loadBlogPost(header, handler); + return; + } + runOnDbThread(new Runnable() { + @Override + public void run() { + try { + long now = System.currentTimeMillis(); + BlogPostHeader header = getPostHeader(m); + byte[] body = getPostBody(m); + long duration = System.currentTimeMillis() - now; + if (LOG.isLoggable(INFO)) + LOG.info("Loading post took " + duration + " ms"); + handler.onResult(new BlogPostItem(groupId, header, body)); + } catch (DbException e) { + if (LOG.isLoggable(WARNING)) + LOG.log(WARNING, e.toString(), e); + handler.onException(e); + } + } + }); + } + + private BlogPostHeader getPostHeader(MessageId m) throws DbException { + BlogPostHeader header = headerCache.get(m); + if (header == null) { + header = blogManager.getPostHeader(m); + headerCache.put(m, header); } - return null; + return header; } - @Override - @Nullable - public MessageId getBlogPostId(int position) { - if (posts == null) return null; - int i = 0; - for (BlogPostItem post : posts) { - if (i == position) return post.getId(); - i++; + private byte[] getPostBody(MessageId m) throws DbException { + byte[] body = bodyCache.get(m); + if (body == null) { + body = blogManager.getPostBody(m); + if (body != null) bodyCache.put(m, body); } - return null; + return body; } @Override - public void canDeleteBlog(final GroupId g, - final ResultExceptionHandler<Boolean, DbException> resultHandler) { + public void canDeleteBlog( + final ResultExceptionHandler<Boolean, DbException> handler) { + if (groupId == null) throw new IllegalStateException(); runOnDbThread(new Runnable() { @Override public void run() { - if (groupId == null) { - resultHandler.onResult(false); - return; - } try { - resultHandler.onResult(blogManager.canBeRemoved(groupId)); + handler.onResult(blogManager.canBeRemoved(groupId)); } catch (DbException e) { if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); - resultHandler.onException(e); + handler.onException(e); } } }); } @Override - public void deleteBlog(final ResultHandler<Boolean> resultHandler) { + public void deleteBlog( + final ResultExceptionHandler<Void, DbException> handler) { + if (groupId == null) throw new IllegalStateException(); runOnDbThread(new Runnable() { @Override public void run() { - if (groupId == null) { - resultHandler.onResult(false); - return; - } try { Blog b = blogManager.getBlog(groupId); blogManager.removeBlog(b); - resultHandler.onResult(true); + handler.onResult(null); } catch (DbException e) { if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); - resultHandler.onResult(false); + handler.onException(e); } } }); diff --git a/briar-android/src/org/briarproject/android/blogs/BlogFragment.java b/briar-android/src/org/briarproject/android/blogs/BlogFragment.java index 0bd866284320f3347e6d576757e228305dc0aeb2..143fc1b10d0a3dca946783c4a0ccb7c6ed204c79 100644 --- a/briar-android/src/org/briarproject/android/blogs/BlogFragment.java +++ b/briar-android/src/org/briarproject/android/blogs/BlogFragment.java @@ -22,11 +22,11 @@ import org.briarproject.android.ActivityComponent; import org.briarproject.android.blogs.BlogController.BlogPostListener; import org.briarproject.android.blogs.BlogPostAdapter.OnBlogPostClickListener; import org.briarproject.android.controller.handler.UiResultExceptionHandler; -import org.briarproject.android.controller.handler.UiResultHandler; import org.briarproject.android.fragment.BaseFragment; import org.briarproject.android.sharing.ShareBlogActivity; import org.briarproject.android.sharing.SharingStatusBlogActivity; import org.briarproject.android.util.BriarRecyclerView; +import org.briarproject.api.blogs.BlogPostHeader; import org.briarproject.api.db.DbException; import org.briarproject.api.sync.GroupId; @@ -59,7 +59,7 @@ public class BlogFragment extends BaseFragment implements BlogPostListener { private boolean myBlog; private BlogPostAdapter adapter; private BriarRecyclerView list; - private MenuItem deleteButton = null; + private MenuItem deleteButton; static BlogFragment newInstance(GroupId groupId, String name, boolean myBlog, boolean isNew) { @@ -85,7 +85,7 @@ public class BlogFragment extends BaseFragment implements BlogPostListener { Bundle args = getArguments(); byte[] b = args.getByteArray(GROUP_ID); - if (b == null) throw new IllegalStateException("No Group found."); + if (b == null) throw new IllegalStateException("No group ID in args"); groupId = new GroupId(b); blogName = args.getString(BLOG_NAME); myBlog = args.getBoolean(IS_MY_BLOG); @@ -98,6 +98,7 @@ public class BlogFragment extends BaseFragment implements BlogPostListener { list = (BriarRecyclerView) v.findViewById(R.id.postList); list.setLayoutManager(new LinearLayoutManager(getActivity())); list.setAdapter(adapter); + list.showProgressBar(); if (myBlog) { list.setEmptyText( getString(R.string.blogs_my_blogs_blog_empty_state)); @@ -126,13 +127,13 @@ public class BlogFragment extends BaseFragment implements BlogPostListener { @Override public void onStart() { super.onStart(); - loadData(false); if (!myBlog) checkIfBlogCanBeDeleted(); } @Override public void onResume() { super.onResume(); + loadBlogPosts(false); list.startPeriodicUpdate(); } @@ -208,41 +209,49 @@ public class BlogFragment extends BaseFragment implements BlogPostListener { } @Override - public void onBlogPostAdded(BlogPostItem post, boolean local) { - adapter.add(post); - if (local) list.scrollToPosition(0); + public void onBlogPostAdded(BlogPostHeader header, final boolean local) { + blogController.loadBlogPost(header, + new UiResultExceptionHandler<BlogPostItem, DbException>( + getActivity()) { + @Override + public void onResultUi(BlogPostItem post) { + adapter.add(post); + if (local) list.scrollToPosition(0); + } + + @Override + public void onExceptionUi(DbException exception) { + // TODO: Decide how to handle errors in the UI + getActivity().finish(); + } + } + ); } - private void loadData(final boolean reload) { - blogController.loadBlog(groupId, reload, - new UiResultHandler<Boolean>(getActivity()) { + void loadBlogPosts(final boolean reload) { + blogController.loadBlogPosts( + new UiResultExceptionHandler<Collection<BlogPostItem>, DbException>( + getActivity()) { @Override - public void onResultUi(Boolean result) { - if (result) { - Collection<BlogPostItem> posts = - blogController.getBlogPosts(); - if (posts.size() > 0) { - adapter.addAll(posts); - if (reload) list.scrollToPosition(0); - } else { - list.showData(); - } + public void onResultUi(Collection<BlogPostItem> posts) { + if (posts.size() > 0) { + adapter.addAll(posts); + if (reload) list.scrollToPosition(0); } else { - Toast.makeText(getActivity(), - R.string.blogs_blog_failed_to_load, - LENGTH_SHORT).show(); - getActivity().supportFinishAfterTransition(); + list.showData(); } } - }); - } - void reload() { - loadData(true); + @Override + public void onExceptionUi(DbException exception) { + // TODO: Decide how to handle errors in the UI + getActivity().finish(); + } + }); } private void checkIfBlogCanBeDeleted() { - blogController.canDeleteBlog(groupId, + blogController.canDeleteBlog( new UiResultExceptionHandler<Boolean, DbException>( getActivity()) { @Override @@ -251,9 +260,11 @@ public class BlogFragment extends BaseFragment implements BlogPostListener { showDeleteButton(); } } + @Override public void onExceptionUi(DbException exception) { - // nothing to do here, delete button is already hidden + // TODO: Decide how to handle errors in the UI + getActivity().finish(); } }); } @@ -290,15 +301,20 @@ public class BlogFragment extends BaseFragment implements BlogPostListener { private void deleteBlog() { blogController.deleteBlog( - new UiResultHandler<Boolean>(getActivity()) { + new UiResultExceptionHandler<Void, DbException>(getActivity()) { @Override - public void onResultUi(Boolean result) { - if (!result) return; + public void onResultUi(Void result) { Toast.makeText(getActivity(), R.string.blogs_blog_removed, LENGTH_SHORT) .show(); getActivity().supportFinishAfterTransition(); } + + @Override + public void onExceptionUi(DbException exception) { + // TODO: Decide how to handle errors in the UI + getActivity().finish(); + } }); } diff --git a/briar-android/src/org/briarproject/android/blogs/BlogPostAdapter.java b/briar-android/src/org/briarproject/android/blogs/BlogPostAdapter.java index a0060a1e73783cdd449bd8721107682c2c118cc8..44d971c83f0d23d5dcdaacb22835fba44cc89b02 100644 --- a/briar-android/src/org/briarproject/android/blogs/BlogPostAdapter.java +++ b/briar-android/src/org/briarproject/android/blogs/BlogPostAdapter.java @@ -3,11 +3,9 @@ package org.briarproject.android.blogs; import android.content.Context; import android.support.v7.util.SortedList; import android.support.v7.widget.RecyclerView; -import android.text.format.DateUtils; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.ImageView; import android.widget.TextView; import org.briarproject.R; @@ -97,7 +95,7 @@ class BlogPostAdapter extends ui.layout.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - listener.onBlogPostClick(ui.getAdapterPosition(), post); + listener.onBlogPostClick(post); } }); } @@ -132,15 +130,12 @@ class BlogPostAdapter extends } static class BlogPostHolder extends RecyclerView.ViewHolder { + private final ViewGroup layout; private final CircleImageView avatar; private final TextView author; private final TrustIndicatorView trust; private final TextView date; - private final TextView unread; - private final ImageView chat; - private final ImageView comment; - private final TextView title; private final TextView body; BlogPostHolder(View v) { @@ -151,16 +146,12 @@ class BlogPostAdapter extends author = (TextView) v.findViewById(R.id.authorName); trust = (TrustIndicatorView) v.findViewById(R.id.trustIndicator); date = (TextView) v.findViewById(R.id.dateView); - unread = (TextView) v.findViewById(R.id.newView); - chat = (ImageView) v.findViewById(R.id.chatView); - comment = (ImageView) v.findViewById(R.id.commentView); - title = (TextView) v.findViewById(R.id.titleView); body = (TextView) v.findViewById(R.id.bodyView); } } interface OnBlogPostClickListener { - void onBlogPostClick(int position, BlogPostItem post); + void onBlogPostClick(BlogPostItem post); } } diff --git a/briar-android/src/org/briarproject/android/blogs/BlogPostFragment.java b/briar-android/src/org/briarproject/android/blogs/BlogPostFragment.java index 89670292e36691c4428fd0b4ed5c683439da036c..9c36a73a50c54d3f6276fa30bfe7deb0408ccdcc 100644 --- a/briar-android/src/org/briarproject/android/blogs/BlogPostFragment.java +++ b/briar-android/src/org/briarproject/android/blogs/BlogPostFragment.java @@ -1,6 +1,5 @@ package org.briarproject.android.blogs; -import android.app.Activity; import android.content.Context; import android.os.Bundle; import android.support.annotation.Nullable; @@ -10,16 +9,15 @@ import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; -import android.widget.Toast; import org.briarproject.R; import org.briarproject.android.ActivityComponent; -import org.briarproject.android.controller.handler.UiResultHandler; +import org.briarproject.android.controller.handler.UiResultExceptionHandler; import org.briarproject.android.fragment.BaseFragment; import org.briarproject.android.util.AndroidUtils; import org.briarproject.android.util.TrustIndicatorView; +import org.briarproject.api.db.DbException; import org.briarproject.api.identity.Author; -import org.briarproject.api.sync.GroupId; import org.briarproject.api.sync.MessageId; import org.briarproject.util.StringUtils; @@ -30,31 +28,27 @@ import javax.inject.Inject; import im.delight.android.identicons.IdenticonDrawable; import static android.view.View.GONE; -import static android.widget.Toast.LENGTH_SHORT; -import static org.briarproject.android.BriarActivity.GROUP_ID; import static org.briarproject.android.util.AndroidUtils.MIN_RESOLUTION; public class BlogPostFragment extends BaseFragment { public final static String TAG = BlogPostFragment.class.getName(); - private static final Logger LOG = Logger.getLogger(TAG); - private final static String BLOG_POST_ID = "briar.BLOG_NAME"; + private static final Logger LOG = Logger.getLogger(TAG); + private static final String BLOG_POST_ID = "briar.BLOG_POST_ID"; - private GroupId groupId; private MessageId postId; private BlogPostViewHolder ui; - private BlogPostItem post = null; - private Runnable refresher = null; + private BlogPostItem post; + private Runnable refresher; @Inject BlogController blogController; - static BlogPostFragment newInstance(GroupId groupId, MessageId postId) { + static BlogPostFragment newInstance(MessageId postId) { BlogPostFragment f = new BlogPostFragment(); Bundle bundle = new Bundle(); - bundle.putByteArray(GROUP_ID, groupId.getBytes()); bundle.putByteArray(BLOG_POST_ID, postId.getBytes()); f.setArguments(bundle); @@ -65,15 +59,11 @@ public class BlogPostFragment extends BaseFragment { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - setHasOptionsMenu(true); - byte[] b = getArguments().getByteArray(GROUP_ID); - if (b == null) throw new IllegalStateException("No Group found."); - groupId = new GroupId(b); - byte[] p = getArguments().getByteArray(BLOG_POST_ID); - if (p == null) throw new IllegalStateException("No MessageId found."); - postId = new MessageId(p); + byte[] b = getArguments().getByteArray(BLOG_POST_ID); + if (b == null) throw new IllegalStateException("No post ID in args"); + postId = new MessageId(b); View v = inflater.inflate(R.layout.fragment_blog_post, container, false); @@ -89,21 +79,20 @@ public class BlogPostFragment extends BaseFragment { @Override public void onStart() { super.onStart(); - blogController.loadBlog(groupId, false, - new UiResultHandler<Boolean>((Activity) listener) { + blogController.loadBlogPost(postId, + new UiResultExceptionHandler<BlogPostItem, DbException>( + getActivity()) { @Override - public void onResultUi(Boolean result) { + public void onResultUi(BlogPostItem post) { listener.hideLoadingScreen(); - if (result) { - post = blogController.getBlogPost(postId); - if (post != null) { - bind(); - } - } else { - Toast.makeText(getActivity(), - R.string.blogs_blog_post_failed_to_load, - LENGTH_SHORT).show(); - } + BlogPostFragment.this.post = post; + bind(); + } + + @Override + public void onExceptionUi(DbException exception) { + // TODO: Decide how to handle errors in the UI + getActivity().finish(); } }); } @@ -157,14 +146,15 @@ public class BlogPostFragment extends BaseFragment { } private static class BlogPostViewHolder { - private ImageView avatar; - private TextView authorName; - private TrustIndicatorView trust; - private TextView date; - private TextView title; - private TextView body; - - BlogPostViewHolder(View v) { + + private final ImageView avatar; + private final TextView authorName; + private final TrustIndicatorView trust; + private final TextView date; + private final TextView title; + private final TextView body; + + private BlogPostViewHolder(View v) { avatar = (ImageView) v.findViewById(R.id.avatar); authorName = (TextView) v.findViewById(R.id.authorName); trust = (TrustIndicatorView) v.findViewById(R.id.trustIndicator); diff --git a/briar-android/src/org/briarproject/android/blogs/BlogPostItem.java b/briar-android/src/org/briarproject/android/blogs/BlogPostItem.java index 10a76b511f92cad3231ef2894af33a4e8c6f7d21..c3aa850af251b88bb5b39092f014d3c57b33cdc8 100644 --- a/briar-android/src/org/briarproject/android/blogs/BlogPostItem.java +++ b/briar-android/src/org/briarproject/android/blogs/BlogPostItem.java @@ -1,6 +1,7 @@ package org.briarproject.android.blogs; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import org.briarproject.api.blogs.BlogPostHeader; import org.briarproject.api.identity.Author; @@ -31,14 +32,11 @@ class BlogPostItem implements Comparable<BlogPostItem> { return groupId; } + @Nullable public String getTitle() { return header.getTitle(); } - public byte[] getBody() { - return body; - } - public long getTimestamp() { return header.getTimestamp(); } @@ -55,18 +53,22 @@ class BlogPostItem implements Comparable<BlogPostItem> { return header.getAuthorStatus(); } - public void setRead(boolean read) { - this.read = read; + public byte[] getBody() { + return body; } public boolean isRead() { return read; } + public void setRead(boolean read) { + this.read = read; + } + @Override public int compareTo(@NonNull BlogPostItem other) { if (this == other) return 0; - // The blog with the newest message comes first + // The newest post comes first long aTime = getTimeReceived(), bTime = other.getTimeReceived(); if (aTime > bTime) return -1; if (aTime < bTime) return 1; diff --git a/briar-android/src/org/briarproject/android/blogs/FeedFragment.java b/briar-android/src/org/briarproject/android/blogs/FeedFragment.java index a337f5a2f6b411e0c42bf40f81a1abd1d679ce89..a83f9beb056b87d5d41ba441c63f08e3c5d0e2c7 100644 --- a/briar-android/src/org/briarproject/android/blogs/FeedFragment.java +++ b/briar-android/src/org/briarproject/android/blogs/FeedFragment.java @@ -182,7 +182,7 @@ public class FeedFragment extends BaseFragment implements } @Override - public void onBlogPostClick(int position, BlogPostItem post) { + public void onBlogPostClick(BlogPostItem post) { byte[] groupId = post.getGroupId().getBytes(); String name = getString(R.string.blogs_personal_blog, post.getAuthor().getName()); diff --git a/briar-android/src/org/briarproject/android/controller/handler/UiResultExceptionHandler.java b/briar-android/src/org/briarproject/android/controller/handler/UiResultExceptionHandler.java index 89659b71d09965fbe1767360e7d99d30a0fc855a..ba2eab195ade5bd7c2ded6a9445041b025d70aec 100644 --- a/briar-android/src/org/briarproject/android/controller/handler/UiResultExceptionHandler.java +++ b/briar-android/src/org/briarproject/android/controller/handler/UiResultExceptionHandler.java @@ -1,6 +1,7 @@ package org.briarproject.android.controller.handler; import android.app.Activity; +import android.support.annotation.UiThread; public abstract class UiResultExceptionHandler<R, E extends Exception> implements ResultExceptionHandler<R, E> { @@ -31,7 +32,9 @@ public abstract class UiResultExceptionHandler<R, E extends Exception> }); } + @UiThread public abstract void onResultUi(R result); + @UiThread public abstract void onExceptionUi(E exception); } diff --git a/briar-android/src/org/briarproject/android/controller/handler/UiResultHandler.java b/briar-android/src/org/briarproject/android/controller/handler/UiResultHandler.java index 5aa32e25a7cae4fef97910fe83c054980c1ffab8..0616b82a2bf2ad263575803c79f1d765a4826315 100644 --- a/briar-android/src/org/briarproject/android/controller/handler/UiResultHandler.java +++ b/briar-android/src/org/briarproject/android/controller/handler/UiResultHandler.java @@ -1,6 +1,7 @@ package org.briarproject.android.controller.handler; import android.app.Activity; +import android.support.annotation.UiThread; public abstract class UiResultHandler<R> implements ResultHandler<R> { @@ -20,5 +21,6 @@ public abstract class UiResultHandler<R> implements ResultHandler<R> { }); } + @UiThread public abstract void onResultUi(R result); } diff --git a/briar-api/src/org/briarproject/api/blogs/BlogManager.java b/briar-api/src/org/briarproject/api/blogs/BlogManager.java index f0f4bc0a12528f17933167a5029f44b1b4570c5b..1d4f0eaeebee2cf20fc018851c8aa1b6b6fe8f23 100644 --- a/briar-api/src/org/briarproject/api/blogs/BlogManager.java +++ b/briar-api/src/org/briarproject/api/blogs/BlogManager.java @@ -7,7 +7,6 @@ import org.briarproject.api.identity.LocalAuthor; import org.briarproject.api.sync.ClientId; import org.briarproject.api.sync.GroupId; import org.briarproject.api.sync.MessageId; -import org.jetbrains.annotations.Nullable; import java.util.Collection; @@ -47,8 +46,10 @@ public interface BlogManager { /** Returns all blogs to which the user subscribes. */ Collection<Blog> getBlogs() throws DbException; + /** Returns the header of the blog post with the given ID. */ + BlogPostHeader getPostHeader(MessageId m) throws DbException; + /** Returns the body of the blog post with the given ID. */ - @Nullable byte[] getPostBody(MessageId m) throws DbException; /** Returns the headers of all posts in the given blog. */ diff --git a/briar-core/src/org/briarproject/blogs/BlogManagerImpl.java b/briar-core/src/org/briarproject/blogs/BlogManagerImpl.java index c58a2d0d84453ef6f325c22b3f0bca2e7743dc40..097e594f46c53dc8ad614cca33475b610b94678e 100644 --- a/briar-core/src/org/briarproject/blogs/BlogManagerImpl.java +++ b/briar-core/src/org/briarproject/blogs/BlogManagerImpl.java @@ -33,7 +33,6 @@ import org.briarproject.api.sync.Message; import org.briarproject.api.sync.MessageId; import org.briarproject.clients.BdfIncomingMessageHook; import org.briarproject.util.StringUtils; -import org.jetbrains.annotations.Nullable; import java.util.ArrayList; import java.util.Collection; @@ -341,10 +340,26 @@ class BlogManagerImpl extends BdfIncomingMessageHook implements BlogManager, } @Override - @Nullable + public BlogPostHeader getPostHeader(MessageId m) throws DbException { + Transaction txn = db.startTransaction(true); + try { + BdfDictionary meta = + clientHelper.getMessageMetadataAsDictionary(txn, m); + BlogPostHeader h = getPostHeaderFromMetadata(txn, m, meta); + txn.setComplete(); + return h; + } catch (FormatException e) { + throw new DbException(e); + } finally { + db.endTransaction(txn); + } + } + + @Override public byte[] getPostBody(MessageId m) throws DbException { try { BdfList message = clientHelper.getMessageAsList(m); + if (message == null) throw new DbException(); return getPostBody(message); } catch (FormatException e) { throw new DbException(e); @@ -362,24 +377,23 @@ class BlogManagerImpl extends BdfIncomingMessageHook implements BlogManager, public Collection<BlogPostHeader> getPostHeaders(GroupId g) throws DbException { - Map<MessageId, BdfDictionary> metadata; + Transaction txn = db.startTransaction(true); try { - metadata = clientHelper.getMessageMetadataAsDictionary(g); - } catch (FormatException e) { - throw new DbException(e); - } - Collection<BlogPostHeader> headers = new ArrayList<BlogPostHeader>(); - for (Entry<MessageId, BdfDictionary> entry : metadata.entrySet()) { - try { - BdfDictionary meta = entry.getValue(); - BlogPostHeader h = - getPostHeaderFromMetadata(null, entry.getKey(), meta); + Map<MessageId, BdfDictionary> metadata = + clientHelper.getMessageMetadataAsDictionary(txn, g); + List<BlogPostHeader> headers = new ArrayList<BlogPostHeader>(); + for (Entry<MessageId, BdfDictionary> entry : metadata.entrySet()) { + BlogPostHeader h = getPostHeaderFromMetadata(txn, + entry.getKey(), entry.getValue()); headers.add(h); - } catch (FormatException e) { - throw new DbException(e); } + txn.setComplete(); + return headers; + } catch (FormatException e) { + throw new DbException(e); + } finally { + db.endTransaction(txn); } - return headers; } @Override @@ -404,7 +418,7 @@ class BlogManagerImpl extends BdfIncomingMessageHook implements BlogManager, return d.getString(KEY_DESCRIPTION, ""); } - private BlogPostHeader getPostHeaderFromMetadata(@Nullable Transaction txn, + private BlogPostHeader getPostHeaderFromMetadata(Transaction txn, MessageId id, BdfDictionary meta) throws DbException, FormatException { @@ -418,11 +432,7 @@ class BlogManagerImpl extends BdfIncomingMessageHook implements BlogManager, byte[] publicKey = d.getRaw(KEY_PUBLIC_KEY); Author author = new Author(authorId, name, publicKey); Status authorStatus; - if (txn == null) - authorStatus = identityManager.getAuthorStatus(authorId); - else { - authorStatus = identityManager.getAuthorStatus(txn, authorId); - } + authorStatus = identityManager.getAuthorStatus(txn, authorId); String contentType = meta.getString(KEY_CONTENT_TYPE); boolean read = meta.getBoolean(KEY_READ);