diff --git a/briar-android/res/layout/activity_blog.xml b/briar-android/res/layout/activity_blog.xml index c0ac19fbe0eac0b76817a2c7bc9bb8566f6aa840..3ff8d409e0e60ee2b749b2e51726b8af8e5cab7c 100644 --- a/briar-android/res/layout/activity_blog.xml +++ b/briar-android/res/layout/activity_blog.xml @@ -1,10 +1,21 @@ <?xml version="1.0" encoding="utf-8"?> -<org.briarproject.android.util.BriarRecyclerView - android:id="@+id/postList" +<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" - android:layout_height="match_parent" - app:scrollToEnd="false" - tools:context=".android.blogs.BlogActivity"/> + android:layout_height="match_parent"> + + <android.support.v4.view.ViewPager + android:id="@+id/pager" + android:layout_width="match_parent" + android:layout_height="match_parent" + tools:context=".android.blogs.BlogActivity"/> + + <ProgressBar + android:id="@+id/progressBar" + style="?android:attr/progressBarStyleLarge" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center"/> + +</FrameLayout> \ No newline at end of file diff --git a/briar-android/res/layout/fragment_blog.xml b/briar-android/res/layout/fragment_blog.xml new file mode 100644 index 0000000000000000000000000000000000000000..c0ac19fbe0eac0b76817a2c7bc9bb8566f6aa840 --- /dev/null +++ b/briar-android/res/layout/fragment_blog.xml @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="utf-8"?> +<org.briarproject.android.util.BriarRecyclerView + android:id="@+id/postList" + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent" + app:scrollToEnd="false" + tools:context=".android.blogs.BlogActivity"/> 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 0000000000000000000000000000000000000000..ee02e64fbbf26756b845ebd18e3c0ce7d86ede78 --- /dev/null +++ b/briar-android/res/layout/fragment_blog_post.xml @@ -0,0 +1,77 @@ +<?xml version="1.0" encoding="utf-8"?> +<ScrollView + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="wrap_content"> + + <RelativeLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:padding="@dimen/margin_activity_horizontal"> + + <de.hdodenhof.circleimageview.CircleImageView + android:id="@+id/avatar" + style="@style/BriarAvatar" + android:layout_width="30dp" + android:layout_height="30dp" + android:layout_marginRight="@dimen/margin_medium" + tools:src="@drawable/ic_launcher"/> + + <TextView + android:id="@+id/authorName" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignTop="@+id/avatar" + android:layout_toEndOf="@+id/avatar" + android:layout_toRightOf="@+id/avatar" + android:textSize="@dimen/text_size_tiny" + tools:text="Author Name"/> + + <TextView + android:id="@+id/date" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignBottom="@id/avatar" + android:layout_below="@+id/authorName" + android:layout_toEndOf="@+id/avatar" + android:layout_toRightOf="@+id/avatar" + android:gravity="bottom" + android:textSize="@dimen/text_size_tiny" + tools:text="yesterday"/> + + <org.briarproject.android.util.TrustIndicatorView + android:id="@+id/trustIndicator" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginLeft="@dimen/margin_small" + android:layout_toRightOf="@+id/authorName" + tools:src="@drawable/trust_indicator_verified"/> + + <TextView + android:id="@+id/title" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentLeft="true" + android:layout_alignParentStart="true" + android:layout_below="@+id/avatar" + android:layout_marginTop="@dimen/margin_medium" + android:textSize="@dimen/text_size_medium" + android:textStyle="bold" + tools:text="This Is A Blog Post Title"/> + + <TextView + android:id="@+id/body" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentEnd="true" + android:layout_alignParentLeft="true" + android:layout_alignParentRight="true" + android:layout_alignParentStart="true" + android:layout_below="@+id/title" + android:layout_marginTop="@dimen/margin_medium" + tools:text="Body of Blog Post. This could be insanely large or just a short text as well."/> + + </RelativeLayout> + +</ScrollView> diff --git a/briar-android/res/values/strings.xml b/briar-android/res/values/strings.xml index 793e97d7a50f08635359e75ab09ce45e1d6c5340..d283c5c978ccdc6b3803b3532c97a5e72297951c 100644 --- a/briar-android/res/values/strings.xml +++ b/briar-android/res/values/strings.xml @@ -267,6 +267,8 @@ <string name="blogs_write_blog_post_title_hint">Add a title (optional)</string> <string name="blogs_write_blog_post_body_hint">Type your blog post here</string> <string name="blogs_publish_blog_post">Publish</string> + <string name="blogs_blog_failed_to_load">Blog failed to load</string> + <string name="blogs_blog_post_failed_to_load">Blog Post failed to load</string> <string name="blogs_blog_list">Blog List</string> <string name="blogs_available_blogs">Available Blogs</string> diff --git a/briar-android/src/org/briarproject/android/ActivityComponent.java b/briar-android/src/org/briarproject/android/ActivityComponent.java index c55e56f831a719fb5f2a7878cad94600e0731637..464fddd6f64fc12a6791b4d3af9aafb24e5d1496 100644 --- a/briar-android/src/org/briarproject/android/ActivityComponent.java +++ b/briar-android/src/org/briarproject/android/ActivityComponent.java @@ -3,6 +3,8 @@ package org.briarproject.android; import android.app.Activity; import org.briarproject.android.blogs.BlogActivity; +import org.briarproject.android.blogs.BlogFragment; +import org.briarproject.android.blogs.BlogPostFragment; import org.briarproject.android.blogs.CreateBlogActivity; import org.briarproject.android.blogs.MyBlogsFragment; import org.briarproject.android.contact.ContactListFragment; @@ -73,6 +75,10 @@ public interface ActivityComponent { void inject(WriteBlogPostActivity activity); + void inject(BlogFragment fragment); + + void inject(BlogPostFragment fragment); + void inject(SettingsActivity activity); void inject(ChangePasswordActivity activity); diff --git a/briar-android/src/org/briarproject/android/ActivityModule.java b/briar-android/src/org/briarproject/android/ActivityModule.java index ea984bca0bab42760c652869ea561f87c85e69ee..d9879e3420e8fa0aa44393ef7f5d10af4ecd8686 100644 --- a/briar-android/src/org/briarproject/android/ActivityModule.java +++ b/briar-android/src/org/briarproject/android/ActivityModule.java @@ -4,6 +4,8 @@ import android.app.Activity; import android.content.Context; import android.content.SharedPreferences; +import org.briarproject.android.blogs.BlogController; +import org.briarproject.android.blogs.BlogControllerImpl; import org.briarproject.android.controller.BriarController; import org.briarproject.android.controller.BriarControllerImpl; import org.briarproject.android.controller.ConfigController; @@ -107,6 +109,13 @@ public class ActivityModule { return forumController; } + @ActivityScope + @Provides + BlogController provideBlogController(BlogControllerImpl blogController) { + activity.addLifecycleController(blogController); + return blogController; + } + @ActivityScope @Provides protected NavDrawerController provideNavDrawerController( diff --git a/briar-android/src/org/briarproject/android/AndroidComponent.java b/briar-android/src/org/briarproject/android/AndroidComponent.java index d6a495eb0ff57af79c6f3774f2aec8422aadb370..3e59233be3dc1542bfa2b4867cb90197cc5c6e39 100644 --- a/briar-android/src/org/briarproject/android/AndroidComponent.java +++ b/briar-android/src/org/briarproject/android/AndroidComponent.java @@ -5,6 +5,7 @@ import org.briarproject.CoreModule; import org.briarproject.android.api.AndroidExecutor; import org.briarproject.android.api.AndroidNotificationManager; import org.briarproject.android.api.ReferenceManager; +import org.briarproject.android.blogs.BlogPersistentData; import org.briarproject.android.forum.ForumPersistentData; import org.briarproject.android.report.BriarReportSender; import org.briarproject.api.blogs.BlogManager; @@ -118,6 +119,8 @@ public interface AndroidComponent extends CoreEagerSingletons { ForumPersistentData forumPersistentData(); + BlogPersistentData blogPersistentData(); + @IoExecutor Executor ioExecutor(); diff --git a/briar-android/src/org/briarproject/android/AppModule.java b/briar-android/src/org/briarproject/android/AppModule.java index 36933a1184aec4c6df22703b391d15046b1c0750..825e639c6c69ab442b1d85d388b2769e68920733 100644 --- a/briar-android/src/org/briarproject/android/AppModule.java +++ b/briar-android/src/org/briarproject/android/AppModule.java @@ -4,6 +4,7 @@ import android.app.Application; import org.briarproject.android.api.AndroidNotificationManager; import org.briarproject.android.api.ReferenceManager; +import org.briarproject.android.blogs.BlogPersistentData; import org.briarproject.android.forum.ForumPersistentData; import org.briarproject.api.crypto.CryptoComponent; import org.briarproject.api.crypto.PublicKey; @@ -143,4 +144,10 @@ public class AppModule { ForumPersistentData provideForumPersistence() { return new ForumPersistentData(); } + + @Provides + @Singleton + BlogPersistentData provideBlogPersistence() { + return new BlogPersistentData(); + } } diff --git a/briar-android/src/org/briarproject/android/blogs/BlogActivity.java b/briar-android/src/org/briarproject/android/blogs/BlogActivity.java index aed08eb1e2cdcada4284b84303f858a63988cc8d..35321f390a96ee09c356de2b294552cc812d9696 100644 --- a/briar-android/src/org/briarproject/android/blogs/BlogActivity.java +++ b/briar-android/src/org/briarproject/android/blogs/BlogActivity.java @@ -2,180 +2,260 @@ package org.briarproject.android.blogs; import android.content.Intent; import android.os.Bundle; -import android.support.design.widget.Snackbar; -import android.support.v4.app.ActivityCompat; -import android.support.v4.app.ActivityOptionsCompat; -import android.support.v7.widget.LinearLayoutManager; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentManager; +import android.support.v4.app.FragmentStatePagerAdapter; +import android.support.v4.view.ViewPager; +import android.view.ViewGroup; +import android.widget.ProgressBar; +import android.widget.Toast; import org.briarproject.R; import org.briarproject.android.ActivityComponent; import org.briarproject.android.BriarActivity; -import org.briarproject.android.util.BriarRecyclerView; -import org.briarproject.api.blogs.BlogManager; -import org.briarproject.api.blogs.BlogPostHeader; -import org.briarproject.api.db.DbException; -import org.briarproject.api.db.NoSuchGroupException; +import org.briarproject.android.blogs.BlogController.BlogPostListener; +import org.briarproject.android.blogs.BlogPostAdapter.OnBlogPostClickListener; +import org.briarproject.android.controller.handler.UiResultHandler; +import org.briarproject.android.fragment.BaseFragment.BaseFragmentListener; import org.briarproject.api.sync.GroupId; +import org.briarproject.api.sync.MessageId; -import java.util.ArrayList; import java.util.Collection; import java.util.logging.Logger; import javax.inject.Inject; -import static android.support.design.widget.Snackbar.LENGTH_LONG; -import static android.support.v4.app.ActivityOptionsCompat.makeCustomAnimation; -import static java.util.logging.Level.INFO; -import static java.util.logging.Level.WARNING; +import static android.view.View.GONE; +import static android.view.View.VISIBLE; +import static android.widget.Toast.LENGTH_SHORT; -public class BlogActivity extends BriarActivity { +public class BlogActivity extends BriarActivity implements BlogPostListener, + OnBlogPostClickListener, BaseFragmentListener { + static final int REQUEST_WRITE_POST = 1; static final String BLOG_NAME = "briar.BLOG_NAME"; static final String IS_MY_BLOG = "briar.IS_MY_BLOG"; static final String IS_NEW_BLOG = "briar.IS_NEW_BLOG"; - private static final int WRITE_POST = 1; + private static final String BLOG_PAGER_ADAPTER = "briar.BLOG_PAGER_ADAPTER"; private static final Logger LOG = Logger.getLogger(BlogActivity.class.getName()); - private BlogPostAdapter adapter; - private BriarRecyclerView list; + private ProgressBar progressBar; + private ViewPager pager; + private BlogPagerAdapter blogPagerAdapter; + private BlogPostPagerAdapter postPagerAdapter; private String blogName; - private boolean myBlog; + private boolean myBlog, isNew; // Fields that are accessed from background threads must be volatile private volatile GroupId groupId = null; - private volatile boolean scrollToTop = false; @Inject - volatile BlogManager blogManager; + BlogController blogController; @Override public void onCreate(Bundle state) { super.onCreate(state); - setContentView(R.layout.activity_blog); - + // GroupId from Intent Intent i = getIntent(); byte[] b = i.getByteArrayExtra(GROUP_ID); if (b == null) throw new IllegalStateException("No Group in intent."); groupId = new GroupId(b); + + // Name of the Blog from Intent blogName = i.getStringExtra(BLOG_NAME); if (blogName != null) setTitle(blogName); + + // Is this our blog and was it just created? myBlog = i.getBooleanExtra(IS_MY_BLOG, false); + isNew = i.getBooleanExtra(IS_NEW_BLOG, false); - adapter = new BlogPostAdapter(this, groupId, blogName); - list = (BriarRecyclerView) this.findViewById(R.id.postList); - list.setLayoutManager(new LinearLayoutManager(this)); - list.setAdapter(adapter); - if (myBlog) { - list.setEmptyText( - getString(R.string.blogs_my_blogs_blog_empty_state)); + setContentView(R.layout.activity_blog); + + pager = (ViewPager) findViewById(R.id.pager); + progressBar = (ProgressBar) findViewById(R.id.progressBar); + hideLoadingScreen(); + + blogPagerAdapter = new BlogPagerAdapter(getSupportFragmentManager()); + if (state == null || state.getBoolean(BLOG_PAGER_ADAPTER, true)) { + pager.setAdapter(blogPagerAdapter); } else { - list.setEmptyText(getString(R.string.blogs_other_blog_empty_state)); + // this initializes and restores the postPagerAdapter + loadBlogPosts(); } + } - // show snackbar if this blog was just created - boolean isNew = i.getBooleanExtra(IS_NEW_BLOG, false); - if (isNew) { - Snackbar s = Snackbar.make(list, R.string.blogs_my_blogs_created, - LENGTH_LONG); - s.getView().setBackgroundResource(R.color.briar_primary); - s.show(); + @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 onResume() { - super.onResume(); - loadBlogPosts(); + public void injectActivity(ActivityComponent component) { + component.inject(this); } @Override - public boolean onCreateOptionsMenu(Menu menu) { - if (myBlog) { - MenuInflater inflater = getMenuInflater(); - inflater.inflate(R.menu.blogs_my_blog_actions, menu); - } - return super.onCreateOptionsMenu(menu); + public void showLoadingScreen(boolean isBlocking, int stringId) { + progressBar.setVisibility(VISIBLE); + } + + private void showLoadingScreen() { + showLoadingScreen(false, 0); } @Override - public boolean onOptionsItemSelected(final MenuItem item) { - switch (item.getItemId()) { - case R.id.action_write_blog_post: - Intent i = new Intent(this, WriteBlogPostActivity.class); - i.putExtra(GROUP_ID, groupId.getBytes()); - i.putExtra(BLOG_NAME, blogName); - ActivityOptionsCompat options = - makeCustomAnimation(this, android.R.anim.slide_in_left, - android.R.anim.slide_out_right); - ActivityCompat.startActivityForResult(this, i, WRITE_POST, - options.toBundle()); - return true; - default: - return super.onOptionsItemSelected(item); - } + public void hideLoadingScreen() { + progressBar.setVisibility(GONE); } @Override - public void onActivityResult(int requestCode, int resultCode, Intent data) { - if (requestCode == WRITE_POST && resultCode == RESULT_OK) { - scrollToTop = true; - } + public void onFragmentCreated(String tag) { + } @Override - public void injectActivity(ActivityComponent component) { - component.inject(this); + public void onBlogPostClick(final int position) { + loadBlogPosts(position, true); } private void loadBlogPosts() { - runOnDbThread(new Runnable() { - @Override - public void run() { - try { - // load blog posts - long now = System.currentTimeMillis(); - Collection<BlogPostItem> posts = new ArrayList<>(); - try { - Collection<BlogPostHeader> header = - blogManager.getPostHeaders(groupId); - for (BlogPostHeader h : header) { - posts.add(new BlogPostItem(h)); + loadBlogPosts(0, false); + } + + private void loadBlogPosts(final int position, final boolean setItem) { + showLoadingScreen(); + blogController + .loadBlog(groupId, false, new UiResultHandler<Boolean>(this) { + @Override + public void onResultUi(Boolean result) { + if (result) { + Collection<BlogPostItem> posts = + blogController.getBlogPosts(); + + if (postPagerAdapter == null) { + postPagerAdapter = new BlogPostPagerAdapter( + getSupportFragmentManager(), + posts.size()); + } else { + postPagerAdapter.setSize(posts.size()); + } + pager.setAdapter(postPagerAdapter); + if (setItem) pager.setCurrentItem(position); + } else { + Toast.makeText(BlogActivity.this, + R.string.blogs_blog_post_failed_to_load, + LENGTH_SHORT).show(); } - } catch (NoSuchGroupException e) { - // Continue } - displayBlogPosts(posts); - long duration = System.currentTimeMillis() - now; - if (LOG.isLoggable(INFO)) - LOG.info("Post header load took " + duration + " ms"); - } catch (DbException e) { - if (LOG.isLoggable(WARNING)) - LOG.log(WARNING, e.toString(), e); - } - } - }); + }); } - private void displayBlogPosts(final Collection<BlogPostItem> items) { + @Override + public void onBlogPostAdded(final BlogPostItem post, final boolean local) { runOnUiThread(new Runnable() { @Override public void run() { - if (items.size() == 0) { - list.showData(); - } else { - adapter.addAll(items); - if (scrollToTop) list.scrollToPosition(0); + if (blogPagerAdapter != null) { + BlogFragment f = blogPagerAdapter.getFragment(); + if (f != null && f.isVisible()) { + f.onBlogPostAdded(post, local); + } + } + + if (postPagerAdapter != null) { + postPagerAdapter.onBlogPostAdded(); + postPagerAdapter.notifyDataSetChanged(); } - scrollToTop = false; } }); } - // TODO listen to events and add new blog posts as they come in + @Override + protected void onActivityResult(int requestCode, int resultCode, + Intent data) { + + // The BlogPostAddedEvent arrives when the controller is not listening, + // so we need to manually reload the blog posts :( + if (requestCode == REQUEST_WRITE_POST && resultCode == RESULT_OK) { + BlogFragment f = blogPagerAdapter.getFragment(); + if (f != null && f.isVisible()) { + f.reload(); + } + } + } + + + private class BlogPagerAdapter extends FragmentStatePagerAdapter { + private BlogFragment fragment = null; + + BlogPagerAdapter(FragmentManager fm) { + super(fm); + } + + @Override + public int getCount() { + return 1; + } + + @Override + public Fragment getItem(int position) { + return BlogFragment.newInstance(groupId, blogName, myBlog, isNew); + } + + @Override + public Object instantiateItem(ViewGroup container, int position) { + // save a reference to the single fragment here for later + fragment = + (BlogFragment) super.instantiateItem(container, position); + return fragment; + } + + BlogFragment getFragment() { + return fragment; + } + } + + private class BlogPostPagerAdapter extends FragmentStatePagerAdapter { + private int size; + + BlogPostPagerAdapter(FragmentManager fm, int size) { + super(fm); + this.size = size; + } + + @Override + public int getCount() { + return size; + } + + @Override + public Fragment getItem(int position) { + MessageId postIdOfPos = blogController.getBlogPostId(position); + return BlogPostFragment.newInstance(groupId, postIdOfPos); + } + + void onBlogPostAdded() { + size++; + } + + void setSize(int size) { + this.size = size; + } + } } diff --git a/briar-android/src/org/briarproject/android/blogs/BlogController.java b/briar-android/src/org/briarproject/android/blogs/BlogController.java new file mode 100644 index 0000000000000000000000000000000000000000..ba9d085c2400d91ce520891bfc275e24f6060ca4 --- /dev/null +++ b/briar-android/src/org/briarproject/android/blogs/BlogController.java @@ -0,0 +1,29 @@ +package org.briarproject.android.blogs; + +import android.support.annotation.Nullable; + +import org.briarproject.android.controller.ActivityLifecycleController; +import org.briarproject.android.controller.handler.UiResultHandler; +import org.briarproject.api.sync.GroupId; +import org.briarproject.api.sync.MessageId; + +import java.util.TreeSet; + +public interface BlogController extends ActivityLifecycleController { + + void loadBlog(final GroupId groupId, final boolean reload, + final UiResultHandler<Boolean> resultHandler); + + TreeSet<BlogPostItem> getBlogPosts(); + + @Nullable + BlogPostItem getBlogPost(MessageId postId); + + @Nullable + MessageId getBlogPostId(int position); + + interface BlogPostListener { + void onBlogPostAdded(final BlogPostItem post, final boolean local); + } + +} diff --git a/briar-android/src/org/briarproject/android/blogs/BlogControllerImpl.java b/briar-android/src/org/briarproject/android/blogs/BlogControllerImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..f90a930953fe4e8f909bd39b1d79d1da8eb851a6 --- /dev/null +++ b/briar-android/src/org/briarproject/android/blogs/BlogControllerImpl.java @@ -0,0 +1,171 @@ +package org.briarproject.android.blogs; + +import android.app.Activity; +import android.support.annotation.Nullable; + +import org.briarproject.android.controller.DbControllerImpl; +import org.briarproject.android.controller.handler.UiResultHandler; +import org.briarproject.api.blogs.BlogManager; +import org.briarproject.api.blogs.BlogPostHeader; +import org.briarproject.api.db.DbException; +import org.briarproject.api.event.BlogPostAddedEvent; +import org.briarproject.api.event.Event; +import org.briarproject.api.event.EventBus; +import org.briarproject.api.event.EventListener; +import org.briarproject.api.event.GroupRemovedEvent; +import org.briarproject.api.sync.GroupId; +import org.briarproject.api.sync.MessageId; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.TreeSet; +import java.util.logging.Logger; + +import javax.inject.Inject; + +import static java.util.logging.Level.INFO; +import static java.util.logging.Level.WARNING; + +public class BlogControllerImpl extends DbControllerImpl + implements BlogController, EventListener { + + private static final Logger LOG = + Logger.getLogger(BlogControllerImpl.class.getName()); + + @Inject + protected Activity activity; + @Inject + protected volatile BlogManager blogManager; + @Inject + protected volatile EventBus eventBus; + @Inject + protected BlogPersistentData data; + + private volatile BlogPostListener listener; + + @Inject + BlogControllerImpl() { + } + + @Override + public void onActivityCreate() { + if (activity instanceof BlogPostListener) { + listener = (BlogPostListener) activity; + } else { + throw new IllegalStateException( + "An activity that injects the BlogController must " + + "implement the BlogPostListener"); + } + } + + @Override + public void onActivityResume() { + eventBus.addListener(this); + } + + @Override + public void onActivityPause() { + eventBus.removeListener(this); + } + + @Override + public void onActivityDestroy() { + if (activity.isFinishing()) { + data.clearAll(); + } + } + + @Override + public void eventOccurred(Event e) { + if (e instanceof BlogPostAddedEvent) { + final BlogPostAddedEvent m = (BlogPostAddedEvent) e; + if (m.getGroupId().equals(data.getGroupId())) { + LOG.info("New blog post added"); + final BlogPostHeader header = m.getHeader(); + try { + final byte[] body = blogManager.getPostBody(header.getId()); + final BlogPostItem post = new BlogPostItem(header, body); + data.addPost(post); + listener.onBlogPostAdded(post, m.isLocal()); + } catch (DbException ex) { + if (LOG.isLoggable(WARNING)) + LOG.log(WARNING, ex.toString(), ex); + } + } + } else if (e instanceof GroupRemovedEvent) { + GroupRemovedEvent s = (GroupRemovedEvent) e; + if (s.getGroup().getId().equals(data.getGroupId())) { + LOG.info("Blog removed"); + activity.runOnUiThread(new Runnable() { + @Override + public void run() { + activity.finish(); + } + }); + } + } + } + + @Override + public void loadBlog(final GroupId groupId, final boolean reload, + final UiResultHandler<Boolean> 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<BlogPostItem> posts = new ArrayList<>(); + Collection<BlogPostHeader> 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<BlogPostItem> getBlogPosts() { + return data.getBlogPosts(); + } + + @Override + @Nullable + public BlogPostItem getBlogPost(MessageId id) { + for (BlogPostItem item : getBlogPosts()) { + if (item.getId().equals(id)) return item; + } + return null; + } + + @Override + @Nullable + public MessageId getBlogPostId(int position) { + int i = 0; + for (BlogPostItem post : getBlogPosts()) { + if (i == position) return post.getId(); + i++; + } + return null; + } + +} diff --git a/briar-android/src/org/briarproject/android/blogs/BlogFragment.java b/briar-android/src/org/briarproject/android/blogs/BlogFragment.java new file mode 100644 index 0000000000000000000000000000000000000000..430cc611a1ef448f66b93d021427b1c5aebddd1a --- /dev/null +++ b/briar-android/src/org/briarproject/android/blogs/BlogFragment.java @@ -0,0 +1,191 @@ +package org.briarproject.android.blogs; + +import android.content.Intent; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.design.widget.Snackbar; +import android.support.v4.app.ActivityCompat; +import android.support.v4.app.ActivityOptionsCompat; +import android.support.v7.widget.LinearLayoutManager; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Toast; + +import org.briarproject.R; +import org.briarproject.android.ActivityComponent; +import org.briarproject.android.blogs.BlogController.BlogPostListener; +import org.briarproject.android.blogs.BlogPostAdapter.OnBlogPostClickListener; +import org.briarproject.android.controller.handler.UiResultHandler; +import org.briarproject.android.fragment.BaseFragment; +import org.briarproject.android.util.BriarRecyclerView; +import org.briarproject.api.sync.GroupId; + +import java.util.Collection; + +import javax.inject.Inject; + +import static android.support.design.widget.Snackbar.LENGTH_LONG; +import static android.support.v4.app.ActivityOptionsCompat.makeCustomAnimation; +import static android.widget.Toast.LENGTH_SHORT; +import static org.briarproject.android.BriarActivity.GROUP_ID; +import static org.briarproject.android.blogs.BlogActivity.BLOG_NAME; +import static org.briarproject.android.blogs.BlogActivity.IS_MY_BLOG; +import static org.briarproject.android.blogs.BlogActivity.IS_NEW_BLOG; +import static org.briarproject.android.blogs.BlogActivity.REQUEST_WRITE_POST; + +public class BlogFragment extends BaseFragment implements BlogPostListener { + + public final static String TAG = BlogFragment.class.getName(); + + @Inject + BlogController blogController; + + private GroupId groupId; + private String blogName; + private boolean myBlog; + private BlogPostAdapter adapter; + private BriarRecyclerView list; + + static BlogFragment newInstance(GroupId groupId, String name, + boolean myBlog, boolean isNew) { + + BlogFragment f = new BlogFragment(); + + Bundle bundle = new Bundle(); + bundle.putByteArray(GROUP_ID, groupId.getBytes()); + bundle.putString(BLOG_NAME, name); + bundle.putBoolean(IS_MY_BLOG, myBlog); + bundle.putBoolean(IS_NEW_BLOG, isNew); + + f.setArguments(bundle); + return f; + } + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + + setHasOptionsMenu(true); + + Bundle args = getArguments(); + byte[] b = args.getByteArray(GROUP_ID); + if (b == null) throw new IllegalStateException("No Group found."); + groupId = new GroupId(b); + blogName = args.getString(BLOG_NAME); + myBlog = args.getBoolean(IS_MY_BLOG); + boolean isNew = args.getBoolean(IS_NEW_BLOG); + + View v = inflater.inflate(R.layout.fragment_blog, container, false); + + adapter = new BlogPostAdapter(getActivity(), + (OnBlogPostClickListener) getActivity()); + list = (BriarRecyclerView) v.findViewById(R.id.postList); + list.setLayoutManager(new LinearLayoutManager(getActivity())); + list.setAdapter(adapter); + if (myBlog) { + list.setEmptyText( + getString(R.string.blogs_my_blogs_blog_empty_state)); + } else { + list.setEmptyText(getString(R.string.blogs_other_blog_empty_state)); + } + + // show snackbar if this blog was just created + if (isNew) { + Snackbar s = Snackbar.make(list, R.string.blogs_my_blogs_created, + LENGTH_LONG); + s.getView().setBackgroundResource(R.color.briar_primary); + s.show(); + + // show only once + args.putBoolean(IS_NEW_BLOG, false); + } + return v; + } + + @Override + public void injectFragment(ActivityComponent component) { + component.inject(this); + } + + @Override + public void onStart() { + super.onStart(); + loadData(false); + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + if (myBlog) { + inflater.inflate(R.menu.blogs_my_blog_actions, menu); + } + super.onCreateOptionsMenu(menu, inflater); + } + + @Override + public boolean onOptionsItemSelected(final MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + getActivity().onBackPressed(); + return true; + case R.id.action_write_blog_post: + Intent i = + new Intent(getActivity(), WriteBlogPostActivity.class); + i.putExtra(GROUP_ID, groupId.getBytes()); + i.putExtra(BLOG_NAME, blogName); + ActivityOptionsCompat options = + makeCustomAnimation(getActivity(), + android.R.anim.slide_in_left, + android.R.anim.slide_out_right); + ActivityCompat.startActivityForResult(getActivity(), i, + REQUEST_WRITE_POST, options.toBundle()); + return true; + default: + return super.onOptionsItemSelected(item); + } + } + + @Override + public String getUniqueTag() { + return TAG; + } + + @Override + public void onBlogPostAdded(BlogPostItem post, boolean local) { + adapter.add(post); + if (local) list.scrollToPosition(0); + } + + private void loadData(final boolean reload) { + blogController.loadBlog(groupId, reload, + new UiResultHandler<Boolean>(getActivity()) { + @Override + public void onResultUi(Boolean result) { + if (result) { + Collection<BlogPostItem> posts = + blogController.getBlogPosts(); + if (posts.size() > 0) { + adapter.addAll(posts); + if (reload) list.scrollToPosition(0); + } else { + list.showData(); + } + } else { + Toast.makeText(getActivity(), + R.string.blogs_blog_failed_to_load, + LENGTH_SHORT).show(); + getActivity().supportFinishAfterTransition(); + } + } + }); + } + + void reload() { + loadData(true); + } + +} diff --git a/briar-android/src/org/briarproject/android/blogs/BlogPersistentData.java b/briar-android/src/org/briarproject/android/blogs/BlogPersistentData.java new file mode 100644 index 0000000000000000000000000000000000000000..a2834c809cd5afc0643be76ccdb7d6d5b3de1025 --- /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<BlogPostItem> posts = new TreeSet<>(); + + public BlogPersistentData() { + + } + + public void setGroupId(GroupId groupId) { + this.groupId = groupId; + } + + public GroupId getGroupId() { + return groupId; + } + + public void setPosts(Collection<BlogPostItem> posts) { + this.posts.clear(); + this.posts.addAll(posts); + } + + void addPost(BlogPostItem post) { + posts.add(post); + } + + TreeSet<BlogPostItem> getBlogPosts() { + return posts; + } + + void clearAll() { + groupId = null; + posts.clear(); + } +} diff --git a/briar-android/src/org/briarproject/android/blogs/BlogPostAdapter.java b/briar-android/src/org/briarproject/android/blogs/BlogPostAdapter.java index 728b33a5fdf25a12049bd0f72bb852e9c7616b98..c071dfe71161d54c5f747fa344a68d052388cd2b 100644 --- a/briar-android/src/org/briarproject/android/blogs/BlogPostAdapter.java +++ b/briar-android/src/org/briarproject/android/blogs/BlogPostAdapter.java @@ -10,7 +10,6 @@ import android.view.ViewGroup; import android.widget.TextView; import org.briarproject.R; -import org.briarproject.api.sync.GroupId; import org.briarproject.util.StringUtils; import java.util.Collection; @@ -61,13 +60,11 @@ class BlogPostAdapter extends }); private final Context ctx; - private final GroupId blogGroupId; - private final String blogTitle; + private final OnBlogPostClickListener listener; - BlogPostAdapter(Context ctx, GroupId blogGroupId, String blogTitle) { + BlogPostAdapter(Context ctx, OnBlogPostClickListener listener) { this.ctx = ctx; - this.blogGroupId = blogGroupId; - this.blogTitle = blogTitle; + this.listener = listener; } @Override @@ -78,7 +75,7 @@ class BlogPostAdapter extends } @Override - public void onBindViewHolder(BlogPostHolder ui, int position) { + public void onBindViewHolder(final BlogPostHolder ui, int position) { final BlogPostItem item = getItem(position); // title @@ -95,7 +92,7 @@ class BlogPostAdapter extends ui.layout.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - // TODO #428 + listener.onBlogPostClick(ui.getAdapterPosition()); } }); @@ -154,4 +151,9 @@ class BlogPostAdapter extends body = (TextView) v.findViewById(R.id.bodyView); } } + + interface OnBlogPostClickListener { + void onBlogPostClick(int position); + } + } diff --git a/briar-android/src/org/briarproject/android/blogs/BlogPostFragment.java b/briar-android/src/org/briarproject/android/blogs/BlogPostFragment.java new file mode 100644 index 0000000000000000000000000000000000000000..54ac2cdda54898ec9f60815fb3cbb63b2e9ef85c --- /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<Boolean>((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); + } + } + +}