diff options
Diffstat (limited to 'app/src')
66 files changed, 3390 insertions, 173 deletions
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 Binary files differindex cde69bc..2e7f377 100644 --- a/app/src/main/res/mipmap-hdpi/ic_launcher.png +++ b/app/src/main/res/mipmap-hdpi/ic_launcher.png diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png Binary files differindex c133a0c..c7d5ca5 100644 --- a/app/src/main/res/mipmap-mdpi/ic_launcher.png +++ b/app/src/main/res/mipmap-mdpi/ic_launcher.png diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png Binary files differindex bfa42f0..8d1bf3a 100644 --- a/app/src/main/res/mipmap-xhdpi/ic_launcher.png +++ b/app/src/main/res/mipmap-xhdpi/ic_launcher.png diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png Binary files differindex 324e72c..9e7e711 100644 --- a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +++ b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png Binary files differnew file mode 100644 index 0000000..63c5a0d --- /dev/null +++ b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png 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 & 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 |