diff --git a/briar-android/AndroidManifest.xml b/briar-android/AndroidManifest.xml index 0c7292f5100da7be7a8ffae881ab1d533a78036f..0249275b5a3edb3e33b157dfb40c68f31d7e6b3c 100644 --- a/briar-android/AndroidManifest.xml +++ b/briar-android/AndroidManifest.xml @@ -182,6 +182,27 @@ /> </activity> + <activity + android:name=".android.blogs.RssFeedImportActivity" + android:label="@string/blogs_rss_feeds_import" + android:parentActivityName=".android.NavDrawerActivity" + android:windowSoftInputMode="stateVisible|adjustResize"> + <meta-data + android:name="android.support.PARENT_ACTIVITY" + android:value=".android.NavDrawerActivity" + /> + </activity> + + <activity + android:name=".android.blogs.RssFeedManageActivity" + android:label="@string/blogs_rss_feeds_manage" + android:parentActivityName=".android.NavDrawerActivity"> + <meta-data + android:name="android.support.PARENT_ACTIVITY" + android:value=".android.NavDrawerActivity" + /> + </activity> + <activity android:name=".android.identity.CreateIdentityActivity" android:label="@string/new_identity_title" diff --git a/briar-android/res/drawable/action_delete_black.xml b/briar-android/res/drawable/action_delete_black.xml new file mode 100644 index 0000000000000000000000000000000000000000..814d0993fb0ef3181d02c780533339e61404a956 --- /dev/null +++ b/briar-android/res/drawable/action_delete_black.xml @@ -0,0 +1,10 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:alpha="0.56" + android:viewportHeight="24.0" + android:viewportWidth="24.0"> + <path + android:fillColor="#000000" + android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z"/> +</vector> diff --git a/briar-android/res/layout/activity_rss_feed_import.xml b/briar-android/res/layout/activity_rss_feed_import.xml new file mode 100644 index 0000000000000000000000000000000000000000..cf00f260f7552fde83a4db161a22e58de4f3d33e --- /dev/null +++ b/briar-android/res/layout/activity_rss_feed_import.xml @@ -0,0 +1,34 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + 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.RssFeedImportActivity"> + + <EditText + android:id="@+id/urlInput" + android:layout_width="match_parent" + android:layout_height="0dp" + android:layout_weight="1" + android:gravity="top" + android:hint="@string/blogs_rss_feeds_import_hint" + android:inputType="textMultiLine|textUri"/> + + <Button + android:id="@+id/importButton" + style="@style/BriarButton" + android:enabled="false" + android:text="@string/blogs_rss_feeds_import_button"/> + + <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/layout/activity_rss_feed_manage.xml b/briar-android/res/layout/activity_rss_feed_manage.xml new file mode 100644 index 0000000000000000000000000000000000000000..a9fc464d9c1b0ec17177d723c7d397d86babe579 --- /dev/null +++ b/briar-android/res/layout/activity_rss_feed_manage.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="utf-8"?> +<org.briarproject.android.util.BriarRecyclerView + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:id="@+id/feedList" + android:layout_width="match_parent" + android:layout_height="match_parent" + app:scrollToEnd="false" + app:emptyText="@string/blogs_rss_feeds_manage_empty_state" + tools:listitem="@layout/list_item_rss_feed"/> \ No newline at end of file diff --git a/briar-android/res/layout/list_item_rss_feed.xml b/briar-android/res/layout/list_item_rss_feed.xml new file mode 100644 index 0000000000000000000000000000000000000000..ffac010d03bc34745b9c66e9dbc7631371ced478 --- /dev/null +++ b/briar-android/res/layout/list_item_rss_feed.xml @@ -0,0 +1,123 @@ +<?xml version="1.0" encoding="utf-8"?> +<RelativeLayout + 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" + android:layout_marginLeft="@dimen/listitem_horizontal_margin" + android:layout_marginStart="@dimen/listitem_horizontal_margin" + android:layout_marginTop="@dimen/listitem_horizontal_margin" + android:background="?attr/selectableItemBackground"> + + <TextView + android:id="@+id/titleView" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentTop="true" + android:maxLines="2" + android:textColor="@color/briar_text_primary" + android:textSize="@dimen/text_size_medium" + tools:text="This is a name of a RSS Feed"/> + + <ImageView + android:id="@+id/deleteButton" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentRight="true" + android:layout_alignParentTop="true" + android:layout_marginEnd="@dimen/listitem_horizontal_margin" + android:layout_marginRight="@dimen/listitem_horizontal_margin" + android:contentDescription="@string/delete_button" + android:src="@drawable/action_delete_black"/> + + <TextView + android:id="@+id/author" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_below="@+id/titleView" + android:layout_marginRight="@dimen/margin_small" + android:paddingTop="@dimen/margin_tiny" + android:text="@string/blogs_rss_feeds_manage_author" + android:textColor="@color/briar_text_secondary" + android:textSize="@dimen/text_size_small"/> + + <TextView + android:id="@+id/authorView" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignBottom="@+id/author" + android:layout_marginEnd="@dimen/listitem_horizontal_margin" + android:layout_marginRight="@dimen/listitem_horizontal_margin" + android:layout_toRightOf="@+id/author" + android:textColor="@color/briar_text_secondary" + android:textSize="@dimen/text_size_small" + tools:text="Bruce Schneier"/> + + <TextView + android:id="@+id/imported" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_below="@+id/author" + android:layout_marginRight="@dimen/margin_small" + android:paddingTop="@dimen/margin_tiny" + android:text="@string/blogs_rss_feeds_manage_imported" + android:textColor="@color/briar_text_secondary" + android:textSize="@dimen/text_size_small"/> + + <TextView + android:id="@+id/importedView" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignBottom="@+id/imported" + android:layout_marginEnd="@dimen/listitem_horizontal_margin" + android:layout_marginRight="@dimen/listitem_horizontal_margin" + android:layout_toRightOf="@+id/imported" + android:textColor="@color/briar_text_secondary" + android:textSize="@dimen/text_size_small" + tools:text="July 4"/> + + <TextView + android:id="@+id/updated" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_below="@+id/imported" + android:layout_marginEnd="@dimen/listitem_horizontal_margin" + android:layout_marginRight="@dimen/margin_small" + android:paddingTop="@dimen/margin_tiny" + android:text="@string/blogs_rss_feeds_manage_updated" + android:textColor="@color/briar_text_secondary" + android:textSize="@dimen/text_size_small"/> + + <TextView + android:id="@+id/updatedView" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignBottom="@+id/updated" + android:layout_marginEnd="@dimen/listitem_horizontal_margin" + android:layout_marginRight="@dimen/listitem_horizontal_margin" + android:layout_toRightOf="@+id/updated" + android:textColor="@color/briar_text_secondary" + android:textSize="@dimen/text_size_small" + tools:text="5 min. ago"/> + + <TextView + android:id="@+id/descriptionView" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_below="@+id/updated" + android:layout_marginEnd="@dimen/listitem_horizontal_margin" + android:layout_marginRight="@dimen/listitem_horizontal_margin" + android:paddingTop="@dimen/margin_medium" + android:textColor="@color/briar_text_secondary" + android:textSize="@dimen/text_size_small" + tools:text="This is a description of the RSS feed. It can be several lines long, but it can also not exist at all if it is not present in the feed itself."/> + + <View + style="@style/Divider.ForumList" + android:layout_alignParentLeft="true" + android:layout_alignParentStart="true" + android:layout_below="@+id/descriptionView" + android:layout_marginTop="@dimen/listitem_horizontal_margin"/> + +</RelativeLayout> + diff --git a/briar-android/res/menu/blogs_feed_actions.xml b/briar-android/res/menu/blogs_feed_actions.xml index 38721c812dd0b37c75a0dd5770bf12e89a187d35..22cf36961f6ccd40bc17eb4cc353c148951c7893 100644 --- a/briar-android/res/menu/blogs_feed_actions.xml +++ b/briar-android/res/menu/blogs_feed_actions.xml @@ -9,4 +9,14 @@ android:title="@string/blogs_write_blog_post" app:showAsAction="always"/> + <item + android:id="@+id/action_rss_feeds_import" + android:title="@string/blogs_rss_feeds_import" + app:showAsAction="never"/> + + <item + android:id="@+id/action_rss_feeds_manage" + android:title="@string/blogs_rss_feeds_manage" + app:showAsAction="never"/> + </menu> \ No newline at end of file diff --git a/briar-android/res/menu/rss_feed_manage_actions.xml b/briar-android/res/menu/rss_feed_manage_actions.xml new file mode 100644 index 0000000000000000000000000000000000000000..c51bb7051bd1b5523cce4e8df38f6758af1ea9ab --- /dev/null +++ b/briar-android/res/menu/rss_feed_manage_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_rss_feeds_import" + android:icon="@drawable/ic_add_white" + android:title="@string/blogs_rss_feeds_import" + app:showAsAction="always"/> + +</menu> \ No newline at end of file diff --git a/briar-android/res/values/attrs.xml b/briar-android/res/values/attrs.xml index 380379e9e6b9a8435453b2db254182ebd06c1fbc..0de786b4b557f43344bfbd48cb5a30f3180aaded 100644 --- a/briar-android/res/values/attrs.xml +++ b/briar-android/res/values/attrs.xml @@ -2,6 +2,7 @@ <resources> <declare-styleable name="BriarRecyclerView"> <attr name="scrollToEnd" format="boolean" /> + <attr name="emptyText" format="string" /> </declare-styleable> </resources> \ No newline at end of file diff --git a/briar-android/res/values/strings.xml b/briar-android/res/values/strings.xml index c80a5d7b7da5eef0ba663e5dc5d5b96afda3d8b5..0c9ce8a08f7e51e9a8df603227ffb27ab9f3c15b 100644 --- a/briar-android/res/values/strings.xml +++ b/briar-android/res/values/strings.xml @@ -309,4 +309,17 @@ <string name="blogs_available_blogs">Available Blogs</string> <string name="blogs_drafts">Drafts</string> + <!-- RSS Feeds --> + <string name="blogs_rss_feeds_import">Import RSS Feed</string> + <string name="blogs_rss_feeds_import_button">Import</string> + <string name="blogs_rss_feeds_import_hint">Enter the URL of the RSS feed</string> + <string name="blogs_rss_feeds_import_error">We are sorry! There was an error importing your feed.</string> + <string name="blogs_rss_feeds_manage">Manage RSS Feeds</string> + <string name="blogs_rss_feeds_manage_imported">Imported:</string> + <string name="blogs_rss_feeds_manage_author">Author:</string> + <string name="blogs_rss_feeds_manage_updated">Last Updated:</string> + <string name="blogs_rss_feeds_manage_delete_error">The feed could not be deleted!</string> + <string name="blogs_rss_feeds_manage_empty_state">You haven\'t imported any RSS feeds.\n\nWhy don\'t you click the plus in the top right screen corner to add your first?</string> + <string name="blogs_rss_feeds_manage_error">There was a problem loading your feeds. Please try again later.</string> + </resources> diff --git a/briar-android/src/org/briarproject/android/ActivityComponent.java b/briar-android/src/org/briarproject/android/ActivityComponent.java index 3f3368b575b7bdfcbbf35964685864515a2fe6d9..7d902e6b4a518be0ea6cb99b59e64f493d1ac2a9 100644 --- a/briar-android/src/org/briarproject/android/ActivityComponent.java +++ b/briar-android/src/org/briarproject/android/ActivityComponent.java @@ -10,6 +10,8 @@ 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.RssFeedImportActivity; +import org.briarproject.android.blogs.RssFeedManageActivity; import org.briarproject.android.blogs.WriteBlogPostActivity; import org.briarproject.android.contact.ContactListFragment; import org.briarproject.android.contact.ConversationActivity; @@ -87,6 +89,10 @@ public interface ActivityComponent { void inject(IntroductionActivity activity); + void inject(RssFeedImportActivity activity); + + void inject(RssFeedManageActivity activity); + // Fragments void inject(ContactListFragment fragment); void inject(ForumListFragment fragment); diff --git a/briar-android/src/org/briarproject/android/AndroidComponent.java b/briar-android/src/org/briarproject/android/AndroidComponent.java index accd45a4cdedfc1b33c49780842fab34b3880741..dbe923c953df5777e13358684bbdbc3808f6fc78 100644 --- a/briar-android/src/org/briarproject/android/AndroidComponent.java +++ b/briar-android/src/org/briarproject/android/AndroidComponent.java @@ -16,6 +16,7 @@ import org.briarproject.api.crypto.PasswordStrengthEstimator; import org.briarproject.api.db.DatabaseConfig; import org.briarproject.api.db.DatabaseExecutor; import org.briarproject.api.event.EventBus; +import org.briarproject.api.feed.FeedManager; import org.briarproject.api.forum.ForumManager; import org.briarproject.api.forum.ForumPostFactory; import org.briarproject.api.forum.ForumSharingManager; @@ -112,6 +113,8 @@ public interface AndroidComponent extends CoreEagerSingletons { AndroidExecutor androidExecutor(); + FeedManager feedManager(); + @IoExecutor Executor ioExecutor(); diff --git a/briar-android/src/org/briarproject/android/blogs/FeedFragment.java b/briar-android/src/org/briarproject/android/blogs/FeedFragment.java index 47d0cc42704dcf101b386c37a486dbea1b103a1e..30e8b6dae1048c77022cd98929e2408bd8de1fde 100644 --- a/briar-android/src/org/briarproject/android/blogs/FeedFragment.java +++ b/briar-android/src/org/briarproject/android/blogs/FeedFragment.java @@ -140,20 +140,31 @@ public class FeedFragment extends BaseFragment implements @Override public boolean onOptionsItemSelected(final MenuItem item) { + if (personalBlog == null) return false; + ActivityOptionsCompat options = + makeCustomAnimation(getActivity(), android.R.anim.slide_in_left, + android.R.anim.slide_out_right); switch (item.getItemId()) { case R.id.action_write_blog_post: - if (personalBlog == null) return false; - Intent i = + Intent i1 = 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, + i1.putExtra(GROUP_ID, personalBlog.getId().getBytes()); + i1.putExtra(BLOG_NAME, personalBlog.getName()); + startActivityForResult(i1, REQUEST_WRITE_POST, options.toBundle()); return true; + case R.id.action_rss_feeds_import: + Intent i2 = + new Intent(getActivity(), RssFeedImportActivity.class); + i2.putExtra(GROUP_ID, personalBlog.getId().getBytes()); + startActivity(i2, options.toBundle()); + return true; + case R.id.action_rss_feeds_manage: + Intent i3 = + new Intent(getActivity(), RssFeedManageActivity.class); + i3.putExtra(GROUP_ID, personalBlog.getId().getBytes()); + startActivity(i3, options.toBundle()); + return true; default: return super.onOptionsItemSelected(item); } diff --git a/briar-android/src/org/briarproject/android/blogs/RssFeedAdapter.java b/briar-android/src/org/briarproject/android/blogs/RssFeedAdapter.java new file mode 100644 index 0000000000000000000000000000000000000000..2ad13eb343991cda0740c1eb1e4d9d7061851b8c --- /dev/null +++ b/briar-android/src/org/briarproject/android/blogs/RssFeedAdapter.java @@ -0,0 +1,192 @@ +package org.briarproject.android.blogs; + +import android.app.Activity; +import android.support.annotation.Nullable; +import android.support.v7.util.SortedList; +import android.support.v7.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import org.briarproject.R; +import org.briarproject.android.util.AndroidUtils; +import org.briarproject.api.feed.Feed; +import org.briarproject.api.sync.GroupId; + +import java.util.Collection; + +import static android.view.View.GONE; +import static android.view.View.VISIBLE; + +class RssFeedAdapter extends + RecyclerView.Adapter<RssFeedAdapter.FeedViewHolder> { + + private SortedList<Feed> feeds = new SortedList<>( + Feed.class, new SortedList.Callback<Feed>() { + + @Override + public int compare(Feed a, Feed b) { + if (a == b) return 0; + long aTime = a.getAdded(), bTime = b.getAdded(); + if (aTime > bTime) return -1; + if (aTime < bTime) return 1; + return 0; + } + + @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(Feed a, Feed b) { + return a.getUpdated() == b.getUpdated(); + } + + @Override + public boolean areItemsTheSame(Feed a, Feed b) { + return a.getUrl().equals(b.getUrl()) && + a.getBlogId().equals(b.getBlogId()) && + a.getAdded() == b.getAdded(); + } + }); + + private final Activity ctx; + private final RssFeedListener listener; + + RssFeedAdapter(Activity ctx, RssFeedListener listener) { + this.ctx = ctx; + this.listener = listener; + } + + @Override + public FeedViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + View v = LayoutInflater.from(ctx).inflate( + R.layout.list_item_rss_feed, parent, false); + return new FeedViewHolder(v); + } + + @Override + public void onBindViewHolder(FeedViewHolder ui, int position) { + final Feed item = getItem(position); + + // Feed Title + if (item.getTitle() != null) { + ui.title.setText(item.getTitle()); + ui.title.setVisibility(VISIBLE); + } else { + ui.title.setVisibility(GONE); + } + + // Delete Button + ui.delete.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + listener.onDeleteClick(item); + } + }); + + // Author + if (item.getAuthor() != null) { + ui.author.setText(item.getAuthor()); + ui.author.setVisibility(VISIBLE); + ui.authorLabel.setVisibility(VISIBLE); + } else { + ui.author.setVisibility(GONE); + ui.authorLabel.setVisibility(GONE); + } + + // Imported and Last Updated + ui.imported.setText(AndroidUtils.formatDate(ctx, item.getAdded())); + ui.updated.setText(AndroidUtils.formatDate(ctx, item.getUpdated())); + + // Description + if (item.getDescription() != null) { + ui.description.setText(item.getDescription()); + ui.description.setVisibility(VISIBLE); + } else { + ui.description.setVisibility(GONE); + } + } + + @Override + public int getItemCount() { + return feeds.size(); + } + + public Feed getItem(int position) { + return feeds.get(position); + } + + @Nullable + public Feed getItem(GroupId g) { + for (int i = 0; i < feeds.size(); i++) { + Feed item = feeds.get(i); + if (item.getBlogId().equals(g)) { + return item; + } + } + return null; + } + + public void addAll(Collection<Feed> items) { + feeds.addAll(items); + } + + public void remove(Feed item) { + feeds.remove(item); + } + + public void clear() { + feeds.clear(); + } + + public boolean isEmpty() { + return feeds.size() == 0; + } + + static class FeedViewHolder extends RecyclerView.ViewHolder { + private final TextView title; + private final ImageView delete; + private final TextView imported; + private final TextView updated; + private final TextView author; + private final TextView authorLabel; + private final TextView description; + + FeedViewHolder(View v) { + super(v); + + title = (TextView) v.findViewById(R.id.titleView); + delete = (ImageView) v.findViewById(R.id.deleteButton); + imported = (TextView) v.findViewById(R.id.importedView); + updated = (TextView) v.findViewById(R.id.updatedView); + author = (TextView) v.findViewById(R.id.authorView); + authorLabel = (TextView) v.findViewById(R.id.author); + description = (TextView) v.findViewById(R.id.descriptionView); + } + } + + interface RssFeedListener { + void onDeleteClick(Feed feed); + } + +} diff --git a/briar-android/src/org/briarproject/android/blogs/RssFeedImportActivity.java b/briar-android/src/org/briarproject/android/blogs/RssFeedImportActivity.java new file mode 100644 index 0000000000000000000000000000000000000000..c7d772cba38a3e0389ca47f78866304fa5f4d00b --- /dev/null +++ b/briar-android/src/org/briarproject/android/blogs/RssFeedImportActivity.java @@ -0,0 +1,178 @@ +package org.briarproject.android.blogs; + +import android.content.DialogInterface; +import android.content.Intent; +import android.os.Bundle; +import android.support.v7.app.AlertDialog; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.widget.Button; +import android.widget.EditText; +import android.widget.ProgressBar; + +import org.briarproject.R; +import org.briarproject.android.ActivityComponent; +import org.briarproject.android.BriarActivity; +import org.briarproject.api.db.DbException; +import org.briarproject.api.feed.FeedManager; +import org.briarproject.api.lifecycle.IoExecutor; +import org.briarproject.api.sync.GroupId; + +import java.io.IOException; +import java.util.concurrent.Executor; +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; + +public class RssFeedImportActivity extends BriarActivity { + + private static final Logger LOG = + Logger.getLogger(RssFeedImportActivity.class.getName()); + + private EditText urlInput; + private Button importButton; + private ProgressBar progressBar; + + @Inject + @IoExecutor + protected Executor ioExecutor; + + // Fields that are accessed from background threads must be volatile + private volatile GroupId groupId = null; + @Inject + @SuppressWarnings("WeakerAccess") + volatile FeedManager feedManager; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // 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); + + setContentView(R.layout.activity_rss_feed_import); + + urlInput = (EditText) findViewById(R.id.urlInput); + urlInput.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) { + enableOrDisableImportButton(); + } + }); + + importButton = (Button) findViewById(R.id.importButton); + importButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + publish(); + } + }); + + progressBar = (ProgressBar) findViewById(R.id.progressBar); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + return super.onCreateOptionsMenu(menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + return super.onOptionsItemSelected(item); + } + + @Override + public void injectActivity(ActivityComponent component) { + component.inject(this); + } + + private void enableOrDisableImportButton() { + String url = urlInput.getText().toString(); + if (url.startsWith("http://") || url.startsWith("https://")) + importButton.setEnabled(true); + else + importButton.setEnabled(false); + } + + private void publish() { + // hide import button, show progress bar + importButton.setVisibility(GONE); + progressBar.setVisibility(VISIBLE); + + importFeed(urlInput.getText().toString()); + } + + private void importFeed(final String url) { + ioExecutor.execute(new Runnable() { + @Override + public void run() { + try { + feedManager.addFeed(url, groupId); + feedImported(); + } catch (DbException | IOException e) { + if (LOG.isLoggable(WARNING)) + LOG.log(WARNING, e.toString(), e); + importFailed(); + } + } + }); + } + + private void feedImported() { + runOnUiThread(new Runnable() { + @Override + public void run() { + supportFinishAfterTransition(); + } + }); + } + + private void importFailed() { + runOnUiThread(new Runnable() { + @Override + public void run() { + // hide progress bar, show publish button + progressBar.setVisibility(GONE); + importButton.setVisibility(VISIBLE); + + // show error dialog + AlertDialog.Builder builder = + new AlertDialog.Builder(RssFeedImportActivity.this, + R.style.BriarDialogTheme); + builder.setMessage(R.string.blogs_rss_feeds_import_error); + builder.setNegativeButton(R.string.cancel_button, null); + builder.setPositiveButton(R.string.try_again_button, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + publish(); + } + }); + AlertDialog dialog = builder.create(); + dialog.show(); + } + }); + } + +} + diff --git a/briar-android/src/org/briarproject/android/blogs/RssFeedManageActivity.java b/briar-android/src/org/briarproject/android/blogs/RssFeedManageActivity.java new file mode 100644 index 0000000000000000000000000000000000000000..69c50dde66a78669e83cefcdd211e768bb9c633e --- /dev/null +++ b/briar-android/src/org/briarproject/android/blogs/RssFeedManageActivity.java @@ -0,0 +1,167 @@ +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 org.briarproject.R; +import org.briarproject.android.ActivityComponent; +import org.briarproject.android.BriarActivity; +import org.briarproject.android.blogs.RssFeedAdapter.RssFeedListener; +import org.briarproject.android.util.BriarRecyclerView; +import org.briarproject.api.db.DbException; +import org.briarproject.api.feed.Feed; +import org.briarproject.api.feed.FeedManager; +import org.briarproject.api.sync.GroupId; + +import java.util.List; +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.WARNING; + +public class RssFeedManageActivity extends BriarActivity + implements RssFeedListener { + + private static final Logger LOG = + Logger.getLogger(RssFeedManageActivity.class.getName()); + + private BriarRecyclerView list; + private RssFeedAdapter adapter; + + // Fields that are accessed from background threads must be volatile + private volatile GroupId groupId = null; + @Inject + @SuppressWarnings("WeakerAccess") + volatile FeedManager feedManager; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // 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); + + setContentView(R.layout.activity_rss_feed_manage); + + adapter = new RssFeedAdapter(this, this); + + list = (BriarRecyclerView) findViewById(R.id.feedList); + list.setLayoutManager(new LinearLayoutManager(this)); + list.setAdapter(adapter); + } + + @Override + public void onResume() { + super.onResume(); + loadFeeds(); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + MenuInflater inflater = getMenuInflater(); + inflater.inflate(R.menu.rss_feed_manage_actions, menu); + return super.onCreateOptionsMenu(menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + onBackPressed(); + return true; + case R.id.action_rss_feeds_import: + Intent i = + new Intent(this, RssFeedImportActivity.class); + i.putExtra(GROUP_ID, groupId.getBytes()); + ActivityOptionsCompat options = + makeCustomAnimation(this, android.R.anim.slide_in_left, + android.R.anim.slide_out_right); + ActivityCompat.startActivity(this, i, options.toBundle()); + return true; + default: + return super.onOptionsItemSelected(item); + } + } + + @Override + public void injectActivity(ActivityComponent component) { + component.inject(this); + } + + @Override + public void onDeleteClick(final Feed feed) { + runOnDbThread(new Runnable() { + @Override + public void run() { + try { + feedManager.removeFeed(feed.getUrl()); + onFeedDeleted(feed); + } catch (DbException e) { + if (LOG.isLoggable(WARNING)) + LOG.log(WARNING, e.toString(), e); + onDeleteError(); + } + } + }); + } + + private void loadFeeds() { + runOnDbThread(new Runnable() { + @Override + public void run() { + try { + addFeeds(feedManager.getFeeds()); + } catch (DbException e) { + if (LOG.isLoggable(WARNING)) + LOG.log(WARNING, e.toString(), e); + list.setEmptyText(R.string.blogs_rss_feeds_manage_error); + list.showData(); + } + } + }); + } + + private void addFeeds(final List<Feed> feeds) { + runOnUiThread(new Runnable() { + @Override + public void run() { + if (feeds.size() == 0) list.showData(); + else adapter.addAll(feeds); + } + }); + } + + private void onFeedDeleted(final Feed feed) { + runOnUiThread(new Runnable() { + @Override + public void run() { + adapter.remove(feed); + } + }); + } + + private void onDeleteError() { + runOnUiThread(new Runnable() { + @Override + public void run() { + Snackbar.make(list, + R.string.blogs_rss_feeds_manage_delete_error, + LENGTH_LONG).show(); + } + }); + } +} + diff --git a/briar-android/src/org/briarproject/android/util/BriarRecyclerView.java b/briar-android/src/org/briarproject/android/util/BriarRecyclerView.java index 6c403747f40d5f146a459cebdaab69d5c4139412..4c68c6fe16cef08716bba9dfcf41fb00293e6310 100644 --- a/briar-android/src/org/briarproject/android/util/BriarRecyclerView.java +++ b/briar-android/src/org/briarproject/android/util/BriarRecyclerView.java @@ -47,6 +47,9 @@ public class BriarRecyclerView extends FrameLayout { R.styleable.BriarRecyclerView); isScrollingToEnd = attributes .getBoolean(R.styleable.BriarRecyclerView_scrollToEnd, true); + String emtpyText = + attributes.getString(R.styleable.BriarRecyclerView_emptyText); + if (emtpyText != null) setEmptyText(emtpyText); attributes.recycle(); } @@ -94,6 +97,11 @@ public class BriarRecyclerView extends FrameLayout { super.onItemRangeInserted(positionStart, itemCount); if (itemCount > 0) showData(); } + @Override + public void onItemRangeRemoved(int positionStart, int itemCount) { + super.onItemRangeRemoved(positionStart, itemCount); + if (itemCount > 0) showData(); + } }; }