diff --git a/core/libs/universal-image-loader-1.8.4.jar b/core/libs/universal-image-loader-1.8.4.jar new file mode 100644 index 00000000..78369c20 Binary files /dev/null and b/core/libs/universal-image-loader-1.8.4.jar differ diff --git a/core/res/layout/activity_rssfeeds.xml b/core/res/layout/activity_rssfeeds.xml new file mode 100644 index 00000000..2bb1895c --- /dev/null +++ b/core/res/layout/activity_rssfeeds.xml @@ -0,0 +1,16 @@ + + + + + + + \ No newline at end of file diff --git a/core/res/layout/activity_rssitems.xml b/core/res/layout/activity_rssitems.xml new file mode 100644 index 00000000..e47b5d8d --- /dev/null +++ b/core/res/layout/activity_rssitems.xml @@ -0,0 +1,16 @@ + + + + + + + \ No newline at end of file diff --git a/core/res/layout/fragment_rssfeeds.xml b/core/res/layout/fragment_rssfeeds.xml new file mode 100644 index 00000000..2d2d56f6 --- /dev/null +++ b/core/res/layout/fragment_rssfeeds.xml @@ -0,0 +1,28 @@ + + + + + + + + \ No newline at end of file diff --git a/core/res/layout/fragment_rssitems.xml b/core/res/layout/fragment_rssitems.xml new file mode 100644 index 00000000..748bd5d3 --- /dev/null +++ b/core/res/layout/fragment_rssitems.xml @@ -0,0 +1,28 @@ + + + + + + + + \ No newline at end of file diff --git a/core/res/layout/list_item_rssfeed.xml b/core/res/layout/list_item_rssfeed.xml new file mode 100644 index 00000000..00ca102a --- /dev/null +++ b/core/res/layout/list_item_rssfeed.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/core/res/layout/list_item_rssitem.xml b/core/res/layout/list_item_rssitem.xml new file mode 100644 index 00000000..eca6a3e4 --- /dev/null +++ b/core/res/layout/list_item_rssitem.xml @@ -0,0 +1,29 @@ + + + + + + + + \ No newline at end of file diff --git a/core/res/menu/fragment_rssfeeds.xml b/core/res/menu/fragment_rssfeeds.xml new file mode 100644 index 00000000..94b4cfbf --- /dev/null +++ b/core/res/menu/fragment_rssfeeds.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/core/res/values/strings.xml b/core/res/values/strings.xml index a5608379..15043d59 100644 --- a/core/res/values/strings.xml +++ b/core/res/values/strings.xml @@ -118,6 +118,9 @@ The Barcode Scanner could not be found. Would you like to install it from the Play Store? No compatible file manager could not be found. Would you like to install IO File Manager from the Play Store? + You have not defined any RSS feeds yet to monitor. Torrent-specific RSS feeds keep you up to date with new releases and you are notified of new items. + The RSS feed is not available or it contains no items + Servers Add new server Search sites diff --git a/core/src/org/ifies/android/sax/Channel.java b/core/src/org/ifies/android/sax/Channel.java new file mode 100644 index 00000000..019edc7a --- /dev/null +++ b/core/src/org/ifies/android/sax/Channel.java @@ -0,0 +1,149 @@ +/* + * 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.ArrayList; +import java.util.Date; +import java.util.List; + +import android.os.Parcel; +import android.os.Parcelable; + +public class Channel implements Parcelable { + + public Channel() { + setCategories(new ArrayList()); + 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/HttpHelper.java b/core/src/org/ifies/android/sax/HttpHelper.java new file mode 100644 index 00000000..529f76af --- /dev/null +++ b/core/src/org/ifies/android/sax/HttpHelper.java @@ -0,0 +1,150 @@ +/* + * This file is part of Transdroid Torrent Search + * + * + * Transdroid Torrent Search is free software: you can redistribute + * it and/or modify it under the terms of the GNU Lesser General + * Public License as published by the Free Software Foundation, + * either version 3 of the License, or (at your option) any later + * version. + * + * Transdroid Torrent Search is distributed in the hope that it will + * be useful, but WITHOUT ANY WARRANTY; without even the implied + * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with Transdroid. If not, see . + */ +package org.ifies.android.sax; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.UnsupportedEncodingException; +import java.util.zip.GZIPInputStream; + +import org.apache.http.Header; +import org.apache.http.HeaderElement; +import org.apache.http.HttpEntity; +import org.apache.http.HttpException; +import org.apache.http.HttpRequest; +import org.apache.http.HttpRequestInterceptor; +import org.apache.http.HttpResponse; +import org.apache.http.HttpResponseInterceptor; +import org.apache.http.entity.HttpEntityWrapper; +import org.apache.http.protocol.HttpContext; + +/** + * Provides a set of general helper methods that can be used in web-based communication. + * + * @author erickok + * + */ +public class HttpHelper { + + /** + * HTTP request interceptor to allow for GZip-encoded data transfer + */ + public static HttpRequestInterceptor gzipRequestInterceptor = new HttpRequestInterceptor() { + public void process(final HttpRequest request, final HttpContext context) throws HttpException, IOException { + if (!request.containsHeader("Accept-Encoding")) { + request.addHeader("Accept-Encoding", "gzip"); + } + } + }; + + /** + * HTTP response interceptor that decodes GZipped data + */ + public static HttpResponseInterceptor gzipResponseInterceptor = new HttpResponseInterceptor() { + public void process(final HttpResponse response, final HttpContext context) throws HttpException, IOException { + HttpEntity entity = response.getEntity(); + Header ceheader = entity.getContentEncoding(); + if (ceheader != null) { + HeaderElement[] codecs = ceheader.getElements(); + for (int i = 0; i < codecs.length; i++) { + + if (codecs[i].getName().equalsIgnoreCase("gzip")) { + response.setEntity(new HttpHelper.GzipDecompressingEntity(response.getEntity())); + return; + } + } + } + } + + }; + + /** + * HTTP entity wrapper to decompress GZipped HTTP responses + */ + private static class GzipDecompressingEntity extends HttpEntityWrapper { + + public GzipDecompressingEntity(final HttpEntity entity) { + super(entity); + } + + @Override + public InputStream getContent() throws IOException, IllegalStateException { + + // the wrapped entity's getContent() decides about repeatability + InputStream wrappedin = wrappedEntity.getContent(); + + return new GZIPInputStream(wrappedin); + } + + @Override + public long getContentLength() { + // length of ungzipped content is not known + return -1; + } + + } + + /* + * To convert the InputStream to String we use the BufferedReader.readLine() + * method. We iterate until the BufferedReader return null which means + * there's no more data to read. Each line will appended to a StringBuilder + * and returned as String. + * + * Taken from http://senior.ceng.metu.edu.tr/2009/praeda/2009/01/11/a-simple-restful-client-at-android/ + */ + public static String ConvertStreamToString(InputStream is, String encoding) throws UnsupportedEncodingException { + InputStreamReader isr; + if (encoding != null) { + isr = new InputStreamReader(is, encoding); + } else { + isr = new InputStreamReader(is); + } + BufferedReader reader = new BufferedReader(isr); + StringBuilder sb = new StringBuilder(); + + String line = null; + try { + while ((line = reader.readLine()) != null) { + sb.append(line + "\n"); + } + } catch (IOException e) { + e.printStackTrace(); + } finally { + try { + is.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + return sb.toString(); + } + + public static String ConvertStreamToString(InputStream is) { + try { + return ConvertStreamToString(is, null); + } catch (UnsupportedEncodingException e) { + // Since this is going to use the default encoding, it is never going to crash on an UnsupportedEncodingException + e.printStackTrace(); + return null; + } + } + +} \ 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 new file mode 100644 index 00000000..07cd4ee0 --- /dev/null +++ b/core/src/org/ifies/android/sax/Item.java @@ -0,0 +1,146 @@ +/* + * 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 new file mode 100644 index 00000000..91288051 --- /dev/null +++ b/core/src/org/ifies/android/sax/RssParser.java @@ -0,0 +1,237 @@ +/* + * 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/gui/navigation/NavigationHelper.java b/core/src/org/transdroid/core/gui/navigation/NavigationHelper.java index 9c813e61..3505d9e9 100644 --- a/core/src/org/transdroid/core/gui/navigation/NavigationHelper.java +++ b/core/src/org/transdroid/core/gui/navigation/NavigationHelper.java @@ -11,6 +11,15 @@ import android.content.pm.PackageManager.NameNotFoundException; import android.text.Spannable; import android.text.SpannableString; import android.text.style.TypefaceSpan; + +import com.nostra13.universalimageloader.cache.disc.impl.FileCountLimitedDiscCache; +import com.nostra13.universalimageloader.cache.disc.naming.Md5FileNameGenerator; +import com.nostra13.universalimageloader.cache.memory.impl.UsingFreqLimitedMemoryCache; +import com.nostra13.universalimageloader.core.DisplayImageOptions; +import com.nostra13.universalimageloader.core.ImageLoader; +import com.nostra13.universalimageloader.core.ImageLoaderConfiguration.Builder; +import com.nostra13.universalimageloader.core.assist.ImageScaleType; + import de.keyboardsurfer.android.widget.crouton.Crouton; import de.keyboardsurfer.android.widget.crouton.Style; @@ -25,6 +34,7 @@ public class NavigationHelper { @RootContext protected Context context; + private static ImageLoader imageCache; /** * Use with {@link Crouton#showText(android.app.Activity, int, Style)} (and variants) to display error messages. @@ -38,6 +48,24 @@ public class NavigationHelper { public static Style CROUTON_INFO_STYLE = new Style.Builder().setBackgroundColor(R.color.crouton_info) .setTextSize(13).setDuration(1500).build(); + /** + * Returns (and initialises, if needed) an image cache that uses memory and (1MB) local storage. + * @return An image cache that loads web images synchronously and transparently + */ + public ImageLoader getImageCache() { + if (imageCache == null) { + 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)); + imageCache.init(imageCacheBuilder.build()); + } + return imageCache; + } + /** * Whether any search-related UI components should be shown in the interface. At the moment returns false only if we * run as Transdroid Lite version. diff --git a/core/src/org/transdroid/core/gui/rss/RssfeedView.java b/core/src/org/transdroid/core/gui/rss/RssfeedView.java new file mode 100644 index 00000000..694f618f --- /dev/null +++ b/core/src/org/transdroid/core/gui/rss/RssfeedView.java @@ -0,0 +1,63 @@ +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 android.content.Context; +import android.view.View; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.ProgressBar; +import android.widget.TextView; + +/** + * View that represents some {@link RssfeedSetting} object and displays name as well as loads a favicon for the feed's + * site and can load how many new items are available. + * @author Eric Kok + */ +@EViewGroup(resName = "list_item_rssfeed") +public class RssfeedView extends LinearLayout { + + private static final String GETFVO_URL = "http://g.etfv.co/%1$s"; + + @Bean + protected NavigationHelper navigationHelper; + + // Views + @ViewById + protected ImageView faviconImage; + @ViewById + protected TextView nameText, newcountText; + @ViewById + protected ProgressBar loadingProgress; + + public RssfeedView(Context context) { + super(context); + } + + public void bind(RssfeedSetting rssfeed) { + + nameText.setText(rssfeed.getName()); + faviconImage.setImageDrawable(null); + loadingProgress.setVisibility(View.VISIBLE); + newcountText.setVisibility(View.VISIBLE); + + // 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(); + + } + + @Background + protected void refreshNewCount() { + // TODO: Implement + } + +} diff --git a/core/src/org/transdroid/core/gui/rss/RssfeedsActivity.java b/core/src/org/transdroid/core/gui/rss/RssfeedsActivity.java new file mode 100644 index 00000000..2a99bce4 --- /dev/null +++ b/core/src/org/transdroid/core/gui/rss/RssfeedsActivity.java @@ -0,0 +1,23 @@ +package org.transdroid.core.gui.rss; + +import org.androidannotations.annotations.Bean; +import org.androidannotations.annotations.EActivity; +import org.androidannotations.annotations.FragmentById; +import org.transdroid.core.app.settings.ApplicationSettings; + +import com.actionbarsherlock.app.SherlockFragmentActivity; + +@EActivity(resName = "activity_rssfeeds") +public class RssfeedsActivity extends SherlockFragmentActivity { + + // Settings + @Bean + protected ApplicationSettings applicationSettings; + + // Contained feeds and items fragments + @FragmentById(resName = "rssfeeds_list") + protected RssfeedsFragment fragmentFeeds; + @FragmentById(resName = "rssitems_list") + protected RssitemsFragment fragmentItems; + +} diff --git a/core/src/org/transdroid/core/gui/rss/RssfeedsAdapter.java b/core/src/org/transdroid/core/gui/rss/RssfeedsAdapter.java new file mode 100644 index 00000000..6fc3fcaa --- /dev/null +++ b/core/src/org/transdroid/core/gui/rss/RssfeedsAdapter.java @@ -0,0 +1,71 @@ +package org.transdroid.core.gui.rss; + +import java.util.List; + +import org.androidannotations.annotations.EBean; +import org.androidannotations.annotations.RootContext; +import org.transdroid.core.app.settings.RssfeedSetting; + +import android.content.Context; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; + +/** + * Adapter that contains a list of RSS feed settings. + * @author Eric Kok + */ +@EBean +public class RssfeedsAdapter extends BaseAdapter { + + private List rssfeeds = 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 + */ + public void update(List newRssfeeds) { + this.rssfeeds = newRssfeeds; + notifyDataSetChanged(); + } + + @Override + public boolean hasStableIds() { + return true; + } + + @Override + public int getCount() { + if (rssfeeds == null) + return 0; + return rssfeeds.size(); + } + + @Override + public RssfeedSetting getItem(int position) { + if (rssfeeds == null) + return null; + return rssfeeds.get(position); + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + RssfeedView rssfeedView; + if (convertView == null) { + rssfeedView = RssfeedView_.build(context); + } else { + rssfeedView = (RssfeedView) convertView; + } + rssfeedView.bind(getItem(position)); + return rssfeedView; + } + +} diff --git a/core/src/org/transdroid/core/gui/rss/RssfeedsFragment.java b/core/src/org/transdroid/core/gui/rss/RssfeedsFragment.java new file mode 100644 index 00000000..5b0824d2 --- /dev/null +++ b/core/src/org/transdroid/core/gui/rss/RssfeedsFragment.java @@ -0,0 +1,43 @@ +package org.transdroid.core.gui.rss; + +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.widget.TextView; + +import com.actionbarsherlock.app.SherlockFragment; +import com.actionbarsherlock.view.SherlockListView; + +/** + * Fragment lists the RSS feeds the user wants to monitor and, if room, the list of items in a feed in a right pane. + * @author Eric Kok + */ +@EFragment(resName = "fragment_rssfeeds") +@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; + @ViewById + protected TextView nosettingsText; + + @AfterViews + protected void init() { + + feedsList.setAdapter(rssfeedsAdapter); + rssfeedsAdapter.update(applicationSettings.getRssfeedSettings()); + + } + +} diff --git a/core/src/org/transdroid/core/gui/rss/RssitemStatusLayout.java b/core/src/org/transdroid/core/gui/rss/RssitemStatusLayout.java new file mode 100644 index 00000000..7c6ff9a5 --- /dev/null +++ b/core/src/org/transdroid/core/gui/rss/RssitemStatusLayout.java @@ -0,0 +1,63 @@ +package org.transdroid.core.gui.rss; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.RectF; +import android.util.AttributeSet; +import fr.marvinlabs.widget.CheckableRelativeLayout; + +/** + * A relative layout that that is checkable (to be used in a contextual action bar) and shows a coloured bar in the far + * left indicating the view status, that is, if the item is new to the user or was viewed earlier. + * @author Eric Kok + */ +public class RssitemStatusLayout extends CheckableRelativeLayout { + + private final float scale = getContext().getResources().getDisplayMetrics().density; + private final int WIDTH = (int) (6 * scale + 0.5f); + + private Boolean isNew = null; + private final Paint oldPaint = new Paint(); + private final Paint newPaint = new Paint(); + private final RectF fullRect = new RectF(); + + public RssitemStatusLayout(Context context) { + super(context); + initPaints(); + setWillNotDraw(false); + } + + public RssitemStatusLayout(Context context, AttributeSet attrs) { + super(context, attrs); + initPaints(); + setWillNotDraw(false); + } + + private void initPaints() { + oldPaint.setColor(0xFF9E9E9E); // Grey + newPaint.setColor(0xFF8ACC12); // Normal green + } + + public void setIsNew(Boolean isNew) { + this.isNew = isNew; + this.invalidate(); + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + int height = getHeight(); + int width = WIDTH; + fullRect.set(0, 0, width, height); + + if (isNew == null) { + return; + } + + canvas.drawRect(fullRect, isNew? newPaint: oldPaint); + + } + +} diff --git a/core/src/org/transdroid/core/gui/rss/RssitemView.java b/core/src/org/transdroid/core/gui/rss/RssitemView.java new file mode 100644 index 00000000..19299981 --- /dev/null +++ b/core/src/org/transdroid/core/gui/rss/RssitemView.java @@ -0,0 +1,37 @@ +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 android.content.Context; +import android.text.format.DateUtils; +import android.widget.TextView; + +/** + * View that represents some {@link Item} object, which is a single item in some RSS feed. + * @author Eric Kok + */ +@EViewGroup(resName = "list_item_rssitem") +public class RssitemView extends RssitemStatusLayout { + + // Views + @ViewById + protected TextView nameText, dateText; + + public RssitemView(Context context) { + super(context); + } + + public void bind(Item rssitem, Date lastViewedItem) { + + 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)); + + } + +} diff --git a/core/src/org/transdroid/core/gui/rss/RssitemsAdapter.java b/core/src/org/transdroid/core/gui/rss/RssitemsAdapter.java new file mode 100644 index 00000000..c6aa7c0e --- /dev/null +++ b/core/src/org/transdroid/core/gui/rss/RssitemsAdapter.java @@ -0,0 +1,83 @@ +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 android.content.Context; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; + +/** + * Adapter that contains a list of {@link Item}s in an RSS feed. + * @author Eric Kok + */ +@EBean +public class RssitemsAdapter extends BaseAdapter { + + private Channel rssfeed = null; + private Date lastViewedItem; + + @RootContext + protected Context context; + + /** + * Allows updating the full RSS feed (channel and contained items), replacing the old data + * @param newRssfeeds The new RSS feed contents + */ + public void update(Channel rssfeed) { + this.rssfeed = rssfeed; + 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; + } + + @Override + public int getCount() { + if (rssfeed == null) + return 0; + return rssfeed.getItems().size(); + } + + @Override + public Item getItem(int position) { + if (rssfeed == null) + return null; + return rssfeed.getItems().get(position); + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + RssitemView rssitemView; + if (convertView == null) { + rssitemView = RssitemView_.build(context); + } else { + rssitemView = (RssitemView) convertView; + } + rssitemView.bind(getItem(position), lastViewedItem); + return rssitemView; + } + +} diff --git a/core/src/org/transdroid/core/gui/rss/RssitemsFragment.java b/core/src/org/transdroid/core/gui/rss/RssitemsFragment.java new file mode 100644 index 00000000..04894f10 --- /dev/null +++ b/core/src/org/transdroid/core/gui/rss/RssitemsFragment.java @@ -0,0 +1,49 @@ +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 android.widget.TextView; + +import com.actionbarsherlock.app.SherlockFragment; +import com.actionbarsherlock.view.SherlockListView; + +/** + * Fragment that lists the items in a specific RSS feed + * @author Eric Kok + */ +@EFragment(resName = "fragment_rssitems") +public class RssitemsFragment extends SherlockFragment { + + @FragmentArg + @InstanceState + protected Channel rssfeed; + @FragmentArg + @InstanceState + protected Date lastViewedItem; + + // Views + @ViewById(resName = "rssfeeds_list") + protected SherlockListView rssitemsList; + @Bean + protected RssitemsAdapter rssitemsAdapter; + @ViewById + protected TextView emptyText; + + @AfterViews + protected void init() { + + rssitemsList.setAdapter(rssitemsAdapter); + rssitemsAdapter.setLastItemViewed(lastViewedItem); + rssitemsAdapter.update(rssfeed); + + } + +} diff --git a/full/AndroidManifest.xml b/full/AndroidManifest.xml index 95092b1e..536c907c 100644 --- a/full/AndroidManifest.xml +++ b/full/AndroidManifest.xml @@ -91,6 +91,12 @@ android:resource="@xml/searchable" /> + + +