diff --git a/briar-android/AndroidManifest.xml b/briar-android/AndroidManifest.xml
index 03664f0ef..702e7e697 100644
--- a/briar-android/AndroidManifest.xml
+++ b/briar-android/AndroidManifest.xml
@@ -150,6 +150,36 @@
/>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/briar-android/res/drawable/ic_repeat.xml b/briar-android/res/drawable/ic_repeat.xml
new file mode 100644
index 000000000..115dbda81
--- /dev/null
+++ b/briar-android/res/drawable/ic_repeat.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/briar-android/res/drawable/trust_indicator_anonymous.xml b/briar-android/res/drawable/trust_indicator_anonymous.xml
index 99ccadd1b..82214b8d4 100644
--- a/briar-android/res/drawable/trust_indicator_anonymous.xml
+++ b/briar-android/res/drawable/trust_indicator_anonymous.xml
@@ -1,7 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/briar-android/res/layout/activity_create_blog.xml b/briar-android/res/layout/activity_create_blog.xml
new file mode 100644
index 000000000..31a1c3df8
--- /dev/null
+++ b/briar-android/res/layout/activity_create_blog.xml
@@ -0,0 +1,69 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/briar-android/res/layout/activity_create_forum.xml b/briar-android/res/layout/activity_create_forum.xml
index b48bce90f..96afb47a9 100644
--- a/briar-android/res/layout/activity_create_forum.xml
+++ b/briar-android/res/layout/activity_create_forum.xml
@@ -32,9 +32,6 @@
-
diff --git a/briar-android/res/layout/activity_create_identity.xml b/briar-android/res/layout/activity_create_identity.xml
index 23309bfa8..5919662cf 100644
--- a/briar-android/res/layout/activity_create_identity.xml
+++ b/briar-android/res/layout/activity_create_identity.xml
@@ -34,8 +34,6 @@
diff --git a/briar-android/res/layout/activity_write_blog_post.xml b/briar-android/res/layout/activity_write_blog_post.xml
new file mode 100644
index 000000000..5c3bcf380
--- /dev/null
+++ b/briar-android/res/layout/activity_write_blog_post.xml
@@ -0,0 +1,56 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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..7e5e73ed1
--- /dev/null
+++ b/briar-android/res/layout/fragment_blog_post.xml
@@ -0,0 +1,73 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/briar-android/res/layout/fragment_blogs_list.xml b/briar-android/res/layout/fragment_blogs_list.xml
new file mode 100644
index 000000000..a552dc0fc
--- /dev/null
+++ b/briar-android/res/layout/fragment_blogs_list.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/briar-android/res/layout/fragment_blogs_my.xml b/briar-android/res/layout/fragment_blogs_my.xml
index a552dc0fc..288adfaa3 100644
--- a/briar-android/res/layout/fragment_blogs_my.xml
+++ b/briar-android/res/layout/fragment_blogs_my.xml
@@ -1,26 +1,7 @@
-
-
-
-
-
-
-
-
\ No newline at end of file
+ tools:listitem="@layout/list_item_blog"/>
diff --git a/briar-android/res/layout/introduction_message.xml b/briar-android/res/layout/introduction_message.xml
index a557059f1..cf7e5a27e 100644
--- a/briar-android/res/layout/introduction_message.xml
+++ b/briar-android/res/layout/introduction_message.xml
@@ -123,10 +123,7 @@
+ android:text="@string/introduction_button"/>
diff --git a/briar-android/res/layout/list_item_blog.xml b/briar-android/res/layout/list_item_blog.xml
new file mode 100644
index 000000000..1fe1f22db
--- /dev/null
+++ b/briar-android/res/layout/list_item_blog.xml
@@ -0,0 +1,79 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/briar-android/res/layout/list_item_blog_post.xml b/briar-android/res/layout/list_item_blog_post.xml
new file mode 100644
index 000000000..0f7e1d532
--- /dev/null
+++ b/briar-android/res/layout/list_item_blog_post.xml
@@ -0,0 +1,118 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/briar-android/res/layout/list_item_forum.xml b/briar-android/res/layout/list_item_forum.xml
index 171a40859..0652bec6b 100644
--- a/briar-android/res/layout/list_item_forum.xml
+++ b/briar-android/res/layout/list_item_forum.xml
@@ -31,7 +31,7 @@
tools:text="This is a name of a forum"/>
+ android:layout_below="@+id/postCountView"/>
diff --git a/briar-android/res/layout/share_forum_message.xml b/briar-android/res/layout/share_forum_message.xml
index 522a0472d..33863fc13 100644
--- a/briar-android/res/layout/share_forum_message.xml
+++ b/briar-android/res/layout/share_forum_message.xml
@@ -34,10 +34,7 @@
+ android:text="@string/forum_share_button"/>
diff --git a/briar-android/res/menu/blogs_feed_actions.xml b/briar-android/res/menu/blogs_feed_actions.xml
new file mode 100644
index 000000000..38721c812
--- /dev/null
+++ b/briar-android/res/menu/blogs_feed_actions.xml
@@ -0,0 +1,12 @@
+
+
\ No newline at end of file
diff --git a/briar-android/res/menu/blogs_my_actions.xml b/briar-android/res/menu/blogs_my_actions.xml
new file mode 100644
index 000000000..9a3ce5b49
--- /dev/null
+++ b/briar-android/res/menu/blogs_my_actions.xml
@@ -0,0 +1,12 @@
+
+
\ No newline at end of file
diff --git a/briar-android/res/menu/blogs_my_blog_actions.xml b/briar-android/res/menu/blogs_my_blog_actions.xml
new file mode 100644
index 000000000..95cb880e7
--- /dev/null
+++ b/briar-android/res/menu/blogs_my_blog_actions.xml
@@ -0,0 +1,18 @@
+
+
\ No newline at end of file
diff --git a/briar-android/res/menu/blogs_write_blog_post_actions.xml b/briar-android/res/menu/blogs_write_blog_post_actions.xml
new file mode 100644
index 000000000..11befe036
--- /dev/null
+++ b/briar-android/res/menu/blogs_write_blog_post_actions.xml
@@ -0,0 +1,12 @@
+
+
\ No newline at end of file
diff --git a/briar-android/res/values/dimens.xml b/briar-android/res/values/dimens.xml
index 0a0fc5ce5..8aa43b669 100644
--- a/briar-android/res/values/dimens.xml
+++ b/briar-android/res/values/dimens.xml
@@ -20,6 +20,7 @@
34sp
16dp
+ 10dp
72dp
56dp
68dp
diff --git a/briar-android/res/values/strings.xml b/briar-android/res/values/strings.xml
index d44bc389a..4bef054ae 100644
--- a/briar-android/res/values/strings.xml
+++ b/briar-android/res/values/strings.xml
@@ -84,12 +84,12 @@
Leave Forum
Left Forum
Sharing Status
- No posts
+ No posts
- %d unread post
- %d unread posts
-
+
- %d post
- %d posts
@@ -251,9 +251,39 @@
Please wait..
- Blogs
+ Micro Blogs
Feed
+
My Blogs
+ Create Blog
+ Add new Blog
+ Blog title (cannot be changed later)
+ A short description of your new blog
+ Potential readers may or may not subscribe to your blog based on the content of the description.
+ You don\'t have any blogs.\n\nWhy don\'t you create one now by clicking the plus in the top right screen corner?
+ This is the place for content of your blog.\n\nIt seems like you haven\'t written anything yet.\n\nPlease tap the pen icon to compose a new blog post.\n\nDon\'t forget to go public and share your blog.
+ Blog created
+ This blog is empty
+ This blog is currently empty.\n\nEither the author hasn\'t written anything yet, or the person who shared this blog with you needs to come online, so posts can be synchronized.
+ NEW
+ more
+ Write Blog Post
+ Add a title (optional)
+ Type your blog post here
+ Publish
+ Blog Post Created
+ New Blog Post Received
+ Scroll To
+ Blog failed to load
+ Blog Post failed to load
+ This is the global blog feed.\n\nIt looks like nobody blogged anything, yet.\n\nBe the first and tap the pen icon to write a new blog post.
+ Delete Blog
+ Are you sure that you want to delete this Blog and all posts?\nNote that this will not delete the blog from other people\'s devices.
+ Delete Blog
+ Keep
+ Blog Deleted
+ Remove Blog
+
Blog List
Available Blogs
Drafts
diff --git a/briar-android/res/values/styles.xml b/briar-android/res/values/styles.xml
index 9251475da..729554b99 100644
--- a/briar-android/res/values/styles.xml
+++ b/briar-android/res/values/styles.xml
@@ -23,6 +23,8 @@
@@ -55,6 +57,17 @@
- @android:color/primary_text_light
+
+
@@ -109,4 +122,9 @@
- @color/briar_text_primary_inverse
+
+
+
\ No newline at end of file
diff --git a/briar-android/res/values/themes.xml b/briar-android/res/values/themes.xml
index e87077f03..49c05904f 100644
--- a/briar-android/res/values/themes.xml
+++ b/briar-android/res/values/themes.xml
@@ -42,6 +42,8 @@
- @color/briar_primary
- @color/briar_primary_dark
- @color/briar_accent
+ - @style/BriarButtonFlat.Positive
+ - @style/BriarButtonFlat.Negative
- @color/briar_text_primary
- @color/briar_text_primary_inverse
- @color/briar_text_secondary
@@ -49,6 +51,12 @@
- @color/briar_text_tertiary
- @color/briar_text_tertiary_inverse
- @color/briar_text_link
+ - @style/DialogAnimation
+
+
+
diff --git a/briar-android/src/org/briarproject/android/ActivityComponent.java b/briar-android/src/org/briarproject/android/ActivityComponent.java
index 3353f62b4..3f3368b57 100644
--- a/briar-android/src/org/briarproject/android/ActivityComponent.java
+++ b/briar-android/src/org/briarproject/android/ActivityComponent.java
@@ -2,7 +2,15 @@ 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.BlogListFragment;
+import org.briarproject.android.blogs.BlogPostFragment;
+import org.briarproject.android.blogs.BlogsFragment;
+import org.briarproject.android.blogs.CreateBlogActivity;
+import org.briarproject.android.blogs.FeedFragment;
import org.briarproject.android.blogs.MyBlogsFragment;
+import org.briarproject.android.blogs.WriteBlogPostActivity;
import org.briarproject.android.contact.ContactListFragment;
import org.briarproject.android.contact.ConversationActivity;
import org.briarproject.android.forum.ForumInvitationsActivity;
@@ -13,7 +21,6 @@ import org.briarproject.android.forum.ForumListFragment;
import org.briarproject.android.forum.ForumSharingStatusActivity;
import org.briarproject.android.forum.ShareForumActivity;
import org.briarproject.android.forum.ShareForumMessageFragment;
-import org.briarproject.android.fragment.BaseFragment;
import org.briarproject.android.identity.CreateIdentityActivity;
import org.briarproject.android.introduction.ContactChooserFragment;
import org.briarproject.android.introduction.IntroductionActivity;
@@ -64,6 +71,16 @@ public interface ActivityComponent {
void inject(ForumActivity activity);
+ void inject(CreateBlogActivity activity);
+
+ void inject(BlogActivity activity);
+
+ void inject(WriteBlogPostActivity activity);
+
+ void inject(BlogFragment fragment);
+
+ void inject(BlogPostFragment fragment);
+
void inject(SettingsActivity activity);
void inject(ChangePasswordActivity activity);
@@ -73,7 +90,9 @@ public interface ActivityComponent {
// Fragments
void inject(ContactListFragment fragment);
void inject(ForumListFragment fragment);
- void inject(BaseFragment fragment);
+ void inject(BlogsFragment fragment);
+ void inject(BlogListFragment fragment);
+ void inject(FeedFragment fragment);
void inject(MyBlogsFragment fragment);
void inject(ChooseIdentityFragment fragment);
void inject(ShowQrCodeFragment fragment);
diff --git a/briar-android/src/org/briarproject/android/ActivityModule.java b/briar-android/src/org/briarproject/android/ActivityModule.java
index ea984bca0..5627651ae 100644
--- a/briar-android/src/org/briarproject/android/ActivityModule.java
+++ b/briar-android/src/org/briarproject/android/ActivityModule.java
@@ -4,6 +4,10 @@ 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.blogs.FeedController;
+import org.briarproject.android.blogs.FeedControllerImpl;
import org.briarproject.android.controller.BriarController;
import org.briarproject.android.controller.BriarControllerImpl;
import org.briarproject.android.controller.ConfigController;
@@ -107,6 +111,20 @@ public class ActivityModule {
return forumController;
}
+ @ActivityScope
+ @Provides
+ BlogController provideBlogController(BlogControllerImpl blogController) {
+ activity.addLifecycleController(blogController);
+ return blogController;
+ }
+
+ @ActivityScope
+ @Provides
+ protected FeedController provideFeedController(
+ FeedControllerImpl feedController) {
+ return feedController;
+ }
+
@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 5bc21c1cc..3e59233be 100644
--- a/briar-android/src/org/briarproject/android/AndroidComponent.java
+++ b/briar-android/src/org/briarproject/android/AndroidComponent.java
@@ -5,8 +5,11 @@ 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;
+import org.briarproject.api.blogs.BlogPostFactory;
import org.briarproject.api.contact.ContactExchangeTask;
import org.briarproject.api.contact.ContactManager;
import org.briarproject.api.crypto.CryptoComponent;
@@ -96,6 +99,10 @@ public interface AndroidComponent extends CoreEagerSingletons {
ForumPostFactory forumPostFactory();
+ BlogManager blogManager();
+
+ BlogPostFactory blogPostFactory();
+
SettingsManager settingsManager();
ContactExchangeTask contactExchangeTask();
@@ -112,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
new file mode 100644
index 000000000..35321f390
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/blogs/BlogActivity.java
@@ -0,0 +1,261 @@
+package org.briarproject.android.blogs;
+
+import android.content.Intent;
+import android.os.Bundle;
+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.fragment.BaseFragment.BaseFragmentListener;
+import org.briarproject.api.sync.GroupId;
+import org.briarproject.api.sync.MessageId;
+
+import java.util.Collection;
+import java.util.logging.Logger;
+
+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 {
+
+ 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 String BLOG_PAGER_ADAPTER = "briar.BLOG_PAGER_ADAPTER";
+ private static final Logger LOG =
+ Logger.getLogger(BlogActivity.class.getName());
+
+ private ProgressBar progressBar;
+ private ViewPager pager;
+ private BlogPagerAdapter blogPagerAdapter;
+ private BlogPostPagerAdapter postPagerAdapter;
+ private String blogName;
+ private boolean myBlog, isNew;
+
+ // Fields that are accessed from background threads must be volatile
+ private volatile GroupId groupId = null;
+ @Inject
+ BlogController blogController;
+
+ @Override
+ public void onCreate(Bundle state) {
+ super.onCreate(state);
+
+ // 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);
+
+ 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 {
+ // this initializes and restores the postPagerAdapter
+ loadBlogPosts();
+ }
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+
+ // remember which adapter we had active
+ outState.putBoolean(BLOG_PAGER_ADAPTER,
+ pager.getAdapter() == blogPagerAdapter);
+ }
+
+ @Override
+ public void onBackPressed() {
+ if (pager.getAdapter() == postPagerAdapter) {
+ pager.setAdapter(blogPagerAdapter);
+ } else {
+ super.onBackPressed();
+ }
+ }
+
+ @Override
+ public void injectActivity(ActivityComponent component) {
+ component.inject(this);
+ }
+
+ @Override
+ public void showLoadingScreen(boolean isBlocking, int stringId) {
+ progressBar.setVisibility(VISIBLE);
+ }
+
+ 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 (blogPagerAdapter != null) {
+ BlogFragment f = blogPagerAdapter.getFragment();
+ if (f != null && f.isVisible()) {
+ f.onBlogPostAdded(post, local);
+ }
+ }
+
+ if (postPagerAdapter != null) {
+ postPagerAdapter.onBlogPostAdded();
+ postPagerAdapter.notifyDataSetChanged();
+ }
+ }
+ });
+ }
+
+ @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..a805c2d34
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/blogs/BlogController.java
@@ -0,0 +1,31 @@
+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);
+
+ void deleteBlog(final UiResultHandler resultHandler);
+
+ 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..d2898602c
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/blogs/BlogControllerImpl.java
@@ -0,0 +1,194 @@
+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.Blog;
+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;
+ }
+
+ @Override
+ public void deleteBlog(final UiResultHandler resultHandler) {
+ runOnDbThread(new Runnable() {
+ @Override
+ public void run() {
+ if (data.getGroupId() == null) {
+ resultHandler.onResult(false);
+ return;
+ }
+ try {
+ Blog b = blogManager.getBlog(data.getGroupId());
+ blogManager.removeBlog(b);
+ resultHandler.onResult(true);
+ } catch (DbException e) {
+ if (LOG.isLoggable(WARNING))
+ LOG.log(WARNING, e.toString(), e);
+ resultHandler.onResult(false);
+ }
+ }
+ });
+ }
+
+}
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..7ee529ca3
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/blogs/BlogFragment.java
@@ -0,0 +1,228 @@
+package org.briarproject.android.blogs;
+
+import android.content.DialogInterface;
+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.app.AlertDialog;
+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;
+ case R.id.action_delete_blog:
+ showDeleteDialog();
+ 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);
+ }
+
+ private void showDeleteDialog() {
+ DialogInterface.OnClickListener okListener =
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ deleteBlog();
+ }
+ };
+ AlertDialog.Builder builder = new AlertDialog.Builder(getActivity(),
+ R.style.BriarDialogTheme);
+ builder.setTitle(getString(R.string.blogs_delete_blog));
+ builder.setMessage(
+ getString(R.string.blogs_delete_blog_dialog_message));
+ builder.setPositiveButton(R.string.blogs_delete_blog_cancel, null);
+ builder.setNegativeButton(R.string.blogs_delete_blog_ok, okListener);
+ builder.show();
+ }
+
+ private void deleteBlog() {
+ blogController.deleteBlog(
+ new UiResultHandler(getActivity()) {
+ @Override
+ public void onResultUi(Boolean result) {
+ if (!result) return;
+ Toast.makeText(getActivity(),
+ R.string.blogs_blog_deleted, LENGTH_SHORT)
+ .show();
+ getActivity().supportFinishAfterTransition();
+ }
+ });
+ }
+
+}
diff --git a/briar-android/src/org/briarproject/android/blogs/BlogListAdapter.java b/briar-android/src/org/briarproject/android/blogs/BlogListAdapter.java
new file mode 100644
index 000000000..92526387f
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/blogs/BlogListAdapter.java
@@ -0,0 +1,210 @@
+package org.briarproject.android.blogs;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.support.annotation.Nullable;
+import android.support.v4.app.ActivityCompat;
+import android.support.v4.app.ActivityOptionsCompat;
+import android.support.v4.content.ContextCompat;
+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.TextView;
+
+import org.briarproject.R;
+import org.briarproject.android.util.TextAvatarView;
+import org.briarproject.api.blogs.Blog;
+import org.briarproject.api.sync.GroupId;
+
+import java.util.Collection;
+
+import static android.support.v4.app.ActivityOptionsCompat.makeCustomAnimation;
+import static android.view.View.GONE;
+import static android.view.View.VISIBLE;
+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;
+
+class BlogListAdapter extends
+ RecyclerView.Adapter {
+
+ private SortedList blogs = new SortedList<>(
+ BlogListItem.class, new SortedList.Callback() {
+
+ @Override
+ public int compare(BlogListItem a, BlogListItem b) {
+ if (a == b) return 0;
+ // The blog with the newest message comes first
+ long aTime = a.getTimestamp(), bTime = b.getTimestamp();
+ if (aTime > bTime) return -1;
+ if (aTime < bTime) return 1;
+ // Break ties by blog name
+ String aName = a.getName();
+ String bName = b.getName();
+ return String.CASE_INSENSITIVE_ORDER.compare(aName, bName);
+ }
+
+ @Override
+ public void onInserted(int position, int count) {
+ notifyItemRangeInserted(position, count);
+ }
+
+ @Override
+ public void onRemoved(int position, int count) {
+ notifyItemRangeRemoved(position, count);
+ }
+
+ @Override
+ public void onMoved(int fromPosition, int toPosition) {
+ notifyItemMoved(fromPosition, toPosition);
+ }
+
+ @Override
+ public void onChanged(int position, int count) {
+ notifyItemRangeChanged(position, count);
+ }
+
+ @Override
+ public boolean areContentsTheSame(BlogListItem a, BlogListItem b) {
+ return a.getBlog().equals(b.getBlog()) &&
+ a.getTimestamp() == b.getTimestamp() &&
+ a.getUnreadCount() == b.getUnreadCount();
+ }
+
+ @Override
+ public boolean areItemsTheSame(BlogListItem a, BlogListItem b) {
+ return a.getBlog().equals(b.getBlog());
+ }
+ });
+
+ private final Activity ctx;
+
+ BlogListAdapter(Activity ctx) {
+ this.ctx = ctx;
+ }
+
+ @Override
+ public BlogViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ View v = LayoutInflater.from(ctx).inflate(
+ R.layout.list_item_blog, parent, false);
+ return new BlogViewHolder(v);
+ }
+
+ @Override
+ public void onBindViewHolder(BlogViewHolder ui, int position) {
+ final BlogListItem item = getItem(position);
+
+ // Avatar
+ ui.avatar.setText(item.getName().substring(0, 1));
+ ui.avatar.setBackgroundBytes(item.getBlog().getId().getBytes());
+ ui.avatar.setUnreadCount(item.getUnreadCount());
+
+ // Blog Name
+ ui.name.setText(item.getName());
+
+ // Post Count
+ int postCount = item.getPostCount();
+ ui.postCount.setText(ctx.getResources()
+ .getQuantityString(R.plurals.posts, postCount, postCount));
+ ui.postCount.setTextColor(
+ ContextCompat.getColor(ctx, R.color.briar_text_secondary));
+
+ // Date and Status
+ if (item.isEmpty()) {
+ ui.date.setVisibility(GONE);
+ ui.avatar.setProblem(true);
+ ui.status.setText(ctx.getString(R.string.blogs_blog_is_empty));
+ ui.status.setVisibility(VISIBLE);
+ } else {
+ long timestamp = item.getTimestamp();
+ ui.date.setText(
+ DateUtils.getRelativeTimeSpanString(ctx, timestamp));
+ ui.date.setVisibility(VISIBLE);
+ ui.avatar.setProblem(false);
+ ui.status.setVisibility(GONE);
+ }
+
+ // Open Blog on Click
+ ui.layout.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ Intent i = new Intent(ctx, BlogActivity.class);
+ Blog b = item.getBlog();
+ i.putExtra(GROUP_ID, b.getId().getBytes());
+ i.putExtra(BLOG_NAME, b.getName());
+ i.putExtra(IS_MY_BLOG, item.isOurs());
+ ActivityOptionsCompat options = ActivityOptionsCompat
+ .makeCustomAnimation(ctx, android.R.anim.fade_in,
+ android.R.anim.fade_out);
+ ActivityCompat.startActivity(ctx, i, options.toBundle());
+ }
+ });
+ }
+
+ @Override
+ public int getItemCount() {
+ return blogs.size();
+ }
+
+ public BlogListItem getItem(int position) {
+ return blogs.get(position);
+ }
+
+ @Nullable
+ public BlogListItem getItem(GroupId g) {
+ for (int i = 0; i < blogs.size(); i++) {
+ BlogListItem item = blogs.get(i);
+ if (item.getBlog().getGroup().getId().equals(g)) {
+ return item;
+ }
+ }
+ return null;
+ }
+
+ public void addAll(Collection items) {
+ blogs.addAll(items);
+ }
+
+ void updateItem(BlogListItem item) {
+ BlogListItem oldItem = getItem(item.getBlog().getGroup().getId());
+ int position = blogs.indexOf(oldItem);
+ blogs.updateItemAt(position, item);
+ }
+
+ public void remove(BlogListItem item) {
+ blogs.remove(item);
+ }
+
+ public void clear() {
+ blogs.clear();
+ }
+
+ public boolean isEmpty() {
+ return blogs.size() == 0;
+ }
+
+ static class BlogViewHolder extends RecyclerView.ViewHolder {
+
+ private final ViewGroup layout;
+ private final TextAvatarView avatar;
+ private final TextView name;
+ private final TextView postCount;
+ private final TextView date;
+ private final TextView status;
+
+ BlogViewHolder(View v) {
+ super(v);
+
+ layout = (ViewGroup) v;
+ avatar = (TextAvatarView) v.findViewById(R.id.avatarView);
+ name = (TextView) v.findViewById(R.id.nameView);
+ postCount = (TextView) v.findViewById(R.id.postCountView);
+ date = (TextView) v.findViewById(R.id.dateView);
+ status = (TextView) v.findViewById(R.id.statusView);
+ }
+ }
+}
diff --git a/briar-android/src/org/briarproject/android/blogs/BlogListFragment.java b/briar-android/src/org/briarproject/android/blogs/BlogListFragment.java
new file mode 100644
index 000000000..61f3dff54
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/blogs/BlogListFragment.java
@@ -0,0 +1,53 @@
+package org.briarproject.android.blogs;
+
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import org.briarproject.R;
+import org.briarproject.android.ActivityComponent;
+import org.briarproject.android.fragment.BaseFragment;
+
+public class BlogListFragment extends BaseFragment {
+
+ public final static String TAG = BlogListFragment.class.getName();
+
+ static BlogListFragment newInstance(int num) {
+ BlogListFragment f = new BlogListFragment();
+
+ Bundle args = new Bundle();
+ args.putInt("num", num);
+ f.setArguments(args);
+
+ return f;
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+
+ View v = inflater.inflate(R.layout.fragment_blogs_list, container,
+ false);
+
+ TextView numView = (TextView) v.findViewById(R.id.num);
+ String num = String.valueOf(getArguments().getInt("num"));
+ numView.setText(num);
+
+ return v;
+ }
+
+ @Override
+ public void injectFragment(ActivityComponent component) {
+ component.inject(this);
+ }
+
+ @Override
+ public String getUniqueTag() {
+ return TAG;
+ }
+
+}
diff --git a/briar-android/src/org/briarproject/android/blogs/BlogListItem.java b/briar-android/src/org/briarproject/android/blogs/BlogListItem.java
new file mode 100644
index 000000000..35c9f532b
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/blogs/BlogListItem.java
@@ -0,0 +1,67 @@
+package org.briarproject.android.blogs;
+
+import org.briarproject.api.blogs.Blog;
+import org.briarproject.api.blogs.BlogPostHeader;
+
+import java.util.Collection;
+
+class BlogListItem {
+
+ private final Blog blog;
+ private final int postCount;
+ private final long timestamp;
+ private final int unread;
+ private final boolean ours;
+
+ BlogListItem(Blog blog, Collection headers, boolean ours) {
+ this.blog = blog;
+ if (headers.isEmpty()) {
+ postCount = 0;
+ timestamp = 0;
+ unread = 0;
+ } else {
+ BlogPostHeader newest = null;
+ long timestamp = -1;
+ int unread = 0;
+ for (BlogPostHeader h : headers) {
+ if (h.getTimestamp() > timestamp) {
+ timestamp = h.getTimestamp();
+ newest = h;
+ }
+ if (!h.isRead()) unread++;
+ }
+ this.postCount = headers.size();
+ this.timestamp = newest.getTimestamp();
+ this.unread = unread;
+ }
+ this.ours = ours;
+ }
+
+ Blog getBlog() {
+ return blog;
+ }
+
+ String getName() {
+ return blog.getName();
+ }
+
+ boolean isEmpty() {
+ return postCount == 0;
+ }
+
+ int getPostCount() {
+ return postCount;
+ }
+
+ long getTimestamp() {
+ return timestamp;
+ }
+
+ int getUnreadCount() {
+ return unread;
+ }
+
+ boolean isOurs() {
+ return ours;
+ }
+}
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
new file mode 100644
index 000000000..dbcce2965
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/blogs/BlogPostAdapter.java
@@ -0,0 +1,166 @@
+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;
+import org.briarproject.android.util.TrustIndicatorView;
+import org.briarproject.api.identity.Author;
+import org.briarproject.util.StringUtils;
+
+import java.util.Collection;
+
+import de.hdodenhof.circleimageview.CircleImageView;
+import im.delight.android.identicons.IdenticonDrawable;
+
+class BlogPostAdapter extends
+ RecyclerView.Adapter {
+
+ private SortedList posts = new SortedList<>(
+ BlogPostItem.class, new SortedList.Callback() {
+
+ @Override
+ public int compare(BlogPostItem a, BlogPostItem b) {
+ return a.compareTo(b);
+ }
+
+ @Override
+ public void onInserted(int position, int count) {
+ notifyItemRangeInserted(position, count);
+ }
+
+ @Override
+ public void onRemoved(int position, int count) {
+ notifyItemRangeRemoved(position, count);
+ }
+
+ @Override
+ public void onMoved(int fromPosition, int toPosition) {
+ notifyItemMoved(fromPosition, toPosition);
+ }
+
+ @Override
+ public void onChanged(int position, int count) {
+ notifyItemRangeChanged(position, count);
+ }
+
+ @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());
+ }
+ });
+
+ private final Context ctx;
+ private final OnBlogPostClickListener listener;
+
+ BlogPostAdapter(Context ctx, OnBlogPostClickListener listener) {
+ this.ctx = ctx;
+ this.listener = listener;
+ }
+
+ @Override
+ public BlogPostHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ View v = LayoutInflater.from(ctx).inflate(
+ R.layout.list_item_blog_post, parent, false);
+ return new BlogPostHolder(v);
+ }
+
+ @Override
+ public void onBindViewHolder(final BlogPostHolder ui, int position) {
+ final BlogPostItem post = getItem(position);
+
+ Author author = post.getAuthor();
+ IdenticonDrawable d = new IdenticonDrawable(author.getId().getBytes());
+ ui.avatar.setImageDrawable(d);
+ ui.author.setText(author.getName());
+ ui.trust.setTrustLevel(post.getAuthorStatus());
+
+ // date
+ ui.date.setText(
+ DateUtils.getRelativeTimeSpanString(ctx, post.getTimestamp()));
+
+ // post body
+ ui.body.setText(StringUtils.fromUtf8(post.getBody()));
+
+ ui.layout.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ listener.onBlogPostClick(ui.getAdapterPosition());
+ }
+ });
+ }
+
+ @Override
+ public int getItemCount() {
+ return posts.size();
+ }
+
+ public BlogPostItem getItem(int position) {
+ return posts.get(position);
+ }
+
+ public void add(BlogPostItem item) {
+ posts.add(item);
+ }
+
+ public void addAll(Collection items) {
+ posts.addAll(items);
+ }
+
+ public void remove(BlogPostItem item) {
+ posts.remove(item);
+ }
+
+ public void clear() {
+ posts.clear();
+ }
+
+ public boolean isEmpty() {
+ return posts.size() == 0;
+ }
+
+ 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) {
+ super(v);
+
+ layout = (ViewGroup) v;
+ avatar = (CircleImageView) v.findViewById(R.id.avatar);
+ 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);
+ }
+
+}
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);
+ }
+ }
+
+}
diff --git a/briar-android/src/org/briarproject/android/blogs/BlogPostItem.java b/briar-android/src/org/briarproject/android/blogs/BlogPostItem.java
new file mode 100644
index 000000000..cdb9c15ef
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/blogs/BlogPostItem.java
@@ -0,0 +1,73 @@
+package org.briarproject.android.blogs;
+
+import android.support.annotation.NonNull;
+
+import org.briarproject.api.blogs.BlogPostHeader;
+import org.briarproject.api.identity.Author;
+import org.briarproject.api.identity.Author.Status;
+import org.briarproject.api.sync.MessageId;
+
+// This class is not thread-safe
+class BlogPostItem implements Comparable {
+
+ private final BlogPostHeader header;
+ private final byte[] body;
+ private boolean read;
+
+ BlogPostItem(BlogPostHeader header, byte[] body) {
+ this.header = header;
+ this.body = body;
+ read = header.isRead();
+ }
+
+ public MessageId getId() {
+ return header.getId();
+ }
+
+ public String getTitle() {
+ return header.getTitle();
+ }
+
+ public byte[] getBody() {
+ return body;
+ }
+
+ public long getTimestamp() {
+ return header.getTimestamp();
+ }
+
+ public long getTimeReceived() {
+ return header.getTimeReceived();
+ }
+
+ public Author getAuthor() {
+ return header.getAuthor();
+ }
+
+ Status getAuthorStatus() {
+ return header.getAuthorStatus();
+ }
+
+ public void setRead(boolean read) {
+ this.read = read;
+ }
+
+ public boolean isRead() {
+ return read;
+ }
+
+ @Override
+ public int compareTo(@NonNull BlogPostItem other) {
+ if (this == other) return 0;
+ // The blog with the newest message comes first
+ long aTime = getTimeReceived(), bTime = other.getTimeReceived();
+ if (aTime > bTime) return -1;
+ if (aTime < bTime) return 1;
+ // Break ties by post title
+ if (getTitle() != null && other.getTitle() != null) {
+ return String.CASE_INSENSITIVE_ORDER
+ .compare(getTitle(), other.getTitle());
+ }
+ return 0;
+ }
+}
diff --git a/briar-android/src/org/briarproject/android/blogs/BlogsFragment.java b/briar-android/src/org/briarproject/android/blogs/BlogsFragment.java
index a736537ff..80f234010 100644
--- a/briar-android/src/org/briarproject/android/blogs/BlogsFragment.java
+++ b/briar-android/src/org/briarproject/android/blogs/BlogsFragment.java
@@ -15,6 +15,8 @@ import org.briarproject.R;
import org.briarproject.android.ActivityComponent;
import org.briarproject.android.fragment.BaseFragment;
+import static android.view.View.GONE;
+
public class BlogsFragment extends BaseFragment {
public final static String TAG = BlogsFragment.class.getName();
@@ -54,6 +56,8 @@ public class BlogsFragment extends BaseFragment {
viewPager.setAdapter(tabAdapter);
tabLayout.setupWithViewPager(viewPager);
+ tabLayout.setVisibility(GONE);
+
if (savedInstanceState != null) {
int position = savedInstanceState.getInt(SELECTED_TAB, 0);
viewPager.setCurrentItem(position);
@@ -88,16 +92,21 @@ public class BlogsFragment extends BaseFragment {
@Override
public int getCount() {
- return titles.length;
+ return 1;
+// return titles.length;
}
@Override
public Fragment getItem(int position) {
- switch (position) {
- // TODO add your fragments here
- default:
- return MyBlogsFragment.newInstance(position);
- }
+ return FeedFragment.newInstance();
+// switch (position) {
+// case 0:
+// return FeedFragment.newInstance();
+// case 1:
+// return new MyBlogsFragment();
+// default:
+// return BlogListFragment.newInstance(position);
+// }
}
@Override
diff --git a/briar-android/src/org/briarproject/android/blogs/CreateBlogActivity.java b/briar-android/src/org/briarproject/android/blogs/CreateBlogActivity.java
new file mode 100644
index 000000000..980d67d1d
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/blogs/CreateBlogActivity.java
@@ -0,0 +1,195 @@
+package org.briarproject.android.blogs;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.design.widget.TextInputEditText;
+import android.support.design.widget.TextInputLayout;
+import android.support.v4.app.ActivityCompat;
+import android.support.v4.app.ActivityOptionsCompat;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.Button;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+import android.widget.TextView.OnEditorActionListener;
+import android.widget.Toast;
+
+import org.briarproject.R;
+import org.briarproject.android.ActivityComponent;
+import org.briarproject.android.BriarActivity;
+import org.briarproject.api.blogs.Blog;
+import org.briarproject.api.blogs.BlogManager;
+import org.briarproject.api.db.DbException;
+import org.briarproject.api.identity.IdentityManager;
+import org.briarproject.api.identity.LocalAuthor;
+import org.briarproject.util.StringUtils;
+
+import java.util.Collection;
+import java.util.logging.Logger;
+
+import javax.inject.Inject;
+
+import static android.support.v4.app.ActivityOptionsCompat.makeCustomAnimation;
+import static android.view.View.GONE;
+import static android.view.View.VISIBLE;
+import static android.widget.Toast.LENGTH_LONG;
+import static java.util.logging.Level.INFO;
+import static java.util.logging.Level.WARNING;
+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.api.blogs.BlogConstants.MAX_BLOG_DESC_LENGTH;
+import static org.briarproject.api.blogs.BlogConstants.MAX_BLOG_TITLE_LENGTH;
+
+public class CreateBlogActivity extends BriarActivity
+ implements OnEditorActionListener, OnClickListener {
+
+ private static final Logger LOG =
+ Logger.getLogger(CreateBlogActivity.class.getName());
+
+ private TextInputEditText titleInput, descInput;
+ private Button button;
+ private ProgressBar progress;
+
+ // Fields that are accessed from background threads must be volatile
+ @Inject
+ protected volatile IdentityManager identityManager;
+ @Inject
+ volatile BlogManager blogManager;
+
+ @Override
+ public void onCreate(Bundle state) {
+ super.onCreate(state);
+
+ setContentView(R.layout.activity_create_blog);
+
+ TextInputLayout titleLayout =
+ (TextInputLayout) findViewById(R.id.titleLayout);
+ if (titleLayout != null) {
+ titleLayout.setCounterMaxLength(MAX_BLOG_TITLE_LENGTH);
+ }
+ titleInput = (TextInputEditText) findViewById(R.id.titleInput);
+ TextWatcher nameEntryWatcher = new TextWatcher() {
+ @Override
+ public void afterTextChanged(Editable s) {
+ }
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count,
+ int after) {
+ }
+ @Override
+ public void onTextChanged(CharSequence text, int start,
+ int lengthBefore, int lengthAfter) {
+ enableOrDisableCreateButton();
+ }
+ };
+ titleInput.setOnEditorActionListener(this);
+ titleInput.addTextChangedListener(nameEntryWatcher);
+
+ TextInputLayout descLayout =
+ (TextInputLayout) findViewById(R.id.descLayout);
+ if (descLayout != null) {
+ descLayout.setCounterMaxLength(MAX_BLOG_DESC_LENGTH);
+ }
+ descInput = (TextInputEditText) findViewById(R.id.descInput);
+ if (descInput != null) {
+ descInput.addTextChangedListener(nameEntryWatcher);
+ }
+
+ button = (Button) findViewById(R.id.createBlogButton);
+ if (button != null) {
+ button.setOnClickListener(this);
+ }
+
+ progress = (ProgressBar) findViewById(R.id.createBlogProgressBar);
+ }
+
+ @Override
+ public void injectActivity(ActivityComponent component) {
+ component.inject(this);
+ }
+
+ private void enableOrDisableCreateButton() {
+ if (progress == null) return; // Not created yet
+ button.setEnabled(validateTitle() && validateDescription());
+ }
+
+ @Override
+ public boolean onEditorAction(TextView textView, int actionId, KeyEvent e) {
+ descInput.requestFocus();
+ return true;
+ }
+
+ private boolean validateTitle() {
+ String name = titleInput.getText().toString();
+ int length = StringUtils.toUtf8(name).length;
+ return length <= MAX_BLOG_TITLE_LENGTH && length > 0;
+ }
+
+ private boolean validateDescription() {
+ String name = descInput.getText().toString();
+ int length = StringUtils.toUtf8(name).length;
+ return length <= MAX_BLOG_DESC_LENGTH && length > 0;
+ }
+
+ @Override
+ public void onClick(View view) {
+ if (view == button) {
+ hideSoftKeyboard(view);
+ if (!validateTitle()) return;
+ button.setVisibility(GONE);
+ progress.setVisibility(VISIBLE);
+ addBlog(titleInput.getText().toString(),
+ descInput.getText().toString());
+ }
+ }
+
+ private void addBlog(final String title, final String description) {
+ runOnDbThread(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ long now = System.currentTimeMillis();
+ Collection authors =
+ identityManager.getLocalAuthors();
+ // take first identity, don't support more for now
+ LocalAuthor author = authors.iterator().next();
+ Blog f = blogManager.addBlog(author, title, description);
+ long duration = System.currentTimeMillis() - now;
+ if (LOG.isLoggable(INFO))
+ LOG.info("Storing blog took " + duration + " ms");
+ displayBlog(f);
+ } catch (DbException e) {
+ // TODO show error, e.g. blog with same title exists
+ if (LOG.isLoggable(WARNING))
+ LOG.log(WARNING, e.toString(), e);
+ finishOnUiThread();
+ }
+ }
+ });
+ }
+
+ private void displayBlog(final Blog b) {
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ Intent i =
+ new Intent(CreateBlogActivity.this, BlogActivity.class);
+ i.putExtra(GROUP_ID, b.getId().getBytes());
+ i.putExtra(BLOG_NAME, b.getName());
+ i.putExtra(IS_MY_BLOG, true);
+ i.putExtra(IS_NEW_BLOG, true);
+ ActivityOptionsCompat options =
+ makeCustomAnimation(CreateBlogActivity.this,
+ android.R.anim.fade_in,
+ android.R.anim.fade_out);
+ ActivityCompat.startActivity(CreateBlogActivity.this, i,
+ options.toBundle());
+ supportFinishAfterTransition();
+ }
+ });
+ }
+}
diff --git a/briar-android/src/org/briarproject/android/blogs/FeedController.java b/briar-android/src/org/briarproject/android/blogs/FeedController.java
new file mode 100644
index 000000000..ad98adeef
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/blogs/FeedController.java
@@ -0,0 +1,25 @@
+package org.briarproject.android.blogs;
+
+import org.briarproject.android.controller.ActivityLifecycleController;
+import org.briarproject.android.controller.handler.UiResultHandler;
+import org.briarproject.api.blogs.Blog;
+
+import java.util.Collection;
+
+public interface FeedController {
+
+ void onResume();
+ void onPause();
+
+ void loadPosts(
+ final UiResultHandler> resultHandler);
+
+ void loadPersonalBlog(final UiResultHandler resultHandler);
+
+ void setOnBlogPostAddedListener(OnBlogPostAddedListener listener);
+
+ interface OnBlogPostAddedListener {
+ void onBlogPostAdded(final BlogPostItem post);
+ }
+
+}
diff --git a/briar-android/src/org/briarproject/android/blogs/FeedControllerImpl.java b/briar-android/src/org/briarproject/android/blogs/FeedControllerImpl.java
new file mode 100644
index 000000000..4e22757f6
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/blogs/FeedControllerImpl.java
@@ -0,0 +1,133 @@
+package org.briarproject.android.blogs;
+
+import org.briarproject.android.controller.DbControllerImpl;
+import org.briarproject.android.controller.handler.UiResultHandler;
+import org.briarproject.api.blogs.Blog;
+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.identity.Author;
+import org.briarproject.api.identity.IdentityManager;
+
+import java.util.ArrayList;
+import java.util.Collection;
+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 FeedControllerImpl extends DbControllerImpl
+ implements FeedController, EventListener {
+
+ private static final Logger LOG =
+ Logger.getLogger(FeedControllerImpl.class.getName());
+
+ @Inject
+ protected volatile BlogManager blogManager;
+ @Inject
+ protected volatile IdentityManager identityManager;
+ @Inject
+ protected volatile EventBus eventBus;
+
+ private volatile OnBlogPostAddedListener listener;
+
+ @Inject
+ FeedControllerImpl() {
+ }
+
+ public void onResume() {
+ eventBus.addListener(this);
+ }
+
+ public void onPause() {
+ eventBus.removeListener(this);
+ }
+
+ @Override
+ public void eventOccurred(Event e) {
+ if (!(e instanceof BlogPostAddedEvent)) return;
+
+ LOG.info("New blog post added");
+ if (listener != null) {
+ final BlogPostAddedEvent m = (BlogPostAddedEvent) e;
+ final BlogPostHeader header = m.getHeader();
+ try {
+ final byte[] body = blogManager.getPostBody(header.getId());
+ final BlogPostItem post = new BlogPostItem(header, body);
+ listener.onBlogPostAdded(post);
+ } catch (DbException ex) {
+ if (LOG.isLoggable(WARNING))
+ LOG.log(WARNING, ex.toString(), ex);
+ }
+ }
+ }
+
+ @Override
+ public void loadPosts(
+ final UiResultHandler> resultHandler) {
+
+ LOG.info("Loading blog posts...");
+ runOnDbThread(new Runnable() {
+ @Override
+ public void run() {
+ Collection posts = new ArrayList<>();
+ try {
+ // load blog posts
+ long now = System.currentTimeMillis();
+ for (Blog b : blogManager.getBlogs()) {
+ Collection header =
+ blogManager.getPostHeaders(b.getId());
+ for (BlogPostHeader h : header) {
+ byte[] body = blogManager.getPostBody(h.getId());
+ posts.add(new BlogPostItem(h, body));
+ }
+ }
+ long duration = System.currentTimeMillis() - now;
+ if (LOG.isLoggable(INFO))
+ LOG.info("Loading posts took " + duration + " ms");
+ resultHandler.onResult(posts);
+ } catch (DbException e) {
+ if (LOG.isLoggable(WARNING))
+ LOG.log(WARNING, e.toString(), e);
+ resultHandler.onResult(null);
+ }
+ }
+ });
+ }
+
+ @Override
+ public void loadPersonalBlog(final UiResultHandler resultHandler) {
+ LOG.info("Loading personal blog...");
+ runOnDbThread(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ // load blog posts
+ long now = System.currentTimeMillis();
+ Author a =
+ identityManager.getLocalAuthors().iterator().next();
+ Blog b = blogManager.getPersonalBlog(a);
+ long duration = System.currentTimeMillis() - now;
+ if (LOG.isLoggable(INFO))
+ LOG.info("Loading pers. blog took " + duration + " ms");
+ resultHandler.onResult(b);
+ } catch (DbException e) {
+ if (LOG.isLoggable(WARNING))
+ LOG.log(WARNING, e.toString(), e);
+ resultHandler.onResult(null);
+ }
+ }
+ });
+ }
+
+ @Override
+ public void setOnBlogPostAddedListener(OnBlogPostAddedListener listener) {
+ this.listener = listener;
+ }
+}
diff --git a/briar-android/src/org/briarproject/android/blogs/FeedFragment.java b/briar-android/src/org/briarproject/android/blogs/FeedFragment.java
new file mode 100644
index 000000000..bc7a1e457
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/blogs/FeedFragment.java
@@ -0,0 +1,203 @@
+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.ActivityOptionsCompat;
+import android.support.v4.content.ContextCompat;
+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.View.OnClickListener;
+import android.view.ViewGroup;
+
+import org.briarproject.R;
+import org.briarproject.android.ActivityComponent;
+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.blogs.Blog;
+
+import java.util.Collection;
+
+import javax.inject.Inject;
+
+import static android.app.Activity.RESULT_OK;
+import static android.support.design.widget.Snackbar.LENGTH_LONG;
+import static android.support.v4.app.ActivityOptionsCompat.makeCustomAnimation;
+import static org.briarproject.android.BriarActivity.GROUP_ID;
+import static org.briarproject.android.blogs.BlogActivity.BLOG_NAME;
+import static org.briarproject.android.blogs.BlogActivity.REQUEST_WRITE_POST;
+
+public class FeedFragment extends BaseFragment implements
+ OnBlogPostClickListener, FeedController.OnBlogPostAddedListener {
+
+ public final static String TAG = FeedFragment.class.getName();
+
+ @Inject
+ FeedController feedController;
+
+ private BlogPostAdapter adapter;
+ private LinearLayoutManager layoutManager;
+ private BriarRecyclerView list;
+ private Blog personalBlog = null;
+
+ static FeedFragment newInstance() {
+ FeedFragment f = new FeedFragment();
+
+ Bundle args = new Bundle();
+ f.setArguments(args);
+
+ return f;
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+
+ setHasOptionsMenu(true);
+ View v = inflater.inflate(R.layout.fragment_blog, container, false);
+
+ adapter = new BlogPostAdapter(getActivity(), this);
+
+ layoutManager = new LinearLayoutManager(getActivity());
+ list = (BriarRecyclerView) v.findViewById(R.id.postList);
+ list.setLayoutManager(layoutManager);
+ list.setAdapter(adapter);
+ list.setEmptyText(R.string.blogs_feed_empty_state);
+
+ return v;
+ }
+
+ @Override
+ public void injectFragment(ActivityComponent component) {
+ component.inject(this);
+ feedController.setOnBlogPostAddedListener(this);
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+
+ // The BlogPostAddedEvent arrives when the controller is not listening
+ if (requestCode == REQUEST_WRITE_POST && resultCode == RESULT_OK) {
+ showSnackBar(R.string.blogs_blog_post_created);
+ }
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ feedController
+ .loadPersonalBlog(new UiResultHandler(getActivity()) {
+ @Override
+ public void onResultUi(Blog b) {
+ personalBlog = b;
+ }
+ });
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ feedController.onResume();
+ feedController.loadPosts(
+ new UiResultHandler>(getActivity()) {
+ @Override
+ public void onResultUi(Collection posts) {
+ if (posts == null) {
+ // TODO show error?
+ } else if (posts.isEmpty()) {
+ list.showData();
+ } else {
+ adapter.addAll(posts);
+ }
+ }
+ });
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ feedController.onPause();
+ // TODO save list position in database/preferences?
+ }
+
+ @Override
+ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+ inflater.inflate(R.menu.blogs_feed_actions, menu);
+ super.onCreateOptionsMenu(menu, inflater);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(final MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.action_write_blog_post:
+ if (personalBlog == null) return false;
+ Intent i =
+ new Intent(getActivity(), WriteBlogPostActivity.class);
+ i.putExtra(GROUP_ID, personalBlog.getId().getBytes());
+ i.putExtra(BLOG_NAME, personalBlog.getName());
+ ActivityOptionsCompat options =
+ makeCustomAnimation(getActivity(),
+ android.R.anim.slide_in_left,
+ android.R.anim.slide_out_right);
+ startActivityForResult(i, REQUEST_WRITE_POST,
+ options.toBundle());
+ return true;
+ default:
+ return super.onOptionsItemSelected(item);
+ }
+ }
+
+ @Override
+ public void onBlogPostAdded(final BlogPostItem post) {
+ getActivity().runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ adapter.add(post);
+ showSnackBar(R.string.blogs_blog_post_received);
+ }
+ });
+ }
+
+ @Override
+ public void onBlogPostClick(int position) {
+ // noop
+ }
+
+ @Override
+ public String getUniqueTag() {
+ return TAG;
+ }
+
+ private void showSnackBar(int stringRes) {
+ int firstVisible =
+ layoutManager.findFirstCompletelyVisibleItemPosition();
+ int lastVisible = layoutManager.findLastCompletelyVisibleItemPosition();
+ int count = adapter.getItemCount();
+ boolean scroll = count > (lastVisible - firstVisible + 1);
+
+ Snackbar s = Snackbar.make(list, stringRes, LENGTH_LONG);
+ s.getView().setBackgroundResource(R.color.briar_primary);
+ if (scroll) {
+ OnClickListener onClick = new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ list.smoothScrollToPosition(0);
+ }
+ };
+ s.setActionTextColor(ContextCompat
+ .getColor(getContext(),
+ R.color.briar_button_positive));
+ s.setAction(R.string.blogs_blog_post_scroll_to, onClick);
+ }
+ s.show();
+ }
+}
diff --git a/briar-android/src/org/briarproject/android/blogs/MyBlogsFragment.java b/briar-android/src/org/briarproject/android/blogs/MyBlogsFragment.java
index 572bc516e..1cc4ee8ac 100644
--- a/briar-android/src/org/briarproject/android/blogs/MyBlogsFragment.java
+++ b/briar-android/src/org/briarproject/android/blogs/MyBlogsFragment.java
@@ -1,49 +1,113 @@
package org.briarproject.android.blogs;
+import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.Nullable;
+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.TextView;
import org.briarproject.R;
import org.briarproject.android.ActivityComponent;
import org.briarproject.android.fragment.BaseFragment;
+import org.briarproject.android.util.BriarRecyclerView;
+import org.briarproject.api.blogs.Blog;
+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.api.identity.IdentityManager;
+import org.briarproject.api.identity.LocalAuthor;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.logging.Logger;
import javax.inject.Inject;
+import static android.support.v4.app.ActivityOptionsCompat.makeCustomAnimation;
+import static java.util.logging.Level.INFO;
+import static java.util.logging.Level.WARNING;
+
public class MyBlogsFragment extends BaseFragment {
public final static String TAG = MyBlogsFragment.class.getName();
+ private static final Logger LOG = Logger.getLogger(TAG);
+ private BriarRecyclerView list;
+ private BlogListAdapter adapter;
+
+ // Fields that are accessed from background threads must be volatile
+ @Inject
+ protected volatile IdentityManager identityManager;
+ @Inject
+ volatile BlogManager blogManager;
+
@Inject
public MyBlogsFragment() {
}
- static MyBlogsFragment newInstance(int num) {
- MyBlogsFragment f = new MyBlogsFragment();
-
- Bundle args = new Bundle();
- args.putInt("num", num);
- f.setArguments(args);
-
- return f;
- }
-
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
- View v = inflater.inflate(R.layout.fragment_blogs_my, container,
- false);
+ setHasOptionsMenu(true);
- TextView numView = (TextView) v.findViewById(R.id.num);
- String num = String.valueOf(getArguments().getInt("num"));
- numView.setText(num);
+ adapter = new BlogListAdapter(getActivity());
- return v;
+ list = (BriarRecyclerView) inflater
+ .inflate(R.layout.fragment_blogs_my, container, false);
+ list.setLayoutManager(new LinearLayoutManager(getActivity()));
+ list.setAdapter(adapter);
+ list.setEmptyText(getString(R.string.blogs_my_blogs_empty_state));
+
+ return list;
+ }
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ listener.getActivityComponent().inject(this);
+ // Starting from here, we can use injected objects
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ adapter.clear();
+ loadBlogs();
+ }
+
+ @Override
+ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+ inflater.inflate(R.menu.blogs_my_actions, menu);
+ super.onCreateOptionsMenu(menu, inflater);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(final MenuItem item) {
+ // Handle presses on the action bar items
+ switch (item.getItemId()) {
+ case R.id.action_create_blog:
+ Intent intent =
+ new Intent(getContext(), CreateBlogActivity.class);
+ ActivityOptionsCompat options =
+ makeCustomAnimation(getActivity(),
+ android.R.anim.slide_in_left,
+ android.R.anim.slide_out_right);
+ ActivityCompat.startActivity(getActivity(), intent,
+ options.toBundle());
+ return true;
+ default:
+ return super.onOptionsItemSelected(item);
+ }
}
@Override
@@ -56,4 +120,49 @@ public class MyBlogsFragment extends BaseFragment {
component.inject(this);
}
+ private void loadBlogs() {
+ listener.runOnDbThread(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ // load blogs
+ long now = System.currentTimeMillis();
+ Collection blogs = new ArrayList<>();
+ Collection authors =
+ identityManager.getLocalAuthors();
+ LocalAuthor a = authors.iterator().next();
+ for (Blog b : blogManager.getBlogs(a)) {
+ try {
+ Collection headers =
+ blogManager.getPostHeaders(b.getId());
+ blogs.add(new BlogListItem(b, headers, true));
+ } catch (NoSuchGroupException e) {
+ // Continue
+ }
+ }
+ displayBlogs(blogs);
+ long duration = System.currentTimeMillis() - now;
+ if (LOG.isLoggable(INFO))
+ LOG.info("Full blog load took " + duration + " ms");
+ } catch (DbException e) {
+ if (LOG.isLoggable(WARNING))
+ LOG.log(WARNING, e.toString(), e);
+ }
+ }
+ });
+ }
+
+ private void displayBlogs(final Collection items) {
+ listener.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ if (items.size() == 0) {
+ list.showData();
+ } else {
+ adapter.addAll(items);
+ }
+ }
+ });
+ }
+
}
diff --git a/briar-android/src/org/briarproject/android/blogs/WriteBlogPostActivity.java b/briar-android/src/org/briarproject/android/blogs/WriteBlogPostActivity.java
new file mode 100644
index 000000000..4d9cc6151
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/blogs/WriteBlogPostActivity.java
@@ -0,0 +1,200 @@
+package org.briarproject.android.blogs;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.design.widget.TextInputEditText;
+import android.support.design.widget.TextInputLayout;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.view.KeyEvent;
+import android.view.View;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+import android.widget.TextView.OnEditorActionListener;
+
+import org.briarproject.R;
+import org.briarproject.android.ActivityComponent;
+import org.briarproject.android.BriarActivity;
+import org.briarproject.api.FormatException;
+import org.briarproject.api.blogs.BlogManager;
+import org.briarproject.api.blogs.BlogPost;
+import org.briarproject.api.blogs.BlogPostFactory;
+import org.briarproject.api.db.DbException;
+import org.briarproject.api.identity.IdentityManager;
+import org.briarproject.api.identity.LocalAuthor;
+import org.briarproject.api.sync.GroupId;
+import org.briarproject.util.StringUtils;
+
+import java.security.GeneralSecurityException;
+import java.util.Collection;
+import java.util.logging.Logger;
+
+import javax.inject.Inject;
+
+import static android.view.View.GONE;
+import static android.view.View.VISIBLE;
+import static java.util.logging.Level.WARNING;
+import static org.briarproject.api.blogs.BlogConstants.MAX_BLOG_POST_BODY_LENGTH;
+import static org.briarproject.api.blogs.BlogConstants.MAX_BLOG_POST_TITLE_LENGTH;
+
+public class WriteBlogPostActivity extends BriarActivity
+ implements OnEditorActionListener {
+
+ private static final Logger LOG =
+ Logger.getLogger(WriteBlogPostActivity.class.getName());
+ private static final String contentType = "text/plain";
+
+ private TextInputEditText titleInput;
+ private EditText bodyInput;
+ private Button publishButton;
+ private ProgressBar progressBar;
+
+ // Fields that are accessed from background threads must be volatile
+ private volatile GroupId groupId;
+ @Inject
+ protected volatile IdentityManager identityManager;
+ @Inject
+ volatile BlogPostFactory blogPostFactory;
+ @Inject
+ volatile BlogManager blogManager;
+
+ @Override
+ public void onCreate(Bundle state) {
+ super.onCreate(state);
+
+ Intent i = getIntent();
+ byte[] b = i.getByteArrayExtra(GROUP_ID);
+ if (b == null) throw new IllegalStateException("No Group in intent.");
+ groupId = new GroupId(b);
+// String blogName = i.getStringExtra(BLOG_NAME);
+// if (blogName != null) setTitle(blogName);
+
+ setContentView(R.layout.activity_write_blog_post);
+// String title =
+// getTitle() + ": " + getString(R.string.blogs_write_blog_post);
+// setTitle(title);
+
+ TextInputLayout titleLayout =
+ (TextInputLayout) findViewById(R.id.titleLayout);
+ if (titleLayout != null) {
+ titleLayout.setCounterMaxLength(MAX_BLOG_POST_TITLE_LENGTH);
+ }
+ titleInput = (TextInputEditText) findViewById(R.id.titleInput);
+ if (titleInput != null) {
+ titleInput.setOnEditorActionListener(this);
+ }
+
+ bodyInput = (EditText) findViewById(R.id.bodyInput);
+ bodyInput.addTextChangedListener(new TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count,
+ int after) {
+ }
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before,
+ int count) {
+ }
+
+ @Override
+ public void afterTextChanged(Editable s) {
+ enableOrDisablePublishButton();
+ }
+ });
+
+ publishButton = (Button) findViewById(R.id.publishButton);
+ publishButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ publish();
+ }
+ });
+
+ progressBar = (ProgressBar) findViewById(R.id.progressBar);
+ }
+
+ @Override
+ public void injectActivity(ActivityComponent component) {
+ component.inject(this);
+ }
+
+ @Override
+ public boolean onEditorAction(TextView textView, int actionId, KeyEvent e) {
+ bodyInput.requestFocus();
+ return true;
+ }
+
+ private void enableOrDisablePublishButton() {
+ int bodyLength =
+ StringUtils.toUtf8(bodyInput.getText().toString()).length;
+ if (bodyLength > 0 && bodyLength <= MAX_BLOG_POST_BODY_LENGTH &&
+ titleInput.getText().length() <= MAX_BLOG_POST_TITLE_LENGTH)
+ publishButton.setEnabled(true);
+ else
+ publishButton.setEnabled(false);
+ }
+
+ private void publish() {
+ // title
+ String title = titleInput.getText().toString();
+ if (title.length() > MAX_BLOG_POST_TITLE_LENGTH) return;
+ if (title.length() == 0) title = null;
+
+ // body
+ byte[] body = StringUtils.toUtf8(bodyInput.getText().toString());
+
+ // hide publish button, show progress bar
+ publishButton.setVisibility(GONE);
+ progressBar.setVisibility(VISIBLE);
+
+ storePost(title, body);
+ }
+
+ private void storePost(final String title, final byte[] body) {
+ runOnDbThread(new Runnable() {
+ @Override
+ public void run() {
+ long now = System.currentTimeMillis();
+ try {
+ Collection authors =
+ identityManager.getLocalAuthors();
+ LocalAuthor author = authors.iterator().next();
+ BlogPost p = blogPostFactory
+ .createBlogPost(groupId, title, now, null, author,
+ contentType, body);
+ blogManager.addLocalPost(p);
+ postPublished();
+ } catch (DbException | GeneralSecurityException | FormatException e) {
+ if (LOG.isLoggable(WARNING))
+ LOG.log(WARNING, e.toString(), e);
+ postFailedToPublish();
+ }
+ }
+ });
+ }
+
+ private void postPublished() {
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ setResult(RESULT_OK);
+ supportFinishAfterTransition();
+ }
+ });
+ }
+
+ private void postFailedToPublish() {
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ // hide progress bar, show publish button
+ progressBar.setVisibility(GONE);
+ publishButton.setVisibility(VISIBLE);
+ // TODO show error
+ }
+ });
+ }
+
+}
diff --git a/briar-android/src/org/briarproject/android/forum/ForumListAdapter.java b/briar-android/src/org/briarproject/android/forum/ForumListAdapter.java
index 17fd01346..88759804a 100644
--- a/briar-android/src/org/briarproject/android/forum/ForumListAdapter.java
+++ b/briar-android/src/org/briarproject/android/forum/ForumListAdapter.java
@@ -104,16 +104,17 @@ class ForumListAdapter extends
// Post Count
int postCount = item.getPostCount();
if (postCount > 0) {
- ui.unread.setText(ctx.getResources()
- .getQuantityString(R.plurals.forum_posts, postCount,
+ ui.avatar.setProblem(false);
+ ui.postCount.setText(ctx.getResources()
+ .getQuantityString(R.plurals.posts, postCount,
postCount));
- ui.unread.setTextColor(
+ ui.postCount.setTextColor(
ContextCompat
.getColor(ctx, R.color.briar_text_secondary));
} else {
ui.avatar.setProblem(true);
- ui.unread.setText(ctx.getString(R.string.forum_no_posts));
- ui.unread.setTextColor(
+ ui.postCount.setText(ctx.getString(R.string.no_posts));
+ ui.postCount.setTextColor(
ContextCompat
.getColor(ctx, R.color.briar_text_tertiary));
}
@@ -187,7 +188,7 @@ class ForumListAdapter extends
private final ViewGroup layout;
private final TextAvatarView avatar;
private final TextView name;
- private final TextView unread;
+ private final TextView postCount;
private final TextView date;
ForumViewHolder(View v) {
@@ -196,7 +197,7 @@ class ForumListAdapter extends
layout = (ViewGroup) v;
avatar = (TextAvatarView) v.findViewById(R.id.avatarView);
name = (TextView) v.findViewById(R.id.forumNameView);
- unread = (TextView) v.findViewById(R.id.unreadView);
+ postCount = (TextView) v.findViewById(R.id.postCountView);
date = (TextView) v.findViewById(R.id.dateView);
}
}
diff --git a/briar-android/src/org/briarproject/android/util/AndroidUtils.java b/briar-android/src/org/briarproject/android/util/AndroidUtils.java
index 252d95574..077dc8aa5 100644
--- a/briar-android/src/org/briarproject/android/util/AndroidUtils.java
+++ b/briar-android/src/org/briarproject/android/util/AndroidUtils.java
@@ -57,6 +57,11 @@ public class AndroidUtils {
til.setError(null);
}
+ public static void setError(TextInputLayout til, int res,
+ boolean condition) {
+ setError(til, til.getContext().getString(res), condition);
+ }
+
public static String getBluetoothAddress(Context ctx,
BluetoothAdapter adapter) {
// Return the adapter's address if it's valid and not fake
diff --git a/briar-android/src/org/briarproject/android/util/BriarRecyclerView.java b/briar-android/src/org/briarproject/android/util/BriarRecyclerView.java
index 7563354b6..6cd36dd6b 100644
--- a/briar-android/src/org/briarproject/android/util/BriarRecyclerView.java
+++ b/briar-android/src/org/briarproject/android/util/BriarRecyclerView.java
@@ -130,6 +130,11 @@ public class BriarRecyclerView extends FrameLayout {
emptyView.setText(text);
}
+ public void setEmptyText(int res) {
+ if (recyclerView == null) initViews();
+ emptyView.setText(res);
+ }
+
public void showProgressBar() {
if (recyclerView == null) initViews();
recyclerView.setVisibility(INVISIBLE);
@@ -158,6 +163,11 @@ public class BriarRecyclerView extends FrameLayout {
recyclerView.scrollToPosition(position);
}
+ public void smoothScrollToPosition(int position) {
+ if (recyclerView == null) initViews();
+ recyclerView.smoothScrollToPosition(position);
+ }
+
public RecyclerView getRecyclerView() {
return this.recyclerView;
}
diff --git a/briar-android/src/org/briarproject/android/util/TextAvatarView.java b/briar-android/src/org/briarproject/android/util/TextAvatarView.java
index cc4ab9148..6033021eb 100644
--- a/briar-android/src/org/briarproject/android/util/TextAvatarView.java
+++ b/briar-android/src/org/briarproject/android/util/TextAvatarView.java
@@ -38,7 +38,7 @@ public class TextAvatarView extends FrameLayout {
}
public void setText(String text) {
- character.setText(text);
+ character.setText(text.toUpperCase());
}
public void setUnreadCount(int count) {
diff --git a/briar-android/src/org/briarproject/android/util/TrustIndicatorView.java b/briar-android/src/org/briarproject/android/util/TrustIndicatorView.java
index ce01bf301..a9ea3c8fb 100644
--- a/briar-android/src/org/briarproject/android/util/TrustIndicatorView.java
+++ b/briar-android/src/org/briarproject/android/util/TrustIndicatorView.java
@@ -8,6 +8,8 @@ import android.widget.ImageView;
import org.briarproject.R;
import org.briarproject.api.identity.Author.Status;
+import static org.briarproject.api.identity.Author.Status.OURSELVES;
+
public class TrustIndicatorView extends ImageView {
public TrustIndicatorView(Context context) {
@@ -24,6 +26,11 @@ public class TrustIndicatorView extends ImageView {
}
public void setTrustLevel(Status status) {
+ if (status == OURSELVES) {
+ setVisibility(GONE);
+ return;
+ }
+
int res;
switch (status) {
case ANONYMOUS:
@@ -39,6 +46,7 @@ public class TrustIndicatorView extends ImageView {
res = R.drawable.trust_indicator_unknown;
}
setImageDrawable(ContextCompat.getDrawable(getContext(), res));
+ setVisibility(VISIBLE);
}
}
diff --git a/briar-api/src/org/briarproject/api/identity/Author.java b/briar-api/src/org/briarproject/api/identity/Author.java
index 9aa543b3a..de15869d3 100644
--- a/briar-api/src/org/briarproject/api/identity/Author.java
+++ b/briar-api/src/org/briarproject/api/identity/Author.java
@@ -5,7 +5,7 @@ import java.io.UnsupportedEncodingException;
/** A pseudonym for a user. */
public class Author {
- public enum Status { ANONYMOUS, UNKNOWN, UNVERIFIED, VERIFIED }
+ public enum Status { ANONYMOUS, UNKNOWN, UNVERIFIED, VERIFIED, OURSELVES }
private final AuthorId id;
private final String name;
diff --git a/briar-core/src/org/briarproject/identity/IdentityManagerImpl.java b/briar-core/src/org/briarproject/identity/IdentityManagerImpl.java
index 5b1434ad1..bd14c3340 100644
--- a/briar-core/src/org/briarproject/identity/IdentityManagerImpl.java
+++ b/briar-core/src/org/briarproject/identity/IdentityManagerImpl.java
@@ -17,6 +17,7 @@ import java.util.concurrent.CopyOnWriteArrayList;
import javax.inject.Inject;
+import static org.briarproject.api.identity.Author.Status.OURSELVES;
import static org.briarproject.api.identity.Author.Status.UNKNOWN;
import static org.briarproject.api.identity.Author.Status.VERIFIED;
@@ -110,7 +111,7 @@ class IdentityManagerImpl implements IdentityManager {
throws DbException {
// Compare to the IDs of the user's identities
for (LocalAuthor a : db.getLocalAuthors(txn))
- if (a.getId().equals(authorId)) return VERIFIED;
+ if (a.getId().equals(authorId)) return OURSELVES;
// Compare to the IDs of contacts' identities
for (Contact c : db.getContacts(txn))
if (c.getAuthor().getId().equals(authorId)) return VERIFIED;