aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/build.gradle2
-rw-r--r--app/libs/volley.jarbin0 -> 85763 bytes
-rwxr-xr-xapp/src/androidTest/java/org/rssin/neurons/FeedSorterTest.java128
-rwxr-xr-xapp/src/androidTest/java/org/rssin/neurons/NeuralNetworkTest.java133
-rw-r--r--app/src/androidTest/java/org/rssin/rss/FeedLoaderTest.java23
-rwxr-xr-x[-rw-r--r--]app/src/main/AndroidManifest.xml35
-rw-r--r--app/src/main/java/org/rssin/android/DefaultStorageProvider.java38
-rw-r--r--app/src/main/java/org/rssin/android/FilterActivity.java142
-rw-r--r--app/src/main/java/org/rssin/android/FilterSettingsActivity.java376
-rwxr-xr-xapp/src/main/java/org/rssin/android/FiltersActivity.java227
-rwxr-xr-xapp/src/main/java/org/rssin/android/FiltersList.java72
-rw-r--r--app/src/main/java/org/rssin/android/InternalStorageProvider.java130
-rw-r--r--app/src/main/java/org/rssin/android/SettingsActivity.java148
-rw-r--r--app/src/main/java/org/rssin/android/SharedPreferencesStorageProvider.java128
-rw-r--r--app/src/main/java/org/rssin/android/UnifiedInboxActivity.java (renamed from app/src/main/java/org/rssin/rssin/UnifiedInboxActivity.java)20
-rw-r--r--app/src/main/java/org/rssin/android/VolleyFetcher.java59
-rw-r--r--app/src/main/java/org/rssin/http/Fetcher.java13
-rw-r--r--app/src/main/java/org/rssin/http/Request.java20
-rw-r--r--app/src/main/java/org/rssin/listener/DismissListener.java15
-rw-r--r--app/src/main/java/org/rssin/listener/ErrorListener.java8
-rw-r--r--app/src/main/java/org/rssin/listener/FallibleListener.java7
-rw-r--r--app/src/main/java/org/rssin/listener/Listener.java8
-rw-r--r--app/src/main/java/org/rssin/listener/RealtimeListener.java8
-rwxr-xr-xapp/src/main/java/org/rssin/neurons/FeedSorter.java246
-rwxr-xr-xapp/src/main/java/org/rssin/neurons/Feedback.java18
-rwxr-xr-xapp/src/main/java/org/rssin/neurons/MultiNeuralNetwork.java22
-rwxr-xr-xapp/src/main/java/org/rssin/neurons/MultiNeuralNetworkPrediction.java27
-rwxr-xr-xapp/src/main/java/org/rssin/neurons/NeuralNetwork.java56
-rwxr-xr-xapp/src/main/java/org/rssin/neurons/NeuralNetworkPrediction.java16
-rwxr-xr-xapp/src/main/java/org/rssin/neurons/Neuron.java26
-rwxr-xr-xapp/src/main/java/org/rssin/neurons/PredictionInterface.java11
-rwxr-xr-xapp/src/main/java/org/rssin/neurons/SentenceSplitter.java36
-rwxr-xr-xapp/src/main/java/org/rssin/neurons/TrainingCase.java25
-rw-r--r--app/src/main/java/org/rssin/rss/Feed.java238
-rw-r--r--app/src/main/java/org/rssin/rss/FeedItem.java93
-rw-r--r--app/src/main/java/org/rssin/rss/FeedLoader.java249
-rw-r--r--app/src/main/java/org/rssin/rssin/Feed.java66
-rwxr-xr-xapp/src/main/java/org/rssin/rssin/FeedLoaderAndSorter.java102
-rwxr-xr-x[-rw-r--r--]app/src/main/java/org/rssin/rssin/Filter.java133
-rw-r--r--app/src/main/java/org/rssin/rssin/Keyword.java19
-rw-r--r--app/src/main/java/org/rssin/storage/FilterStorageProvider.java14
-rw-r--r--app/src/main/java/org/rssin/storage/Storable.java10
-rw-r--r--app/src/main/java/org/rssin/storage/StorageProvider.java28
-rwxr-xr-xapp/src/main/java/org/rssin/tools/SerializationTools.java69
-rw-r--r--app/src/main/res/layout/activity_filter.xml13
-rw-r--r--app/src/main/res/layout/activity_filter_settings.xml38
-rw-r--r--app/src/main/res/layout/activity_filters.xml17
-rw-r--r--app/src/main/res/layout/activity_unified_inbox.xml3
-rw-r--r--app/src/main/res/layout/fragment_filter_settings_feeds.xml34
-rw-r--r--app/src/main/res/layout/item_feeditem.xml19
-rw-r--r--app/src/main/res/layout/item_filter.xml19
-rw-r--r--app/src/main/res/layout/item_filter_settings_feed.xml19
-rw-r--r--app/src/main/res/layout/item_filter_settings_keyword.xml14
-rw-r--r--app/src/main/res/menu/menu_filter.xml6
-rw-r--r--app/src/main/res/menu/menu_filter_settings.xml23
-rw-r--r--app/src/main/res/menu/menu_filters.xml14
-rw-r--r--app/src/main/res/menu/menu_unified_inbox.xml3
-rw-r--r--app/src/main/res/mipmap-hdpi/ic_launcher.pngbin3418 -> 2791 bytes
-rw-r--r--app/src/main/res/mipmap-mdpi/ic_launcher.pngbin2206 -> 1838 bytes
-rw-r--r--app/src/main/res/mipmap-xhdpi/ic_launcher.pngbin4842 -> 3976 bytes
-rw-r--r--app/src/main/res/mipmap-xxhdpi/ic_launcher.pngbin7718 -> 6489 bytes
-rw-r--r--app/src/main/res/mipmap-xxxhdpi/ic_launcher.pngbin0 -> 9188 bytes
-rw-r--r--app/src/main/res/values/dimens.xml7
-rw-r--r--app/src/main/res/values/strings.xml28
-rw-r--r--app/src/main/res/values/strings_activity_settings.xml34
-rw-r--r--app/src/main/res/xml/pref_data_sync.xml21
-rw-r--r--app/src/main/res/xml/pref_headers.xml6
-rw-r--r--app/src/main/res/xml/pref_main.xml3
-rw-r--r--docs/classes.txt19
69 files changed, 3392 insertions, 192 deletions
diff --git a/app/build.gradle b/app/build.gradle
index 3663e88..95bbeb0 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -22,4 +22,6 @@ android {
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'com.android.support:appcompat-v7:22.0.0'
+ compile 'com.android.support:support-v4:22.0.0'
+ compile files('libs/volley.jar')
}
diff --git a/app/libs/volley.jar b/app/libs/volley.jar
new file mode 100644
index 0000000..d9279b4
--- /dev/null
+++ b/app/libs/volley.jar
Binary files differ
diff --git a/app/src/androidTest/java/org/rssin/neurons/FeedSorterTest.java b/app/src/androidTest/java/org/rssin/neurons/FeedSorterTest.java
new file mode 100755
index 0000000..a6e3581
--- /dev/null
+++ b/app/src/androidTest/java/org/rssin/neurons/FeedSorterTest.java
@@ -0,0 +1,128 @@
+package org.rssin.neurons;
+
+import android.util.Log;
+
+import junit.framework.Assert;
+import junit.framework.TestCase;
+
+import org.rssin.rss.FeedItem;
+
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.LinkedList;
+import java.util.List;
+
+public class FeedSorterTest extends TestCase {
+
+ public void testFeedback() throws Exception {
+ Log.d("FeedSorterTest", "Dit is een test");
+ Assert.assertTrue(true);
+ }
+
+ public void testSortItemsByCategory() throws Exception {
+ List<String> gameList = new ArrayList<>();
+ gameList.add("Games");
+ List<String> sportList = new ArrayList<>();
+ sportList.add("Sport");
+
+ FeedItem[] likedItems = new FeedItem[]
+ {
+ new FeedItem("", new Date(), "TITEL", "DESCRIPTION", "", "Randy", gameList, "", "", ""),
+ };
+
+ FeedItem[] dislikedItems = new FeedItem[]
+ {
+ new FeedItem("", new Date(), "TITEL", "DESCRIPTION", "", "Randy", sportList, "", "", ""),
+ };
+
+ FeedSorter s = new FeedSorter();
+
+ //I like games & I hate sports
+ trainNetwork(likedItems, dislikedItems, s);
+
+ FeedItem sportsItem = new FeedItem("", new Date(2014, 1, 1, 1, 2, 1), "SPORT ARTICLE", "DESCRIPTION", "", "Randy", sportList, "", "", "");
+ FeedItem gamesItem = new FeedItem("", new Date(2014, 1, 1, 1, 1, 1), "GAME ARTICLE", "DESCRIPTION", "", "Randy", gameList, "", "", "");
+
+ testSortingOrder(s, sportsItem, gamesItem);
+ }
+
+ public void testSortItemsByTitle() throws Exception {
+ List<String> emptyList = new ArrayList<>();
+
+ FeedItem[] likedItems = new FeedItem[]
+ {
+ new FeedItem("", new Date(), "Video games are cool", "DESCRIPTION", "", "Randy", emptyList, "", "", ""),
+ new FeedItem("", new Date(), "The new video game", "DESCRIPTION", "", "Camil", emptyList, "", "", ""),
+ new FeedItem("", new Date(), "Best games of 2015", "DESCRIPTION", "", "Jos", emptyList, "", "", ""),
+ };
+
+ FeedItem[] dislikedItems = new FeedItem[]
+ {
+ new FeedItem("", new Date(), "Video of a cat", "DESCRIPTION", "", "Randy", emptyList, "", "", ""),
+ new FeedItem("", new Date(), "It's raining", "DESCRIPTION", "", "Joep", emptyList, "", "", ""),
+ new FeedItem("", new Date(), "Shocking video of a cat in the rain.", "DESCRIPTION", "", "Joep", emptyList, "", "", ""),
+ };
+
+ FeedSorter s = new FeedSorter();
+
+ //I like games & I hate sports
+ trainNetwork(likedItems, dislikedItems, s);
+
+ FeedItem dislikedItem = new FeedItem("", new Date(2014, 1, 1, 1, 2, 1), "Another cool video of a cat in the sun.", "DESCRIPTION", "", "Randy", emptyList, "", "", "");
+ FeedItem likedItem = new FeedItem("", new Date(2014, 1, 1, 1, 1, 1), "Coolest retro games", "DESCRIPTION", "", "Jos", emptyList, "", "", "");
+
+ testSortingOrder(s, dislikedItem, likedItem);
+ }
+
+ public void testSortItemsByAuthor() throws Exception {
+ List<String> emptyList = new ArrayList<>();
+
+ FeedItem[] likedItems = new FeedItem[]
+ {
+ new FeedItem("", new Date(), "Best games of 2015", "DESCRIPTION", "", "Jos", emptyList, "", "", ""),
+ new FeedItem("", new Date(), "It's raining cats and dogs!", "DESCRIPTION", "", "Jos", emptyList, "", "", ""),
+ };
+
+ FeedItem[] dislikedItems = new FeedItem[]
+ {
+ new FeedItem("", new Date(), "Video of a cat", "DESCRIPTION", "", "Randy", emptyList, "", "", ""),
+ new FeedItem("", new Date(), "It's raining", "DESCRIPTION", "", "Joep", emptyList, "", "", ""),
+ new FeedItem("", new Date(), "Shocking video of a cat in the rain.", "DESCRIPTION", "", "Joep", emptyList, "", "", ""),
+ new FeedItem("", new Date(), "Video games are cool", "DESCRIPTION", "", "Randy", emptyList, "", "", ""),
+ new FeedItem("", new Date(), "The new video game", "DESCRIPTION", "", "Camil", emptyList, "", "", ""),
+ };
+
+ FeedSorter s = new FeedSorter();
+
+ //I like games & I hate sports
+ trainNetwork(likedItems, dislikedItems, s);
+
+ FeedItem dislikedItem = new FeedItem("", new Date(2014, 1, 1, 1, 2, 1), "Another cool video of a cat in the sun.", "DESCRIPTION", "", "Randy", emptyList, "", "", "");
+ FeedItem likedItem = new FeedItem("", new Date(2014, 1, 1, 1, 1, 1), "Coolest retro games", "DESCRIPTION", "", "Jos", emptyList, "", "", "");
+
+ testSortingOrder(s, dislikedItem, likedItem);
+ }
+
+ private void testSortingOrder(FeedSorter s, FeedItem dislikedItem, FeedItem likedItem) {
+ List<FeedItem> testItems = new LinkedList<>();
+ testItems.add(dislikedItem);
+ testItems.add(likedItem);
+
+ List<FeedItem> sortedItems = s.sortItems(testItems);
+ Assert.assertEquals(sortedItems.get(0), likedItem);
+ Assert.assertEquals(sortedItems.get(1), dislikedItem);
+ }
+
+ private void trainNetwork(FeedItem[] likedItems, FeedItem[] dislikedItems, FeedSorter s) {
+ for(int i = 0; i < 200; i++) {
+ for (FeedItem item : likedItems) {
+ s.feedback(item, Feedback.Like);
+ }
+
+ for (FeedItem item : dislikedItems) {
+ s.feedback(item, Feedback.Dislike);
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/app/src/androidTest/java/org/rssin/neurons/NeuralNetworkTest.java b/app/src/androidTest/java/org/rssin/neurons/NeuralNetworkTest.java
new file mode 100755
index 0000000..57776f3
--- /dev/null
+++ b/app/src/androidTest/java/org/rssin/neurons/NeuralNetworkTest.java
@@ -0,0 +1,133 @@
+package org.rssin.neurons;
+
+import junit.framework.Assert;
+import junit.framework.TestCase;
+
+public class NeuralNetworkTest extends TestCase {
+
+ public void testAnd() throws Exception {
+ MultiNeuralNetwork nn = new MultiNeuralNetwork(10, 2);
+ nn.addInput();//bias
+ nn.addInput();//inputA
+ nn.addInput();//inputB
+
+ //Simple AND
+ for (int i = 0; i < 300; i++)
+ {
+ PredictionInterface p1 = nn.computeOutput(new double[] {
+ 1,
+ -1,
+ -1,
+ });
+ p1.learn(-1);
+
+ PredictionInterface p2 = nn.computeOutput(new double[] {
+ 1,
+ -1,
+ 1,
+ });
+ p2.learn(-1);
+
+ PredictionInterface p3 = nn.computeOutput(new double[] {
+ 1,
+ 1,
+ -1,
+ });
+ p3.learn(-1);
+
+ PredictionInterface p4 = nn.computeOutput(new double[] {
+ 1,
+ 1,
+ 1,
+ });
+ p4.learn(1);
+ }
+
+ Assert.assertTrue(nn.computeOutput(new double[] {
+ 1,
+ -1,
+ -1,
+ }).getOutput() < 0);
+
+ Assert.assertTrue(nn.computeOutput(new double[] {
+ 1,
+ 1,
+ -1,
+ }).getOutput() < 0);
+
+ Assert.assertTrue(nn.computeOutput(new double[] {
+ 1,
+ -1,
+ 1,
+ }).getOutput() < 0);
+
+ Assert.assertTrue(nn.computeOutput(new double[] {
+ 1,
+ 1,
+ 1,
+ }).getOutput() > 0);
+ }
+
+ public void testXor() throws Exception {
+ MultiNeuralNetwork nn = new MultiNeuralNetwork(10, 2);
+ nn.addInput();
+ nn.addInput();
+ nn.addInput();
+
+ //Simple AND
+ for (int i = 0; i < 300; i++)
+ {
+ PredictionInterface p1 = nn.computeOutput(new double[] {
+ 1,
+ -1,
+ -1,
+ });
+ p1.learn(-1);
+
+ PredictionInterface p2 = nn.computeOutput(new double[] {
+ 1,
+ -1,
+ 1,
+ });
+ p2.learn(1);
+
+ PredictionInterface p3 = nn.computeOutput(new double[] {
+ 1,
+ 1,
+ -1,
+ });
+ p3.learn(1);
+
+ PredictionInterface p4 = nn.computeOutput(new double[] {
+ 1,
+ 1,
+ 1,
+ });
+ p4.learn(-1);
+ }
+
+ Assert.assertTrue(nn.computeOutput(new double[] {
+ 1,
+ -1,
+ -1,
+ }).getOutput() < 0);
+
+ Assert.assertTrue(nn.computeOutput(new double[] {
+ 1,
+ 1,
+ -1,
+ }).getOutput() > 0);
+
+ Assert.assertTrue(nn.computeOutput(new double[] {
+ 1,
+ -1,
+ 1,
+ }).getOutput() > 0);
+
+ Assert.assertTrue(nn.computeOutput(new double[] {
+ 1,
+ 1,
+ 1,
+ }).getOutput() < 0);
+ }
+} \ No newline at end of file
diff --git a/app/src/androidTest/java/org/rssin/rss/FeedLoaderTest.java b/app/src/androidTest/java/org/rssin/rss/FeedLoaderTest.java
new file mode 100644
index 0000000..f4bbfdd
--- /dev/null
+++ b/app/src/androidTest/java/org/rssin/rss/FeedLoaderTest.java
@@ -0,0 +1,23 @@
+package org.rssin.rss;
+
+import junit.framework.Assert;
+import junit.framework.TestCase;
+
+import java.net.URL;
+
+/**
+ * Created by Randy on 21-5-2015.
+ */
+public class FeedLoaderTest extends TestCase {
+
+
+ public void testFetchXML() throws Exception {
+ String urlstring = "http://www.pcworld.com/index.rss";
+ URL url = new URL(urlstring);
+ FeedLoader loader = new FeedLoader(url);
+ loader.fetchXML();
+ FeedItem f = loader.getFeed().getPosts().get(0);
+ Assert.assertEquals(f.getTitle(), "Amazon adds local groceries and meals to one-hour Prime Now delivery service");
+ }
+
+} \ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 58b8540..e4f63c3 100644..100755
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -1,16 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.rssin.rssin" >
- <uses-permission
- android:name="android.permission.INTERNET"
- />
+
+ <uses-permission android:name="android.permission.INTERNET" />
+
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
<activity
- android:name=".UnifiedInboxActivity"
+ android:name="org.rssin.android.UnifiedInboxActivity"
android:label="@string/app_name" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
@@ -18,6 +18,33 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
+ <activity
+ android:name="org.rssin.android.SettingsActivity"
+ android:label="@string/title_activity_settings" >
+ </activity>
+ <activity
+ android:name="org.rssin.android.FiltersActivity"
+ android:label="@string/title_activity_filters"
+ android:parentActivityName="org.rssin.android.UnifiedInboxActivity" >
+ <meta-data
+ android:name="android.support.PARENT_ACTIVITY"
+ android:value="org.rssin.android.UnifiedInboxActivity" />
+ </activity>
+ <activity
+ android:name="org.rssin.android.FilterSettingsActivity"
+ android:label="@string/title_activity_filter_settings" >
+ <meta-data
+ android:name="android.support.PARENT_ACTIVITY"
+ android:value="org.rssin.android.FiltersActivity" />
+ </activity>
+ <activity
+ android:name="org.rssin.android.FilterActivity"
+ android:label="@string/title_activity_filter"
+ android:parentActivityName="org.rssin.android.FiltersActivity" >
+ <meta-data
+ android:name="android.support.PARENT_ACTIVITY"
+ android:value="org.rssin.android.FiltersActivity" />
+ </activity>
</application>
</manifest>
diff --git a/app/src/main/java/org/rssin/android/DefaultStorageProvider.java b/app/src/main/java/org/rssin/android/DefaultStorageProvider.java
new file mode 100644
index 0000000..f47419d
--- /dev/null
+++ b/app/src/main/java/org/rssin/android/DefaultStorageProvider.java
@@ -0,0 +1,38 @@
+package org.rssin.android;
+
+import android.content.Context;
+
+/**
+ * Default Storage Provider
+ * At the moment, this is merely a wrapper for SharedPreferencesStorageProvider.
+ * This class enables us to easily change the storage method we use throughout the app.
+ *
+ * @author Camil Staps
+ */
+class DefaultStorageProvider extends SharedPreferencesStorageProvider {
+
+ protected DefaultStorageProvider(Context context) {
+ super(context);
+ }
+
+ /**
+ * Get a possibly new instance of the StorageProvider
+ * @param context if there is no instance yet, we use this to build it
+ * @return
+ */
+ public static DefaultStorageProvider getInstance(Context context) {
+ if (instance == null) {
+ instance = new DefaultStorageProvider(context);
+ }
+ return (DefaultStorageProvider) instance;
+ }
+
+ /**
+ * Get the saved instance, and return null if it doesn't exist
+ * @return
+ */
+ public static DefaultStorageProvider getInstance() {
+ return (DefaultStorageProvider) instance;
+ }
+
+}
diff --git a/app/src/main/java/org/rssin/android/FilterActivity.java b/app/src/main/java/org/rssin/android/FilterActivity.java
new file mode 100644
index 0000000..b8631db
--- /dev/null
+++ b/app/src/main/java/org/rssin/android/FilterActivity.java
@@ -0,0 +1,142 @@
+package org.rssin.android;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.support.v7.app.ActionBarActivity;
+import android.os.Bundle;
+import android.text.Html;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.ListView;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.android.volley.VolleyError;
+
+import org.rssin.listener.FallibleListener;
+import org.rssin.rss.FeedItem;
+import org.rssin.rssin.FeedLoaderAndSorter;
+import org.rssin.rssin.Filter;
+import org.rssin.rssin.R;
+
+import java.io.IOException;
+import java.util.List;
+
+public class FilterActivity extends ActionBarActivity {
+
+ private FiltersList filtersList;
+ private Filter filter;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_filter);
+
+ try {
+ filtersList = FiltersList.getInstance(this);
+ } catch (IOException e) {
+ Toast.makeText(this, getResources().getString(R.string.error_load_filters), Toast.LENGTH_SHORT).show();
+ }
+
+ Intent intent = getIntent();
+ int filterHashCode = intent.getIntExtra("filter", -1);
+
+ // @todo Check on -1? Shouldn't happen anyway.
+ filter = filtersList.getFilterFromHashCode(filterHashCode);
+ filter.ensureFeedSorter(DefaultStorageProvider.getInstance(this));
+
+ setTitle(filter.getTitle());
+
+ final Activity activity = this;
+ final ListView itemsListView = (ListView) findViewById(R.id.filter_items_list);
+ FeedLoaderAndSorter loaderAndSorter = new FeedLoaderAndSorter(filter);
+ loaderAndSorter.getFilteredFeedItems(new VolleyFetcher(this), new FallibleListener<List<FeedItem>, VolleyError>() {
+ @Override
+ public void onReceive(List<FeedItem> data) {
+ FeedItemAdapter feedItemAdapter = new FeedItemAdapter(activity, R.layout.item_feeditem, data);
+ itemsListView.setAdapter(feedItemAdapter);
+ }
+
+ @Override
+ public void onError(VolleyError error) {
+ Toast.makeText(getBaseContext(), getResources().getString(R.string.error_net_load), Toast.LENGTH_SHORT).show();
+ Log.e("FA", "VolleyError", error);
+ }
+ });
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ getMenuInflater().inflate(R.menu.menu_filter, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ int id = item.getItemId();
+
+ if (id == R.id.action_settings) {
+ Intent intent = new Intent(getApplicationContext(), FilterSettingsActivity.class);
+ intent.putExtra("filter", filter.hashCode());
+ startActivity(intent);
+ return true;
+ }
+
+ return super.onOptionsItemSelected(item);
+ }
+
+ /**
+ * Custom ArrayAdapter to display Keywords
+ */
+ private static class FeedItemAdapter extends ArrayAdapter<FeedItem> {
+ Context context;
+ int layoutResourceId;
+ List<FeedItem> feedItems;
+
+ public FeedItemAdapter(Context context, int resource, List<FeedItem> objects) {
+ super(context, resource, objects);
+ this.context = context;
+ layoutResourceId = resource;
+ feedItems = objects;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ View row = convertView;
+ KeywordHolder holder = null;
+
+ if (row == null) {
+ LayoutInflater inflater = ((Activity) context).getLayoutInflater();
+ row = inflater.inflate(layoutResourceId, parent, false);
+
+ holder = new KeywordHolder();
+ holder.title = (TextView) row.findViewById(R.id.feeditem_title);
+ holder.summary = (TextView) row.findViewById(R.id.feeditem_summary);
+
+ row.setTag(holder);
+ } else {
+ holder = (KeywordHolder) row.getTag();
+ }
+
+ FeedItem feedItem = feedItems.get(position);
+ holder.title.setText(feedItem.getTitle());
+ holder.summary.setText(Html.fromHtml(feedItem.getDescription()));
+
+ return row;
+ }
+
+ /**
+ * TextViews holder
+ */
+ private static class KeywordHolder {
+ TextView title;
+ TextView summary;
+ }
+ }
+}
diff --git a/app/src/main/java/org/rssin/android/FilterSettingsActivity.java b/app/src/main/java/org/rssin/android/FilterSettingsActivity.java
new file mode 100644
index 0000000..9639731
--- /dev/null
+++ b/app/src/main/java/org/rssin/android/FilterSettingsActivity.java
@@ -0,0 +1,376 @@
+package org.rssin.android;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.support.v7.app.ActionBarActivity;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.ListView;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import org.rssin.rssin.Feed;
+import org.rssin.rssin.Filter;
+import org.rssin.rssin.Keyword;
+import org.rssin.rssin.R;
+
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.List;
+
+public class FilterSettingsActivity extends ActionBarActivity {
+
+ private FiltersList filtersList;
+ private Filter filter;
+ private KeywordAdapter keywordAdapter;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_filter_settings);
+
+ try {
+ filtersList = FiltersList.getInstance(this);
+ } catch (IOException e) {
+ Toast.makeText(this, getResources().getString(R.string.error_load_filters), Toast.LENGTH_SHORT).show();
+ }
+
+ Intent intent = getIntent();
+ int filterHashCode = intent.getIntExtra("filter", -1);
+
+ // @todo Check on -1? Shouldn't happen anyway.
+ filter = filtersList.getFilterFromHashCode(filterHashCode);
+
+ setTitle();
+
+ keywordAdapter = new KeywordAdapter(this, R.layout.item_filter_settings_keyword, filter.getKeywords());
+ ListView keywordsListView = (ListView) findViewById(R.id.filter_settings_feeds_list);
+ keywordsListView.setAdapter(keywordAdapter);
+ keywordsListView.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() {
+ @Override
+ public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
+ Keyword keyword = keywordAdapter.getItem(position);
+ try {
+ filter.getKeywords().remove(keyword);
+ filtersList.save();
+ keywordAdapter.notifyDataSetChanged();
+ return true;
+ } catch (Exception e) {
+ filter.getKeywords().add(keyword);
+ keywordAdapter.notifyDataSetChanged();
+ Toast.makeText(getBaseContext(), getResources().getString(R.string.error_save_filters), Toast.LENGTH_SHORT).show();
+ return false;
+ }
+ }
+ });
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ getMenuInflater().inflate(R.menu.menu_filter_settings, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ int id = item.getItemId();
+
+ switch (id) {
+ case R.id.filter_settings_action_feeds:
+ openFeedsDialog();
+ return true;
+ case R.id.filter_settings_action_title:
+ openTitleDialog();
+ return true;
+ case R.id.filter_settings_action_delete:
+ filtersList.getFilters().remove(filter);
+ try {
+ filtersList.save();
+ finish();
+ } catch (Exception e) {
+ Toast.makeText(this, getResources().getString(R.string.error_delete_filter), Toast.LENGTH_SHORT).show();
+ }
+ return true;
+ }
+
+ return super.onOptionsItemSelected(item);
+ }
+
+ /**
+ * Set the title according to the filter
+ */
+ public void setTitle() {
+ if (filter != null) {
+ setTitle(getResources().getString(R.string.title_activity_filter_settings) + ": " + filter.getTitle());
+ }
+ }
+
+ /**
+ * Open dialog to edit feeds
+ */
+ public void openFeedsDialog() {
+ FeedsDialogFragment f = new FeedsDialogFragment();
+ Bundle bundle = new Bundle();
+ bundle.putInt("filter", filter.hashCode());
+ f.setArguments(bundle);
+ f.show(getFragmentManager(), "");
+ }
+
+ /**
+ * Open dialog to edit title
+ * For the moment, we temporarily disable rotating because we can't get it working otherwise.
+ * @todo make rotating possible
+ */
+ public void openTitleDialog() {
+ setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_NOSENSOR);
+
+ AlertDialog.Builder alert = new AlertDialog.Builder(this);
+
+ alert.setTitle("Title");
+ alert.setMessage("New title:");
+
+ final EditText input = new EditText(this);
+ input.setText(filter.getTitle());
+ input.setFocusable(true);
+ input.requestFocus();
+
+ AlertDialog dialog = alert
+ .setView(input)
+ .setPositiveButton(getResources().getString(R.string.button_apply), new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int whichButton) {
+ String value = input.getText().toString();
+ try {
+ filter.setTitle(value);
+ filtersList.save();
+ setTitle();
+ } catch (Exception e) {
+ Toast.makeText(getBaseContext(), getResources().getString(R.string.error_save_filters), Toast.LENGTH_SHORT).show();
+ }
+ setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR);
+ }
+ })
+ .setNegativeButton(getResources().getString(R.string.button_cancel), new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR);
+ }
+ })
+ .setOnCancelListener(new DialogInterface.OnCancelListener() {
+ @Override
+ public void onCancel(DialogInterface dialog) {
+ setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR);
+ }
+ })
+ .create();
+
+ dialog.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE);
+ dialog.show();
+ }
+
+ /**
+ * Add keyword
+ * @param v ignored, used because this is an onClick method
+ */
+ public void addKeyword(View v) {
+ EditText editText = (EditText) findViewById(R.id.filter_settings_add_keyword);
+ String keyword = editText.getText().toString();
+
+ Keyword k = new Keyword(keyword);
+ filter.getKeywords().add(k);
+ try {
+ filter.store(DefaultStorageProvider.getInstance());
+ keywordAdapter.notifyDataSetChanged();
+ editText.setText("");
+ } catch (Exception e) {
+ filter.getKeywords().remove(k);
+ keywordAdapter.notifyDataSetChanged();
+ Toast.makeText(this, getResources().getString(R.string.error_save_filters), Toast.LENGTH_SHORT).show();
+ Log.e("FSA", e.getMessage(), e);
+ }
+ }
+
+ /**
+ * Custom Dialog to display & edit feeds
+ */
+ public static class FeedsDialogFragment extends DialogFragment {
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ final View view = getActivity().getLayoutInflater().inflate(R.layout.fragment_filter_settings_feeds, null);
+
+ final FiltersList filtersList = FiltersList.getInstance();
+ final Filter filter = filtersList.getFilterFromHashCode(getArguments().getInt("filter"));
+
+ final FeedAdapter feedAdapter = new FeedAdapter(getActivity(), R.layout.item_filter_settings_feed, filter.getFeeds());
+ ListView feedsListView = (ListView) view.findViewById(R.id.filter_settings_feeds_list);
+ feedsListView.setAdapter(feedAdapter);
+
+ feedsListView.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() {
+ @Override
+ public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
+ Feed feed = feedAdapter.getItem(position);
+ filter.getFeeds().remove(feed);
+ try {
+ filtersList.save();
+ feedAdapter.notifyDataSetChanged();
+ return true;
+ } catch (Exception e) {
+ Toast.makeText(getActivity(), getResources().getString(R.string.error_save_filters), Toast.LENGTH_SHORT).show();
+ filter.getFeeds().add(feed);
+ return false;
+ }
+ }
+ });
+
+ view.findViewById(R.id.filter_settings_add_feed_button).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ EditText editText = (EditText) view.findViewById(R.id.filter_settings_add_feed);
+ String value = editText.getText().toString();
+ try {
+ URL url = new URL(value);
+ Feed feed = new Feed(
+ url,
+ url.getHost() + " " + value.substring(value.lastIndexOf('/') + 1, value.lastIndexOf('.')));
+ filter.getFeeds().add(feed);
+
+ try {
+ filtersList.save();
+ feedAdapter.notifyDataSetChanged();
+ editText.setText("");
+ } catch (Exception e) {
+ Toast.makeText(getActivity(), getResources().getString(R.string.error_save_filters), Toast.LENGTH_SHORT).show();
+ filter.getFeeds().remove(feed);
+ }
+ } catch (MalformedURLException e) {
+ Toast.makeText(getActivity(), getResources().getString(R.string.error_invalid_url), Toast.LENGTH_SHORT).show();
+ }
+ }
+ });
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+ builder.setMessage(R.string.filter_settings_feeds)
+ .setPositiveButton(R.string.button_ok, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ // @todo FIRE ZE MISSILES!
+ }
+ });
+ builder.setView(view);
+
+ return builder.create();
+ }
+
+ /**
+ * Custom ArrayAdapter to display feeds
+ */
+ private static class FeedAdapter extends ArrayAdapter<Feed> {
+
+ Context context;
+ int layoutResourceId;
+ List<Feed> feeds;
+
+ public FeedAdapter(Context context, int resource, List<Feed> objects) {
+ super(context, resource, objects);
+ this.context = context;
+ layoutResourceId = resource;
+ feeds = objects;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ View row = convertView;
+ FeedHolder holder;
+
+ if (row == null) {
+ LayoutInflater inflater = ((Activity) context).getLayoutInflater();
+ row = inflater.inflate(layoutResourceId, parent, false);
+
+ holder = new FeedHolder();
+ holder.title = (TextView) row.findViewById(R.id.filter_settings_feed_item_title);
+ holder.url = (TextView) row.findViewById(R.id.filter_settings_feed_item_url);
+
+ row.setTag(holder);
+ } else {
+ holder = (FeedHolder) row.getTag();
+ }
+
+ Feed feed = feeds.get(position);
+ holder.title.setText(feed.getTitle());
+ holder.url.setText(feed.getURL().toString());
+
+ return row;
+ }
+
+ /**
+ * TextViews holder
+ */
+ private static class FeedHolder {
+ TextView title;
+ TextView url;
+ }
+ }
+ }
+
+ /**
+ * Custom ArrayAdapter to display Keywords
+ */
+ private static class KeywordAdapter extends ArrayAdapter<Keyword> {
+ Context context;
+ int layoutResourceId;
+ List<Keyword> keywords;
+
+ public KeywordAdapter(Context context, int resource, List<Keyword> objects) {
+ super(context, resource, objects);
+ this.context = context;
+ layoutResourceId = resource;
+ keywords = objects;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ View row = convertView;
+ KeywordHolder holder = null;
+
+ if (row == null) {
+ LayoutInflater inflater = ((Activity) context).getLayoutInflater();
+ row = inflater.inflate(layoutResourceId, parent, false);
+
+ holder = new KeywordHolder();
+ holder.title = (TextView) row.findViewById(R.id.filter_settings_keyword_item_title);
+
+ row.setTag(holder);
+ } else {
+ holder = (KeywordHolder) row.getTag();
+ }
+
+ Keyword keyword = keywords.get(position);
+ holder.title.setText(keyword.getKeyword());
+
+ return row;
+ }
+
+ /**
+ * TextViews holder
+ */
+ private static class KeywordHolder {
+ TextView title;
+ }
+ }
+
+}
diff --git a/app/src/main/java/org/rssin/android/FiltersActivity.java b/app/src/main/java/org/rssin/android/FiltersActivity.java
new file mode 100755
index 0000000..ab7061b
--- /dev/null
+++ b/app/src/main/java/org/rssin/android/FiltersActivity.java
@@ -0,0 +1,227 @@
+package org.rssin.android;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.support.v7.app.ActionBarActivity;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.EditText;
+import android.widget.ListView;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import org.rssin.rssin.Feed;
+import org.rssin.rssin.Filter;
+import org.rssin.rssin.Keyword;
+import org.rssin.rssin.R;
+
+import java.io.IOException;
+import java.io.InvalidClassException;
+import java.net.MalformedURLException;
+import java.util.List;
+
+/**
+ * List of filters
+ *
+ * Short tap opens the list with current articles that the filter returns
+ * Long tap opens filter settings
+ *
+ * @author Camil Staps
+ */
+public class FiltersActivity extends ActionBarActivity {
+
+ private FiltersList filtersList;
+ private ListView filtersView;
+
+ private AdapterView.OnItemClickListener onFilterClickListener;
+ private AdapterView.OnItemLongClickListener onFilterLongClickListener;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_filters);
+
+ filtersView = (ListView) findViewById(R.id.filters_list);
+
+ try {
+ filtersList = FiltersList.getInstance(this);
+ } catch (IOException ex) {
+ Toast.makeText(this, "Couldn't load filters.", Toast.LENGTH_SHORT).show();
+ Log.e("FILTER", "IOException", ex);
+ finish();
+ }
+
+ final FilterAdapter adapter = new FilterAdapter(this, R.layout.item_filter, filtersList.getFilters());
+ filtersView.setAdapter(adapter);
+
+ setupListeners();
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ getMenuInflater().inflate(R.menu.menu_filters, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ int id = item.getItemId();
+
+ if (id == R.id.filters_action_add) {
+ openAddDialog();
+ return true;
+ }
+
+ return super.onOptionsItemSelected(item);
+ }
+
+ /**
+ * Setup listeners for the list items
+ */
+ private void setupListeners() {
+ onFilterClickListener = new AdapterView.OnItemClickListener() {
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ Filter item = (Filter) parent.getItemAtPosition(position);
+ openFilter(item);
+ }
+ };
+ onFilterLongClickListener = new AdapterView.OnItemLongClickListener() {
+ @Override
+ public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
+ Filter item = (Filter) parent.getItemAtPosition(position);
+ openFilterSettings(item);
+ return true;
+ }
+ };
+
+ filtersView.setOnItemClickListener(onFilterClickListener);
+ filtersView.setOnItemLongClickListener(onFilterLongClickListener);
+ }
+
+ /**
+ * Open dialog to create new filter
+ * For the moment, we temporarily disable rotating because we can't get it working otherwise.
+ * @todo make rotating possible
+ */
+ public void openAddDialog() {
+ setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_NOSENSOR);
+
+ AlertDialog.Builder alert = new AlertDialog.Builder(this);
+
+ alert.setTitle("Add filter");
+ alert.setMessage("Title:");
+
+ final EditText input = new EditText(this);
+ input.setFocusable(true);
+ input.requestFocus();
+
+ AlertDialog dialog = alert
+ .setView(input)
+ .setPositiveButton(getResources().getString(R.string.button_apply), new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int whichButton) {
+ String value = input.getText().toString();
+ try {
+ Filter f = new Filter(value);
+ filtersList.getFilters().add(f);
+ filtersList.save();
+ openFilterSettings(f);
+ } catch (Exception e) {
+ Toast.makeText(getBaseContext(), getResources().getString(R.string.error_save_filters), Toast.LENGTH_SHORT).show();
+ }
+ setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR);
+ }
+ })
+ .setNegativeButton(getResources().getString(R.string.button_cancel), new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR);
+ }
+ })
+ .setOnCancelListener(new DialogInterface.OnCancelListener() {
+ @Override
+ public void onCancel(DialogInterface dialog) {
+ setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR);
+ }
+ })
+ .create();
+
+ dialog.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE);
+ dialog.show();
+ }
+
+ public void openFilterSettings(Filter f) {
+ Intent intent = new Intent(getApplicationContext(), FilterSettingsActivity.class);
+ intent.putExtra("filter", f.hashCode());
+ startActivity(intent);
+ }
+
+ public void openFilter(Filter f) {
+ Intent intent = new Intent(getApplicationContext(), FilterActivity.class);
+ intent.putExtra("filter", f.hashCode());
+ startActivity(intent);
+ }
+
+ /**
+ * Custom ArrayAdapter to display filters with our own menu item
+ */
+ private static class FilterAdapter extends ArrayAdapter<Filter> {
+
+ Context context;
+ int layoutResourceId;
+ List<Filter> filters;
+
+ public FilterAdapter(Context context, int resource, List<Filter> objects) {
+ super(context, resource, objects);
+ this.context = context;
+ layoutResourceId = resource;
+ filters = objects;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ View row = convertView;
+ FilterHolder holder = null;
+
+ if (row == null) {
+ LayoutInflater inflater = ((Activity) context).getLayoutInflater();
+ row = inflater.inflate(layoutResourceId, parent, false);
+
+ holder = new FilterHolder();
+ holder.title = (TextView) row.findViewById(R.id.filter_item_title);
+ holder.keywords = (TextView) row.findViewById(R.id.filter_item_keywords);
+
+ row.setTag(holder);
+ } else {
+ holder = (FilterHolder) row.getTag();
+ }
+
+ Filter filter = filters.get(position);
+ holder.title.setText(filter.getTitle());
+ holder.keywords.setText(filter.getKeywordsAsString());
+
+ return row;
+ }
+
+ /**
+ * TextViews holder
+ */
+ private static class FilterHolder {
+ TextView title;
+ TextView keywords;
+ }
+ }
+
+}
diff --git a/app/src/main/java/org/rssin/android/FiltersList.java b/app/src/main/java/org/rssin/android/FiltersList.java
new file mode 100755
index 0000000..aa2a92b
--- /dev/null
+++ b/app/src/main/java/org/rssin/android/FiltersList.java
@@ -0,0 +1,72 @@
+package org.rssin.android;
+
+import android.content.Context;
+
+import org.rssin.rssin.Filter;
+
+import java.io.IOException;
+import java.util.List;
+
+/**
+ * A list of filters that can be saved using the DefaultStorageProvider
+ * @author Camil Staps
+ */
+class FiltersList {
+
+ private static FiltersList instance;
+
+ private final List<Filter> filters;
+ private final DefaultStorageProvider storageProvider;
+
+ /**
+ * Fetch from storage provider
+ * @param context needed to get the storageprovider
+ * @throws IOException if data is corrupted, and deserializing fails
+ */
+ protected FiltersList(Context context) throws IOException {
+ storageProvider = DefaultStorageProvider.getInstance(context);
+ filters = storageProvider.allFilters();
+ }
+
+ public static FiltersList getInstance(Context context) throws IOException {
+ if (instance == null)
+ instance = new FiltersList(context);
+ return instance;
+ }
+
+ public static FiltersList getInstance() {
+ return instance;
+ }
+
+ public List<Filter> getFilters() {
+ return filters;
+ }
+
+ /**
+ * Save all filters
+ * @throws Exception if serializing or saving failed
+ */
+ public synchronized void save() throws Exception {
+ Exception e = null;
+ for (Filter filter : filters) {
+ try {
+ filter.store(storageProvider);
+ } catch (Exception ex) {
+ e = ex;
+ }
+ }
+ if (e != null) {
+ throw e;
+ }
+ }
+
+ public Filter getFilterFromHashCode(int hashcode) {
+ for (Filter f : filters) {
+ if (f.hashCode() == hashcode) {
+ return f;
+ }
+ }
+ return null;
+ }
+
+}
diff --git a/app/src/main/java/org/rssin/android/InternalStorageProvider.java b/app/src/main/java/org/rssin/android/InternalStorageProvider.java
new file mode 100644
index 0000000..feff5f0
--- /dev/null
+++ b/app/src/main/java/org/rssin/android/InternalStorageProvider.java
@@ -0,0 +1,130 @@
+package org.rssin.android;
+
+import android.content.Context;
+import android.util.Log;
+
+import org.rssin.neurons.FeedSorter;
+import org.rssin.rssin.Filter;
+import org.rssin.storage.FilterStorageProvider;
+import org.rssin.storage.Storable;
+import org.rssin.storage.StorageProvider;
+
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A way to store data in internal storage
+ * @author Camil Staps
+ */
+class InternalStorageProvider implements StorageProvider<String, Storable>, FilterStorageProvider<String> {
+ protected static final String PREFIX = "storage_", PREFIX_FILTER = "f_", PREFIX_FEEDSORTER = "fs_";
+
+ private static InternalStorageProvider instance;
+
+ private final Context context;
+
+ private InternalStorageProvider(Context context) {
+ this.context = context;
+ }
+
+ public static InternalStorageProvider getInstance(Context context) {
+ if (instance == null)
+ instance = new InternalStorageProvider(context);
+ return instance;
+ }
+
+ public static InternalStorageProvider getInstance() {
+ return instance;
+ }
+
+ @Override
+ public synchronized void store(String key, Storable element) throws FileNotFoundException, IOException {
+ FileOutputStream fos = context.openFileOutput(getFilename(key, element), Context.MODE_PRIVATE);
+ ObjectOutputStream oos = new ObjectOutputStream(fos);
+ oos.writeObject(element);
+ oos.close();
+ fos.close();
+ }
+
+ @Override
+ public Storable fetch(String key, Class className) throws ClassCastException, FileNotFoundException, IOException {
+ return fetchFromFilename(getFilename(key, className));
+ }
+
+ public Storable fetchFromFilename(String fileName) throws ClassCastException, FileNotFoundException, IOException {
+ FileInputStream fis = context.openFileInput(fileName);
+ ObjectInputStream ois = new ObjectInputStream(fis);
+ try {
+ return (Storable) ois.readObject();
+ } catch (ClassNotFoundException e) {
+ return null;
+ }
+ }
+
+ @Override
+ public String uniqueKey() {
+ return Integer.toString((int) System.currentTimeMillis());
+ }
+
+ /**
+ * Get the filename for a key and object
+ * @param key
+ * @param object
+ * @return
+ */
+ protected String getFilename(String key, Storable object) {
+ return getFilename(key, object.getClass());
+ }
+
+ /**
+ * Get the filename for a key and class
+ * @param key
+ * @param className
+ * @return
+ */
+ protected String getFilename(String key, Class className) {
+ if (className == Filter.class) {
+ return PREFIX + PREFIX_FILTER + sanitize(key);
+ } else if (className == FeedSorter.class){
+ return PREFIX + PREFIX_FEEDSORTER + sanitize(key);
+ } else {
+ return PREFIX + sanitize(key);
+ }
+ }
+
+ /**
+ * Sanitize a string to use for a filename
+ * @param key
+ * @return
+ */
+ public static String sanitize(String key) {
+ return key.replaceAll("\\W+", "");
+ }
+
+ @Override
+ public List<Filter> allFilters() {
+ List<Filter> filters = new ArrayList<>();
+ for (String fileName : context.fileList()) {
+ Log.d("ISP", "File: " + fileName);
+ try {
+ filters.add((Filter) fetchFromFilename(fileName));
+ } catch (ClassCastException | IOException e) {}
+ }
+ return filters;
+ }
+
+ @Override
+ public Filter getFilter(String key) {
+ try {
+ return (Filter) fetch(key, Filter.class);
+ } catch (ClassCastException | IOException e) {
+ return null;
+ }
+ }
+}
diff --git a/app/src/main/java/org/rssin/android/SettingsActivity.java b/app/src/main/java/org/rssin/android/SettingsActivity.java
new file mode 100644
index 0000000..6ef38e2
--- /dev/null
+++ b/app/src/main/java/org/rssin/android/SettingsActivity.java
@@ -0,0 +1,148 @@
+package org.rssin.android;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.os.Build;
+import android.os.Bundle;
+import android.preference.ListPreference;
+import android.preference.Preference;
+import android.preference.PreferenceActivity;
+import android.preference.PreferenceCategory;
+import android.preference.PreferenceFragment;
+import android.preference.PreferenceManager;
+
+import org.rssin.rssin.R;
+
+/**
+ * Global app settings
+ *
+ * @author Camil Staps
+ */
+public class SettingsActivity extends PreferenceActivity {
+ /**
+ * Determines whether to always show the simplified settings UI, where
+ * settings are presented in a single list. When false, settings are shown
+ * as a master/detail two-pane view on tablets. When true, a single pane is
+ * shown on tablets.
+ *
+ * For now, this is true, so that we don't have to think about tablets.
+ */
+ private static final boolean ALWAYS_SIMPLE_PREFS = true;
+
+ @Override
+ protected void onPostCreate(Bundle savedInstanceState) {
+ super.onPostCreate(savedInstanceState);
+ setupSimplePreferencesScreen();
+ }
+
+ /**
+ * Shows the simplified settings UI if the device configuration if the
+ * device configuration dictates that a simplified, single-pane UI should be
+ * shown.
+ */
+ private void setupSimplePreferencesScreen() {
+ if (!isSimplePreferences(this)) {
+ return;
+ }
+
+ // Add 'general' preferences.
+ addPreferencesFromResource(R.xml.pref_main);
+
+ // Add 'data and sync' preferences, and a corresponding header.
+ PreferenceCategory fakeHeader = new PreferenceCategory(this);
+ fakeHeader.setTitle(R.string.pref_header_data_sync);
+ getPreferenceScreen().addPreference(fakeHeader);
+ addPreferencesFromResource(R.xml.pref_data_sync);
+
+ // Bind the summaries of EditText/List/Dialog/Ringtone preferences to
+ // their values. When their values change, their summaries are updated
+ // to reflect the new value, per the Android Design guidelines.
+ bindPreferenceSummaryToValue(findPreference("sync_frequency"));
+ }
+
+ @Override
+ public boolean onIsMultiPane() {
+ return isXLargeTablet(this) && !isSimplePreferences(this);
+ }
+
+ /**
+ * Helper method to determine if the device has an extra-large screen. For
+ * example, 10" tablets are extra-large.
+ */
+ private static boolean isXLargeTablet(Context context) {
+ return (context.getResources().getConfiguration().screenLayout
+ & Configuration.SCREENLAYOUT_SIZE_MASK) >= Configuration.SCREENLAYOUT_SIZE_XLARGE;
+ }
+
+ /**
+ * Determines whether the simplified settings UI should be shown. This is
+ * true if this is forced via {@link #ALWAYS_SIMPLE_PREFS}, or the device
+ * doesn't have newer APIs like {@link PreferenceFragment}, or the device
+ * doesn't have an extra-large screen. In these cases, a single-pane
+ * "simplified" settings UI should be shown.
+ */
+ private static boolean isSimplePreferences(Context context) {
+ return ALWAYS_SIMPLE_PREFS
+ || Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB
+ || !isXLargeTablet(context);
+ }
+
+ /**
+ * A preference value change listener that updates the preference's summary
+ * to reflect its new value.
+ */
+ private static Preference.OnPreferenceChangeListener sBindPreferenceSummaryToValueListener = new Preference.OnPreferenceChangeListener() {
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object value) {
+ String stringValue = value.toString();
+
+ if (preference instanceof ListPreference) {
+ ListPreference listPreference = (ListPreference) preference;
+ int index = listPreference.findIndexOfValue(stringValue);
+
+ preference.setSummary(index >= 0
+ ? listPreference.getEntries()[index]
+ : null);
+ } else {
+ preference.setSummary(stringValue);
+ }
+ return true;
+ }
+ };
+
+ /**
+ * Binds a preference's summary to its value. More specifically, when the
+ * preference's value is changed, its summary (line of text below the
+ * preference title) is updated to reflect the value. The summary is also
+ * immediately updated upon calling this method. The exact display format is
+ * dependent on the type of preference.
+ *
+ * @see #sBindPreferenceSummaryToValueListener
+ */
+ private static void bindPreferenceSummaryToValue(Preference preference) {
+ // Set the listener to watch for value changes.
+ preference.setOnPreferenceChangeListener(sBindPreferenceSummaryToValueListener);
+
+ // Trigger the listener immediately with the preference's
+ // current value.
+ sBindPreferenceSummaryToValueListener.onPreferenceChange(preference,
+ PreferenceManager
+ .getDefaultSharedPreferences(preference.getContext())
+ .getString(preference.getKey(), ""));
+ }
+
+ /**
+ * This fragment shows data and sync preferences only. It is used when the
+ * activity is showing a two-pane settings UI.
+ */
+ @TargetApi(Build.VERSION_CODES.HONEYCOMB)
+ public static class DataSyncPreferenceFragment extends PreferenceFragment {
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ addPreferencesFromResource(R.xml.pref_data_sync);
+ bindPreferenceSummaryToValue(findPreference("sync_frequency"));
+ }
+ }
+}
diff --git a/app/src/main/java/org/rssin/android/SharedPreferencesStorageProvider.java b/app/src/main/java/org/rssin/android/SharedPreferencesStorageProvider.java
new file mode 100644
index 0000000..fdcbe04
--- /dev/null
+++ b/app/src/main/java/org/rssin/android/SharedPreferencesStorageProvider.java
@@ -0,0 +1,128 @@
+package org.rssin.android;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.util.Base64;
+import android.util.Log;
+
+import org.rssin.rssin.Filter;
+import org.rssin.storage.FilterStorageProvider;
+import org.rssin.storage.Storable;
+import org.rssin.storage.StorageProvider;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * A StorageProvider using SharedPreferences
+ * @author Camil Staps
+ */
+class SharedPreferencesStorageProvider implements StorageProvider, FilterStorageProvider {
+
+ /**
+ * Under this key, we will store the administration for the class itself.
+ * For example, a list of the keys that are currently in use.
+ */
+ private static final String ADMIN_PREF_KEY = "administration";
+
+ protected static SharedPreferencesStorageProvider instance;
+
+ protected Context context;
+
+ protected SharedPreferencesStorageProvider(Context context) {
+ this.context = context;
+ }
+
+ /**
+ * Get a possibly new instance of the storage provider
+ * @param context if there is no instance yet, create it using this context's shared preferences.
+ * @return
+ */
+ public static SharedPreferencesStorageProvider getInstance(Context context) {
+ if (instance == null) {
+ instance = new SharedPreferencesStorageProvider(context);
+ }
+
+ return instance;
+ }
+
+ /**
+ * Get the saved instance, and return null if it doesn't exist
+ * @return
+ */
+ public static SharedPreferencesStorageProvider getInstance() {
+ return instance;
+ }
+
+ @Override
+ public void store(Object key, Storable element) throws Exception {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ ObjectOutputStream oos = new ObjectOutputStream(baos);
+ oos.writeObject(element);
+ oos.close();
+ String string = Base64.encodeToString(baos.toByteArray(), Base64.DEFAULT);
+
+ context.getSharedPreferences(key.toString(), Context.MODE_PRIVATE).edit().putString(element.getClass().getName(), string).commit();
+
+ if (element.getClass() == Filter.class) {
+ storeFilterKey(key);
+ }
+ }
+
+ @Override
+ public Storable fetch(Object key, Class className) throws Exception {
+ String serialized = context.getSharedPreferences(key.toString(), Context.MODE_PRIVATE).getString(className.getName(), null);
+ if (serialized == null) {
+ throw new IOException("No sharedPreference with key " + key.toString() + " and class " + className.getName());
+ }
+ ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(Base64.decode(serialized, Base64.DEFAULT)));
+ return (Storable) className.cast(ois.readObject());
+ }
+
+ @Override
+ public Object uniqueKey() {
+ return Integer.toString((int) System.currentTimeMillis());
+ }
+
+ @Override
+ public List<Filter> allFilters() {
+ Set<String> names = context.getSharedPreferences(ADMIN_PREF_KEY, Context.MODE_PRIVATE).getStringSet("filters", new HashSet<String>());
+ List<Filter> filters = new ArrayList<>();
+ for (String name : names) {
+ Filter filter = getFilter(name);
+ if (filter != null) {
+ filters.add(filter);
+ }
+ }
+ return filters;
+ }
+
+ @Override
+ public Filter getFilter(Object key) {
+ try {
+ return (Filter) fetch(key.toString(), Filter.class);
+ } catch (Exception e) {
+ Log.e("SPSP", "Failed fetching filter", e);
+ return null;
+ }
+ }
+
+ /**
+ * Keep track of a new key for a filter in the administration SharedPreferences
+ * @param key
+ * @throws Exception
+ */
+ protected void storeFilterKey(Object key) throws Exception {
+ SharedPreferences sharedPreferences = context.getSharedPreferences(ADMIN_PREF_KEY, Context.MODE_PRIVATE);
+ Set<String> names = sharedPreferences.getStringSet("filters", new HashSet<String>());
+ names.add(key.toString());
+ sharedPreferences.edit().putStringSet("filters", names).apply();
+ }
+}
diff --git a/app/src/main/java/org/rssin/rssin/UnifiedInboxActivity.java b/app/src/main/java/org/rssin/android/UnifiedInboxActivity.java
index b1bf0dc..6283dae 100644
--- a/app/src/main/java/org/rssin/rssin/UnifiedInboxActivity.java
+++ b/app/src/main/java/org/rssin/android/UnifiedInboxActivity.java
@@ -1,11 +1,17 @@
-package org.rssin.rssin;
+package org.rssin.android;
+import android.content.Intent;
import android.support.v7.app.ActionBarActivity;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
+import org.rssin.rssin.R;
+/**
+ * Unified view of all filters
+ * @author Camil Staps
+ */
public class UnifiedInboxActivity extends ActionBarActivity {
@Override
@@ -14,24 +20,22 @@ public class UnifiedInboxActivity extends ActionBarActivity {
setContentView(R.layout.activity_unified_inbox);
}
-
@Override
public boolean onCreateOptionsMenu(Menu menu) {
- // Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.menu_unified_inbox, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
- // Handle action bar item clicks here. The action bar will
- // automatically handle clicks on the Home/Up button, so long
- // as you specify a parent activity in AndroidManifest.xml.
int id = item.getItemId();
- //noinspection SimplifiableIfStatement
if (id == R.id.action_settings) {
- return true;
+ Intent intent = new Intent(this, SettingsActivity.class);
+ startActivity(intent);
+ } else if (id == R.id.action_filters) {
+ Intent intent = new Intent(this, FiltersActivity.class);
+ startActivity(intent);
}
return super.onOptionsItemSelected(item);
diff --git a/app/src/main/java/org/rssin/android/VolleyFetcher.java b/app/src/main/java/org/rssin/android/VolleyFetcher.java
new file mode 100644
index 0000000..8513128
--- /dev/null
+++ b/app/src/main/java/org/rssin/android/VolleyFetcher.java
@@ -0,0 +1,59 @@
+package org.rssin.android;
+
+import android.content.Context;
+
+import com.android.volley.Request.Method;
+import com.android.volley.RequestQueue;
+import com.android.volley.VolleyError;
+import com.android.volley.toolbox.StringRequest;
+import com.android.volley.toolbox.Volley;
+
+import org.rssin.http.Fetcher;
+import org.rssin.http.Request;
+import org.rssin.listener.ErrorListener;
+import org.rssin.listener.Listener;
+
+/**
+ * Created by camilstaps on 21-5-15.
+ */
+class VolleyFetcher implements Fetcher {
+
+ private final Context context;
+ private final RequestQueue requestQueue;
+
+ public VolleyFetcher(Context context) {
+ this.context = context;
+ requestQueue = Volley.newRequestQueue(context);
+ }
+
+ @Override
+ public void fetch(Request request) {
+ fetch(request, null);
+ }
+
+ @Override
+ public void fetch(Request request, final Listener listener) {
+ StringRequest stringRequest = new StringRequest(
+ Method.GET,
+ request.getURL().toString(),
+ new com.android.volley.Response.Listener<String>() {
+ @Override
+ public void onResponse(String s) {
+ try {
+ listener.onReceive(s);
+ } catch (ClassCastException e) {}
+ }
+ },
+ new com.android.volley.Response.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError volleyError) {
+ try {
+ ((ErrorListener<VolleyError>) listener).onError(volleyError);
+ } catch (ClassCastException e) {}
+ }
+ });
+
+ requestQueue.add(stringRequest);
+ }
+
+}
diff --git a/app/src/main/java/org/rssin/http/Fetcher.java b/app/src/main/java/org/rssin/http/Fetcher.java
new file mode 100644
index 0000000..0052e38
--- /dev/null
+++ b/app/src/main/java/org/rssin/http/Fetcher.java
@@ -0,0 +1,13 @@
+package org.rssin.http;
+
+import org.rssin.listener.Listener;
+
+/**
+ * Load an HTTP request
+ */
+public interface Fetcher {
+
+ void fetch(Request request);
+ void fetch(Request request, Listener listener);
+
+}
diff --git a/app/src/main/java/org/rssin/http/Request.java b/app/src/main/java/org/rssin/http/Request.java
new file mode 100644
index 0000000..b9861a5
--- /dev/null
+++ b/app/src/main/java/org/rssin/http/Request.java
@@ -0,0 +1,20 @@
+package org.rssin.http;
+
+import java.net.URL;
+
+/**
+ * Created by camilstaps on 21-5-15.
+ */
+public class Request {
+
+ URL url;
+
+ public Request(URL url) {
+ this.url = url;
+ }
+
+ public URL getURL() {
+ return url;
+ }
+
+}
diff --git a/app/src/main/java/org/rssin/listener/DismissListener.java b/app/src/main/java/org/rssin/listener/DismissListener.java
new file mode 100644
index 0000000..cfdc51d
--- /dev/null
+++ b/app/src/main/java/org/rssin/listener/DismissListener.java
@@ -0,0 +1,15 @@
+package org.rssin.listener;
+
+/**
+ * Created by camilstaps on 21-5-15.
+ */
+public class DismissListener implements Listener, RealtimeListener, ErrorListener, FallibleListener {
+ @Override
+ public void onError(Object error) {}
+
+ @Override
+ public void finish() {}
+
+ @Override
+ public void onReceive(Object data) {}
+}
diff --git a/app/src/main/java/org/rssin/listener/ErrorListener.java b/app/src/main/java/org/rssin/listener/ErrorListener.java
new file mode 100644
index 0000000..5ad4f1b
--- /dev/null
+++ b/app/src/main/java/org/rssin/listener/ErrorListener.java
@@ -0,0 +1,8 @@
+package org.rssin.listener;
+
+/**
+ * Created by camilstaps on 21-5-15.
+ */
+public interface ErrorListener<T> {
+ void onError(T error);
+}
diff --git a/app/src/main/java/org/rssin/listener/FallibleListener.java b/app/src/main/java/org/rssin/listener/FallibleListener.java
new file mode 100644
index 0000000..4e1d113
--- /dev/null
+++ b/app/src/main/java/org/rssin/listener/FallibleListener.java
@@ -0,0 +1,7 @@
+package org.rssin.listener;
+
+/**
+ * Created by camilstaps on 21-5-15.
+ */
+public interface FallibleListener<T,E> extends Listener<T>, ErrorListener<E> {
+}
diff --git a/app/src/main/java/org/rssin/listener/Listener.java b/app/src/main/java/org/rssin/listener/Listener.java
new file mode 100644
index 0000000..db1764c
--- /dev/null
+++ b/app/src/main/java/org/rssin/listener/Listener.java
@@ -0,0 +1,8 @@
+package org.rssin.listener;
+
+/**
+ * Created by camilstaps on 21-5-15.
+ */
+public interface Listener<T> {
+ void onReceive(T data);
+}
diff --git a/app/src/main/java/org/rssin/listener/RealtimeListener.java b/app/src/main/java/org/rssin/listener/RealtimeListener.java
new file mode 100644
index 0000000..82a4c91
--- /dev/null
+++ b/app/src/main/java/org/rssin/listener/RealtimeListener.java
@@ -0,0 +1,8 @@
+package org.rssin.listener;
+
+/**
+ * Created by camilstaps on 21-5-15.
+ */
+public interface RealtimeListener extends Listener {
+ void finish();
+}
diff --git a/app/src/main/java/org/rssin/neurons/FeedSorter.java b/app/src/main/java/org/rssin/neurons/FeedSorter.java
index 43e26c0..910057f 100755
--- a/app/src/main/java/org/rssin/neurons/FeedSorter.java
+++ b/app/src/main/java/org/rssin/neurons/FeedSorter.java
@@ -1,36 +1,73 @@
package org.rssin.neurons;
+import org.rssin.rss.FeedItem;
+import org.rssin.storage.Storable;
+import org.rssin.tools.SerializationTools;
+
import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Calendar;
+import java.util.Collections;
+import java.util.Comparator;
import java.util.Hashtable;
+import java.util.List;
import java.util.TimeZone;
/**
- * Created by Jos on 14-5-2015.
+ * @author Jos.
*/
-public class FeedSorter {
+public class FeedSorter implements Storable {
+ private static final long serialVersionUID = 0;
+
+ private final SentenceSplitter splitter = new SentenceSplitter();
private MultiNeuralNetwork nn = new MultiNeuralNetwork(25, 50);
+ private List<TrainingCase> trainingCases = new ArrayList<>();
private int[] isNthMonthInput = new int[12];
private int[] isNthWeekDayInput = new int[7];
- private int isMorning, isAfternoon, isEvening, isNight;
- private Hashtable<String, Integer> categoryInputs = new Hashtable<String, Integer>();
- private Hashtable<String, Integer> wordInputs = new Hashtable<String, Integer>();
- private Hashtable<String, Integer> feedSourceInputs = new Hashtable<String, Integer>();
+ private int isMorning, isAfternoon, isEvening, isNight, biasInput;
+ private Hashtable<String, Integer> categoryInputs = new Hashtable<>();
+ private Hashtable<String, Integer> wordInputs = new Hashtable<>();
+ private Hashtable<String, Integer> authorInputs = new Hashtable<>();
- public FeedSorter() {
- //TODO: Load Neural Network
- createNewNetwork();
+ private void writeObject(java.io.ObjectOutputStream stream) throws IOException {
+ stream.writeObject(nn);
+ stream.writeObject(trainingCases);
+ SerializationTools.writeArray(isNthMonthInput, stream);
+ SerializationTools.writeArray(isNthWeekDayInput, stream);
+ stream.writeInt(isMorning);
+ stream.writeInt(isAfternoon);
+ stream.writeInt(isEvening);
+ stream.writeInt(isNight);
+ stream.writeInt(biasInput);
+ stream.writeObject(categoryInputs);
+ stream.writeObject(wordInputs);
+ stream.writeObject(authorInputs);
}
- private void createNewNetwork() {
- for(int i = 0; i < 12; i++)
- {
+ private void readObject(java.io.ObjectInputStream stream) throws IOException, ClassNotFoundException {
+ nn = (MultiNeuralNetwork) stream.readObject();
+ trainingCases = (List<TrainingCase>) stream.readObject();
+ isNthMonthInput = SerializationTools.readArrayInt(stream);
+ isNthWeekDayInput = SerializationTools.readArrayInt(stream);
+ isMorning = stream.readInt();
+ isAfternoon = stream.readInt();
+ isEvening = stream.readInt();
+ isNight = stream.readInt();
+ biasInput = stream.readInt();
+ categoryInputs = (Hashtable<String, Integer>) stream.readObject();
+ wordInputs = (Hashtable<String, Integer>) stream.readObject();
+ authorInputs = (Hashtable<String, Integer>) stream.readObject();
+ }
+
+ public FeedSorter() {
+ biasInput = nn.addInput();
+ for (int i = 0; i < 12; i++) {
isNthMonthInput[i] = nn.addInput();
}
- for(int i = 0; i < 7; i++)
- {
+ for (int i = 0; i < 7; i++) {
isNthWeekDayInput[i] = nn.addInput();
}
@@ -40,58 +77,131 @@ public class FeedSorter {
isNight = nn.addInput();
}
-// private PredictionInterface getPrediction(FeedItem item) {
-// double[] inputs = new double[nn.getInputCount()];
-//
-// //Initialize all inputs to -1 / false
-// for(int i = 0; i < inputs.length; i++)
-// {
-// inputs[i] = -1;
-// }
-//
-// //Set month
-// Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
-// for(int i = 0; i < isNthMonthInput.length; i++)
-// {
-// if(cal.get(Calendar.MONTH) - cal.getMinimum(Calendar.MONTH) == i)
-// {
-// inputs[isNthMonthInput[i]] = 1;
-// }
-// }
-//
-// //Set weekday
-// for(int i = 0; i < isNthWeekDayInput.length; i++)
-// {
-// if(cal.get(Calendar.DAY_OF_WEEK) - cal.getMinimum(Calendar.DAY_OF_WEEK) == i)
-// {
-// inputs[isNthMonthInput[i]] = 1;
-// }
-// }
-//
-// //Set day
-// int hourOfDay = cal.get(Calendar.HOUR_OF_DAY);
-// if(hourOfDay > 6 && hourOfDay < 12)
-// {
-// inputs[isMorning] = 1;
-// }else if(hourOfDay >= 12 && hourOfDay <= 6)
-// {
-// inputs[isAfternoon] = 1;
-// }else if(hourOfDay >= 6 && hourOfDay < 23)
-// {
-// inputs[isEvening] = 1;
-// }else if(hourOfDay >= 23 || hourOfDay <= 6)
-// {
-// inputs[isNight] = 1;
-// }
-//
-// //TODO: source, category, title, text, etc of FeedItems
-//
-// return nn.computeOutput(inputs);
-// }
-
-// public List<FeedItem> sortItems(List<FeedItem> items) {
-// // Sort list based on something like date + nn.computeOutput() * DAY.
-// throw new NotImplementedException();
-// return items;
-// }
+ private PredictionInterface getPrediction(FeedItem item) {
+ List<String> words = splitter.splitSentence(item.getTitle());
+
+ addNewInputs(item.getCategory(), categoryInputs);
+ addNewInputs(words, wordInputs);
+ addNewInput(item.getAuthor(), authorInputs);
+
+ double[] inputs = newArrayInitializedToNegativeOne();
+ inputs[biasInput] = 1;
+
+ Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
+
+ //Set month & weekday
+ inputs[isNthMonthInput[cal.get(Calendar.MONTH) - cal.getMinimum(Calendar.MONTH)]] = 1;
+ inputs[isNthWeekDayInput[cal.get(Calendar.DAY_OF_WEEK) - cal.getMinimum(Calendar.DAY_OF_WEEK)]] = 1;
+
+ //Set time
+ int hourOfDay = cal.get(Calendar.HOUR_OF_DAY);
+ if (hourOfDay > 6 && hourOfDay < 12) {
+ inputs[isMorning] = 1;
+ } else if (hourOfDay >= 12 && hourOfDay <= 6) {
+ inputs[isAfternoon] = 1;
+ } else if (hourOfDay >= 6 && hourOfDay < 23) {
+ inputs[isEvening] = 1;
+ } else if (hourOfDay >= 23 || hourOfDay <= 6) {
+ inputs[isNight] = 1;
+ }
+
+ for (String category : item.getCategory()) {
+ inputs[categoryInputs.get(category.toLowerCase())] = 1;
+ }
+
+ for (String word : words) {
+ inputs[wordInputs.get(word)] = 1;
+ }
+
+ if (item.getAuthor() != null) {
+ inputs[authorInputs.get(item.getAuthor().toLowerCase())] = 1;
+ }
+
+ return nn.computeOutput(inputs);
+ }
+
+ private double[] newArrayInitializedToNegativeOne() {
+ double[] inputs = new double[nn.getInputCount()];
+ Arrays.fill(inputs, 0, inputs.length, -1);
+ return inputs;
+ }
+
+ private void addNewInputs(Iterable<String> words, Hashtable<String, Integer> map) {
+ for (String word : words) {
+ addNewInput(word, map);
+ }
+ }
+
+ private void addNewInput(String word, Hashtable<String, Integer> map) {
+ if (word != null) {
+ word = word.toLowerCase();
+ if (!map.containsKey(word)) {
+ map.put(word, nn.addInput());
+ }
+ }
+ }
+
+ /**
+ * Provides feedback to the neural network.
+ *
+ * @param item The feeditem.
+ * @param feedback The feedback. Like will move these types of items up in the list,
+ * dislike will move them down.
+ */
+ public void feedback(FeedItem item, Feedback feedback) {
+ PredictionInterface prediction = getPrediction(item);
+ prediction.learn(feedback.toExpectedOutput());
+ trainingCases.add(new TrainingCase(prediction.getInputs(), feedback));
+
+ final int MAX_TRAINING_HISTORY = 250;
+ while (trainingCases.size() > MAX_TRAINING_HISTORY) {
+ trainingCases.remove(0);
+ }
+ }
+
+ /**
+ * Runs an iteration of training, using feedback that was provided previously using FeedSorter.feedback(...).
+ */
+ public void train() {
+ for (TrainingCase t : trainingCases) {
+ double[] inputs = t.getInputs();
+ if (inputs.length < nn.getInputCount()) {
+ // Resize array to fit new input size
+ inputs = Arrays.copyOf(inputs, nn.getInputCount());
+ }
+
+ PredictionInterface prediction = nn.computeOutput(inputs);
+ prediction.learn(t.getFeedback().toExpectedOutput());
+ }
+ }
+
+ /**
+ * Returns a sorted list of all the items in the List, according to the neural network.
+ *
+ * @param items The list of items.
+ * @return A new, sorted, list of items. The parameter items is not modified.
+ */
+ public List<FeedItem> sortItems(List<FeedItem> items) {
+ final int SECONDS_IN_DAY = 24 * 60 * 60;
+
+ final List<FeedItem> newItems = new ArrayList<>(items);
+ final Hashtable<FeedItem, Long> predictions = new Hashtable<>();
+
+ for (FeedItem item : newItems) {
+ PredictionInterface prediction = getPrediction(item);
+ predictions.put(item, (long) (prediction.getOutput() * SECONDS_IN_DAY));
+ }
+
+ Collections.sort(newItems, new Comparator<FeedItem>() {
+ @Override
+ public int compare(FeedItem lhs, FeedItem rhs) {
+ long lhsScore = lhs.getPubDate().getTime() / 1000 + predictions.get(lhs);
+ long rhsScore = rhs.getPubDate().getTime() / 1000 + predictions.get(rhs);
+
+ return (int) Math.signum(rhsScore - lhsScore);
+ }
+ });
+
+ return newItems;
+ }
}
diff --git a/app/src/main/java/org/rssin/neurons/Feedback.java b/app/src/main/java/org/rssin/neurons/Feedback.java
new file mode 100755
index 0000000..fe59b1b
--- /dev/null
+++ b/app/src/main/java/org/rssin/neurons/Feedback.java
@@ -0,0 +1,18 @@
+package org.rssin.neurons;
+
+/**
+ * @author Jos.
+ */
+public enum Feedback {
+ Like(1.0d), Dislike(-1.0d);
+
+ private final double expectedOutput;
+
+ private Feedback(double expectedOutput) {
+ this.expectedOutput = expectedOutput;
+ }
+
+ double toExpectedOutput() {
+ return expectedOutput;
+ }
+}
diff --git a/app/src/main/java/org/rssin/neurons/MultiNeuralNetwork.java b/app/src/main/java/org/rssin/neurons/MultiNeuralNetwork.java
index 492f67c..0f5c31b 100755
--- a/app/src/main/java/org/rssin/neurons/MultiNeuralNetwork.java
+++ b/app/src/main/java/org/rssin/neurons/MultiNeuralNetwork.java
@@ -1,11 +1,26 @@
package org.rssin.neurons;
+import org.rssin.tools.SerializationTools;
+
+import java.io.IOException;
+import java.io.Serializable;
+
/**
- * Created by Jos on 14-5-2015.
+ * @author Jos
+ * Is used to migitate the problem of neural networks ending up in the wrong local minimum.
*/
-public class MultiNeuralNetwork {
+class MultiNeuralNetwork implements Serializable {
+ private static final long serialVersionUID = 0;
private NeuralNetwork[] networks;
+ private void writeObject(java.io.ObjectOutputStream stream) throws IOException {
+ SerializationTools.writeArray(networks, stream);
+ }
+
+ private void readObject(java.io.ObjectInputStream stream) throws IOException, ClassNotFoundException {
+ networks = SerializationTools.readArray(stream, NeuralNetwork.class);
+ }
+
public MultiNeuralNetwork(int numNetworks, int numHiddenNodes) {
networks = new NeuralNetwork[numNetworks];
for (int i = 0; i < networks.length; i++) {
@@ -24,8 +39,7 @@ public class MultiNeuralNetwork {
public PredictionInterface computeOutput(double[] inputs) {
PredictionInterface[] predictions = new PredictionInterface[networks.length];
- for(int i = 0; i < predictions.length; i++)
- {
+ for (int i = 0; i < predictions.length; i++) {
predictions[i] = networks[i].computeOutput(inputs);
}
diff --git a/app/src/main/java/org/rssin/neurons/MultiNeuralNetworkPrediction.java b/app/src/main/java/org/rssin/neurons/MultiNeuralNetworkPrediction.java
index 06164d0..f618749 100755
--- a/app/src/main/java/org/rssin/neurons/MultiNeuralNetworkPrediction.java
+++ b/app/src/main/java/org/rssin/neurons/MultiNeuralNetworkPrediction.java
@@ -1,17 +1,20 @@
package org.rssin.neurons;
/**
- * Created by Jos on 14-5-2015.
+ * @author Jos.
*/
-public class MultiNeuralNetworkPrediction implements PredictionInterface {
- private PredictionInterface[] predictions;
- MultiNeuralNetworkPrediction(PredictionInterface[] predictions)
- {
+class MultiNeuralNetworkPrediction implements PredictionInterface {
+ private final PredictionInterface[] predictions;
+
+ MultiNeuralNetworkPrediction(PredictionInterface[] predictions) {
+ if (predictions.length <= 0) {
+ throw new IllegalArgumentException("predictions");
+ }
+
this.predictions = predictions;
}
- public double getOutput()
- {
+ public double getOutput() {
double average = 0;
for (PredictionInterface prediction : predictions) {
average += prediction.getOutput();
@@ -20,11 +23,13 @@ public class MultiNeuralNetworkPrediction implements PredictionInterface {
return average / (double) predictions.length;
}
- public void learn(double expectedOutput)
- {
- for(PredictionInterface prediction : predictions)
- {
+ public void learn(double expectedOutput) {
+ for (PredictionInterface prediction : predictions) {
prediction.learn(expectedOutput);
}
}
+
+ public double[] getInputs() {
+ return predictions[0].getInputs();
+ }
}
diff --git a/app/src/main/java/org/rssin/neurons/NeuralNetwork.java b/app/src/main/java/org/rssin/neurons/NeuralNetwork.java
index d761e35..2cbe284 100755
--- a/app/src/main/java/org/rssin/neurons/NeuralNetwork.java
+++ b/app/src/main/java/org/rssin/neurons/NeuralNetwork.java
@@ -1,15 +1,32 @@
package org.rssin.neurons;
+import android.annotation.SuppressLint;
+
+import org.rssin.tools.SerializationTools;
+
+import java.io.IOException;
+import java.io.Serializable;
+
/**
- * Created by Jos on 14-5-2015.
+ * @author Jos.
*/
-public class NeuralNetwork {
+class NeuralNetwork implements Serializable {
+ private static final long serialVersionUID = 0;
private Neuron[] hiddenNodes;
private Neuron outputNode;
- public NeuralNetwork(int numHiddenNodes) {
- if(numHiddenNodes < 1)
- {
+ private void writeObject(java.io.ObjectOutputStream stream) throws IOException {
+ SerializationTools.writeArray(hiddenNodes, stream);
+ stream.writeObject(outputNode);
+ }
+
+ private void readObject(java.io.ObjectInputStream stream) throws IOException, ClassNotFoundException {
+ hiddenNodes = SerializationTools.readArray(stream, Neuron.class);
+ outputNode = (Neuron) stream.readObject();
+ }
+
+ NeuralNetwork(int numHiddenNodes) {
+ if (numHiddenNodes < 1) {
throw new IllegalArgumentException("numHiddenNodes must be > 0");
}
@@ -22,18 +39,22 @@ public class NeuralNetwork {
outputNode = new Neuron(numHiddenNodes + 1);
}
- public int addInput() {
+ @SuppressLint("Assert")
+ int addInput() {
+ assert hiddenNodes.length > 0;
+
int result = 0;
- for (int i = 0; i < hiddenNodes.length; i++) {
- result = hiddenNodes[i].AddWeight();
+ for (Neuron hiddenNode : hiddenNodes) {
+ result = hiddenNode.addWeight();
}
return result;
}
- public PredictionInterface computeOutput(double[] inputs) {
+ PredictionInterface computeOutput(double[] inputs) {
double[] intermediateValues = new double[outputNode.getWeightCount()];
+ //Output of hidden neurons
for (int neuronNum = 0; neuronNum < hiddenNodes.length; neuronNum++) {
Neuron n = hiddenNodes[neuronNum];
@@ -54,27 +75,32 @@ public class NeuralNetwork {
}
void learn(NeuralNetworkPrediction p, double expectedOutput) {
+ //TODO: See if adding momentum helps avoid local minimum
double actualOutput = p.getOutput();
double[] intermediateValues = p.getIntermediateValues();
double[] inputs = p.getInputs();
- double[] hiddenGradients = new double[hiddenNodes.length];
-
- //Calculate output gradients
+ //Calculate output gradient
double outputDerivative = (1 - actualOutput) * (1 + actualOutput);
//Derivative of HyperTan function
double outputGradient = outputDerivative * (expectedOutput - actualOutput);
- //Calulate hidden gradients
+ //Calculate hidden gradients
+ double[] hiddenGradients = new double[hiddenNodes.length];
for (int i = 0; i < hiddenGradients.length; i++) {
//Derivative of HyperTan function
double hiddenDerivative = (1 - intermediateValues[i]) * (1 + intermediateValues[i]);
hiddenGradients[i] = hiddenDerivative * outputGradient * outputNode.getWeight(i);
}
+ updateWeights(intermediateValues, inputs, hiddenGradients, outputGradient);
+ }
+
+ private void updateWeights(double[] intermediateValues, double[] inputs, double[] hiddenGradients, double outputGradient) {
+ final double learningRate = 0.2;
+
//Update input => hidden weights.
- final double learningRate = 0.3;
for (int neuronNum = 0; neuronNum < hiddenNodes.length; neuronNum++) {
Neuron n = hiddenNodes[neuronNum];
@@ -99,7 +125,7 @@ public class NeuralNetwork {
else return Math.tanh(x);
}
- public int getInputCount() {
+ int getInputCount() {
return hiddenNodes[0].getWeightCount();
}
}
diff --git a/app/src/main/java/org/rssin/neurons/NeuralNetworkPrediction.java b/app/src/main/java/org/rssin/neurons/NeuralNetworkPrediction.java
index cd34c8e..169caee 100755
--- a/app/src/main/java/org/rssin/neurons/NeuralNetworkPrediction.java
+++ b/app/src/main/java/org/rssin/neurons/NeuralNetworkPrediction.java
@@ -1,13 +1,13 @@
package org.rssin.neurons;
/**
- * Created by Jos on 14-5-2015.
+ * @author Jos.
*/
-public class NeuralNetworkPrediction implements PredictionInterface {
- private double[] inputs;
- private double[] intermediateValues;
- private double output;
- private NeuralNetwork nn;
+class NeuralNetworkPrediction implements PredictionInterface {
+ private final double[] inputs;
+ private final double[] intermediateValues;
+ private final double output;
+ private final NeuralNetwork nn;
NeuralNetworkPrediction(NeuralNetwork nn, double[] inputs, double[] intermediateValues, double output) {
this.inputs = inputs;
@@ -16,11 +16,11 @@ public class NeuralNetworkPrediction implements PredictionInterface {
this.nn = nn;
}
- double[] getInputs() {
+ public double[] getInputs() {
return inputs;
}
- double[] getIntermediateValues() {
+ public double[] getIntermediateValues() {
return intermediateValues;
}
diff --git a/app/src/main/java/org/rssin/neurons/Neuron.java b/app/src/main/java/org/rssin/neurons/Neuron.java
index 5df9d47..6a4970a 100755
--- a/app/src/main/java/org/rssin/neurons/Neuron.java
+++ b/app/src/main/java/org/rssin/neurons/Neuron.java
@@ -1,16 +1,30 @@
package org.rssin.neurons;
+import org.rssin.tools.SerializationTools;
+
+import java.io.IOException;
+import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
/**
- * Created by Jos on 14-5-2015.
+ * @author Jos.
*/
-class Neuron {
- private static Random r = new Random();
+class Neuron implements Serializable {
+ private static final long serialVersionUID = 0;
+ private static final Random r = new Random();
+
+ private List<Double> weights = new ArrayList<>();
+
+ private void writeObject(java.io.ObjectOutputStream stream) throws IOException {
+ SerializationTools.writeList(weights, stream);
+ }
+
+ private void readObject(java.io.ObjectInputStream stream) throws IOException, ClassNotFoundException {
+ weights = SerializationTools.readList(stream);
+ }
- private List<Double> weights = new ArrayList<Double>();
public Neuron() {
}
@@ -22,7 +36,9 @@ class Neuron {
}
public int addWeight() {
- weights.add(r.nextDouble() * 2 - 1);
+ // Initial values range from -.5 to .5. The exact value does not matter,
+ // as long as they aren't all 0.
+ weights.add(r.nextDouble() - .5);
return weights.size() - 1;
}
diff --git a/app/src/main/java/org/rssin/neurons/PredictionInterface.java b/app/src/main/java/org/rssin/neurons/PredictionInterface.java
index d2c04b9..ff46992 100755
--- a/app/src/main/java/org/rssin/neurons/PredictionInterface.java
+++ b/app/src/main/java/org/rssin/neurons/PredictionInterface.java
@@ -1,9 +1,12 @@
package org.rssin.neurons;
/**
- * Created by Jos on 14-5-2015.
+ * @author Jos.
*/
-public interface PredictionInterface {
- public double getOutput();
- public void learn(double expectedOutput);
+interface PredictionInterface {
+ double getOutput();
+
+ void learn(double expectedOutput);
+
+ double[] getInputs();
}
diff --git a/app/src/main/java/org/rssin/neurons/SentenceSplitter.java b/app/src/main/java/org/rssin/neurons/SentenceSplitter.java
new file mode 100755
index 0000000..29e34bc
--- /dev/null
+++ b/app/src/main/java/org/rssin/neurons/SentenceSplitter.java
@@ -0,0 +1,36 @@
+package org.rssin.neurons;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * @author Jos.
+ */
+public class SentenceSplitter implements Serializable {
+ private static final long serialVersionUID = 0;
+
+ private final Pattern wordMatch = Pattern.compile("[\\w-]+");//For unicode support, add the Pattern.UNICODE_CHARACTER_CLASS flag. Works only in Java 7+.
+
+ public SentenceSplitter() {
+ }
+
+ /**
+ * Returns all the words in a sentence.
+ *
+ * @param sentence The sentence.
+ * @return The list of words in a sentence.
+ */
+ public List<String> splitSentence(String sentence) {
+ List<String> allMatches = new ArrayList<>();
+ Matcher m = wordMatch.matcher(sentence);
+
+ while (m.find()) {
+ allMatches.add(m.group().toLowerCase());
+ }
+
+ return allMatches;
+ }
+}
diff --git a/app/src/main/java/org/rssin/neurons/TrainingCase.java b/app/src/main/java/org/rssin/neurons/TrainingCase.java
new file mode 100755
index 0000000..69f72cb
--- /dev/null
+++ b/app/src/main/java/org/rssin/neurons/TrainingCase.java
@@ -0,0 +1,25 @@
+package org.rssin.neurons;
+
+import java.io.Serializable;
+
+/**
+ * @author Jos.
+ */
+class TrainingCase implements Serializable {
+ private static final long serialVersionUID = 0;
+ private final double[] inputs;
+ private final Feedback feedback;
+
+ public TrainingCase(double[] inputs, Feedback feedback) {
+ this.inputs = inputs;
+ this.feedback = feedback;
+ }
+
+ public double[] getInputs() {
+ return inputs;
+ }
+
+ public Feedback getFeedback() {
+ return feedback;
+ }
+}
diff --git a/app/src/main/java/org/rssin/rss/Feed.java b/app/src/main/java/org/rssin/rss/Feed.java
index 5d65991..3718af2 100644
--- a/app/src/main/java/org/rssin/rss/Feed.java
+++ b/app/src/main/java/org/rssin/rss/Feed.java
@@ -1,7 +1,241 @@
package org.rssin.rss;
+import java.io.Serializable;
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.LinkedList;
+import java.util.List;
+
/**
* Created by Randy on 19-5-2015.
*/
-public class Feed {
-}
+public class Feed implements Serializable {
+ private static final long serialVersionUID = 2;
+
+ private List<String> category = new LinkedList<>();
+ private String cloud;
+ private String copyright;
+ private String description;
+ private String docs;
+ private String generator;
+ private String imageurl;
+ private String imagetitle;
+ private String imagelink;
+ private String language;
+ private Date lastBuildDate;
+ private String link;
+ private String managingEditor;
+ private Date pubDate;
+ private String rating;
+ private String skipDays;
+ private String skipHours;
+ private String textInput;
+ private String title;
+ private String ttl;
+ private String webMaster;
+
+ private List<FeedItem> posts = new ArrayList<>();
+
+ public Feed(List<String> category, String cloud, String copyright, String generator,
+ String imageurl, String imagetitle, String imagelink, String language,
+ Date lastBuildDate, String link, String managingEditor, Date pubDate,
+ String rating, String skipDays, String skipHours, String textInput,
+ String title, String ttl, String webMaster) {
+ this.category = category;
+ this.setCloud(cloud);
+ this.setCopyright(copyright);
+ this.setGenerator(generator);
+ this.setImageurl(imageurl);
+ this.setImagetitle(imagetitle);
+ this.setImagelink(imagelink);
+ this.setLanguage(language);
+ this.setLastBuildDate(lastBuildDate);
+ this.setLink(link);
+ this.setManagingEditor(managingEditor);
+ this.setPubDate(pubDate);
+ this.setRating(rating);
+ this.setSkipDays(skipDays);
+ this.setSkipHours(skipHours);
+ this.setTextInput(textInput);
+ this.setTitle(title);
+ this.setTtl(ttl);
+ this.setWebMaster(webMaster);
+ }
+
+ public void addPost(FeedItem post) {
+ posts.add(post);
+ }
+
+ public List<FeedItem> getPosts() {
+ return posts;
+ }
+
+ public String getCloud() {
+ return cloud;
+ }
+
+ public void setCloud(String cloud) {
+ this.cloud = cloud;
+ }
+
+ public List<String> getCategory() {
+ return category;
+ }
+
+ public void setCategory(String category) {
+ this.category.add(category);
+ }
+
+ public String getCopyright() {
+ return copyright;
+ }
+
+ public void setCopyright(String copyright) {
+ this.copyright = copyright;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ public String getDocs() {
+ return docs;
+ }
+
+ public void setDocs(String docs) {
+ this.docs = docs;
+ }
+
+ public String getGenerator() {
+ return generator;
+ }
+
+ public void setGenerator(String generator) {
+ this.generator = generator;
+ }
+
+ public String getImageurl() {
+ return imageurl;
+ }
+
+ public void setImageurl(String imageurl) {
+ this.imageurl = imageurl;
+ }
+
+ public String getImagetitle() {
+ return imagetitle;
+ }
+
+ public void setImagetitle(String imagetitle) {
+ this.imagetitle = imagetitle;
+ }
+
+ public String getImagelink() {
+ return imagelink;
+ }
+
+ public void setImagelink(String imagelink) {
+ this.imagelink = imagelink;
+ }
+
+ public String getLanguage() {
+ return language;
+ }
+
+ public void setLanguage(String language) {
+ this.language = language;
+ }
+
+ public Date getLastBuildDate() {
+ return lastBuildDate;
+ }
+
+ public void setLastBuildDate(Date lastBuildDate) {
+ this.lastBuildDate = lastBuildDate;
+ }
+
+ public String getLink() {
+ return link;
+ }
+
+ public void setLink(String link) {
+ this.link = link;
+ }
+
+ public String getManagingEditor() {
+ return managingEditor;
+ }
+
+ public void setManagingEditor(String managingEditor) {
+ this.managingEditor = managingEditor;
+ }
+
+ public Date getPubDate() {
+ return pubDate;
+ }
+
+ public void setPubDate(Date pubDate) {
+ this.pubDate = pubDate;
+ }
+
+ public String getRating() {
+ return rating;
+ }
+
+ public void setRating(String rating) {
+ this.rating = rating;
+ }
+
+ public String getSkipDays() {
+ return skipDays;
+ }
+
+ public void setSkipDays(String skipDays) {
+ this.skipDays = skipDays;
+ }
+
+ public String getSkipHours() {
+ return skipHours;
+ }
+
+ public void setSkipHours(String skipHours) {
+ this.skipHours = skipHours;
+ }
+
+ public String getTextInput() {
+ return textInput;
+ }
+
+ public void setTextInput(String textInput) {
+ this.textInput = textInput;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ public String getTtl() {
+ return ttl;
+ }
+
+ public void setTtl(String ttl) {
+ this.ttl = ttl;
+ }
+
+ public String getWebMaster() {
+ return webMaster;
+ }
+
+ public void setWebMaster(String webMaster) {
+ this.webMaster = webMaster;
+ }
+} \ No newline at end of file
diff --git a/app/src/main/java/org/rssin/rss/FeedItem.java b/app/src/main/java/org/rssin/rss/FeedItem.java
index 2ddad4f..bb074cd 100644
--- a/app/src/main/java/org/rssin/rss/FeedItem.java
+++ b/app/src/main/java/org/rssin/rss/FeedItem.java
@@ -1,7 +1,8 @@
package org.rssin.rss;
-import java.net.URL;
import java.util.Date;
+import java.util.LinkedList;
+import java.util.List;
/**
* Created by Randy on 19-5-2015.
@@ -12,26 +13,27 @@ public class FeedItem {
private Date pubDate;
private String title;
private String description;
- private URL link;
+ private String link;
private String author;
- private String[] category;
- private URL comments;
+ private List<String> category = new LinkedList<>();
+ private String comments;
private String enclosure;
private String source;
+ private boolean isRead;
- public FeedItem(String guid, Date pubDate, String title, String description, URL link,
- String author, String[] category, URL comments, String enclosure, String source)
+ public FeedItem(String guid, Date pubDate, String title, String description, String link,
+ String author, List<String> category, String comments, String enclosure, String source)
{
- this.guid = guid;
- this.pubDate = pubDate;
- this.title = title;
- this.description = description;
- this.link = link;
- this.author = author;
+ this.setGuid(guid);
+ this.setPubDate(pubDate);
+ this.setTitle(title);
+ this.setDescription(description);
+ this.setLink(link);
+ this.setAuthor(author);
this.category = category;
- this.comments = comments;
- this.enclosure = enclosure;
- this.source = source;
+ this.setComments(comments);
+ this.setEnclosure(enclosure);
+ this.setSource(source);
}
public String getGuid() {
@@ -50,7 +52,7 @@ public class FeedItem {
return description;
}
- public URL getLink() {
+ public String getLink() {
return link;
}
@@ -58,11 +60,11 @@ public class FeedItem {
return author;
}
- public String[] getCategory() {
+ public List<String> getCategory() {
return category;
}
- public URL getComments() {
+ public String getComments() {
return comments;
}
@@ -73,4 +75,59 @@ public class FeedItem {
public String getSource() {
return source;
}
+
+ void setGuid(String guid) {
+ this.guid = guid;
+ }
+
+
+ void setPubDate(Date pubDate) {
+ this.pubDate = pubDate;
+ }
+
+ void setTitle(String title) {
+ this.title = title;
+ }
+
+ void setDescription(String description) {
+ this.description = description;
+ }
+
+ void setLink(String link) {
+ this.link = link;
+ }
+
+ void setAuthor(String author) {
+ this.author = author;
+ }
+
+ void setCategory(String category) {
+ this.category.add(category);
+ }
+
+ void setComments(String comments) {
+ this.comments = comments;
+ }
+
+ void setEnclosure(String enclosure) {
+ this.enclosure = enclosure;
+ }
+
+ void setSource(String source) {
+ this.source = source;
+ }
+
+ @Override
+ public String toString()
+ {
+ return title + "\n" + description + " - " + author;
+ }
+
+ public boolean isRead() {
+ return isRead;
+ }
+
+ public void setIsRead(boolean isRead) {
+ this.isRead = isRead;
+ }
}
diff --git a/app/src/main/java/org/rssin/rss/FeedLoader.java b/app/src/main/java/org/rssin/rss/FeedLoader.java
index c69cb4c..3220826 100644
--- a/app/src/main/java/org/rssin/rss/FeedLoader.java
+++ b/app/src/main/java/org/rssin/rss/FeedLoader.java
@@ -1,7 +1,256 @@
package org.rssin.rss;
+import java.io.ByteArrayInputStream;
+import java.net.URL;
+import java.util.Date;
+import java.util.LinkedList;
+
+import org.rssin.http.Fetcher;
+import org.rssin.http.Request;
+import org.rssin.listener.FallibleListener;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlPullParserFactory;
+
+
/**
* Created by Randy on 19-5-2015.
*/
public class FeedLoader {
+
+ private Feed feed;
+
+ private URL urlString = null;
+ private XmlPullParserFactory xmlFactoryObject;
+ public volatile boolean parsingComplete = true;
+
+ private String text;
+
+ public FeedLoader(URL url){
+ this.setUrlString(url);
+ }
+
+ /**
+ * parses an XML file and saves all information in the feed.
+ * @param myParser the parser in question.
+ */
+ public void parseXMLAndStoreIt(XmlPullParser myParser) {
+ int event;
+ FeedItem post = null;
+ boolean chan = true; //as long as the first item hasn't been reached, all
+ //information is saved in the feed itself, rather than
+ //in separate items.
+ try {
+ event = myParser.getEventType();
+ while (event != XmlPullParser.END_DOCUMENT) {
+ String name = myParser.getName();
+ switch (event) {
+ case XmlPullParser.START_TAG:
+ switch (name) {
+ case "item":
+ post = new FeedItem(null, null, null, null, null,
+ null, new LinkedList<String>(), null, null, null);
+ chan = false; //this starts collecting information for the
+ //separate items.
+ break;
+ case "image":
+ imageTagParse(myParser);
+ case "channel":
+ feed = new Feed(new LinkedList<String>(), null, null, null, null,
+ null, null, null, null, null, null, null, null, null, null,
+ null, null, null, null);
+ chan = true;
+ break;
+ }
+ break;
+ case XmlPullParser.TEXT:
+ text = myParser.getText();
+ break;
+ case XmlPullParser.END_TAG:
+ assert post != null;
+ if (!chan) { //this saves all the item-related information.
+ switch (name) {
+ case "guid":
+ post.setGuid(text);
+ break;
+ case "pubDate":
+ post.setPubDate(new Date(text));
+ break;
+ case "title":
+ post.setTitle(text);
+ break;
+ case "description":
+ post.setDescription(text);
+ break;
+ case "link":
+ post.setLink(text);
+ break;
+ case "author":
+ post.setAuthor(text);
+ break;
+ case "category":
+ post.setCategory(text);
+ break;
+ case "comments":
+ post.setComments(text);
+ break;
+ case "enclosure":
+ post.setEnclosure(text);
+ break;
+ case "source":
+ post.setSource(text);
+ break;
+ case "item":
+ getFeed().addPost(post);
+ break;
+ }
+ } else { //this saves all the feed-related information.
+ switch (name) {
+ case "category":
+ getFeed().setCategory(text);
+ break;
+ case "cloud":
+ getFeed().setCloud(text);
+ break;
+ case "copyright":
+ getFeed().setCopyright(text);
+ break;
+ case "description":
+ getFeed().setDescription(text);
+ break;
+ case "docs":
+ getFeed().setDocs(text);
+ break;
+ case "generator":
+ getFeed().setGenerator(text);
+ break;
+ case "image":
+ break;
+ case "language":
+ getFeed().setLanguage(text);
+ break;
+ case "lastBuildDate":
+ getFeed().setLastBuildDate(new Date(text));
+ break;
+ case "link":
+ getFeed().setLink(text);
+ break;
+ case "managingEditor":
+ getFeed().setManagingEditor(text);
+ break;
+ case "pubDate":
+ getFeed().setPubDate(new Date(text));
+ break;
+ case "rating":
+ getFeed().setRating(text);
+ break;
+ case "skipDays":
+ getFeed().setSkipDays(text);
+ break;
+ case "skipHours":
+ getFeed().setSkipHours(text);
+ break;
+ case "textInput":
+ getFeed().setTextInput(text);
+ break;
+ case "title":
+ getFeed().setTitle(text);
+ break;
+ case "ttl":
+ getFeed().setTtl(text);
+ break;
+ case "webMaster":
+ getFeed().setWebMaster(text);
+ break;
+ case "item":
+ getFeed().addPost(post);
+ break;
+ }
+ }
+ break;
+ }
+ event = myParser.next();
+ }
+ parsingComplete = false;
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * This function parses the child elements of the <image>tag.
+ * @param myParser XML parser.
+ * @throws XmlPullParserException
+ */
+ public void imageTagParse(XmlPullParser myParser) throws XmlPullParserException {
+ int event;
+ boolean imageloop = true;
+ event = myParser.getEventType();
+ while (imageloop) {
+ String name = myParser.getName();
+ switch (event) {
+ case XmlPullParser.START_TAG:
+ break;
+ case XmlPullParser.TEXT:
+ text = myParser.getText();
+ break;
+ case XmlPullParser.END_TAG:
+ switch (name) {
+ case "url":
+ getFeed().setImageurl(text);
+ break;
+ case "title":
+ getFeed().setImagetitle(text);
+ break;
+ case "link":
+ getFeed().setImagelink(text);
+ break;
+ case "image":
+ imageloop = false;
+ break;
+ }
+ }
+ }
+ }
+
+ /**
+ * Retrieves the XML and parses it.
+ */
+ public void fetchXML(Fetcher fetcher, final FallibleListener<?,Object> listener) {
+ fetcher.fetch(
+ new Request(urlString),
+ new FallibleListener<String,Object>() {
+ @Override
+ public void onReceive(String data) {
+ try {
+ xmlFactoryObject = XmlPullParserFactory.newInstance();
+ XmlPullParser parser = xmlFactoryObject.newPullParser();
+ parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false);
+ parser.setInput(new ByteArrayInputStream(data.getBytes()), null);
+ parseXMLAndStoreIt(parser);
+ listener.onReceive(null);
+ } catch (XmlPullParserException e) {
+ listener.onError(e);
+ }
+ }
+
+ @Override
+ public void onError(Object error) {
+ listener.onError(error);
+ }
+ });
+ }
+
+ public void setFeed(Feed feed) {
+ this.feed = feed;
+ }
+
+ public Feed getFeed() {
+ return feed;
+ }
+
+ public void setUrlString(URL urlString) {
+ this.urlString = urlString;
+ }
+
}
diff --git a/app/src/main/java/org/rssin/rssin/Feed.java b/app/src/main/java/org/rssin/rssin/Feed.java
new file mode 100644
index 0000000..a2009ca
--- /dev/null
+++ b/app/src/main/java/org/rssin/rssin/Feed.java
@@ -0,0 +1,66 @@
+package org.rssin.rssin;
+
+import java.io.Serializable;
+import java.net.MalformedURLException;
+import java.net.URL;
+
+/**
+ * Feed holder
+ * @author Camil Staps
+ */
+public class Feed implements Serializable {
+ private static int serialVersionUID = 0;
+
+ /**
+ * This feed does not have any information about the current status of the feed.
+ * It is merely a reference to an external feed, and a name (title)
+ */
+ private String title;
+ private URL url;
+
+ public Feed(URL url) {
+ this.url = url;
+ this.title = url.toString();
+ }
+
+ public Feed(String url) throws MalformedURLException {
+ this.url = new URL(url);
+ this.title = url;
+ }
+
+ public Feed(URL url, String title) {
+ this.url = url;
+ this.title = title;
+ }
+
+ public Feed(String url, String title) throws MalformedURLException {
+ this.url = new URL(url);
+ this.title = title;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ public URL getURL() {
+ return url;
+ }
+
+ public void setURL(URL url) {
+ this.url = url;
+ }
+
+ public void setURL(String url) throws MalformedURLException {
+ this.url = new URL(url);
+ }
+
+ @Override
+ public String toString() {
+ return title + ": " + url.toString();
+ }
+
+}
diff --git a/app/src/main/java/org/rssin/rssin/FeedLoaderAndSorter.java b/app/src/main/java/org/rssin/rssin/FeedLoaderAndSorter.java
new file mode 100755
index 0000000..4113436
--- /dev/null
+++ b/app/src/main/java/org/rssin/rssin/FeedLoaderAndSorter.java
@@ -0,0 +1,102 @@
+package org.rssin.rssin;
+
+import org.rssin.http.Fetcher;
+import org.rssin.listener.FallibleListener;
+import org.rssin.listener.Listener;
+import org.rssin.listener.RealtimeListener;
+import org.rssin.neurons.FeedSorter;
+import org.rssin.rss.FeedItem;
+import org.rssin.rss.FeedLoader;
+import org.rssin.storage.StorageProvider;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Created by Jos on 20-5-2015.
+ */
+public class FeedLoaderAndSorter {
+ private Filter filter;
+
+ public FeedLoaderAndSorter(Filter filter) {
+ this.filter = filter;
+ }
+
+ /**
+ * Loads the feed(s), filters it, sorts it, and returns the result.
+ * @param fetcher HTTP Fetcher
+ * @param listener Listener for when the fetcher finishes
+ */
+ public void getFilteredFeedItems(Fetcher fetcher, final Listener<List<FeedItem>> listener)
+ {
+ final List<FeedItem> resultingItems = new ArrayList<>();
+ final Counter counter = new Counter(filter.getFeeds().size());
+ final FeedSorter sorter = filter.getFeedSorter();
+
+ for (Feed feed : filter.getFeeds()) {
+ final FeedLoader loader = new FeedLoader(feed.getURL());
+ loader.fetchXML(fetcher, new FallibleListener<Object, Object>() {
+ @Override
+ public void onReceive(Object data) {
+ for (FeedItem item : loader.getFeed().getPosts()) {
+ if(matchesKeyword(item)) {
+ resultingItems.add(item);
+ }
+ }
+
+ sorter.sortItems(resultingItems);
+
+ if (counter.decr().isZero() || listener.getClass() == RealtimeListener.class) {
+ listener.onReceive(resultingItems);
+ if (counter.decr().isZero() && listener.getClass() == RealtimeListener.class) {
+ ((RealtimeListener) listener).finish();
+ }
+ }
+ }
+
+ @Override
+ public void onError(Object error) {
+ try {
+ ((FallibleListener) listener).onError(error);
+ } catch (ClassCastException e) {}
+ }
+ });
+ }
+ }
+
+ private static class Counter {
+ int count;
+
+ Counter (int initial) {
+ count = initial;
+ }
+
+ public Counter decr() {
+ count--;
+ return this;
+ }
+
+ public boolean isZero() {
+ return count == 0;
+ }
+ }
+
+ private boolean matchesKeyword(FeedItem item)
+ {
+ for(Keyword keyword : filter.getKeywords())
+ {
+ if(contains(item.getTitle(), keyword.getKeyword()))
+ {
+ return true;
+ }
+ }
+
+ return filter.getKeywords().size() == 0;
+ }
+
+ private static boolean contains(String haystack, String needle) {
+ return haystack != null
+ && needle != null
+ && haystack.toLowerCase().contains(needle.toLowerCase());
+ }
+}
diff --git a/app/src/main/java/org/rssin/rssin/Filter.java b/app/src/main/java/org/rssin/rssin/Filter.java
index 6bb191f..2e1188e 100644..100755
--- a/app/src/main/java/org/rssin/rssin/Filter.java
+++ b/app/src/main/java/org/rssin/rssin/Filter.java
@@ -1,45 +1,140 @@
package org.rssin.rssin;
-import org.rssin.rss.Feed;
+import android.text.TextUtils;
+import android.util.Log;
-import java.io.Serializable;
-import java.util.HashSet;
-import java.util.Set;
+import org.rssin.neurons.FeedSorter;
+import org.rssin.storage.Storable;
+import org.rssin.storage.StorageProvider;
+
+import java.util.ArrayList;
+import java.util.List;
/**
- * Created by camilstaps on 19-5-15.
+ * Filter holder
+ * @author Camil Staps
*/
-public class Filter implements Serializable {
+public class Filter implements Storable {
+
+ private static final long serialVersionUID = 0;
+
+ /**
+ * A filter is a list of Feeds with a list of Keywords. A title can be added as well.
+ */
+ private final List<Feed> feeds;
+ private final List<Keyword> keywords;
+ private String title = "";
- private final Set<Feed> feeds;
- private final Set<Keyword> keywords;
+ private Object storageKey;
+ private transient FeedSorter feedSorter;
public Filter() {
- feeds = new HashSet<>();
- keywords = new HashSet<>();
+ feeds = new ArrayList<>();
+ keywords = new ArrayList<>();
}
- public Filter(Set<Keyword> keywords) {
- this.feeds = new HashSet<>();
- this.keywords = keywords;
+ public Filter(String title) {
+ setTitle(title);
+ feeds = new ArrayList<>();
+ keywords = new ArrayList<>();
}
- public Filter(HashSet<Feed> feeds) {
- this.feeds = feeds;
- this.keywords = new HashSet<>();
+ public Filter(String title, List<Keyword> keywords) {
+ this.feeds = new ArrayList<>();
+ this.keywords = keywords;
+ setTitle(title);
}
- public Filter(HashSet<Keyword> keywords, HashSet<Feed> feeds) {
+ public Filter(String title, List<Keyword> keywords, List<Feed> feeds) {
this.feeds = feeds;
this.keywords = keywords;
+ setTitle(title);
}
- public Set<Feed> getFeeds() {
+ public List<Feed> getFeeds() {
return feeds;
}
- public Set<Keyword> getKeywords() {
+ public List<Keyword> getKeywords() {
return keywords;
}
+ /**
+ * The Keyword's toString() method, joined with ", "
+ * @return
+ */
+ public String getKeywordsAsString() {
+ Keyword[] keywords = new Keyword[this.keywords.size()];
+ int i = 0;
+ for (Keyword keyword : this.keywords) {
+ keywords[i++] = keyword;
+ }
+ return TextUtils.join(", ", keywords);
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public void setTitle(String title) {
+ this.title = title.trim();
+ }
+
+ public FeedSorter getFeedSorter() {
+ return feedSorter;
+ }
+
+ /**
+ * Ensure that there is a feedSorter linked to this object
+ * Because the feedSorter attribute is transient (if not, serialization for changing settings takes too long),
+ * we need to manually ensure we get the FeedSorter as well every time we need it.
+ * @param storageProvider
+ */
+ public synchronized void ensureFeedSorter(StorageProvider storageProvider) {
+ if (storageKey == null) {
+ storageKey = storageProvider.uniqueKey();
+ }
+
+ try {
+ feedSorter = (FeedSorter) storageProvider.fetch(storageKey, FeedSorter.class);
+ } catch (Exception e) {
+ feedSorter = new FeedSorter();
+ try {
+ storageProvider.store(storageKey, feedSorter);
+ } catch (Exception e1) {}
+ }
+ }
+
+ @Override
+ public int hashCode() {
+ StringBuilder sb = new StringBuilder(title);
+ for (Feed f : feeds)
+ sb.append(f.toString());
+ for (Keyword k : keywords)
+ sb.append(k.toString());
+
+ return sb.toString().hashCode();
+ }
+
+ /**
+ * Save the Filter
+ * @param storageProvider
+ * @throws Exception
+ */
+ public synchronized void store(StorageProvider storageProvider) throws Exception {
+ if (storageKey == null) {
+ storageKey = storageProvider.uniqueKey();
+ }
+ storageProvider.store(storageKey, this);
+ }
+
+ /**
+ * Save the FeedSorter of this Filter
+ * @param storageProvider
+ * @throws Exception
+ */
+ public synchronized void storeFeedSorter(StorageProvider storageProvider) throws Exception {
+ ensureFeedSorter(storageProvider);
+ storageProvider.store(storageKey, feedSorter);
+ }
}
diff --git a/app/src/main/java/org/rssin/rssin/Keyword.java b/app/src/main/java/org/rssin/rssin/Keyword.java
index 690d627..9b5adfb 100644
--- a/app/src/main/java/org/rssin/rssin/Keyword.java
+++ b/app/src/main/java/org/rssin/rssin/Keyword.java
@@ -1,18 +1,31 @@
package org.rssin.rssin;
+import java.io.Serializable;
+
/**
- * Created by camilstaps on 19-5-15.
+ * Keyword holder
+ * @author Camil Staps
*/
-public class Keyword {
+public class Keyword implements Serializable {
+
+ private static final long serialVersionUID = 0;
+ /**
+ * For now, Keywords only have a single String keyword. Later, more options may be added
+ */
private final String keyword;
public Keyword(String keyword) {
- this.keyword = keyword;
+ this.keyword = keyword.trim();
}
public String getKeyword() {
return keyword;
}
+ @Override
+ public String toString() {
+ return keyword;
+ }
+
}
diff --git a/app/src/main/java/org/rssin/storage/FilterStorageProvider.java b/app/src/main/java/org/rssin/storage/FilterStorageProvider.java
new file mode 100644
index 0000000..538562f
--- /dev/null
+++ b/app/src/main/java/org/rssin/storage/FilterStorageProvider.java
@@ -0,0 +1,14 @@
+package org.rssin.storage;
+
+import org.rssin.rssin.Filter;
+
+import java.util.List;
+
+/**
+ * The FilterStorageProvider can get a list of all stored filters, and a specific Filter from a key
+ * @author Camil Staps
+ */
+public interface FilterStorageProvider<K> {
+ List<Filter> allFilters();
+ Filter getFilter(K key);
+}
diff --git a/app/src/main/java/org/rssin/storage/Storable.java b/app/src/main/java/org/rssin/storage/Storable.java
new file mode 100644
index 0000000..ef34245
--- /dev/null
+++ b/app/src/main/java/org/rssin/storage/Storable.java
@@ -0,0 +1,10 @@
+package org.rssin.storage;
+
+import java.io.Serializable;
+
+/**
+ * An element which can be stored using a StorageProvider
+ * @author Camil Staps
+ */
+public interface Storable extends Serializable {
+}
diff --git a/app/src/main/java/org/rssin/storage/StorageProvider.java b/app/src/main/java/org/rssin/storage/StorageProvider.java
new file mode 100644
index 0000000..7e382c2
--- /dev/null
+++ b/app/src/main/java/org/rssin/storage/StorageProvider.java
@@ -0,0 +1,28 @@
+package org.rssin.storage;
+
+/**
+ * A storage provider is able to store and fetch Storables to keys of type K.
+ * @author Camil Staps
+ */
+public interface StorageProvider<K,E extends Storable> {
+ /**
+ * Store an element
+ * @param key
+ * @param element
+ * @return
+ */
+ void store(K key, E element) throws Exception;
+
+ /**
+ * Fetch an element
+ * @param key
+ * @return
+ */
+ E fetch(K key, Class className) throws Exception;
+
+ /**
+ * Get a new, unique, usable key
+ * @return
+ */
+ K uniqueKey();
+}
diff --git a/app/src/main/java/org/rssin/tools/SerializationTools.java b/app/src/main/java/org/rssin/tools/SerializationTools.java
new file mode 100755
index 0000000..0886ded
--- /dev/null
+++ b/app/src/main/java/org/rssin/tools/SerializationTools.java
@@ -0,0 +1,69 @@
+package org.rssin.tools;
+
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.lang.reflect.Array;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author Jos.
+ */
+public class SerializationTools {
+ public static void writeArray(int[] array, ObjectOutputStream stream) throws IOException {
+ stream.write(array.length);
+ for(int i : array)
+ {
+ stream.write(i);
+ }
+ }
+
+ public static int[] readArrayInt(ObjectInputStream stream) throws IOException {
+ int[] array = new int[stream.readInt()];
+ for(int i = 0; i < array.length; i++)
+ {
+ array[i] = stream.readInt();
+ }
+
+ return array;
+ }
+
+ public static <T> void writeArray(T[] array, ObjectOutputStream stream) throws IOException {
+ stream.write(array.length);
+ for(T i : array)
+ {
+ stream.writeObject(i);
+ }
+ }
+
+ public static <T> T[] readArray(ObjectInputStream stream, Class<T> c) throws IOException, ClassNotFoundException {
+ T[] array = (T[]) Array.newInstance(c, stream.readInt());
+ for(int i = 0; i < array.length; i++)
+ {
+ array[i] = (T) stream.readObject();
+ }
+
+ return array;
+ }
+
+ public static <T> void writeList(List<T> list, ObjectOutputStream stream) throws IOException {
+ stream.write(list.size());
+ for(T i : list)
+ {
+ stream.writeObject(i);
+ }
+ }
+
+ public static <T> List<T> readList(ObjectInputStream stream) throws IOException, ClassNotFoundException {
+ List<T> array = new ArrayList<>();
+ int count = stream.readInt();
+
+ for(int i = 0; i < count; i++)
+ {
+ array.add((T) stream.readObject());
+ }
+
+ return array;
+ }
+}
diff --git a/app/src/main/res/layout/activity_filter.xml b/app/src/main/res/layout/activity_filter.xml
new file mode 100644
index 0000000..b144d08
--- /dev/null
+++ b/app/src/main/res/layout/activity_filter.xml
@@ -0,0 +1,13 @@
+<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="match_parent"
+ tools:context="org.rssin.android.FilterActivity">
+
+ <ListView
+ android:id="@+id/filter_items_list"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"/>
+
+</RelativeLayout>
diff --git a/app/src/main/res/layout/activity_filter_settings.xml b/app/src/main/res/layout/activity_filter_settings.xml
new file mode 100644
index 0000000..6ee5331
--- /dev/null
+++ b/app/src/main/res/layout/activity_filter_settings.xml
@@ -0,0 +1,38 @@
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ tools:context="org.rssin.android.FilterSettingsActivity"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:paddingLeft="0dp"
+ android:paddingRight="0dp"
+ android:paddingTop="0dp"
+ android:paddingBottom="0dp"
+ android:orientation="vertical">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <EditText
+ android:id="@+id/filter_settings_add_keyword"
+ android:layout_width="0dip"
+ android:layout_weight="1"
+ android:layout_height="wrap_content"
+ android:maxLines="1"/>
+
+ <Button
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/filter_settings_add_keyword"
+ android:onClick="addKeyword"/>
+
+ </LinearLayout>
+
+ <ListView
+ android:id="@+id/filter_settings_feeds_list"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"></ListView>
+
+</LinearLayout>
diff --git a/app/src/main/res/layout/activity_filters.xml b/app/src/main/res/layout/activity_filters.xml
new file mode 100644
index 0000000..700e939
--- /dev/null
+++ b/app/src/main/res/layout/activity_filters.xml
@@ -0,0 +1,17 @@
+<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="match_parent"
+ android:paddingLeft="0dp"
+ android:paddingRight="0dp"
+ android:paddingTop="0dp"
+ android:paddingBottom="0dp"
+ tools:context="org.rssin.android.FiltersActivity">
+
+ <ListView
+ android:id="@+id/filters_list"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"></ListView>
+
+</RelativeLayout>
diff --git a/app/src/main/res/layout/activity_unified_inbox.xml b/app/src/main/res/layout/activity_unified_inbox.xml
index 1f25ce7..5c56465 100644
--- a/app/src/main/res/layout/activity_unified_inbox.xml
+++ b/app/src/main/res/layout/activity_unified_inbox.xml
@@ -5,7 +5,4 @@
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingBottom="@dimen/activity_vertical_margin" tools:context=".UnifiedInboxActivity">
- <TextView android:text="@string/hello_world" android:layout_width="wrap_content"
- android:layout_height="wrap_content" />
-
</RelativeLayout>
diff --git a/app/src/main/res/layout/fragment_filter_settings_feeds.xml b/app/src/main/res/layout/fragment_filter_settings_feeds.xml
new file mode 100644
index 0000000..9c1e1f4
--- /dev/null
+++ b/app/src/main/res/layout/fragment_filter_settings_feeds.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <EditText
+ android:id="@+id/filter_settings_add_feed"
+ android:layout_width="0dip"
+ android:layout_weight="1"
+ android:layout_height="wrap_content"
+ android:maxLines="1"
+ android:inputType="textUri"/>
+
+ <Button
+ android:id="@+id/filter_settings_add_feed_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/filter_settings_add_feed"/>
+
+ </LinearLayout>
+
+ <ListView
+ android:id="@+id/filter_settings_feeds_list"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"/>
+
+</LinearLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/item_feeditem.xml b/app/src/main/res/layout/item_feeditem.xml
new file mode 100644
index 0000000..9f4c553
--- /dev/null
+++ b/app/src/main/res/layout/item_feeditem.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:padding="10dp">
+
+ <TextView android:id="@+id/feeditem_title"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:textSize="@dimen/font_size_normal"/>
+
+ <TextView android:id="@+id/feeditem_summary"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:textSize="@dimen/font_size_small"/>
+
+</LinearLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/item_filter.xml b/app/src/main/res/layout/item_filter.xml
new file mode 100644
index 0000000..c91a1a5
--- /dev/null
+++ b/app/src/main/res/layout/item_filter.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:padding="10dp">
+
+ <TextView android:id="@+id/filter_item_title"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:textSize="@dimen/font_size_large"/>
+
+ <TextView android:id="@+id/filter_item_keywords"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:textSize="@dimen/font_size_normal"/>
+
+</LinearLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/item_filter_settings_feed.xml b/app/src/main/res/layout/item_filter_settings_feed.xml
new file mode 100644
index 0000000..ed3d596
--- /dev/null
+++ b/app/src/main/res/layout/item_filter_settings_feed.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:padding="10dp">
+
+ <TextView android:id="@+id/filter_settings_feed_item_title"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:textSize="@dimen/font_size_large"/>
+
+ <TextView android:id="@+id/filter_settings_feed_item_url"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:textSize="@dimen/font_size_normal"/>
+
+</LinearLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/item_filter_settings_keyword.xml b/app/src/main/res/layout/item_filter_settings_keyword.xml
new file mode 100644
index 0000000..36d8c59
--- /dev/null
+++ b/app/src/main/res/layout/item_filter_settings_keyword.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent">
+
+ <TextView android:id="@+id/filter_settings_keyword_item_title"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:padding="10dp"
+ android:textSize="@dimen/font_size_normal"/>
+
+</LinearLayout> \ No newline at end of file
diff --git a/app/src/main/res/menu/menu_filter.xml b/app/src/main/res/menu/menu_filter.xml
new file mode 100644
index 0000000..aaf1ca1
--- /dev/null
+++ b/app/src/main/res/menu/menu_filter.xml
@@ -0,0 +1,6 @@
+<menu 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" tools:context="org.rssin.android.FilterActivity">
+ <item android:id="@+id/action_settings" android:title="@string/action_settings"
+ android:orderInCategory="100" app:showAsAction="never" />
+</menu>
diff --git a/app/src/main/res/menu/menu_filter_settings.xml b/app/src/main/res/menu/menu_filter_settings.xml
new file mode 100644
index 0000000..848c9df
--- /dev/null
+++ b/app/src/main/res/menu/menu_filter_settings.xml
@@ -0,0 +1,23 @@
+<?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"
+ xmlns:tools="http://schemas.android.com/tools"
+ tools:context=".FilterSettingsActivity">
+
+ <item
+ android:id="@+id/filter_settings_action_feeds"
+ android:title="@string/filter_settings_action_feeds"
+ app:showAsAction="ifRoom" />
+
+ <item
+ android:id="@+id/filter_settings_action_title"
+ android:title="@string/filter_settings_action_title"
+ app:showAsAction="ifRoom" />
+
+ <item
+ android:id="@+id/filter_settings_action_delete"
+ android:title="@string/filter_settings_action_delete"
+ app:showAsAction="never" />
+
+</menu>
diff --git a/app/src/main/res/menu/menu_filters.xml b/app/src/main/res/menu/menu_filters.xml
new file mode 100644
index 0000000..5973bd0
--- /dev/null
+++ b/app/src/main/res/menu/menu_filters.xml
@@ -0,0 +1,14 @@
+<?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"
+ xmlns:tools="http://schemas.android.com/tools"
+ tools:context=".FilterSettingsActivity">
+
+ <item
+ android:id="@+id/filters_action_add"
+ android:title="@string/filters_action_add"
+ android:orderInCategory="100"
+ app:showAsAction="ifRoom" />
+
+</menu>
diff --git a/app/src/main/res/menu/menu_unified_inbox.xml b/app/src/main/res/menu/menu_unified_inbox.xml
index 2c72c6d..2800171 100644
--- a/app/src/main/res/menu/menu_unified_inbox.xml
+++ b/app/src/main/res/menu/menu_unified_inbox.xml
@@ -1,6 +1,9 @@
<menu 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" tools:context=".UnifiedInboxActivity">
+
<item android:id="@+id/action_settings" android:title="@string/action_settings"
android:orderInCategory="100" app:showAsAction="never" />
+ <item android:id="@+id/action_filters" android:title="@string/action_filters"
+ android:orderInCategory="100" app:showAsAction="never" />
</menu>
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png
index cde69bc..2e7f377 100644
--- a/app/src/main/res/mipmap-hdpi/ic_launcher.png
+++ b/app/src/main/res/mipmap-hdpi/ic_launcher.png
Binary files differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png
index c133a0c..c7d5ca5 100644
--- a/app/src/main/res/mipmap-mdpi/ic_launcher.png
+++ b/app/src/main/res/mipmap-mdpi/ic_launcher.png
Binary files differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png
index bfa42f0..8d1bf3a 100644
--- a/app/src/main/res/mipmap-xhdpi/ic_launcher.png
+++ b/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Binary files differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
index 324e72c..9e7e711 100644
--- a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
+++ b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..63c5a0d
--- /dev/null
+++ b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Binary files differ
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
index 47c8224..0121f92 100644
--- a/app/src/main/res/values/dimens.xml
+++ b/app/src/main/res/values/dimens.xml
@@ -2,4 +2,11 @@
<!-- Default screen margins, per the Android Design guidelines. -->
<dimen name="activity_horizontal_margin">16dp</dimen>
<dimen name="activity_vertical_margin">16dp</dimen>
+
+ <dimen name="listview_item_padding">16dp</dimen>
+
+ <dimen name="font_size_huge">24sp</dimen>
+ <dimen name="font_size_large">20sp</dimen>
+ <dimen name="font_size_normal">16sp</dimen>
+ <dimen name="font_size_small">14sp</dimen>
</resources>
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index bbe8b60..da60e14 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1,6 +1,32 @@
<resources>
<string name="app_name">RSSin</string>
- <string name="hello_world">Hello world!</string>
<string name="action_settings">Settings</string>
+ <string name="action_filters">Filters</string>
+
+ <string name="button_ok">OK</string>
+ <string name="button_apply">Apply</string>
+ <string name="button_cancel">Cancel</string>
+
+ <string name="title_activity_filters">Filters</string>
+ <string name="title_activity_filter_settings">Filter</string>
+ <string name="title_activity_filter">Filter</string>
+
+ <string name="filters_action_add">Add</string>
+
+ <string name="filter_settings_edit_keywords">Edit</string>
+ <string name="filter_settings_feeds">Feeds</string>
+ <string name="filter_settings_add_keyword">Add</string>
+ <string name="filter_settings_add_feed">Add</string>
+ <string name="filter_settings_action_feeds">Feeds</string>
+ <string name="filter_settings_action_title">Title</string>
+ <string name="filter_settings_action_delete">Delete</string>
+
+ <string name="error_save_filters">Couldn\'t save filter</string>
+ <string name="error_load_filters">Couldn\'t load filters</string>
+ <string name="error_delete_filter">Couldn\'t delete filter</string>
+
+ <string name="error_invalid_url">Invalid URL</string>
+
+ <string name="error_net_load">Internet problem</string>
</resources>
diff --git a/app/src/main/res/values/strings_activity_settings.xml b/app/src/main/res/values/strings_activity_settings.xml
new file mode 100644
index 0000000..73fa482
--- /dev/null
+++ b/app/src/main/res/values/strings_activity_settings.xml
@@ -0,0 +1,34 @@
+<resources>
+ <string name="title_activity_settings">Settings</string>
+
+ <!-- Example General settings -->
+ <string name="pref_header_general">General</string>
+
+ <!-- Example settings for Data & Sync -->
+ <string name="pref_header_data_sync">Data &amp; sync</string>
+
+ <string name="pref_title_sync_frequency">Sync frequency</string>
+ <string-array name="pref_sync_frequency_titles">
+ <item>1 minute</item>
+ <item>2 minutes</item>
+ <item>5 minutes</item>
+ <item>10 minutes</item>
+ <item>15 minutes</item>
+ <item>30 minutes</item>
+ <item>1 hour</item>
+ <item>Never</item>
+ </string-array>
+ <string-array name="pref_sync_frequency_values">
+ <item>1</item>
+ <item>2</item>
+ <item>5</item>
+ <item>10</item>
+ <item>15</item>
+ <item>30</item>
+ <item>60</item>
+ <item>-1</item>
+ </string-array>
+
+ <string name="pref_title_system_sync_settings">System sync settings</string>
+
+</resources>
diff --git a/app/src/main/res/xml/pref_data_sync.xml b/app/src/main/res/xml/pref_data_sync.xml
new file mode 100644
index 0000000..ffda831
--- /dev/null
+++ b/app/src/main/res/xml/pref_data_sync.xml
@@ -0,0 +1,21 @@
+<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <!-- NOTE: Hide buttons to simplify the UI. Users can touch outside the dialog to
+ dismiss it. -->
+ <!-- NOTE: ListPreference's summary should be set to its value by the activity code. -->
+ <ListPreference
+ android:key="sync_frequency"
+ android:title="@string/pref_title_sync_frequency"
+ android:entries="@array/pref_sync_frequency_titles"
+ android:entryValues="@array/pref_sync_frequency_values"
+ android:defaultValue="180"
+ android:negativeButtonText="@null"
+ android:positiveButtonText="@null" />
+
+ <!-- This preference simply launches an intent when selected. Use this UI sparingly, per
+ design guidelines. -->
+ <Preference android:title="@string/pref_title_system_sync_settings">
+ <intent android:action="android.settings.SYNC_SETTINGS" />
+ </Preference>
+
+</PreferenceScreen>
diff --git a/app/src/main/res/xml/pref_headers.xml b/app/src/main/res/xml/pref_headers.xml
new file mode 100644
index 0000000..1c58203
--- /dev/null
+++ b/app/src/main/res/xml/pref_headers.xml
@@ -0,0 +1,6 @@
+<preference-headers xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <header android:fragment="org.rssin.android.SettingsActivity$DataSyncPreferenceFragment"
+ android:title="@string/pref_header_data_sync" />
+
+</preference-headers>
diff --git a/app/src/main/res/xml/pref_main.xml b/app/src/main/res/xml/pref_main.xml
new file mode 100644
index 0000000..dfc2102
--- /dev/null
+++ b/app/src/main/res/xml/pref_main.xml
@@ -0,0 +1,3 @@
+<?xml version="1.0" encoding="utf-8"?>
+<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
+</PreferenceScreen> \ No newline at end of file
diff --git a/docs/classes.txt b/docs/classes.txt
deleted file mode 100644
index 0a53503..0000000
--- a/docs/classes.txt
+++ /dev/null
@@ -1,19 +0,0 @@
-org.rssin.android.FeedActivity
-org.rssin.android.FilterActivity
-org.rssin.android.FiltersActivity
-org.rssin.android.InboxActivity
-org.rssin.android.SettingsActivity
-org.rssin.android.UnifiedInboxActivity
-org.rssin.android.PollFeedsService
-org.rssin.neurons.FeedSorter
-org.rssin.neurons.MultiNeuralNetwork
-org.rssin.neurons.NeuralNetwork
-org.rssin.neurons.Neuron
-org.rssin.rss.Feed
-org.rssin.rss.FeedItem
-org.rssin.rss.FeedLoader
-org.rssin.rssin.Filter
-org.rssin.rssin.Keyword
-org.rssin.summaries.Summary
-org.rssin.summaries.SummaryAPI
-org.rssin.summaries.SummaryAPIInterface