diff --git a/app/src/main/java/org/transdroid/core/gui/rss/RssFeedsActivity.java b/app/src/main/java/org/transdroid/core/gui/rss/RssFeedsActivity.java
new file mode 100644
index 00000000..8fc448ce
--- /dev/null
+++ b/app/src/main/java/org/transdroid/core/gui/rss/RssFeedsActivity.java
@@ -0,0 +1,424 @@
+/*
+ * Copyright 2010-2018 Eric Kok et al.
+ *
+ * Transdroid is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Transdroid is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Transdroid. If not, see .
+ */
+package org.transdroid.core.gui.rss;
+
+import android.annotation.TargetApi;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.design.widget.TabLayout;
+import android.support.v4.view.PagerAdapter;
+import android.support.v4.view.ViewPager;
+import android.support.v7.app.AppCompatActivity;
+import android.support.v7.widget.Toolbar;
+import android.text.TextUtils;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.nispok.snackbar.Snackbar;
+import com.nispok.snackbar.SnackbarManager;
+import com.nispok.snackbar.enums.SnackbarType;
+
+import org.androidannotations.annotations.AfterViews;
+import org.androidannotations.annotations.Background;
+import org.androidannotations.annotations.Bean;
+import org.androidannotations.annotations.EActivity;
+import org.androidannotations.annotations.FragmentById;
+import org.androidannotations.annotations.InstanceState;
+import org.androidannotations.annotations.NonConfigurationInstance;
+import org.androidannotations.annotations.OptionsItem;
+import org.androidannotations.annotations.UiThread;
+import org.androidannotations.annotations.ViewById;
+import org.transdroid.R;
+import org.transdroid.core.app.settings.ApplicationSettings;
+import org.transdroid.core.app.settings.RssfeedSetting;
+import org.transdroid.core.app.settings.ServerSetting;
+import org.transdroid.core.app.settings.SettingsUtils;
+import org.transdroid.core.gui.TorrentsActivity_;
+import org.transdroid.core.gui.lists.LocalTorrent;
+import org.transdroid.core.gui.log.Log;
+import org.transdroid.core.gui.navigation.NavigationHelper;
+import org.transdroid.core.gui.remoterss.RemoteRssFragment;
+import org.transdroid.core.gui.remoterss.data.RemoteRssChannel;
+import org.transdroid.core.gui.remoterss.data.RemoteRssItem;
+import org.transdroid.core.gui.remoterss.data.RemoteRssSupplier;
+import org.transdroid.core.rssparser.Channel;
+import org.transdroid.core.rssparser.RssParser;
+import org.transdroid.core.service.ConnectivityHelper;
+import org.transdroid.daemon.Daemon;
+import org.transdroid.daemon.DaemonException;
+import org.transdroid.daemon.IDaemonAdapter;
+import org.transdroid.daemon.task.DaemonTaskSuccessResult;
+
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Date;
+import java.util.List;
+
+@EActivity(R.layout.activity_rssfeeds)
+public class RssFeedsActivity extends AppCompatActivity {
+
+ // Settings and local data
+ @Bean
+ protected Log log;
+ @Bean
+ protected ApplicationSettings applicationSettings;
+
+ protected static final int RSS_FEEDS_LOCAL = 0;
+ protected static final int RSS_FEEDS_REMOTE = 1;
+
+ @FragmentById(R.id.rssfeeds_fragment)
+ protected RssFeedsFragment fragmentLocalFeeds;
+ @FragmentById(R.id.rssitems_fragment)
+ protected RssItemsFragment fragmentItems;
+ @FragmentById(R.id.remoterss_fragment)
+ protected RemoteRssFragment fragmentRemoteFeeds;
+
+ @ViewById(R.id.rssfeeds_toolbar)
+ protected Toolbar rssFeedsToolbar;
+ @ViewById(R.id.rssfeeds_tabs)
+ protected TabLayout tabLayout;
+ @ViewById(R.id.rssfeeds_pager)
+ protected ViewPager viewPager;
+
+ // remote RSS stuff
+ @NonConfigurationInstance
+ protected ArrayList feeds;
+ @InstanceState
+ protected int selectedFilter;
+ @NonConfigurationInstance
+ protected ArrayList recentItems;
+ @Bean
+ protected ConnectivityHelper connectivityHelper;
+
+
+ protected class LayoutPagerAdapter extends PagerAdapter {
+ boolean hasRemoteRss;
+ String serverName;
+
+ public LayoutPagerAdapter(boolean hasRemoteRss, String name) {
+ super();
+
+ this.hasRemoteRss = hasRemoteRss;
+ this.serverName = (name.length() > 0 ? name : getString(R.string.navigation_rss_tabs_remote));
+ }
+
+ @NonNull
+ @Override
+ public Object instantiateItem(@NonNull ViewGroup container, int position) {
+ int resId = 0;
+
+ if (position == RSS_FEEDS_LOCAL) {
+ resId = R.id.layout_rssfeeds_local;
+ }
+ else if (position == RSS_FEEDS_REMOTE) {
+ resId = R.id.layout_rss_feeds_remote;
+ }
+
+ return findViewById(resId);
+ }
+
+ @Override
+ public int getCount() {
+ return (this.hasRemoteRss ? 2 : 1);
+ }
+
+ @Override
+ public boolean isViewFromObject(@NonNull View view, @NonNull Object o) {
+ return (view == o);
+ }
+
+ @Override
+ public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
+ container.removeView((View) object);
+ }
+
+ @Nullable
+ @Override
+ public CharSequence getPageTitle(int position) {
+ switch (position) {
+ case RSS_FEEDS_LOCAL:
+ return getString(R.string.navigation_rss_tabs_local);
+ case RSS_FEEDS_REMOTE:
+ return this.serverName;
+ }
+
+ return super.getPageTitle(position);
+ }
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ SettingsUtils.applyDayNightTheme(this);
+ super.onCreate(savedInstanceState);
+ }
+
+ @AfterViews
+ protected void init() {
+ setSupportActionBar(rssFeedsToolbar);
+ getSupportActionBar().setTitle(NavigationHelper.buildCondensedFontString(getString(R.string.rss_feeds)));
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+
+ IDaemonAdapter currentConnection = this.getCurrentConnection();
+ boolean hasRemoteRss = Daemon.supportsRemoteRssManagement(currentConnection.getType());
+
+ PagerAdapter pagerAdapter = new LayoutPagerAdapter(hasRemoteRss, currentConnection.getSettings().getName());
+ viewPager.setAdapter(pagerAdapter);
+ tabLayout.setupWithViewPager(viewPager);
+ viewPager.setCurrentItem(0);
+
+ if (!hasRemoteRss) {
+ tabLayout.setVisibility(View.GONE);
+ }
+ }
+
+ @TargetApi(Build.VERSION_CODES.HONEYCOMB)
+ @OptionsItem(android.R.id.home)
+ protected void navigateUp() {
+ TorrentsActivity_.intent(this).flags(Intent.FLAG_ACTIVITY_CLEAR_TOP).start();
+ }
+
+ /**
+ * Reload the RSS feed settings and start loading all the feeds. To be called from contained fragments.
+ */
+ public void refreshFeeds() {
+ List loaders = new ArrayList<>();
+ // For each RSS feed setting the user created, start a loader that retrieved the RSS feed (via a background
+ // thread) and, on success, determines the new items in the feed
+ for (RssfeedSetting setting : applicationSettings.getRssfeedSettings()) {
+ RssfeedLoader loader = new RssfeedLoader(setting);
+ loaders.add(loader);
+ loadRssfeed(loader);
+ }
+
+ fragmentLocalFeeds.update(loaders);
+ }
+
+ /**
+ * Performs the loading of the RSS feed content and parsing of items, in a background thread.
+ * @param loader The RSS feed loader for which to retrieve the contents
+ */
+ @Background
+ protected void loadRssfeed(RssfeedLoader loader) {
+ try {
+ // Load and parse the feed
+ RssParser parser =
+ new RssParser(loader.getSetting().getUrl(), loader.getSetting().getExcludeFilter(), loader.getSetting().getIncludeFilter());
+ parser.parse();
+ handleRssfeedResult(loader, parser.getChannel(), false);
+ } catch (Exception e) {
+ // Catch any error that may occurred and register this failure
+ handleRssfeedResult(loader, null, true);
+ log.i(this, "RSS feed " + loader.getSetting().getUrl() + " error: " + e.toString());
+ }
+ }
+
+ /**
+ * Stores the retrieved RSS feed content channel into the loader and updates the RSS feed in the feeds list fragment.
+ * @param loader The RSS feed loader that was executed
+ * @param channel The data that was retrieved, or null if it could not be parsed
+ * @param hasError True if a connection error occurred in the loading of the feed; false otherwise
+ */
+ @UiThread
+ protected void handleRssfeedResult(RssfeedLoader loader, Channel channel, boolean hasError) {
+ loader.update(channel, hasError);
+
+ fragmentLocalFeeds.notifyDataSetChanged();
+ }
+
+ /**
+ * Opens an RSS feed in the dedicated fragment (if there was space in the UI) or a new {@link RssItemsActivity}. Optionally this also registers in
+ * the user preferences that the feed was now viewed, so that in the future the new items can be properly marked.
+ * @param loader The RSS feed loader (with settings and the loaded content channel) to show
+ * @param markAsViewedNow True if the user settings should be updated to reflect this feed's last viewed date; false otherwise
+ */
+ public void openRssfeed(RssfeedLoader loader, boolean markAsViewedNow) {
+
+ // The RSS feed content was loaded and can now be shown in the dedicated fragment or a new activity
+ if (fragmentItems != null && fragmentItems.isAdded()) {
+
+ // If desired, update the lastViewedDate and lastViewedItemUrl of this feed in the user setting; this won't
+ // be loaded until the RSS feeds screen in opened again.
+ if (!loader.hasError() && loader.getChannel() != null && markAsViewedNow) {
+ String lastViewedItemUrl = null;
+ if (loader.getChannel().getItems() != null && loader.getChannel().getItems().size() > 0) {
+ lastViewedItemUrl = loader.getChannel().getItems().get(0).getTheLink();
+ }
+ applicationSettings.setRssfeedLastViewer(loader.getSetting().getOrder(), new Date(), lastViewedItemUrl);
+ }
+ fragmentItems.update(loader.getChannel(), loader.hasError(), loader.getSetting().requiresExternalAuthentication());
+
+ } else {
+
+ // Error message or not yet loaded? Show a toast message instead of opening the items activity
+ if (loader.hasError()) {
+ SnackbarManager.show(Snackbar.with(this).text(R.string.rss_error).colorResource(R.color.red));
+ return;
+ }
+ if (loader.getChannel() == null || loader.getChannel().getItems().size() == 0) {
+ SnackbarManager.show(Snackbar.with(this).text(R.string.rss_notloaded).colorResource(R.color.red));
+ return;
+ }
+
+ // If desired, update the lastViewedDate and lastViewedItemUrl of this feed in the user setting; this won't
+ // be loaded until the RSS feeds screen in opened again
+ if (markAsViewedNow) {
+ String lastViewedItemUrl = null;
+ if (loader.getChannel().getItems() != null && loader.getChannel().getItems().size() > 0) {
+ lastViewedItemUrl = loader.getChannel().getItems().get(0).getTheLink();
+ }
+ applicationSettings.setRssfeedLastViewer(loader.getSetting().getOrder(), new Date(), lastViewedItemUrl);
+ }
+
+ String name = loader.getChannel().getTitle();
+ if (TextUtils.isEmpty(name)) {
+ name = loader.getSetting().getName();
+ }
+ if (TextUtils.isEmpty(name) && !TextUtils.isEmpty(loader.getSetting().getUrl())) {
+ name = Uri.parse(loader.getSetting().getUrl()).getHost();
+ }
+ RssItemsActivity_.intent(this).rssfeed(loader.getChannel()).rssfeedName(name)
+ .requiresExternalAuthentication(loader.getSetting().requiresExternalAuthentication()).start();
+
+ }
+ }
+
+ protected IDaemonAdapter getCurrentConnection() {
+ ServerSetting lastUsed = applicationSettings.getLastUsedServer();
+ return lastUsed.createServerAdapter(connectivityHelper.getConnectedNetworkName(), this);
+ }
+
+ // @Background
+ public void refreshRemoteFeeds() {
+ // Connect to the last used server
+ IDaemonAdapter currentConnection = this.getCurrentConnection();
+
+ // remote rss not supported for this connection type
+ if (currentConnection instanceof RemoteRssSupplier == false) {
+ return;
+ }
+
+ try {
+ feeds = ((RemoteRssSupplier) (currentConnection)).getRemoteRssChannels(log);
+
+ // By default it displays the latest items within the last month.
+ recentItems = new ArrayList<>();
+ Calendar calendar = Calendar.getInstance();
+ calendar.add(Calendar.MONTH, -1);
+ Date oneMonthAgo = calendar.getTime();
+
+ for (RemoteRssChannel feed : feeds) {
+ for (RemoteRssItem item : feed.getItems()) {
+ if (item.getTimestamp().after(oneMonthAgo)) {
+ recentItems.add(item);
+ }
+ }
+ }
+
+ // Sort by -newest
+ Collections.sort(recentItems, new Comparator() {
+ @Override
+ public int compare(RemoteRssItem lhs, RemoteRssItem rhs) {
+ return rhs.getTimestamp().compareTo(lhs.getTimestamp());
+ }
+ });
+ } catch (DaemonException e) {
+ onCommunicationError(e);
+ return;
+ }
+
+ // @UIThread
+ fragmentRemoteFeeds.updateRemoteItems(
+ selectedFilter == 0 ? recentItems : feeds.get(selectedFilter -1).getItems(),
+ false /* allow android to restore scroll position */ );
+ showRemoteChannelFilters();
+ }
+
+ @UiThread
+ protected void onCommunicationError(DaemonException daemonException) {
+ //noinspection ThrowableResultOfMethodCallIgnored
+ log.i(this, daemonException.toString());
+ String error = getString(LocalTorrent.getResourceForDaemonException(daemonException));
+ SnackbarManager.show(Snackbar.with(this).text(error).colorResource(R.color.red).type(SnackbarType.MULTI_LINE));
+ }
+
+
+ public void onFeedSelected(int position) {
+ selectedFilter = position;
+
+ if (position == 0) {
+ fragmentRemoteFeeds.updateRemoteItems(recentItems, true);
+ }
+ else {
+ RemoteRssChannel channel = feeds.get(selectedFilter -1);
+ fragmentRemoteFeeds.updateRemoteItems(channel.getItems(), true);
+ }
+ }
+
+ /**
+ * Download the item in a background thread and display success/fail accordingly.
+ */
+ @Background
+ public void downloadRemoteRssItem(RemoteRssItem item) {
+ final RemoteRssSupplier supplier = (RemoteRssSupplier) this.getCurrentConnection();
+
+ try {
+ RemoteRssChannel channel = feeds.get(selectedFilter);
+ supplier.downloadRemoteRssItem(log, item, channel);
+ onTaskSucceeded(null, getString(R.string.result_added, item.getTitle()));
+ } catch (DaemonException e) {
+ onTaskFailed(getString(LocalTorrent.getResourceForDaemonException(e)));
+ }
+ }
+
+ @UiThread
+ protected void onTaskSucceeded(DaemonTaskSuccessResult result, String successMessage) {
+ SnackbarManager.show(Snackbar.with(this).text(successMessage));
+ }
+
+ @UiThread
+ protected void onTaskFailed(String message) {
+ SnackbarManager.show(Snackbar.with(this)
+ .text(message)
+ .colorResource(R.color.red)
+ .type(SnackbarType.MULTI_LINE)
+ );
+ }
+
+ private void showRemoteChannelFilters() {
+ List feedLabels = new ArrayList<>(feeds.size() +1);
+ feedLabels.add(new RemoteRssChannel() {
+ @Override
+ public String getName() {
+ return getString(R.string.remoterss_filter_allrecent);
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ }
+ });
+ feedLabels.addAll(feeds);
+
+ fragmentRemoteFeeds.updateChannelFilters(feedLabels);
+ }
+}