From f7d5c1f63c3aa4a78af4881c7b3178fab62c8e4e Mon Sep 17 00:00:00 2001
From: Torsten Grote <t@grobox.de>
Date: Tue, 7 Jun 2016 19:23:22 -0300
Subject: [PATCH] Add an activity to write blog posts

Closes #411
---
 briar-android/AndroidManifest.xml             |  11 +
 .../res/layout/activity_write_blog_post.xml   |  56 ++++++
 .../menu/blogs_write_blog_post_actions.xml    |  12 ++
 briar-android/res/values/strings.xml          |   3 +
 .../android/ActivityComponent.java            |   3 +
 .../android/blogs/BlogActivity.java           |  15 +-
 .../android/blogs/BlogPostAdapter.java        |  28 +--
 .../android/blogs/BlogPostItem.java           |  19 +-
 .../android/blogs/WriteBlogPostActivity.java  | 188 ++++++++++++++++++
 9 files changed, 318 insertions(+), 17 deletions(-)
 create mode 100644 briar-android/res/layout/activity_write_blog_post.xml
 create mode 100644 briar-android/res/menu/blogs_write_blog_post_actions.xml
 create mode 100644 briar-android/src/org/briarproject/android/blogs/WriteBlogPostActivity.java

diff --git a/briar-android/AndroidManifest.xml b/briar-android/AndroidManifest.xml
index 0e11dbd84e..f335e5b5b7 100644
--- a/briar-android/AndroidManifest.xml
+++ b/briar-android/AndroidManifest.xml
@@ -168,6 +168,17 @@
 				/>
 		</activity>
 
+		<activity
+			android:name=".android.blogs.WriteBlogPostActivity"
+			android:label="@string/blogs_write_blog_post"
+			android:parentActivityName=".android.blogs.BlogActivity"
+			android:windowSoftInputMode="stateVisible|adjustResize">
+			<meta-data
+				android:name="android.support.PARENT_ACTIVITY"
+				android:value=".android.blogs.BlogActivity"
+				/>
+		</activity>
+
 		<activity
 			android:name=".android.identity.CreateIdentityActivity"
 			android:label="@string/new_identity_title"
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 0000000000..5c3bcf3804
--- /dev/null
+++ b/briar-android/res/layout/activity_write_blog_post.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+	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"
+	android:orientation="vertical"
+	android:padding="@dimen/margin_small"
+	tools:context=".android.blogs.WriteBlogPostActivity">
+
+	<android.support.design.widget.TextInputLayout
+		android:id="@+id/titleLayout"
+		android:layout_width="match_parent"
+		android:layout_height="wrap_content"
+		android:visibility="gone"
+		app:counterEnabled="true"
+		app:counterOverflowTextAppearance="@style/BriarTextCounter.Overflow">
+
+		<android.support.design.widget.TextInputEditText
+			android:id="@+id/titleInput"
+			android:layout_width="match_parent"
+			android:layout_height="wrap_content"
+			android:hint="@string/blogs_write_blog_post_title_hint"
+			android:inputType="textCapWords|textCapSentences|textAutoCorrect"/>
+
+	</android.support.design.widget.TextInputLayout>
+
+	<EditText
+		android:id="@+id/bodyInput"
+		android:layout_width="match_parent"
+		android:layout_height="0dp"
+		android:layout_weight="1"
+		android:gravity="bottom"
+		android:hint="@string/blogs_write_blog_post_body_hint"
+		android:inputType="textMultiLine|textLongMessage|textCapSentences|textAutoCorrect">
+
+		<requestFocus/>
+
+	</EditText>
+
+	<Button
+		android:id="@+id/publishButton"
+		style="@style/BriarButton"
+		android:enabled="false"
+		android:text="@string/blogs_publish_blog_post"/>
+
+	<ProgressBar
+		android:id="@+id/progressBar"
+		style="?android:attr/progressBarStyle"
+		android:layout_width="wrap_content"
+		android:layout_height="wrap_content"
+		android:layout_gravity="center"
+		android:visibility="gone"/>
+
+</LinearLayout>
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 0000000000..11befe0369
--- /dev/null
+++ b/briar-android/res/menu/blogs_write_blog_post_actions.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu
+	xmlns:android="http://schemas.android.com/apk/res/android"
+	xmlns:app="http://schemas.android.com/apk/res-auto">
+
+	<item
+		android:id="@+id/action_publish_blog_post"
+		android:title="@string/blogs_publish_blog_post"
+		android:icon="@drawable/social_send_now_white"
+		app:showAsAction="always"/>
+
+</menu>
\ No newline at end of file
diff --git a/briar-android/res/values/strings.xml b/briar-android/res/values/strings.xml
index 6b2ca75b13..793e97d7a5 100644
--- a/briar-android/res/values/strings.xml
+++ b/briar-android/res/values/strings.xml
@@ -264,6 +264,9 @@
 	<string name="tag_new">NEW</string>
 	<string name="blogs_post_more">more</string>
 	<string name="blogs_write_blog_post">Write Blog Post</string>
+	<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_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 64ecc4cdb5..c55e56f831 100644
--- a/briar-android/src/org/briarproject/android/ActivityComponent.java
+++ b/briar-android/src/org/briarproject/android/ActivityComponent.java
@@ -6,6 +6,7 @@ import org.briarproject.android.blogs.BlogActivity;
 import org.briarproject.android.blogs.CreateBlogActivity;
 import org.briarproject.android.blogs.MyBlogsFragment;
 import org.briarproject.android.contact.ContactListFragment;
+import org.briarproject.android.blogs.WriteBlogPostActivity;
 import org.briarproject.android.contact.ConversationActivity;
 import org.briarproject.android.forum.AvailableForumsActivity;
 import org.briarproject.android.forum.ContactSelectorFragment;
@@ -70,6 +71,8 @@ public interface ActivityComponent {
 
 	void inject(BlogActivity activity);
 
+	void inject(WriteBlogPostActivity activity);
+
 	void inject(SettingsActivity activity);
 
 	void inject(ChangePasswordActivity activity);
diff --git a/briar-android/src/org/briarproject/android/blogs/BlogActivity.java b/briar-android/src/org/briarproject/android/blogs/BlogActivity.java
index 097f6b092d..aed08eb1e2 100644
--- a/briar-android/src/org/briarproject/android/blogs/BlogActivity.java
+++ b/briar-android/src/org/briarproject/android/blogs/BlogActivity.java
@@ -3,6 +3,8 @@ 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;
@@ -25,6 +27,7 @@ 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;
 
@@ -63,7 +66,7 @@ public class BlogActivity extends BriarActivity {
 		if (blogName != null) setTitle(blogName);
 		myBlog = i.getBooleanExtra(IS_MY_BLOG, false);
 
-		adapter = new BlogPostAdapter(this, blogName);
+		adapter = new BlogPostAdapter(this, groupId, blogName);
 		list = (BriarRecyclerView) this.findViewById(R.id.postList);
 		list.setLayoutManager(new LinearLayoutManager(this));
 		list.setAdapter(adapter);
@@ -103,11 +106,15 @@ public class BlogActivity extends BriarActivity {
 	public boolean onOptionsItemSelected(final MenuItem item) {
 		switch (item.getItemId()) {
 			case R.id.action_write_blog_post:
-/*				Intent i = new Intent(this, WriteBlogPostActivity.class);
+				Intent i = new Intent(this, WriteBlogPostActivity.class);
 				i.putExtra(GROUP_ID, groupId.getBytes());
 				i.putExtra(BLOG_NAME, blogName);
-				startActivityForResult(i, WRITE_POST);
-*/				return true;
+				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);
 		}
diff --git a/briar-android/src/org/briarproject/android/blogs/BlogPostAdapter.java b/briar-android/src/org/briarproject/android/blogs/BlogPostAdapter.java
index 8aea31ee3c..728b33a5fd 100644
--- a/briar-android/src/org/briarproject/android/blogs/BlogPostAdapter.java
+++ b/briar-android/src/org/briarproject/android/blogs/BlogPostAdapter.java
@@ -10,6 +10,7 @@ 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;
@@ -25,17 +26,7 @@ class BlogPostAdapter extends
 
 		@Override
 		public int compare(BlogPostItem a, BlogPostItem 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 post title
-			if (a.getTitle() != null && b.getTitle() != null) {
-				return String.CASE_INSENSITIVE_ORDER
-						.compare(a.getTitle(), b.getTitle());
-			}
-			return 0;
+			return a.compareTo(b);
 		}
 
 		@Override
@@ -70,10 +61,12 @@ class BlogPostAdapter extends
 	});
 
 	private final Context ctx;
+	private final GroupId blogGroupId;
 	private final String blogTitle;
 
-	BlogPostAdapter(Context ctx, String blogTitle) {
+	BlogPostAdapter(Context ctx, GroupId blogGroupId, String blogTitle) {
 		this.ctx = ctx;
+		this.blogGroupId = blogGroupId;
 		this.blogTitle = blogTitle;
 	}
 
@@ -99,6 +92,13 @@ class BlogPostAdapter extends
 		// post body
 		ui.body.setText(StringUtils.fromUtf8(item.getBody()));
 
+		ui.layout.setOnClickListener(new View.OnClickListener() {
+			@Override
+			public void onClick(View v) {
+				// TODO #428
+			}
+		});
+
 		// date
 		ui.date.setText(
 				DateUtils.getRelativeTimeSpanString(ctx, item.getTimestamp()));
@@ -117,6 +117,10 @@ class BlogPostAdapter extends
 		return posts.get(position);
 	}
 
+	public void add(BlogPostItem item) {
+		posts.add(item);
+	}
+
 	public void addAll(Collection<BlogPostItem> items) {
 		posts.addAll(items);
 	}
diff --git a/briar-android/src/org/briarproject/android/blogs/BlogPostItem.java b/briar-android/src/org/briarproject/android/blogs/BlogPostItem.java
index 3efdea6c73..f4cb2a66b5 100644
--- a/briar-android/src/org/briarproject/android/blogs/BlogPostItem.java
+++ b/briar-android/src/org/briarproject/android/blogs/BlogPostItem.java
@@ -1,12 +1,14 @@
 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 {
+class BlogPostItem implements Comparable<BlogPostItem> {
 
 	private final BlogPostHeader header;
 	private final byte[] body;
@@ -49,4 +51,19 @@ class BlogPostItem {
 	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 = getTimestamp(), bTime = other.getTimestamp();
+		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/WriteBlogPostActivity.java b/briar-android/src/org/briarproject/android/blogs/WriteBlogPostActivity.java
new file mode 100644
index 0000000000..91873c9ff0
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/blogs/WriteBlogPostActivity.java
@@ -0,0 +1,188 @@
+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) {
+				showOrHidePublishButton();
+			}
+		});
+
+		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 showOrHidePublishButton() {
+		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<LocalAuthor> 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) {
+					// TODO show error
+					if (LOG.isLoggable(WARNING))
+						LOG.log(WARNING, e.toString(), e);
+				}
+			}
+		});
+	}
+
+	private void postPublished() {
+		runOnUiThread(new Runnable() {
+			@Override
+			public void run() {
+				setResult(RESULT_OK);
+				supportFinishAfterTransition();
+			}
+		});
+	}
+
+}
-- 
GitLab