From 23021d81c76d17b80e05cf0594f2dfa59be1bdbd Mon Sep 17 00:00:00 2001 From: Eric Kok Date: Mon, 24 Jun 2013 18:26:26 +0200 Subject: [PATCH] Loading and parsing of RSS feeds and showing loading status in the UI. --- core/res/layout-w600dp/activity_torrents.xml | 2 +- core/res/layout-w900dp/activity_rssfeeds.xml | 26 ++ core/res/layout-w900dp/activity_torrents.xml | 2 +- core/res/layout/activity_rssfeeds.xml | 1 - core/res/layout/activity_rssitems.xml | 1 - core/src/org/ifies/android/sax/Channel.java | 149 ----------- core/src/org/ifies/android/sax/Item.java | 146 ----------- core/src/org/ifies/android/sax/RssParser.java | 237 ------------------ .../app/settings/ApplicationSettings.java | 88 +++---- .../core/app/settings/RssfeedSetting.java | 33 ++- .../transdroid/core/gui/TorrentsActivity.java | 6 + .../core/gui/navigation/NavigationHelper.java | 20 +- .../core/gui/rss/RssfeedLoader.java | 60 +++++ .../transdroid/core/gui/rss/RssfeedView.java | 31 +-- .../core/gui/rss/RssfeedsActivity.java | 74 +++++- .../core/gui/rss/RssfeedsAdapter.java | 23 +- .../core/gui/rss/RssfeedsFragment.java | 36 ++- .../transdroid/core/gui/rss/RssitemView.java | 8 +- .../core/gui/rss/RssitemsActivity.java | 26 ++ .../core/gui/rss/RssitemsAdapter.java | 19 +- .../core/gui/rss/RssitemsFragment.java | 23 +- .../transdroid/core/rssparser/Channel.java | 153 +++++++++++ .../core/rssparser}/HttpHelper.java | 2 +- .../org/transdroid/core/rssparser/Item.java | 157 ++++++++++++ .../transdroid/core/rssparser/RssParser.java | 235 +++++++++++++++++ 25 files changed, 885 insertions(+), 673 deletions(-) create mode 100644 core/res/layout-w900dp/activity_rssfeeds.xml delete mode 100644 core/src/org/ifies/android/sax/Channel.java delete mode 100644 core/src/org/ifies/android/sax/Item.java delete mode 100644 core/src/org/ifies/android/sax/RssParser.java create mode 100644 core/src/org/transdroid/core/gui/rss/RssfeedLoader.java create mode 100644 core/src/org/transdroid/core/gui/rss/RssitemsActivity.java create mode 100644 core/src/org/transdroid/core/rssparser/Channel.java rename core/src/org/{ifies/android/sax => transdroid/core/rssparser}/HttpHelper.java (99%) create mode 100644 core/src/org/transdroid/core/rssparser/Item.java create mode 100644 core/src/org/transdroid/core/rssparser/RssParser.java diff --git a/core/res/layout-w600dp/activity_torrents.xml b/core/res/layout-w600dp/activity_torrents.xml index 746cc9e0..b19c1959 100644 --- a/core/res/layout-w600dp/activity_torrents.xml +++ b/core/res/layout-w600dp/activity_torrents.xml @@ -1,5 +1,5 @@ - + + + + + + + + \ No newline at end of file diff --git a/core/res/layout-w900dp/activity_torrents.xml b/core/res/layout-w900dp/activity_torrents.xml index a686fff0..8ffcf39b 100644 --- a/core/res/layout-w900dp/activity_torrents.xml +++ b/core/res/layout-w900dp/activity_torrents.xml @@ -1,5 +1,5 @@ - + - - ()); - setItems(new ArrayList()); - } - - public void setId(int id) { - m_Id = id; - } - public int getId() { - return m_Id; - } - - public void setTitle(String title) { - m_Title = title; - } - - public String getTitle() { - return m_Title; - } - - public void setLink(String link) { - m_Link = link; - } - - public String getLink() { - return m_Link; - } - - public void setDescription(String description) { - m_Description = description; - } - - public String getDescription() { - return m_Description; - } - - public void setPubDate(Date date) { - m_PubDate = date; - } - - public Date getPubDate() { - return m_PubDate; - } - - public void setLastBuildDate(long lastBuildDate) { - m_LastBuildDate = lastBuildDate; - } - - public long getLastBuildDate() { - return m_LastBuildDate; - } - - public void setCategories(List categories) { - m_Categories = categories; - } - - public void addCategory(String category) { - m_Categories.add(category); - } - - public List getCategories() { - return m_Categories; - } - - public void setItems(List items) { - m_Items = items; - } - - public void addItem(Item item) { - m_Items.add(item); - } - - public List getItems() { - return m_Items; - } - - public void setImage(String image) { - m_Image = image; - } - - public String getImage() { - return m_Image; - } - - private int m_Id; - private String m_Title; - private String m_Link; - private String m_Description; - private Date m_PubDate; - private long m_LastBuildDate; - private List m_Categories; - private List m_Items; - private String m_Image; - - @Override - public int describeContents() { - return 0; - } - @Override - public void writeToParcel(Parcel out, int flags) { - out.writeInt(m_Id); - out.writeString(m_Title); - out.writeString(m_Link); - out.writeString(m_Description); - out.writeLong(m_PubDate == null? -1: m_PubDate.getTime()); - out.writeLong(m_LastBuildDate); - out.writeStringList(m_Categories); - out.writeTypedList(m_Items); - out.writeString(m_Image); - } - public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { - public Channel createFromParcel(Parcel in) { - return new Channel(in); - } - public Channel[] newArray(int size) { - return new Channel[size]; - } - }; - private Channel(Parcel in) { - m_Id = in.readInt(); - m_Title = in.readString(); - m_Link = in.readString(); - m_Description = in.readString(); - long pubDate = in.readLong(); - m_PubDate = pubDate == -1? null: new Date(pubDate); - m_LastBuildDate = in.readLong(); - m_Categories = new ArrayList(); - in.readTypedList(m_Items, Item.CREATOR); - in.readStringList(m_Categories); - m_Image = in.readString(); - } - -} \ No newline at end of file diff --git a/core/src/org/ifies/android/sax/Item.java b/core/src/org/ifies/android/sax/Item.java deleted file mode 100644 index 07cd4ee0..00000000 --- a/core/src/org/ifies/android/sax/Item.java +++ /dev/null @@ -1,146 +0,0 @@ -/* - * Taken from the 'Learning Android' project,; - * released as Public Domain software at - * http://github.com/digitalspaghetti/learning-android - */ -package org.ifies.android.sax; - -import java.util.Date; - -import android.os.Parcel; -import android.os.Parcelable; - -public class Item implements Comparable, Parcelable { - - public void setId(int id) { - this._id = id; - } - - public int getId() { - return _id; - } - - public void setTitle(String title) { - this.title = title; - } - - public String getTitle() { - return this.title; - } - - public void setDescription(String description) { - this.description = description; - } - - public String getDescription() { - return this.description; - } - - public void setLink(String link) { - this.link = link; - } - - public String getLink() { - return this.link; - } - - public void setPubdate(Date pubdate) { - this.pubDate = pubdate; - } - - public Date getPubdate() { - return this.pubDate; - } - - public void setEnclosureUrl(String enclosureUrl) { - this.enclosureUrl = enclosureUrl; - } - - public void setEnclosureLength(long enclosureLength) { - this.enclosureLength = enclosureLength; - } - - public void setEnclosureType(String enclosureType) { - this.enclosureType = enclosureType; - } - - public String getEnclosureUrl() { - return this.enclosureUrl; - } - - public String getEnclosureType() { - return this.enclosureType; - } - - public long getEnclosureLength() { - return this.enclosureLength; - } - - private int _id; - private String title; - private String link; - private String description; - private Date pubDate; - private String enclosureUrl; - private String enclosureType; - private long enclosureLength; - - /** - * Returns 'the' item link, which preferably is the enclosure url, but otherwise the link (or null if that is empty too) - * @return A single link url to be used - */ - public String getTheLink() { - if (this.getEnclosureUrl() != null) { - return this.getEnclosureUrl(); - } else { - return this.getLink(); - } - } - - /** - * CompareTo is used to compare (and sort) item based on their publication dates - */ - @Override - public int compareTo(Item another) { - if (another == null || this.pubDate == null || another.getPubdate() == null) { - return 0; - } - return this.pubDate.compareTo(another.getPubdate()); - } - - @Override - public int describeContents() { - return 0; - } - @Override - public void writeToParcel(Parcel out, int flags) { - out.writeInt(_id); - out.writeString(title); - out.writeString(link); - out.writeString(description); - out.writeLong(pubDate == null? -1: pubDate.getTime()); - out.writeString(enclosureUrl); - out.writeString(enclosureType); - out.writeLong(enclosureLength); - } - public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { - public Item createFromParcel(Parcel in) { - return new Item(in); - } - public Item[] newArray(int size) { - return new Item[size]; - } - }; - private Item(Parcel in) { - _id = in.readInt(); - title = in.readString(); - link = in.readString(); - description = in.readString(); - long pubDateIn = in.readLong(); - pubDate = pubDateIn == -1? null: new Date(pubDateIn); - enclosureUrl = in.readString(); - enclosureType = in.readString(); - enclosureLength = in.readLong(); - } - -} \ No newline at end of file diff --git a/core/src/org/ifies/android/sax/RssParser.java b/core/src/org/ifies/android/sax/RssParser.java deleted file mode 100644 index 91288051..00000000 --- a/core/src/org/ifies/android/sax/RssParser.java +++ /dev/null @@ -1,237 +0,0 @@ -/* - * Taken from the 'Learning Android' project,; - * released as Public Domain software at - * http://github.com/digitalspaghetti/learning-android - * and modified heavily for Transdroid - */ -package org.ifies.android.sax; - -import java.io.IOException; -import java.util.Date; - -import javax.xml.parsers.ParserConfigurationException; -import javax.xml.parsers.SAXParser; -import javax.xml.parsers.SAXParserFactory; - -import org.apache.http.HttpResponse; -import org.apache.http.client.methods.HttpGet; -import org.apache.http.conn.scheme.PlainSocketFactory; -import org.apache.http.conn.scheme.Scheme; -import org.apache.http.conn.scheme.SchemeRegistry; -import org.apache.http.impl.client.DefaultHttpClient; -import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager; -import org.apache.http.params.BasicHttpParams; -import org.apache.http.params.HttpConnectionParams; -import org.apache.http.params.HttpParams; -import org.xml.sax.Attributes; -import org.xml.sax.SAXException; -import org.xml.sax.helpers.DefaultHandler; - -public class RssParser extends DefaultHandler -{ - /** - * The constructor for the RSS Parser - * @param url - */ - public RssParser(String url) { - this.urlString = url; - this.text = new StringBuilder(); - } - - /** - * Returns the feed as a RssFeed, which is a ListArray - * @return RssFeed rssFeed - */ - public Channel getChannel() { - return (this.channel); - } - - public void parse() throws ParserConfigurationException, SAXException, IOException { - - DefaultHttpClient httpclient = initialise(); - HttpResponse result = httpclient.execute(new HttpGet(urlString)); - //FileInputStream urlInputStream = new FileInputStream("/sdcard/rsstest2.txt"); - SAXParserFactory spf = SAXParserFactory.newInstance(); - if (spf != null) { - SAXParser sp = spf.newSAXParser(); - sp.parse(result.getEntity().getContent(), this); - } - - } - - /** - * Instantiates an HTTP client that can be used for all requests. - * @param connectionTimeout The connection timeout in milliseconds - * @throws DaemonException On conflicting or missing settings - */ - protected DefaultHttpClient initialise() { - - SchemeRegistry registry = new SchemeRegistry(); - registry.register(new Scheme("http", new PlainSocketFactory(), 80)); - - HttpParams httpparams = new BasicHttpParams(); - HttpConnectionParams.setConnectionTimeout(httpparams, 5000); - HttpConnectionParams.setSoTimeout(httpparams, 5000); - DefaultHttpClient httpclient = new DefaultHttpClient(new ThreadSafeClientConnManager(httpparams, registry), httpparams); - - httpclient.addRequestInterceptor(HttpHelper.gzipRequestInterceptor); - httpclient.addResponseInterceptor(HttpHelper.gzipResponseInterceptor); - - return httpclient; - - } - - /** - * By default creates a standard Item (with title, description and links), which - * may to overriden to add more data. - * @return A possibly decorated Item instance - */ - protected Item createNewItem() { - return new Item(); - } - - public void startElement(String uri, String localName, String qName, Attributes attributes) { - - /** First lets check for the channel */ - if (localName.equalsIgnoreCase("channel")) { - this.channel = new Channel(); - } - - /** Now lets check for an item */ - if (localName.equalsIgnoreCase("item") && (this.channel != null)) { - this.item = createNewItem(); - this.channel.addItem(this.item); - } - - /** Now lets check for an image */ - if (localName.equalsIgnoreCase("image") && (this.channel != null)) { - this.imgStatus = true; - } - - /** Checking for a enclosure */ - if (localName.equalsIgnoreCase("enclosure")) { - /** Lets check we are in an item */ - if (this.item != null && attributes != null && attributes.getLength() > 0) { - if (attributes.getValue("url") != null) { - this.item.setEnclosureUrl(parseLink(attributes.getValue("url"))); - } - if (attributes.getValue("type") != null) { - this.item.setEnclosureType(attributes.getValue("type")); - } - if (attributes.getValue("length") != null) { - this.item.setEnclosureLength(Long.parseLong(attributes.getValue("length"))); - } - } - } - - } - - /** - * This is where we actually parse for the elements contents - */ - @SuppressWarnings("deprecation") - public void endElement(String uri, String localName, String qName) { - /** Check we have an RSS Feed */ - if (this.channel == null) { - return; - } - - /** Check are at the end of an item */ - if (localName.equalsIgnoreCase("item")) { - this.item = null; - } - - /** Check we are at the end of an image */ - if (localName.equalsIgnoreCase("image")) - this.imgStatus = false; - - /** Now we need to parse which title we are in */ - if (localName.equalsIgnoreCase("title")) - { - /** We are an item, so we set the item title */ - if (this.item != null){ - this.item.setTitle(this.text.toString().trim()); - /** We are in an image */ - } else { - this.channel.setTitle(this.text.toString().trim()); - } - } - - /** Now we are checking for a link */ - if (localName.equalsIgnoreCase("link")) { - /** Check we are in an item **/ - if (this.item != null) { - this.item.setLink(parseLink(this.text.toString())); - /** Check we are in an image */ - } else if (this.imgStatus) { - this.channel.setImage(parseLink(this.text.toString())); - /** Check we are in a channel */ - } else { - this.channel.setLink(parseLink(this.text.toString())); - } - } - - /** Checking for a description */ - if (localName.equalsIgnoreCase("description")) { - /** Lets check we are in an item */ - if (this.item != null) { - this.item.setDescription(this.text.toString().trim()); - /** Lets check we are in the channel */ - } else { - this.channel.setDescription(this.text.toString().trim()); - } - } - - /** Checking for a pubdate */ - if (localName.equalsIgnoreCase("pubDate")) { - /** Lets check we are in an item */ - if (this.item != null) { - try { - this.item.setPubdate(new Date(Date.parse(this.text.toString().trim()))); - } catch (Exception e) { - // Date is malformed (not parsable by Date.parse) - } - /** Lets check we are in the channel */ - } else { - try { - this.channel.setPubDate(new Date(Date.parse(this.text.toString().trim()))); - } catch (Exception e) { - // Date is malformed (not parsable by Date.parse) - } - } - } - - /** Check for the category */ - if (localName.equalsIgnoreCase("category") && (this.item != null)) { - this.channel.addCategory(this.text.toString().trim()); - } - - addAdditionalData(localName, this.item, this.text.toString()); - - this.text.setLength(0); - } - - /** - * May be overridden to add additional data from tags that are not standard in RSS. - * Not used by this default RSS style parser. - * @param localName The tag name - * @param item The Item we are currently parsing - * @param text The new text content - */ - protected void addAdditionalData(String localName, Item item, String text) { } - - public void characters(char[] ch, int start, int length) { - this.text.append(ch, start, length); - } - - private String parseLink(String string) { - return string.trim(); - } - - private String urlString; - private Channel channel; - private StringBuilder text; - private Item item; - private boolean imgStatus; - -} \ No newline at end of file diff --git a/core/src/org/transdroid/core/app/settings/ApplicationSettings.java b/core/src/org/transdroid/core/app/settings/ApplicationSettings.java index 6d376232..24297afb 100644 --- a/core/src/org/transdroid/core/app/settings/ApplicationSettings.java +++ b/core/src/org/transdroid/core/app/settings/ApplicationSettings.java @@ -1,6 +1,7 @@ package org.transdroid.core.app.settings; import java.util.ArrayList; +import java.util.Date; import java.util.List; import org.androidannotations.annotations.EBean; @@ -64,40 +65,30 @@ public class ApplicationSettings { Daemon type = Daemon.fromCode(prefs.getString("server_type_" + order, null)); boolean ssl = prefs.getBoolean("server_sslenabled_" + order, false); String defaultPort = Integer.toString(Daemon.getDefaultPortNumber(type, ssl)); - return new ServerSetting(order, - prefs.getString("server_name_" + order, null), - type, - prefs.getString("server_address_" + order, null), - prefs.getString("server_localaddress_" + order, null), - prefs.getString("server_localnetwork_" + order, null), - Integer.parseInt(prefs.getString("server_port_" + order, defaultPort)), - ssl, - prefs.getBoolean("server_ssltrustall_" + order, false), - prefs.getString("server_ssltrustkey_" + order, null), - prefs.getString("server_folder_" + order, null), - prefs.getBoolean("server_useauth_" + order, true), - prefs.getString("server_user_" + order, null), - prefs.getString("server_pass_" + order, null), - prefs.getString("server_extrapass_" + order, null), - OS.fromCode(prefs.getString("server_os_" + order, "type_linux")), - prefs.getString("server_downloaddir_" + order, null), - prefs.getString("server_ftpurl_" + order, null), - prefs.getString("server_ftppass_" + order, null), - Integer.parseInt(prefs.getString("server_timeout_"+ order, "8")), - prefs.getBoolean("server_alarmfinished_" + order, true), - prefs.getBoolean("server_alarmnew_" + order, false), false); + return new ServerSetting(order, prefs.getString("server_name_" + order, null), type, prefs.getString( + "server_address_" + order, null), prefs.getString("server_localaddress_" + order, null), + prefs.getString("server_localnetwork_" + order, null), Integer.parseInt(prefs.getString("server_port_" + + order, defaultPort)), ssl, prefs.getBoolean("server_ssltrustall_" + order, false), + prefs.getString("server_ssltrustkey_" + order, null), prefs.getString("server_folder_" + order, null), + prefs.getBoolean("server_useauth_" + order, true), prefs.getString("server_user_" + order, null), + prefs.getString("server_pass_" + order, null), prefs.getString("server_extrapass_" + order, null), + OS.fromCode(prefs.getString("server_os_" + order, "type_linux")), prefs.getString("server_downloaddir_" + + order, null), prefs.getString("server_ftpurl_" + order, null), prefs.getString( + "server_ftppass_" + order, null), Integer.parseInt(prefs.getString("server_timeout_" + order, + "8")), prefs.getBoolean("server_alarmfinished_" + order, true), prefs.getBoolean( + "server_alarmnew_" + order, false), false); // @formatter:on } /** - * Removes all settings related to a configured server. Since servers are ordered, the order of the remaining + * Removes all settings related to a configured server. Since servers are ordered, the order of the remaining * servers will be updated accordingly. * @param order The identifying order number/key of the settings to remove */ public void removeServerSettings(int order) { if (prefs.getString("server_type_" + order, null) == null) return; // The settings that were requested to be removed do not exist - + // Copy all settings higher than the supplied order number to the previous spot Editor edit = prefs.edit(); int max = getMaxServer(); @@ -148,7 +139,7 @@ public class ApplicationSettings { edit.remove("server_alarmfinished_" + max); edit.remove("server_alarmfinished_" + max); edit.commit(); - + } /** @@ -228,9 +219,8 @@ public class ApplicationSettings { */ public WebsearchSetting getWebsearchSetting(int order) { // @formatter:off - return new WebsearchSetting(order, - prefs.getString("websearch_name_" + order, null), - prefs.getString("websearch_baseurl_" + order, null)); + return new WebsearchSetting(order, prefs.getString("websearch_name_" + order, null), prefs.getString( + "websearch_baseurl_" + order, null)); // @formatter:on } @@ -242,7 +232,7 @@ public class ApplicationSettings { public void removeWebsearchSettings(int order) { if (prefs.getString("websearch_baseurl_" + order, null) == null) return; // The settings that were requested to be removed do not exist - + // Copy all settings higher than the supplied order number to the previous spot Editor edit = prefs.edit(); int max = getMaxWebsearch(); @@ -255,7 +245,7 @@ public class ApplicationSettings { edit.remove("websearch_name_" + max); edit.remove("websearch_baseurl_" + max); edit.commit(); - + } /** @@ -288,11 +278,10 @@ public class ApplicationSettings { */ public RssfeedSetting getRssfeedSetting(int order) { // @formatter:off - return new RssfeedSetting(order, - prefs.getString("rssfeed_name_" + order, null), - prefs.getString("rssfeed_url_" + order, null), - prefs.getBoolean("rssfeed_reqauth_" + order, false), - prefs.getString("rssfeed_lastnew_" + order, null)); + long lastViewed = prefs.getLong("rssfeed_lastviewed_" + order, -1); + return new RssfeedSetting(order, prefs.getString("rssfeed_name_" + order, null), prefs.getString("rssfeed_url_" + + order, null), prefs.getBoolean("rssfeed_reqauth_" + order, false), lastViewed == -1L ? null + : new Date(lastViewed)); // @formatter:on } @@ -304,7 +293,7 @@ public class ApplicationSettings { public void removeRssfeedSettings(int order) { if (prefs.getString("rssfeed_url_" + order, null) == null) return; // The settings that were requested to be removed do not exist - + // Copy all settings higher than the supplied order number to the previous spot Editor edit = prefs.edit(); int max = getMaxRssfeed(); @@ -312,16 +301,30 @@ public class ApplicationSettings { edit.putString("rssfeed_name_" + i, prefs.getString("rssfeed_name_" + (i + 1), null)); edit.putString("rssfeed_url_" + i, prefs.getString("rssfeed_url_" + (i + 1), null)); edit.putBoolean("rssfeed_reqauth_" + i, prefs.getBoolean("rssfeed_reqauth_" + (i + 1), false)); - edit.putString("rssfeed_lastnew_" + i, prefs.getString("rssfeed_lastnew_" + (i + 1), null)); + edit.putLong("rssfeed_lastviewed_" + i, prefs.getLong("rssfeed_lastviewed_" + (i + 1), -1)); } // Remove the last settings, of which we are now sure are no longer required edit.remove("rssfeed_name_" + max); edit.remove("rssfeed_url_" + max); edit.remove("rssfeed_reqauth_" + max); - edit.remove("rssfeed_lastnew_" + max); + edit.remove("rssfeed_lastviewed_" + max); edit.commit(); - + + } + + /** + * Registers for some RSS feed (as identified by its order numbe/key) the last date and time that it was viewed by + * the user. This is used to determine which items in an RSS feed are 'new'. Warning: any previously retrieved + * {@link RssfeedSetting} object is now no longer in sync, as this will not automatically be updated in the object. + * Use {@link #getRssfeedSetting(int)} to get fresh data. + * @param order The identifying order number/key of the settings of te RSS feed that was viewed + * @param lastViewed The date and time that the feed was last viewed; typically now + */ + public void setRssfeedLastViewer(int order, Date lastViewed) { + if (prefs.getString("rssfeed_url_" + order, null) == null) + return; // The settings that were requested to be removed do not exist + prefs.edit().putLong("rssfeed_lastviewed_" + order, lastViewed.getTime()).commit(); } /** @@ -335,12 +338,13 @@ public class ApplicationSettings { } /** - * Returns the sort order property that the user last used. Use together with {@link #getLastUsedSortDescending()} to - * get the full last used sort settings. + * Returns the sort order property that the user last used. Use together with {@link #getLastUsedSortDescending()} + * to get the full last used sort settings. * @return The last used sort order enumeration value */ public TorrentsSortBy getLastUsedSortOrder() { - return TorrentsSortBy.getStatus(prefs.getInt("system_lastusedsortorder", TorrentsSortBy.Alphanumeric.getCode())); + return TorrentsSortBy + .getStatus(prefs.getInt("system_lastusedsortorder", TorrentsSortBy.Alphanumeric.getCode())); } /** diff --git a/core/src/org/transdroid/core/app/settings/RssfeedSetting.java b/core/src/org/transdroid/core/app/settings/RssfeedSetting.java index cd90f932..462b6156 100644 --- a/core/src/org/transdroid/core/app/settings/RssfeedSetting.java +++ b/core/src/org/transdroid/core/app/settings/RssfeedSetting.java @@ -1,5 +1,7 @@ package org.transdroid.core.app.settings; +import java.util.Date; + import org.transdroid.core.gui.lists.SimpleListItem; import android.net.Uri; @@ -12,19 +14,19 @@ import android.text.TextUtils; public class RssfeedSetting implements SimpleListItem { private static final String DEFAULT_NAME = "Default"; - + private final int order; private final String name; private final String url; private final boolean requiresAuth; - private String lastNew; + private Date lastViewed; - public RssfeedSetting(int order, String name, String baseUrl, boolean needsAuth, String lastNew) { + public RssfeedSetting(int order, String name, String baseUrl, boolean needsAuth, Date lastViewed) { this.order = order; this.name = name; this.url = baseUrl; this.requiresAuth = needsAuth; - this.lastNew = lastNew; + this.lastViewed = lastViewed; } public int getOrder() { @@ -37,7 +39,7 @@ public class RssfeedSetting implements SimpleListItem { return name; if (!TextUtils.isEmpty(url)) { String host = Uri.parse(url).getHost(); - return host == null? DEFAULT_NAME: host; + return host == null ? DEFAULT_NAME : host; } return DEFAULT_NAME; } @@ -51,20 +53,13 @@ public class RssfeedSetting implements SimpleListItem { } /** - * Returns the URL of the item that was the newest last time we checked this feed + * Returns the URL of the item that was the newest last time we checked this feed. Note that this is NOT updated + * automatically after the settings were loaded from {@link ApplicationSettings}; instead the settings have to be + * manually loaded again using {@link ApplicationSettings#getRssfeedSetting(int)}. * @return The last new item's URL as URL-encoded string */ - public String getLastNew() { - // TODO: Persist this into Preferences - return this.lastNew; - } - - /** - * Record the URL of what is now the last item we retrieved - * @param lastNew The URL of the last new item as URL-encoded string - */ - public void setLastNew(String lastNew) { - this.lastNew = lastNew; + public Date getLastViewed() { + return this.lastViewed; } /** @@ -74,7 +69,7 @@ public class RssfeedSetting implements SimpleListItem { public String getHumanReadableIdentifier() { String host = Uri.parse(url).getHost(); String path = Uri.parse(url).getPath(); - return (host == null? null: host + (path == null? "": path)); + return (host == null ? null : host + (path == null ? "" : path)); } - + } diff --git a/core/src/org/transdroid/core/gui/TorrentsActivity.java b/core/src/org/transdroid/core/gui/TorrentsActivity.java index 1d6a10d4..a9c60cb5 100644 --- a/core/src/org/transdroid/core/gui/TorrentsActivity.java +++ b/core/src/org/transdroid/core/gui/TorrentsActivity.java @@ -25,6 +25,7 @@ import org.transdroid.core.gui.lists.LocalTorrent; import org.transdroid.core.gui.lists.SimpleListItem; import org.transdroid.core.gui.log.*; import org.transdroid.core.gui.navigation.*; +import org.transdroid.core.gui.rss.RssfeedsActivity_; import org.transdroid.core.gui.search.BarcodeHelper; import org.transdroid.core.gui.search.FilePickerHelper; import org.transdroid.core.gui.search.UrlEntryDialog; @@ -474,6 +475,11 @@ public class TorrentsActivity extends SherlockFragmentActivity implements OnNavi updateTurtleMode(false); } + @OptionsItem(resName = "action_rss") + protected void openRss() { + RssfeedsActivity_.intent(this).start(); + } + @OptionsItem(resName = "action_settings") protected void openSettings() { MainSettingsActivity_.intent(this).start(); diff --git a/core/src/org/transdroid/core/gui/navigation/NavigationHelper.java b/core/src/org/transdroid/core/gui/navigation/NavigationHelper.java index 3505d9e9..87f19769 100644 --- a/core/src/org/transdroid/core/gui/navigation/NavigationHelper.java +++ b/core/src/org/transdroid/core/gui/navigation/NavigationHelper.java @@ -54,14 +54,22 @@ public class NavigationHelper { */ public ImageLoader getImageCache() { if (imageCache == null) { + // @formatter:off imageCache = ImageLoader.getInstance(); - Builder imageCacheBuilder = new Builder(context).defaultDisplayImageOptions( - new DisplayImageOptions.Builder().cacheInMemory().cacheOnDisc() - .imageScaleType(ImageScaleType.IN_SAMPLE_INT).build()).memoryCache( - new UsingFreqLimitedMemoryCache(1024 * 1024)); - imageCacheBuilder.discCache(new FileCountLimitedDiscCache(context.getCacheDir(), - new Md5FileNameGenerator(), 25)); + Builder imageCacheBuilder = new Builder(context) + .defaultDisplayImageOptions( + new DisplayImageOptions.Builder() + .cacheInMemory() + .cacheOnDisc() + .imageScaleType(ImageScaleType.IN_SAMPLE_INT) + .showImageForEmptyUri(R.drawable.ic_launcher) + .build()) + .memoryCache( + new UsingFreqLimitedMemoryCache(1024 * 1024)) + .discCache( + new FileCountLimitedDiscCache(context.getCacheDir(), new Md5FileNameGenerator(), 25)); imageCache.init(imageCacheBuilder.build()); + // @formatter:on } return imageCache; } diff --git a/core/src/org/transdroid/core/gui/rss/RssfeedLoader.java b/core/src/org/transdroid/core/gui/rss/RssfeedLoader.java new file mode 100644 index 00000000..630c7a19 --- /dev/null +++ b/core/src/org/transdroid/core/gui/rss/RssfeedLoader.java @@ -0,0 +1,60 @@ +package org.transdroid.core.gui.rss; + +import org.transdroid.core.app.settings.RssfeedSetting; +import org.transdroid.core.rssparser.Channel; +import org.transdroid.core.rssparser.Item; + +/** + * A container class that holds RSS feed settings and, after they have been retrieved, the contents as {@link Channel}, + * the number of new items and an indication of a connection error. + * @author Eric Kok + */ +public class RssfeedLoader { + + private final RssfeedSetting setting; + private Channel channel = null; + private int newCount = -1; + private boolean hasError = false; + + public RssfeedLoader(RssfeedSetting setting) { + this.setting = setting; + } + + public void update(Channel channel, boolean hasError) { + this.channel = channel; + this.hasError = hasError; + if (channel == null || hasError) { + hasError = true; + newCount = -1; + return; + } + // Count the number of new items, based on the date that this RSS feed was last viewed by the user + newCount = 0; + for (Item item : channel.getItems()) { + if (item.getPubdate() == null || setting.getLastViewed() == null + || item.getPubdate().after(setting.getLastViewed())) { + newCount++; + item.setIsNew(true); + } else { + item.setIsNew(true); + } + } + } + + public Channel getChannel() { + return channel; + } + + public RssfeedSetting getSetting() { + return setting; + } + + public int getNewCount() { + return newCount; + } + + public boolean hasError() { + return hasError ; + } + +} diff --git a/core/src/org/transdroid/core/gui/rss/RssfeedView.java b/core/src/org/transdroid/core/gui/rss/RssfeedView.java index 694f618f..1775fc20 100644 --- a/core/src/org/transdroid/core/gui/rss/RssfeedView.java +++ b/core/src/org/transdroid/core/gui/rss/RssfeedView.java @@ -1,11 +1,11 @@ package org.transdroid.core.gui.rss; -import org.androidannotations.annotations.Background; import org.androidannotations.annotations.Bean; import org.androidannotations.annotations.EViewGroup; import org.androidannotations.annotations.ViewById; import org.transdroid.core.app.settings.RssfeedSetting; import org.transdroid.core.gui.navigation.NavigationHelper; +import org.transdroid.core.rssparser.Channel; import android.content.Context; import android.view.View; @@ -39,25 +39,28 @@ public class RssfeedView extends LinearLayout { super(context); } - public void bind(RssfeedSetting rssfeed) { + public void bind(RssfeedLoader rssfeedLoader) { - nameText.setText(rssfeed.getName()); - faviconImage.setImageDrawable(null); - loadingProgress.setVisibility(View.VISIBLE); - newcountText.setVisibility(View.VISIBLE); + // Show the RSS feed name and either a loading indicator or the number of new items + nameText.setText(rssfeedLoader.getSetting().getName()); + if (rssfeedLoader.hasError() || rssfeedLoader.getChannel() != null) { + loadingProgress.setVisibility(View.GONE); + newcountText.setVisibility(View.VISIBLE); + newcountText.setText(rssfeedLoader.hasError()? "?": Integer.toString(rssfeedLoader.getNewCount())); + } else { + loadingProgress.setVisibility(View.VISIBLE); + newcountText.setVisibility(View.GONE); + } - // Load the RSS feed site' favicon + // Clear and then asynchronously load the RSS feed site' favicon // Uses the g.etfv.co service to resolve the favicon of any feed URL - navigationHelper.getImageCache().displayImage(String.format(GETFVO_URL, rssfeed), faviconImage); - - // Refresh the number of new items in this feed - refreshNewCount(); + faviconImage.setImageDrawable(null); + navigationHelper.getImageCache().displayImage(String.format(GETFVO_URL, rssfeedLoader), faviconImage); } - @Background - protected void refreshNewCount() { - // TODO: Implement + public Channel getChannel() { + return null; } } diff --git a/core/src/org/transdroid/core/gui/rss/RssfeedsActivity.java b/core/src/org/transdroid/core/gui/rss/RssfeedsActivity.java index 2a99bce4..4a9fd0a5 100644 --- a/core/src/org/transdroid/core/gui/rss/RssfeedsActivity.java +++ b/core/src/org/transdroid/core/gui/rss/RssfeedsActivity.java @@ -1,18 +1,31 @@ package org.transdroid.core.gui.rss; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import javax.xml.parsers.ParserConfigurationException; + +import org.androidannotations.annotations.Background; import org.androidannotations.annotations.Bean; import org.androidannotations.annotations.EActivity; import org.androidannotations.annotations.FragmentById; +import org.androidannotations.annotations.UiThread; import org.transdroid.core.app.settings.ApplicationSettings; +import org.transdroid.core.app.settings.RssfeedSetting; +import org.transdroid.core.rssparser.Channel; +import org.transdroid.core.rssparser.RssParser; +import org.xml.sax.SAXException; import com.actionbarsherlock.app.SherlockFragmentActivity; @EActivity(resName = "activity_rssfeeds") public class RssfeedsActivity extends SherlockFragmentActivity { - // Settings + // Settings and local data @Bean protected ApplicationSettings applicationSettings; + protected List loaders; // Contained feeds and items fragments @FragmentById(resName = "rssfeeds_list") @@ -20,4 +33,63 @@ public class RssfeedsActivity extends SherlockFragmentActivity { @FragmentById(resName = "rssitems_list") protected RssitemsFragment fragmentItems; + @Override + protected void onResume() { + super.onResume(); + loaders = new ArrayList(); + // For each RSS feed setting the user created, start a loader that retrieved the RSS feed (via a background + // thread) and, on success, determines the new items in the feed + for (RssfeedSetting setting : applicationSettings.getRssfeedSettings()) { + RssfeedLoader loader = new RssfeedLoader(setting); + loaders.add(loader); + loadRssfeed(loader); + } + fragmentFeeds.update(loaders); + } + + /** + * Performs the loading of the RSS feed content and parsing of items, in a background thread. + * @param loader The RSS feed loader for which to retrieve the contents + */ + @Background + protected void loadRssfeed(RssfeedLoader loader) { + RssParser parser = new RssParser(loader.getSetting().getUrl()); + try { + parser.parse(); + handleRssfeedResult(loader, parser.getChannel(), true); + } catch (ParserConfigurationException e) { + handleRssfeedResult(loader, null, true); + } catch (SAXException e) { + handleRssfeedResult(loader, null, true); + } catch (IOException e) { + handleRssfeedResult(loader, null, true); + } + + } + + /** + * Stores the retrieved RSS feed content channel into the loader and updates the RSS feed in the feeds list + * fragment. + * @param loader The RSS feed loader that was executed + * @param channel The data that was retrieved, or null if it could not be parsed + * @param hasError True if a connection error occurred in the loading of the feed; false otherwise + */ + @UiThread + protected void handleRssfeedResult(RssfeedLoader loader, Channel channel, boolean hasError) { + loader.update(channel, hasError); + fragmentFeeds.notifyDataSetChanged(); + } + + /** + * Opens an RSS feed in the dedicated fragment (if there was space in the UI) or a new {@link RssitemsActivity}. + * @param loader The RSS feed loader (with settings and the loaded content channel) to show + */ + public void openRssfeed(RssfeedLoader loader) { + if (fragmentItems != null) { + fragmentItems.update(loader.getChannel()); + } else { + RssitemsActivity_.intent(this).rssfeed(loader.getChannel()).start(); + } + } + } diff --git a/core/src/org/transdroid/core/gui/rss/RssfeedsAdapter.java b/core/src/org/transdroid/core/gui/rss/RssfeedsAdapter.java index 6fc3fcaa..305d7867 100644 --- a/core/src/org/transdroid/core/gui/rss/RssfeedsAdapter.java +++ b/core/src/org/transdroid/core/gui/rss/RssfeedsAdapter.java @@ -5,6 +5,7 @@ import java.util.List; import org.androidannotations.annotations.EBean; import org.androidannotations.annotations.RootContext; import org.transdroid.core.app.settings.RssfeedSetting; +import org.transdroid.core.rssparser.Channel; import android.content.Context; import android.view.View; @@ -12,23 +13,23 @@ import android.view.ViewGroup; import android.widget.BaseAdapter; /** - * Adapter that contains a list of RSS feed settings. + * Adapter that contains a list of {@link RssfeedSetting}s, each with associated loaded RSS feed {@link Channel}. * @author Eric Kok */ @EBean public class RssfeedsAdapter extends BaseAdapter { - private List rssfeeds = null; + private List loaders = null; @RootContext protected Context context; /** - * Allows updating the full internal list of feeds at once, replacing the old list - * @param newRssfeeds The new list of RSS feed settings objects + * Allows updating the full internal list of feed loaders at once, replacing the old list + * @param loaders The new list of RSS feed loader objects, which pair settings and a loaded channel */ - public void update(List newRssfeeds) { - this.rssfeeds = newRssfeeds; + public void update(List loaders) { + this.loaders = loaders; notifyDataSetChanged(); } @@ -39,16 +40,16 @@ public class RssfeedsAdapter extends BaseAdapter { @Override public int getCount() { - if (rssfeeds == null) + if (loaders == null) return 0; - return rssfeeds.size(); + return loaders.size(); } @Override - public RssfeedSetting getItem(int position) { - if (rssfeeds == null) + public RssfeedLoader getItem(int position) { + if (loaders == null) return null; - return rssfeeds.get(position); + return loaders.get(position); } @Override diff --git a/core/src/org/transdroid/core/gui/rss/RssfeedsFragment.java b/core/src/org/transdroid/core/gui/rss/RssfeedsFragment.java index 5b0824d2..b87dd0d7 100644 --- a/core/src/org/transdroid/core/gui/rss/RssfeedsFragment.java +++ b/core/src/org/transdroid/core/gui/rss/RssfeedsFragment.java @@ -1,12 +1,16 @@ package org.transdroid.core.gui.rss; +import java.util.List; + import org.androidannotations.annotations.AfterViews; import org.androidannotations.annotations.Bean; import org.androidannotations.annotations.EFragment; import org.androidannotations.annotations.OptionsMenu; import org.androidannotations.annotations.ViewById; -import org.transdroid.core.app.settings.ApplicationSettings; +import android.view.View; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; import android.widget.TextView; import com.actionbarsherlock.app.SherlockFragment; @@ -20,24 +24,36 @@ import com.actionbarsherlock.view.SherlockListView; @OptionsMenu(resName = "fragment_rssfeeds") public class RssfeedsFragment extends SherlockFragment { - // Settings - @Bean - protected ApplicationSettings applicationSettings; - @Bean - protected RssfeedsAdapter rssfeedsAdapter; - // Views @ViewById(resName = "rssfeeds_list") protected SherlockListView feedsList; + @Bean + protected RssfeedsAdapter rssfeedsAdapter; @ViewById protected TextView nosettingsText; @AfterViews protected void init() { - feedsList.setAdapter(rssfeedsAdapter); - rssfeedsAdapter.update(applicationSettings.getRssfeedSettings()); - + feedsList.setOnItemClickListener(onRssfeedSelected); } + public void update(List loaders) { + rssfeedsAdapter.update(loaders); + } + + private OnItemClickListener onRssfeedSelected = new OnItemClickListener() { + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + ((RssfeedsActivity)getActivity()).openRssfeed(rssfeedsAdapter.getItem(position)); + } + }; + + /** + * Notifies the contained list of RSS feeds that the underlying data has been changed. + */ + public void notifyDataSetChanged() { + rssfeedsAdapter.notifyDataSetChanged(); + } + } diff --git a/core/src/org/transdroid/core/gui/rss/RssitemView.java b/core/src/org/transdroid/core/gui/rss/RssitemView.java index 19299981..00d782d9 100644 --- a/core/src/org/transdroid/core/gui/rss/RssitemView.java +++ b/core/src/org/transdroid/core/gui/rss/RssitemView.java @@ -1,10 +1,8 @@ package org.transdroid.core.gui.rss; -import java.util.Date; - import org.androidannotations.annotations.EViewGroup; import org.androidannotations.annotations.ViewById; -import org.ifies.android.sax.Item; +import org.transdroid.core.rssparser.Item; import android.content.Context; import android.text.format.DateUtils; @@ -25,12 +23,12 @@ public class RssitemView extends RssitemStatusLayout { super(context); } - public void bind(Item rssitem, Date lastViewedItem) { + public void bind(Item rssitem) { nameText.setText(rssitem.getTitle()); dateText.setText(DateUtils.getRelativeDateTimeString(getContext(), rssitem.getPubdate().getTime(), DateUtils.SECOND_IN_MILLIS, DateUtils.WEEK_IN_MILLIS, DateUtils.FORMAT_ABBREV_MONTH)); - setIsNew(rssitem.getPubdate().after(lastViewedItem)); + setIsNew(rssitem.isNew()); } diff --git a/core/src/org/transdroid/core/gui/rss/RssitemsActivity.java b/core/src/org/transdroid/core/gui/rss/RssitemsActivity.java new file mode 100644 index 00000000..63972666 --- /dev/null +++ b/core/src/org/transdroid/core/gui/rss/RssitemsActivity.java @@ -0,0 +1,26 @@ +package org.transdroid.core.gui.rss; + +import org.androidannotations.annotations.AfterViews; +import org.androidannotations.annotations.EActivity; +import org.androidannotations.annotations.Extra; +import org.androidannotations.annotations.FragmentById; +import org.transdroid.core.rssparser.Channel; + +import com.actionbarsherlock.app.SherlockFragmentActivity; + +@EActivity(resName = "activity_rssfeeds") +public class RssitemsActivity extends SherlockFragmentActivity { + + @Extra + protected Channel rssfeed = null; + + @FragmentById(resName = "rssitems_list") + protected RssitemsFragment fragmentItems; + + @AfterViews + protected void init() { + // Get the intent extras and show them to the already loaded fragment + fragmentItems.update(rssfeed); + } + +} diff --git a/core/src/org/transdroid/core/gui/rss/RssitemsAdapter.java b/core/src/org/transdroid/core/gui/rss/RssitemsAdapter.java index c6aa7c0e..e174ff4d 100644 --- a/core/src/org/transdroid/core/gui/rss/RssitemsAdapter.java +++ b/core/src/org/transdroid/core/gui/rss/RssitemsAdapter.java @@ -1,11 +1,9 @@ package org.transdroid.core.gui.rss; -import java.util.Date; - import org.androidannotations.annotations.EBean; import org.androidannotations.annotations.RootContext; -import org.ifies.android.sax.Channel; -import org.ifies.android.sax.Item; +import org.transdroid.core.rssparser.Channel; +import org.transdroid.core.rssparser.Item; import android.content.Context; import android.view.View; @@ -20,7 +18,6 @@ import android.widget.BaseAdapter; public class RssitemsAdapter extends BaseAdapter { private Channel rssfeed = null; - private Date lastViewedItem; @RootContext protected Context context; @@ -34,16 +31,6 @@ public class RssitemsAdapter extends BaseAdapter { notifyDataSetChanged(); } - /** - * Registers the date that the user last viewed this feed. Any RSS items after this date will be visually marked as - * new. - * @param lastViewedItem The date after which RSS items should be marked as new - */ - public void setLastItemViewed(Date lastViewedItem) { - this.lastViewedItem = lastViewedItem; - notifyDataSetChanged(); - } - @Override public boolean hasStableIds() { return true; @@ -76,7 +63,7 @@ public class RssitemsAdapter extends BaseAdapter { } else { rssitemView = (RssitemView) convertView; } - rssitemView.bind(getItem(position), lastViewedItem); + rssitemView.bind(getItem(position)); return rssitemView; } diff --git a/core/src/org/transdroid/core/gui/rss/RssitemsFragment.java b/core/src/org/transdroid/core/gui/rss/RssitemsFragment.java index 04894f10..80f32efb 100644 --- a/core/src/org/transdroid/core/gui/rss/RssitemsFragment.java +++ b/core/src/org/transdroid/core/gui/rss/RssitemsFragment.java @@ -1,14 +1,11 @@ package org.transdroid.core.gui.rss; -import java.util.Date; - import org.androidannotations.annotations.AfterViews; import org.androidannotations.annotations.Bean; import org.androidannotations.annotations.EFragment; -import org.androidannotations.annotations.FragmentArg; import org.androidannotations.annotations.InstanceState; import org.androidannotations.annotations.ViewById; -import org.ifies.android.sax.Channel; +import org.transdroid.core.rssparser.Channel; import android.widget.TextView; @@ -22,12 +19,8 @@ import com.actionbarsherlock.view.SherlockListView; @EFragment(resName = "fragment_rssitems") public class RssitemsFragment extends SherlockFragment { - @FragmentArg - @InstanceState - protected Channel rssfeed; - @FragmentArg @InstanceState - protected Date lastViewedItem; + protected Channel rssfeed = null; // Views @ViewById(resName = "rssfeeds_list") @@ -39,11 +32,17 @@ public class RssitemsFragment extends SherlockFragment { @AfterViews protected void init() { - rssitemsList.setAdapter(rssitemsAdapter); - rssitemsAdapter.setLastItemViewed(lastViewedItem); - rssitemsAdapter.update(rssfeed); + update(rssfeed); + } + /** + * Update the shown RSS items in the list and the known last view date. This is automatically called when the + * fragment is instantiated by its build, but should be manually called if it was instantiated empty. + * @param rssfeed The RSS feed Channel object that was retrieved + */ + public void update(Channel rssfeed) { + rssitemsAdapter.update(rssfeed); } } diff --git a/core/src/org/transdroid/core/rssparser/Channel.java b/core/src/org/transdroid/core/rssparser/Channel.java new file mode 100644 index 00000000..6da864ef --- /dev/null +++ b/core/src/org/transdroid/core/rssparser/Channel.java @@ -0,0 +1,153 @@ +/* + * Taken from the 'Learning Android' project, released as Public Domain software at + * http://github.com/digitalspaghetti/learning-android and modified heavily for Transdroid + */ +package org.transdroid.core.rssparser; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import android.os.Parcel; +import android.os.Parcelable; + +public class Channel implements Parcelable { + + private int id; + private String title; + private String link; + private String description; + private Date pubDate; + private long lastBuildDate; + private List categories; + private List items; + private String image; + + public Channel() { + this.categories = new ArrayList(); + this.items = new ArrayList(); + } + + public void setId(int id) { + this.id = id; + } + + public int getId() { + return id; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getTitle() { + return title; + } + + public void setLink(String link) { + this.link = link; + } + + public String getLink() { + return link; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getDescription() { + return description; + } + + public void setPubDate(Date date) { + this.pubDate = date; + } + + public Date getPubDate() { + return pubDate; + } + + public void setLastBuildDate(long lastBuildDate) { + this.lastBuildDate = lastBuildDate; + } + + public long getLastBuildDate() { + return lastBuildDate; + } + + public void setCategories(List categories) { + this.categories = categories; + } + + public void addCategory(String category) { + this.categories.add(category); + } + + public List getCategories() { + return categories; + } + + public void setItems(List items) { + this.items = items; + } + + public void addItem(Item item) { + this.items.add(item); + } + + public List getItems() { + return items; + } + + public void setImage(String image) { + this.image = image; + } + + public String getImage() { + return image; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel out, int flags) { + out.writeInt(id); + out.writeString(title); + out.writeString(link); + out.writeString(description); + out.writeLong(pubDate == null ? -1 : pubDate.getTime()); + out.writeLong(lastBuildDate); + out.writeStringList(categories); + out.writeTypedList(items); + out.writeString(image); + } + + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + public Channel createFromParcel(Parcel in) { + return new Channel(in); + } + + public Channel[] newArray(int size) { + return new Channel[size]; + } + }; + + private Channel(Parcel in) { + id = in.readInt(); + title = in.readString(); + link = in.readString(); + description = in.readString(); + long pubDateIn = in.readLong(); + pubDate = pubDateIn == -1 ? null : new Date(pubDateIn); + lastBuildDate = in.readLong(); + categories = new ArrayList(); + in.readTypedList(items, Item.CREATOR); + in.readStringList(categories); + image = in.readString(); + } + +} \ No newline at end of file diff --git a/core/src/org/ifies/android/sax/HttpHelper.java b/core/src/org/transdroid/core/rssparser/HttpHelper.java similarity index 99% rename from core/src/org/ifies/android/sax/HttpHelper.java rename to core/src/org/transdroid/core/rssparser/HttpHelper.java index 529f76af..371a975a 100644 --- a/core/src/org/ifies/android/sax/HttpHelper.java +++ b/core/src/org/transdroid/core/rssparser/HttpHelper.java @@ -16,7 +16,7 @@ * You should have received a copy of the GNU Lesser General Public * License along with Transdroid. If not, see . */ -package org.ifies.android.sax; +package org.transdroid.core.rssparser; import java.io.BufferedReader; import java.io.IOException; diff --git a/core/src/org/transdroid/core/rssparser/Item.java b/core/src/org/transdroid/core/rssparser/Item.java new file mode 100644 index 00000000..8c30ed35 --- /dev/null +++ b/core/src/org/transdroid/core/rssparser/Item.java @@ -0,0 +1,157 @@ +/* + * Taken from the 'Learning Android' project, released as Public Domain software at + * http://github.com/digitalspaghetti/learning-android and modified heavily for Transdroid + */ +package org.transdroid.core.rssparser; + +import java.util.Date; + +import android.os.Parcel; +import android.os.Parcelable; + +public class Item implements Parcelable { + + private int id; + private String title; + private String link; + private String description; + private Date pubDate; + private String enclosureUrl; + private String enclosureType; + private long enclosureLength; + + /** + * isNew is not parsed from the RSS feed but may be set using {@link #setIsNew(boolean)} manually + */ + private boolean isNew; + + public void setId(int id) { + this.id = id; + } + + public int getId() { + return id; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getTitle() { + return this.title; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getDescription() { + return this.description; + } + + public void setLink(String link) { + this.link = link; + } + + public String getLink() { + return this.link; + } + + public void setPubdate(Date pubdate) { + this.pubDate = pubdate; + } + + public Date getPubdate() { + return this.pubDate; + } + + public void setEnclosureUrl(String enclosureUrl) { + this.enclosureUrl = enclosureUrl; + } + + public void setEnclosureLength(long enclosureLength) { + this.enclosureLength = enclosureLength; + } + + public void setEnclosureType(String enclosureType) { + this.enclosureType = enclosureType; + } + + public String getEnclosureUrl() { + return this.enclosureUrl; + } + + public String getEnclosureType() { + return this.enclosureType; + } + + public long getEnclosureLength() { + return this.enclosureLength; + } + + public void setIsNew(boolean isNew) { + this.isNew = isNew; + } + + public boolean isNew() { + return isNew; + } + + /** + * Returns 'the' item link, which preferably is the enclosure url, but otherwise the link (or null if that is empty + * too). + * @return A single link url to be used + */ + public String getTheLink() { + if (this.getEnclosureUrl() != null) { + return this.getEnclosureUrl(); + } else { + return this.getLink(); + } + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel out, int flags) { + out.writeInt(id); + out.writeString(title); + out.writeString(link); + out.writeString(description); + out.writeLong(pubDate == null ? -1 : pubDate.getTime()); + out.writeString(enclosureUrl); + out.writeString(enclosureType); + out.writeLong(enclosureLength); + out.writeInt(isNew? 1: 0); + } + + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + public Item createFromParcel(Parcel in) { + return new Item(in); + } + + public Item[] newArray(int size) { + return new Item[size]; + } + }; + + public Item() { + } + + private Item(Parcel in) { + id = in.readInt(); + title = in.readString(); + link = in.readString(); + description = in.readString(); + long pubDateIn = in.readLong(); + pubDate = pubDateIn == -1 ? null : new Date(pubDateIn); + enclosureUrl = in.readString(); + enclosureType = in.readString(); + enclosureLength = in.readLong(); + isNew = in.readInt() == 1? true: false; + } + +} \ No newline at end of file diff --git a/core/src/org/transdroid/core/rssparser/RssParser.java b/core/src/org/transdroid/core/rssparser/RssParser.java new file mode 100644 index 00000000..ba5d809a --- /dev/null +++ b/core/src/org/transdroid/core/rssparser/RssParser.java @@ -0,0 +1,235 @@ +/* + * Taken from the 'Learning Android' project, released as Public Domain software at + * http://github.com/digitalspaghetti/learning-android and modified heavily for Transdroid + */ +package org.transdroid.core.rssparser; + +import java.io.IOException; +import java.util.Date; + +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.parsers.SAXParser; +import javax.xml.parsers.SAXParserFactory; + +import org.apache.http.HttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.conn.scheme.PlainSocketFactory; +import org.apache.http.conn.scheme.Scheme; +import org.apache.http.conn.scheme.SchemeRegistry; +import org.apache.http.impl.client.DefaultHttpClient; +import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager; +import org.apache.http.params.BasicHttpParams; +import org.apache.http.params.HttpConnectionParams; +import org.apache.http.params.HttpParams; +import org.xml.sax.Attributes; +import org.xml.sax.SAXException; +import org.xml.sax.helpers.DefaultHandler; + +public class RssParser extends DefaultHandler { + + private String urlString; + private Channel channel; + private StringBuilder text; + private Item item; + private boolean imageStatus; + + /** + * The constructor for the RSS parser; call {@link #parse()} to synchronously create an HTTP connection and parse + * the RSS feed contents. The results can be retrieved with {@link #getChannel()}. + * @param url + */ + public RssParser(String url) { + this.urlString = url; + this.text = new StringBuilder(); + } + + /** + * Returns the loaded RSS feed as channel which contains the individual {@link Item}s + * @return A channel object that ocntains the feed details and individual items + */ + public Channel getChannel() { + return this.channel; + } + + /** + * Initialises an HTTP connection, retrieves the content and parses the RSS feed as standard XML. + * @throws ParserConfigurationException Thrown if the SX parser is not working corectly + * @throws SAXException Thrown if the SAX parser can encounters non-standard XML content + * @throws IOException Thrown if the RSS feed content can not be retrieved, such as when no connection is available + */ + public void parse() throws ParserConfigurationException, SAXException, IOException { + + DefaultHttpClient httpclient = initialise(); + HttpResponse result = httpclient.execute(new HttpGet(urlString)); + SAXParserFactory spf = SAXParserFactory.newInstance(); + if (spf != null) { + SAXParser sp = spf.newSAXParser(); + sp.parse(result.getEntity().getContent(), this); + } + + } + + private DefaultHttpClient initialise() { + + SchemeRegistry registry = new SchemeRegistry(); + registry.register(new Scheme("http", new PlainSocketFactory(), 80)); + + HttpParams httpparams = new BasicHttpParams(); + HttpConnectionParams.setConnectionTimeout(httpparams, 5000); + HttpConnectionParams.setSoTimeout(httpparams, 5000); + DefaultHttpClient httpclient = new DefaultHttpClient(new ThreadSafeClientConnManager(httpparams, registry), + httpparams); + + httpclient.addRequestInterceptor(HttpHelper.gzipRequestInterceptor); + httpclient.addResponseInterceptor(HttpHelper.gzipResponseInterceptor); + + return httpclient; + + } + + /** + * By default creates a standard Item (with title, description and links), which may to overridden to add more data + * (i.e. custom tags that a feed may supply). + * @return A possibly decorated Item instance + */ + protected Item createNewItem() { + return new Item(); + } + + @Override + public final void startElement(String uri, String localName, String qName, Attributes attributes) { + + /** First lets check for the channel */ + if (localName.equalsIgnoreCase("channel")) { + this.channel = new Channel(); + } + + /** Now lets check for an item */ + if (localName.equalsIgnoreCase("item") && (this.channel != null)) { + this.item = createNewItem(); + this.channel.addItem(this.item); + } + + /** Now lets check for an image */ + if (localName.equalsIgnoreCase("image") && (this.channel != null)) { + this.imageStatus = true; + } + + /** Checking for a enclosure */ + if (localName.equalsIgnoreCase("enclosure")) { + /** Lets check we are in an item */ + if (this.item != null && attributes != null && attributes.getLength() > 0) { + if (attributes.getValue("url") != null) { + this.item.setEnclosureUrl(attributes.getValue("url").trim()); + } + if (attributes.getValue("type") != null) { + this.item.setEnclosureType(attributes.getValue("type")); + } + if (attributes.getValue("length") != null) { + this.item.setEnclosureLength(Long.parseLong(attributes.getValue("length"))); + } + } + } + + } + + /** + * This is where we actually parse for the elements contents + */ + @SuppressWarnings("deprecation") + public final void endElement(String uri, String localName, String qName) { + /** Check we have an RSS Feed */ + if (this.channel == null) { + return; + } + + /** Check are at the end of an item */ + if (localName.equalsIgnoreCase("item")) { + this.item = null; + } + + /** Check we are at the end of an image */ + if (localName.equalsIgnoreCase("image")) + this.imageStatus = false; + + /** Now we need to parse which title we are in */ + if (localName.equalsIgnoreCase("title")) { + /** We are an item, so we set the item title */ + if (this.item != null) { + this.item.setTitle(this.text.toString().trim()); + /** We are in an image */ + } else { + this.channel.setTitle(this.text.toString().trim()); + } + } + + /** Now we are checking for a link */ + if (localName.equalsIgnoreCase("link")) { + /** Check we are in an item **/ + if (this.item != null) { + this.item.setLink(this.text.toString().trim()); + /** Check we are in an image */ + } else if (this.imageStatus) { + this.channel.setImage(this.text.toString().trim()); + /** Check we are in a channel */ + } else { + this.channel.setLink(this.text.toString().trim()); + } + } + + /** Checking for a description */ + if (localName.equalsIgnoreCase("description")) { + /** Lets check we are in an item */ + if (this.item != null) { + this.item.setDescription(this.text.toString().trim()); + /** Lets check we are in the channel */ + } else { + this.channel.setDescription(this.text.toString().trim()); + } + } + + /** Checking for a pubdate */ + if (localName.equalsIgnoreCase("pubDate")) { + /** Lets check we are in an item */ + if (this.item != null) { + try { + this.item.setPubdate(new Date(Date.parse(this.text.toString().trim()))); + } catch (Exception e) { + // Date is malformed (not parsable by Date.parse) + } + /** Lets check we are in the channel */ + } else { + try { + this.channel.setPubDate(new Date(Date.parse(this.text.toString().trim()))); + } catch (Exception e) { + // Date is malformed (not parsable by Date.parse) + } + } + } + + /** Check for the category */ + if (localName.equalsIgnoreCase("category") && (this.item != null)) { + this.channel.addCategory(this.text.toString().trim()); + } + + addAdditionalData(localName, this.item, this.text.toString()); + + this.text.setLength(0); + } + + /** + * May be overridden to add additional data from tags that are not standard in RSS. Not used by this default RSS + * style parser. Usually used in conjunction with {@link #createNewItem()}. + * @param localName The tag name + * @param item The Item we are currently parsing + * @param text The new text content + */ + protected void addAdditionalData(String localName, Item item, String text) { + } + + @Override + public final void characters(char[] ch, int start, int length) { + this.text.append(ch, start, length); + } + +} \ No newline at end of file