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