diff --git a/briar-android/res/layout/activity_blog.xml b/briar-android/res/layout/activity_blog.xml
index c0ac19fbe..3ff8d409e 100644
--- a/briar-android/res/layout/activity_blog.xml
+++ b/briar-android/res/layout/activity_blog.xml
@@ -1,10 +1,21 @@
-
+ android:layout_height="match_parent">
+
+
+
+
+
+
\ No newline at end of file
diff --git a/briar-android/res/layout/fragment_blog.xml b/briar-android/res/layout/fragment_blog.xml
new file mode 100644
index 000000000..c0ac19fbe
--- /dev/null
+++ b/briar-android/res/layout/fragment_blog.xml
@@ -0,0 +1,10 @@
+
+
diff --git a/briar-android/res/layout/fragment_blog_post.xml b/briar-android/res/layout/fragment_blog_post.xml
new file mode 100644
index 000000000..ee02e64fb
--- /dev/null
+++ b/briar-android/res/layout/fragment_blog_post.xml
@@ -0,0 +1,77 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/briar-android/res/values/strings.xml b/briar-android/res/values/strings.xml
index 793e97d7a..d283c5c97 100644
--- a/briar-android/res/values/strings.xml
+++ b/briar-android/res/values/strings.xml
@@ -267,6 +267,8 @@
Add a title (optional)
Type your blog post here
Publish
+ Blog failed to load
+ Blog Post failed to load
Blog List
Available Blogs
diff --git a/briar-android/src/org/briarproject/android/ActivityComponent.java b/briar-android/src/org/briarproject/android/ActivityComponent.java
index c55e56f83..464fddd6f 100644
--- a/briar-android/src/org/briarproject/android/ActivityComponent.java
+++ b/briar-android/src/org/briarproject/android/ActivityComponent.java
@@ -3,6 +3,8 @@ package org.briarproject.android;
import android.app.Activity;
import org.briarproject.android.blogs.BlogActivity;
+import org.briarproject.android.blogs.BlogFragment;
+import org.briarproject.android.blogs.BlogPostFragment;
import org.briarproject.android.blogs.CreateBlogActivity;
import org.briarproject.android.blogs.MyBlogsFragment;
import org.briarproject.android.contact.ContactListFragment;
@@ -73,6 +75,10 @@ public interface ActivityComponent {
void inject(WriteBlogPostActivity activity);
+ void inject(BlogFragment fragment);
+
+ void inject(BlogPostFragment fragment);
+
void inject(SettingsActivity activity);
void inject(ChangePasswordActivity activity);
diff --git a/briar-android/src/org/briarproject/android/ActivityModule.java b/briar-android/src/org/briarproject/android/ActivityModule.java
index ea984bca0..d9879e342 100644
--- a/briar-android/src/org/briarproject/android/ActivityModule.java
+++ b/briar-android/src/org/briarproject/android/ActivityModule.java
@@ -4,6 +4,8 @@ import android.app.Activity;
import android.content.Context;
import android.content.SharedPreferences;
+import org.briarproject.android.blogs.BlogController;
+import org.briarproject.android.blogs.BlogControllerImpl;
import org.briarproject.android.controller.BriarController;
import org.briarproject.android.controller.BriarControllerImpl;
import org.briarproject.android.controller.ConfigController;
@@ -107,6 +109,13 @@ public class ActivityModule {
return forumController;
}
+ @ActivityScope
+ @Provides
+ BlogController provideBlogController(BlogControllerImpl blogController) {
+ activity.addLifecycleController(blogController);
+ return blogController;
+ }
+
@ActivityScope
@Provides
protected NavDrawerController provideNavDrawerController(
diff --git a/briar-android/src/org/briarproject/android/AndroidComponent.java b/briar-android/src/org/briarproject/android/AndroidComponent.java
index d6a495eb0..3e59233be 100644
--- a/briar-android/src/org/briarproject/android/AndroidComponent.java
+++ b/briar-android/src/org/briarproject/android/AndroidComponent.java
@@ -5,6 +5,7 @@ import org.briarproject.CoreModule;
import org.briarproject.android.api.AndroidExecutor;
import org.briarproject.android.api.AndroidNotificationManager;
import org.briarproject.android.api.ReferenceManager;
+import org.briarproject.android.blogs.BlogPersistentData;
import org.briarproject.android.forum.ForumPersistentData;
import org.briarproject.android.report.BriarReportSender;
import org.briarproject.api.blogs.BlogManager;
@@ -118,6 +119,8 @@ public interface AndroidComponent extends CoreEagerSingletons {
ForumPersistentData forumPersistentData();
+ BlogPersistentData blogPersistentData();
+
@IoExecutor
Executor ioExecutor();
diff --git a/briar-android/src/org/briarproject/android/AppModule.java b/briar-android/src/org/briarproject/android/AppModule.java
index 36933a118..825e639c6 100644
--- a/briar-android/src/org/briarproject/android/AppModule.java
+++ b/briar-android/src/org/briarproject/android/AppModule.java
@@ -4,6 +4,7 @@ import android.app.Application;
import org.briarproject.android.api.AndroidNotificationManager;
import org.briarproject.android.api.ReferenceManager;
+import org.briarproject.android.blogs.BlogPersistentData;
import org.briarproject.android.forum.ForumPersistentData;
import org.briarproject.api.crypto.CryptoComponent;
import org.briarproject.api.crypto.PublicKey;
@@ -143,4 +144,10 @@ public class AppModule {
ForumPersistentData provideForumPersistence() {
return new ForumPersistentData();
}
+
+ @Provides
+ @Singleton
+ BlogPersistentData provideBlogPersistence() {
+ return new BlogPersistentData();
+ }
}
diff --git a/briar-android/src/org/briarproject/android/blogs/BlogActivity.java b/briar-android/src/org/briarproject/android/blogs/BlogActivity.java
index aed08eb1e..35321f390 100644
--- a/briar-android/src/org/briarproject/android/blogs/BlogActivity.java
+++ b/briar-android/src/org/briarproject/android/blogs/BlogActivity.java
@@ -2,128 +2,105 @@ package org.briarproject.android.blogs;
import android.content.Intent;
import android.os.Bundle;
-import android.support.design.widget.Snackbar;
-import android.support.v4.app.ActivityCompat;
-import android.support.v4.app.ActivityOptionsCompat;
-import android.support.v7.widget.LinearLayoutManager;
-import android.view.Menu;
-import android.view.MenuInflater;
-import android.view.MenuItem;
+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.util.BriarRecyclerView;
-import org.briarproject.api.blogs.BlogManager;
-import org.briarproject.api.blogs.BlogPostHeader;
-import org.briarproject.api.db.DbException;
-import org.briarproject.api.db.NoSuchGroupException;
+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.fragment.BaseFragment.BaseFragmentListener;
import org.briarproject.api.sync.GroupId;
+import org.briarproject.api.sync.MessageId;
-import java.util.ArrayList;
import java.util.Collection;
import java.util.logging.Logger;
import javax.inject.Inject;
-import static android.support.design.widget.Snackbar.LENGTH_LONG;
-import static android.support.v4.app.ActivityOptionsCompat.makeCustomAnimation;
-import static java.util.logging.Level.INFO;
-import static java.util.logging.Level.WARNING;
+import static android.view.View.GONE;
+import static android.view.View.VISIBLE;
+import static android.widget.Toast.LENGTH_SHORT;
-public class BlogActivity extends BriarActivity {
+public class BlogActivity extends BriarActivity implements BlogPostListener,
+ OnBlogPostClickListener, BaseFragmentListener {
+ static final int REQUEST_WRITE_POST = 1;
static final String BLOG_NAME = "briar.BLOG_NAME";
static final String IS_MY_BLOG = "briar.IS_MY_BLOG";
static final String IS_NEW_BLOG = "briar.IS_NEW_BLOG";
- private static final int WRITE_POST = 1;
+ private static final String BLOG_PAGER_ADAPTER = "briar.BLOG_PAGER_ADAPTER";
private static final Logger LOG =
Logger.getLogger(BlogActivity.class.getName());
- private BlogPostAdapter adapter;
- private BriarRecyclerView list;
+ private ProgressBar progressBar;
+ private ViewPager pager;
+ private BlogPagerAdapter blogPagerAdapter;
+ private BlogPostPagerAdapter postPagerAdapter;
private String blogName;
- private boolean myBlog;
+ private boolean myBlog, isNew;
// Fields that are accessed from background threads must be volatile
private volatile GroupId groupId = null;
- private volatile boolean scrollToTop = false;
@Inject
- volatile BlogManager blogManager;
+ BlogController blogController;
@Override
public void onCreate(Bundle state) {
super.onCreate(state);
- setContentView(R.layout.activity_blog);
-
+ // GroupId from Intent
Intent i = getIntent();
byte[] b = i.getByteArrayExtra(GROUP_ID);
if (b == null) throw new IllegalStateException("No Group in intent.");
groupId = new GroupId(b);
+
+ // Name of the Blog from Intent
blogName = i.getStringExtra(BLOG_NAME);
if (blogName != null) setTitle(blogName);
+
+ // Is this our blog and was it just created?
myBlog = i.getBooleanExtra(IS_MY_BLOG, false);
+ isNew = i.getBooleanExtra(IS_NEW_BLOG, false);
- adapter = new BlogPostAdapter(this, groupId, blogName);
- list = (BriarRecyclerView) this.findViewById(R.id.postList);
- list.setLayoutManager(new LinearLayoutManager(this));
- list.setAdapter(adapter);
- if (myBlog) {
- list.setEmptyText(
- getString(R.string.blogs_my_blogs_blog_empty_state));
+ setContentView(R.layout.activity_blog);
+
+ 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)) {
+ pager.setAdapter(blogPagerAdapter);
} else {
- list.setEmptyText(getString(R.string.blogs_other_blog_empty_state));
- }
-
- // show snackbar if this blog was just created
- boolean isNew = i.getBooleanExtra(IS_NEW_BLOG, false);
- if (isNew) {
- Snackbar s = Snackbar.make(list, R.string.blogs_my_blogs_created,
- LENGTH_LONG);
- s.getView().setBackgroundResource(R.color.briar_primary);
- s.show();
+ // this initializes and restores the postPagerAdapter
+ loadBlogPosts();
}
}
@Override
- public void onResume() {
- super.onResume();
- loadBlogPosts();
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+
+ // remember which adapter we had active
+ outState.putBoolean(BLOG_PAGER_ADAPTER,
+ pager.getAdapter() == blogPagerAdapter);
}
@Override
- public boolean onCreateOptionsMenu(Menu menu) {
- if (myBlog) {
- MenuInflater inflater = getMenuInflater();
- inflater.inflate(R.menu.blogs_my_blog_actions, menu);
- }
- return super.onCreateOptionsMenu(menu);
- }
-
- @Override
- public boolean onOptionsItemSelected(final MenuItem item) {
- switch (item.getItemId()) {
- case R.id.action_write_blog_post:
- Intent i = new Intent(this, WriteBlogPostActivity.class);
- i.putExtra(GROUP_ID, groupId.getBytes());
- i.putExtra(BLOG_NAME, blogName);
- ActivityOptionsCompat options =
- makeCustomAnimation(this, android.R.anim.slide_in_left,
- android.R.anim.slide_out_right);
- ActivityCompat.startActivityForResult(this, i, WRITE_POST,
- options.toBundle());
- return true;
- default:
- return super.onOptionsItemSelected(item);
- }
- }
-
- @Override
- public void onActivityResult(int requestCode, int resultCode, Intent data) {
- if (requestCode == WRITE_POST && resultCode == RESULT_OK) {
- scrollToTop = true;
+ public void onBackPressed() {
+ if (pager.getAdapter() == postPagerAdapter) {
+ pager.setAdapter(blogPagerAdapter);
+ } else {
+ super.onBackPressed();
}
}
@@ -132,50 +109,153 @@ public class BlogActivity extends BriarActivity {
component.inject(this);
}
- private void loadBlogPosts() {
- runOnDbThread(new Runnable() {
- @Override
- public void run() {
- try {
- // load blog posts
- long now = System.currentTimeMillis();
- Collection posts = new ArrayList<>();
- try {
- Collection header =
- blogManager.getPostHeaders(groupId);
- for (BlogPostHeader h : header) {
- posts.add(new BlogPostItem(h));
- }
- } catch (NoSuchGroupException e) {
- // Continue
- }
- displayBlogPosts(posts);
- long duration = System.currentTimeMillis() - now;
- if (LOG.isLoggable(INFO))
- LOG.info("Post header load took " + duration + " ms");
- } catch (DbException e) {
- if (LOG.isLoggable(WARNING))
- LOG.log(WARNING, e.toString(), e);
- }
- }
- });
+ @Override
+ public void showLoadingScreen(boolean isBlocking, int stringId) {
+ progressBar.setVisibility(VISIBLE);
}
- private void displayBlogPosts(final Collection items) {
+ private void showLoadingScreen() {
+ showLoadingScreen(false, 0);
+ }
+
+ @Override
+ public void hideLoadingScreen() {
+ progressBar.setVisibility(GONE);
+ }
+
+ @Override
+ public void onFragmentCreated(String tag) {
+
+ }
+
+ @Override
+ public void onBlogPostClick(final int position) {
+ loadBlogPosts(position, true);
+ }
+
+ private void loadBlogPosts() {
+ loadBlogPosts(0, false);
+ }
+
+ private void loadBlogPosts(final int position, final boolean setItem) {
+ showLoadingScreen();
+ blogController
+ .loadBlog(groupId, false, new UiResultHandler(this) {
+ @Override
+ public void onResultUi(Boolean result) {
+ if (result) {
+ Collection 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();
+ }
+ }
+ });
+ }
+
+ @Override
+ public void onBlogPostAdded(final BlogPostItem post, final boolean local) {
runOnUiThread(new Runnable() {
@Override
public void run() {
- if (items.size() == 0) {
- list.showData();
- } else {
- adapter.addAll(items);
- if (scrollToTop) list.scrollToPosition(0);
+ if (blogPagerAdapter != null) {
+ BlogFragment f = blogPagerAdapter.getFragment();
+ if (f != null && f.isVisible()) {
+ f.onBlogPostAdded(post, local);
+ }
+ }
+
+ if (postPagerAdapter != null) {
+ postPagerAdapter.onBlogPostAdded();
+ postPagerAdapter.notifyDataSetChanged();
}
- scrollToTop = false;
}
});
}
- // TODO listen to events and add new blog posts as they come in
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode,
+ Intent data) {
+
+ // 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();
+ }
+ }
+ }
+
+
+ private class BlogPagerAdapter extends FragmentStatePagerAdapter {
+ private BlogFragment fragment = null;
+
+ BlogPagerAdapter(FragmentManager fm) {
+ super(fm);
+ }
+
+ @Override
+ public int getCount() {
+ return 1;
+ }
+
+ @Override
+ public Fragment getItem(int position) {
+ return BlogFragment.newInstance(groupId, blogName, myBlog, isNew);
+ }
+
+ @Override
+ public Object instantiateItem(ViewGroup container, int position) {
+ // save a reference to the single fragment here for later
+ fragment =
+ (BlogFragment) super.instantiateItem(container, position);
+ return fragment;
+ }
+
+ BlogFragment getFragment() {
+ return fragment;
+ }
+ }
+
+ private class BlogPostPagerAdapter extends FragmentStatePagerAdapter {
+ private int size;
+
+ BlogPostPagerAdapter(FragmentManager fm, int size) {
+ super(fm);
+ this.size = size;
+ }
+
+ @Override
+ public int getCount() {
+ return size;
+ }
+
+ @Override
+ public Fragment getItem(int position) {
+ MessageId postIdOfPos = blogController.getBlogPostId(position);
+ return BlogPostFragment.newInstance(groupId, postIdOfPos);
+ }
+
+ void onBlogPostAdded() {
+ size++;
+ }
+
+ void setSize(int size) {
+ this.size = size;
+ }
+ }
}
diff --git a/briar-android/src/org/briarproject/android/blogs/BlogController.java b/briar-android/src/org/briarproject/android/blogs/BlogController.java
new file mode 100644
index 000000000..ba9d085c2
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/blogs/BlogController.java
@@ -0,0 +1,29 @@
+package org.briarproject.android.blogs;
+
+import android.support.annotation.Nullable;
+
+import org.briarproject.android.controller.ActivityLifecycleController;
+import org.briarproject.android.controller.handler.UiResultHandler;
+import org.briarproject.api.sync.GroupId;
+import org.briarproject.api.sync.MessageId;
+
+import java.util.TreeSet;
+
+public interface BlogController extends ActivityLifecycleController {
+
+ void loadBlog(final GroupId groupId, final boolean reload,
+ final UiResultHandler resultHandler);
+
+ TreeSet getBlogPosts();
+
+ @Nullable
+ BlogPostItem getBlogPost(MessageId postId);
+
+ @Nullable
+ MessageId getBlogPostId(int position);
+
+ interface BlogPostListener {
+ void onBlogPostAdded(final BlogPostItem post, final boolean local);
+ }
+
+}
diff --git a/briar-android/src/org/briarproject/android/blogs/BlogControllerImpl.java b/briar-android/src/org/briarproject/android/blogs/BlogControllerImpl.java
new file mode 100644
index 000000000..f90a93095
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/blogs/BlogControllerImpl.java
@@ -0,0 +1,171 @@
+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.UiResultHandler;
+import org.briarproject.api.blogs.BlogManager;
+import org.briarproject.api.blogs.BlogPostHeader;
+import org.briarproject.api.db.DbException;
+import org.briarproject.api.event.BlogPostAddedEvent;
+import org.briarproject.api.event.Event;
+import org.briarproject.api.event.EventBus;
+import org.briarproject.api.event.EventListener;
+import org.briarproject.api.event.GroupRemovedEvent;
+import org.briarproject.api.sync.GroupId;
+import org.briarproject.api.sync.MessageId;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.TreeSet;
+import java.util.logging.Logger;
+
+import javax.inject.Inject;
+
+import static java.util.logging.Level.INFO;
+import static java.util.logging.Level.WARNING;
+
+public class BlogControllerImpl extends DbControllerImpl
+ implements BlogController, EventListener {
+
+ private static final Logger LOG =
+ Logger.getLogger(BlogControllerImpl.class.getName());
+
+ @Inject
+ protected Activity activity;
+ @Inject
+ protected volatile BlogManager blogManager;
+ @Inject
+ protected volatile EventBus eventBus;
+ @Inject
+ protected BlogPersistentData data;
+
+ private volatile BlogPostListener listener;
+
+ @Inject
+ BlogControllerImpl() {
+ }
+
+ @Override
+ public void onActivityCreate() {
+ if (activity instanceof BlogPostListener) {
+ listener = (BlogPostListener) activity;
+ } else {
+ throw new IllegalStateException(
+ "An activity that injects the BlogController must " +
+ "implement the BlogPostListener");
+ }
+ }
+
+ @Override
+ public void onActivityResume() {
+ eventBus.addListener(this);
+ }
+
+ @Override
+ public void onActivityPause() {
+ eventBus.removeListener(this);
+ }
+
+ @Override
+ public void onActivityDestroy() {
+ if (activity.isFinishing()) {
+ data.clearAll();
+ }
+ }
+
+ @Override
+ public void eventOccurred(Event e) {
+ if (e instanceof BlogPostAddedEvent) {
+ final BlogPostAddedEvent m = (BlogPostAddedEvent) e;
+ if (m.getGroupId().equals(data.getGroupId())) {
+ LOG.info("New blog post added");
+ final BlogPostHeader header = m.getHeader();
+ try {
+ final byte[] body = blogManager.getPostBody(header.getId());
+ final BlogPostItem post = new BlogPostItem(header, body);
+ data.addPost(post);
+ listener.onBlogPostAdded(post, m.isLocal());
+ } catch (DbException ex) {
+ if (LOG.isLoggable(WARNING))
+ LOG.log(WARNING, ex.toString(), ex);
+ }
+ }
+ } else if (e instanceof GroupRemovedEvent) {
+ GroupRemovedEvent s = (GroupRemovedEvent) e;
+ if (s.getGroup().getId().equals(data.getGroupId())) {
+ LOG.info("Blog removed");
+ activity.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ activity.finish();
+ }
+ });
+ }
+ }
+ }
+
+ @Override
+ public void loadBlog(final GroupId groupId, final boolean reload,
+ final UiResultHandler resultHandler) {
+
+ LOG.info("Loading blog...");
+ runOnDbThread(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ if (reload || data.getGroupId() == null ||
+ !data.getGroupId().equals(groupId)) {
+ data.setGroupId(groupId);
+ // load blog posts
+ long now = System.currentTimeMillis();
+ Collection posts = new ArrayList<>();
+ Collection header =
+ blogManager.getPostHeaders(groupId);
+ for (BlogPostHeader h : header) {
+ byte[] body = blogManager.getPostBody(h.getId());
+ posts.add(new BlogPostItem(h, body));
+ }
+ data.setPosts(posts);
+ long duration = System.currentTimeMillis() - now;
+ if (LOG.isLoggable(INFO))
+ LOG.info("Post header load took " + duration +
+ " ms");
+ }
+ resultHandler.onResult(true);
+ } catch (DbException e) {
+ if (LOG.isLoggable(WARNING))
+ LOG.log(WARNING, e.toString(), e);
+ resultHandler.onResult(false);
+ }
+ }
+ });
+ }
+
+ @Override
+ public TreeSet getBlogPosts() {
+ return data.getBlogPosts();
+ }
+
+ @Override
+ @Nullable
+ public BlogPostItem getBlogPost(MessageId id) {
+ for (BlogPostItem item : getBlogPosts()) {
+ if (item.getId().equals(id)) return item;
+ }
+ return null;
+ }
+
+ @Override
+ @Nullable
+ public MessageId getBlogPostId(int position) {
+ int i = 0;
+ for (BlogPostItem post : getBlogPosts()) {
+ if (i == position) return post.getId();
+ i++;
+ }
+ return null;
+ }
+
+}
diff --git a/briar-android/src/org/briarproject/android/blogs/BlogFragment.java b/briar-android/src/org/briarproject/android/blogs/BlogFragment.java
new file mode 100644
index 000000000..430cc611a
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/blogs/BlogFragment.java
@@ -0,0 +1,191 @@
+package org.briarproject.android.blogs;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.support.design.widget.Snackbar;
+import android.support.v4.app.ActivityCompat;
+import android.support.v4.app.ActivityOptionsCompat;
+import android.support.v7.widget.LinearLayoutManager;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Toast;
+
+import org.briarproject.R;
+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.UiResultHandler;
+import org.briarproject.android.fragment.BaseFragment;
+import org.briarproject.android.util.BriarRecyclerView;
+import org.briarproject.api.sync.GroupId;
+
+import java.util.Collection;
+
+import javax.inject.Inject;
+
+import static android.support.design.widget.Snackbar.LENGTH_LONG;
+import static android.support.v4.app.ActivityOptionsCompat.makeCustomAnimation;
+import static android.widget.Toast.LENGTH_SHORT;
+import static org.briarproject.android.BriarActivity.GROUP_ID;
+import static org.briarproject.android.blogs.BlogActivity.BLOG_NAME;
+import static org.briarproject.android.blogs.BlogActivity.IS_MY_BLOG;
+import static org.briarproject.android.blogs.BlogActivity.IS_NEW_BLOG;
+import static org.briarproject.android.blogs.BlogActivity.REQUEST_WRITE_POST;
+
+public class BlogFragment extends BaseFragment implements BlogPostListener {
+
+ public final static String TAG = BlogFragment.class.getName();
+
+ @Inject
+ BlogController blogController;
+
+ private GroupId groupId;
+ private String blogName;
+ private boolean myBlog;
+ private BlogPostAdapter adapter;
+ private BriarRecyclerView list;
+
+ static BlogFragment newInstance(GroupId groupId, String name,
+ boolean myBlog, boolean isNew) {
+
+ BlogFragment f = new BlogFragment();
+
+ Bundle bundle = new Bundle();
+ bundle.putByteArray(GROUP_ID, groupId.getBytes());
+ bundle.putString(BLOG_NAME, name);
+ bundle.putBoolean(IS_MY_BLOG, myBlog);
+ bundle.putBoolean(IS_NEW_BLOG, isNew);
+
+ f.setArguments(bundle);
+ return f;
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+
+ setHasOptionsMenu(true);
+
+ Bundle args = getArguments();
+ byte[] b = args.getByteArray(GROUP_ID);
+ if (b == null) throw new IllegalStateException("No Group found.");
+ groupId = new GroupId(b);
+ blogName = args.getString(BLOG_NAME);
+ myBlog = args.getBoolean(IS_MY_BLOG);
+ boolean isNew = args.getBoolean(IS_NEW_BLOG);
+
+ View v = inflater.inflate(R.layout.fragment_blog, container, false);
+
+ adapter = new BlogPostAdapter(getActivity(),
+ (OnBlogPostClickListener) getActivity());
+ list = (BriarRecyclerView) v.findViewById(R.id.postList);
+ list.setLayoutManager(new LinearLayoutManager(getActivity()));
+ list.setAdapter(adapter);
+ if (myBlog) {
+ list.setEmptyText(
+ getString(R.string.blogs_my_blogs_blog_empty_state));
+ } else {
+ list.setEmptyText(getString(R.string.blogs_other_blog_empty_state));
+ }
+
+ // show snackbar if this blog was just created
+ if (isNew) {
+ Snackbar s = Snackbar.make(list, R.string.blogs_my_blogs_created,
+ LENGTH_LONG);
+ s.getView().setBackgroundResource(R.color.briar_primary);
+ s.show();
+
+ // show only once
+ args.putBoolean(IS_NEW_BLOG, false);
+ }
+ return v;
+ }
+
+ @Override
+ public void injectFragment(ActivityComponent component) {
+ component.inject(this);
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ loadData(false);
+ }
+
+ @Override
+ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+ if (myBlog) {
+ inflater.inflate(R.menu.blogs_my_blog_actions, menu);
+ }
+ super.onCreateOptionsMenu(menu, inflater);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(final MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ getActivity().onBackPressed();
+ return true;
+ case R.id.action_write_blog_post:
+ Intent i =
+ new Intent(getActivity(), WriteBlogPostActivity.class);
+ i.putExtra(GROUP_ID, groupId.getBytes());
+ i.putExtra(BLOG_NAME, blogName);
+ ActivityOptionsCompat options =
+ makeCustomAnimation(getActivity(),
+ android.R.anim.slide_in_left,
+ android.R.anim.slide_out_right);
+ ActivityCompat.startActivityForResult(getActivity(), i,
+ REQUEST_WRITE_POST, options.toBundle());
+ return true;
+ default:
+ return super.onOptionsItemSelected(item);
+ }
+ }
+
+ @Override
+ public String getUniqueTag() {
+ return TAG;
+ }
+
+ @Override
+ public void onBlogPostAdded(BlogPostItem post, boolean local) {
+ adapter.add(post);
+ if (local) list.scrollToPosition(0);
+ }
+
+ private void loadData(final boolean reload) {
+ blogController.loadBlog(groupId, reload,
+ new UiResultHandler(getActivity()) {
+ @Override
+ public void onResultUi(Boolean result) {
+ if (result) {
+ Collection posts =
+ blogController.getBlogPosts();
+ if (posts.size() > 0) {
+ adapter.addAll(posts);
+ if (reload) list.scrollToPosition(0);
+ } else {
+ list.showData();
+ }
+ } else {
+ Toast.makeText(getActivity(),
+ R.string.blogs_blog_failed_to_load,
+ LENGTH_SHORT).show();
+ getActivity().supportFinishAfterTransition();
+ }
+ }
+ });
+ }
+
+ void reload() {
+ loadData(true);
+ }
+
+}
diff --git a/briar-android/src/org/briarproject/android/blogs/BlogPersistentData.java b/briar-android/src/org/briarproject/android/blogs/BlogPersistentData.java
new file mode 100644
index 000000000..a2834c809
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/blogs/BlogPersistentData.java
@@ -0,0 +1,49 @@
+package org.briarproject.android.blogs;
+
+import org.briarproject.api.sync.GroupId;
+
+import java.util.Collection;
+import java.util.TreeSet;
+
+import javax.inject.Inject;
+
+/**
+ * This class is a singleton that defines the data that should persist, i.e.
+ * still be present in memory after activity restarts. This class is not thread
+ * safe.
+ */
+public class BlogPersistentData {
+
+ private volatile GroupId groupId;
+ private volatile TreeSet posts = new TreeSet<>();
+
+ public BlogPersistentData() {
+
+ }
+
+ public void setGroupId(GroupId groupId) {
+ this.groupId = groupId;
+ }
+
+ public GroupId getGroupId() {
+ return groupId;
+ }
+
+ public void setPosts(Collection posts) {
+ this.posts.clear();
+ this.posts.addAll(posts);
+ }
+
+ void addPost(BlogPostItem post) {
+ posts.add(post);
+ }
+
+ TreeSet getBlogPosts() {
+ return posts;
+ }
+
+ void clearAll() {
+ groupId = null;
+ posts.clear();
+ }
+}
diff --git a/briar-android/src/org/briarproject/android/blogs/BlogPostAdapter.java b/briar-android/src/org/briarproject/android/blogs/BlogPostAdapter.java
index 728b33a5f..c071dfe71 100644
--- a/briar-android/src/org/briarproject/android/blogs/BlogPostAdapter.java
+++ b/briar-android/src/org/briarproject/android/blogs/BlogPostAdapter.java
@@ -10,7 +10,6 @@ import android.view.ViewGroup;
import android.widget.TextView;
import org.briarproject.R;
-import org.briarproject.api.sync.GroupId;
import org.briarproject.util.StringUtils;
import java.util.Collection;
@@ -61,13 +60,11 @@ class BlogPostAdapter extends
});
private final Context ctx;
- private final GroupId blogGroupId;
- private final String blogTitle;
+ private final OnBlogPostClickListener listener;
- BlogPostAdapter(Context ctx, GroupId blogGroupId, String blogTitle) {
+ BlogPostAdapter(Context ctx, OnBlogPostClickListener listener) {
this.ctx = ctx;
- this.blogGroupId = blogGroupId;
- this.blogTitle = blogTitle;
+ this.listener = listener;
}
@Override
@@ -78,7 +75,7 @@ class BlogPostAdapter extends
}
@Override
- public void onBindViewHolder(BlogPostHolder ui, int position) {
+ public void onBindViewHolder(final BlogPostHolder ui, int position) {
final BlogPostItem item = getItem(position);
// title
@@ -95,7 +92,7 @@ class BlogPostAdapter extends
ui.layout.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
- // TODO #428
+ listener.onBlogPostClick(ui.getAdapterPosition());
}
});
@@ -154,4 +151,9 @@ class BlogPostAdapter extends
body = (TextView) v.findViewById(R.id.bodyView);
}
}
+
+ interface OnBlogPostClickListener {
+ void onBlogPostClick(int position);
+ }
+
}
diff --git a/briar-android/src/org/briarproject/android/blogs/BlogPostFragment.java b/briar-android/src/org/briarproject/android/blogs/BlogPostFragment.java
new file mode 100644
index 000000000..54ac2cdda
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/blogs/BlogPostFragment.java
@@ -0,0 +1,157 @@
+package org.briarproject.android.blogs;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.text.format.DateUtils;
+import android.view.LayoutInflater;
+import android.view.MenuItem;
+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.fragment.BaseFragment;
+import org.briarproject.android.util.TrustIndicatorView;
+import org.briarproject.api.identity.Author;
+import org.briarproject.api.sync.GroupId;
+import org.briarproject.api.sync.MessageId;
+import org.briarproject.util.StringUtils;
+
+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;
+
+public class BlogPostFragment extends BaseFragment {
+
+ public final static String TAG = BlogPostFragment.class.getName();
+
+ private final static String BLOG_POST_ID = "briar.BLOG_NAME";
+
+ private GroupId groupId;
+ private MessageId postId;
+ private BlogPostViewHolder ui;
+
+ @Inject
+ BlogController blogController;
+
+ static BlogPostFragment newInstance(GroupId groupId, 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);
+ return f;
+ }
+
+ @Nullable
+ @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);
+
+ View v = inflater.inflate(R.layout.fragment_blog_post, container,
+ false);
+ ui = new BlogPostViewHolder(v);
+ return v;
+ }
+
+ @Override
+ public void injectFragment(ActivityComponent component) {
+ component.inject(this);
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ blogController.loadBlog(groupId, false,
+ new UiResultHandler((Activity) listener) {
+ @Override
+ public void onResultUi(Boolean result) {
+ listener.hideLoadingScreen();
+ if (result) {
+ BlogPostItem post =
+ blogController.getBlogPost(postId);
+ if (post != null) {
+ bind(post);
+ }
+ } else {
+ Toast.makeText(getActivity(),
+ R.string.blogs_blog_post_failed_to_load,
+ LENGTH_SHORT).show();
+ }
+ }
+ });
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(final MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ getActivity().onBackPressed();
+ return true;
+ default:
+ return super.onOptionsItemSelected(item);
+ }
+ }
+
+ @Override
+ public String getUniqueTag() {
+ return TAG;
+ }
+
+ private void bind(BlogPostItem post) {
+ Author author = post.getAuthor();
+ IdenticonDrawable d = new IdenticonDrawable(author.getId().getBytes());
+ ui.avatar.setImageDrawable(d);
+ ui.authorName.setText(author.getName());
+ ui.trust.setTrustLevel(post.getAuthorStatus());
+ ui.date.setText(
+ DateUtils.getRelativeTimeSpanString(post.getTimestamp()));
+
+ if (post.getTitle() != null) {
+ ui.title.setText(post.getTitle());
+ } else {
+ ui.title.setVisibility(GONE);
+ }
+
+ ui.body.setText(StringUtils.fromUtf8(post.getBody()));
+ }
+
+ 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) {
+ avatar = (ImageView) v.findViewById(R.id.avatar);
+ authorName = (TextView) v.findViewById(R.id.authorName);
+ trust = (TrustIndicatorView) v.findViewById(R.id.trustIndicator);
+ date = (TextView) v.findViewById(R.id.date);
+ title = (TextView) v.findViewById(R.id.title);
+ body = (TextView) v.findViewById(R.id.body);
+ }
+ }
+
+}