Java tutorial
/* * Copyright 2012 Brendan McCarthy (brendan@oddsoftware.net) * * This file is part of Feedscribe. * * Feedscribe is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 * as published by the Free Software Foundation. * * Feedscribe is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Feedscribe. If not, see <http://www.gnu.org/licenses/>. */ package net.oddsoftware.android.feedscribe.data; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.Reader; import java.io.StringReader; import java.net.MalformedURLException; import java.net.ProxySelector; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.Iterator; import java.util.Locale; import java.util.TimeZone; import java.util.zip.GZIPInputStream; import java.util.zip.InflaterInputStream; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import org.apache.http.Header; import org.apache.http.HttpEntity; import org.apache.http.HttpHost; import org.apache.http.HttpResponse; import org.apache.http.StatusLine; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpUriRequest; import org.apache.http.impl.client.DefaultHttpClient; import org.apache.http.impl.conn.ProxySelectorRoutePlanner; import org.apache.http.protocol.BasicHttpContext; import org.apache.http.protocol.ExecutionContext; import org.apache.http.protocol.HttpContext; import org.htmlcleaner.CleanerProperties; import org.htmlcleaner.ContentNode; import org.htmlcleaner.HtmlCleaner; import org.htmlcleaner.HtmlNode; import org.htmlcleaner.SpecialEntity; import org.htmlcleaner.TagNode; import org.htmlcleaner.TagNodeVisitor; import org.w3c.dom.DOMException; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.xml.sax.SAXException; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlPullParserFactory; import org.xmlpull.v1.XmlSerializer; import android.content.Context; import android.content.pm.PackageInfo; import android.content.pm.PackageManager.NameNotFoundException; import android.os.Environment; import net.oddsoftware.android.feedscribe.*; import net.oddsoftware.android.html.HttpCache; import net.oddsoftware.android.utils.Logger; import net.oddsoftware.android.utils.Utilities; public class FeedManager { private static FeedManager mInstance = null; private FeedDBAdaptor mDB; int mPackageVersion; int mPreviousPackageVersion; public static String USER_AGENT = "Mozilla/5.0 (Linux; U; Android 2.1; en-us; Nexus One Build/ERD62)" + " AppleWebKit/530.17 (KHTML, like Gecko) Version/4.0 Mobile Safari/530.17"; private FeedUpdateListener mFeedUpdateListener = null; private ArrayList<Download> mDownloads; private FeedConfig mFeedConfig = null; private Logger mLog = null; public static final SimpleDateFormat rfc822DateFormats[] = new SimpleDateFormat[] { // Sat, 22 Jan 2011 19:25:00 +1100 new SimpleDateFormat("EEE, d MMM yy HH:mm:ss z", Locale.US), new SimpleDateFormat("EEE, d MMM yy HH:mm z", Locale.US), new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss z", Locale.US), new SimpleDateFormat("EEE, d MMM yyyy HH:mm z", Locale.US), new SimpleDateFormat("d MMM yy HH:mm z", Locale.US), new SimpleDateFormat("d MMM yy HH:mm:ss z", Locale.US), new SimpleDateFormat("d MMM yyyy HH:mm z", Locale.US), new SimpleDateFormat("d MMM yyyy HH:mm:ss z", Locale.US), new SimpleDateFormat("d MMM yyyy HH:mm:ss", Locale.US), // Here is an example of an invalid RFC822 date-time. This is commonly seen in RSS 1.0 feeds generated by older versions of Movable Type: // 2002-10-02T08:00:00-05:00 //new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssz"), }; public static final int MAX_ETAG_LENGTH = 200; public synchronized static FeedManager getInstance(Context ctx) { if (mInstance == null) { mInstance = new FeedManager(ctx); } return mInstance; } public synchronized static void closeInstance() { if (mInstance != null) { mInstance.close(); mInstance = null; } } protected FeedManager(Context ctx) { mDB = new FeedDBAdaptor(ctx); mDB.open(); mFeedConfig = FeedConfig.getInstance(ctx); mLog = Globals.LOG; // try and pull the package version from the package manager mPackageVersion = Globals.VERSION_CODE; mPreviousPackageVersion = 0; try { PackageInfo info = ctx.getPackageManager().getPackageInfo(ctx.getPackageName(), 0); mPackageVersion = info.versionCode; } catch (NameNotFoundException exc) { if (mLog.e()) mLog.e("unable to get package version ", exc); } mDownloads = new ArrayList<Download>(); loadDownloads(); loadConfig(); } public void close() { mDB.close(); } public ArrayList<ShortFeedItem> getShortItems(int feedTypes) { ArrayList<ShortFeedItem> result = new ArrayList<ShortFeedItem>(); ArrayList<Feed> feeds = mDB.getFeeds(feedTypes); for (Feed feed : feeds) { ArrayList<ShortFeedItem> items = mDB.getShortFeedItems(feed.mId, null, false); result.addAll(items); } Collections.sort(result); Collections.reverse(result); return result; } public ArrayList<ShortFeedItem> getShortItems(int feedTypes, String query) { String[] terms = query.split("\\s"); ArrayList<ShortFeedItem> result = new ArrayList<ShortFeedItem>(); ArrayList<Feed> feeds = mDB.getFeeds(feedTypes); for (Feed feed : feeds) { ArrayList<ShortFeedItem> items = mDB.getShortFeedItems(feed.mId, terms, false); result.addAll(items); } Collections.sort(result); Collections.reverse(result); return result; } public ArrayList<ShortFeedItem> getShortItems(long feedId) { ArrayList<ShortFeedItem> result = mDB.getShortFeedItems(feedId, null, false); if (result != null) { Collections.sort(result); Collections.reverse(result); } return result; } public void deDuplicate(ArrayList<ShortFeedItem> items) { int i = 1; while (i < items.size()) { ShortFeedItem cur = items.get(i); ShortFeedItem prev = items.get(i - 1); boolean remove = false; if (cur.mLink.equals(prev.mLink)) { remove = true; } else if (cur.mTitle.equals(prev.mTitle)) { try { URI currentURI = new URI(cur.mLink); URI prevURI = new URI(prev.mLink); if (currentURI.getHost().equals(prevURI.getHost())) { if (mLog.d()) mLog.d("removing duplicate from display because of title and host match"); remove = true; } } catch (URISyntaxException exc) { if (mLog.e()) mLog.e("deDuplicateArray error parsing links", exc); } } if (remove) { if (mLog.d()) mLog.d("removing duplicate from display, link is " + cur.mLink); items.remove(i); } else { ++i; } } } public FeedItem getItemById(long id) { return mDB.getFeedItem(id); } public boolean updateItem(FeedItem item) { return mDB.updateFeedItem(item); } public boolean updateItemFlags(FeedItem item) { return mDB.updateFeedItemFlags(item); } /** @return true if any updates were attempted */ public boolean updateItems(long feedId, boolean forceUpdate, int minIntervalMinutes) { ArrayList<Feed> feeds = mDB.getFeeds(); Date now = new Date(); ArrayList<Feed> newFeeds = new ArrayList<Feed>(); if (forceUpdate) { newFeeds = feeds; } else { for (Feed feed : feeds) { FeedStatus status = mDB.getFeedStatus(feed.mId); if (status == null) { status = new FeedStatus(); status.mFeedId = feed.mId; } if (calculateUpdateTime(status, minIntervalMinutes).before(now)) { newFeeds.add(feed); } } } if (newFeeds.size() == 0) { return false; // no updates } if (mFeedUpdateListener != null) { mFeedUpdateListener.feedUpdateProgress(0, newFeeds.size()); } ArrayList<FeedItem> updatedItems = new ArrayList<FeedItem>(); HttpCache httpCache = new HttpCache(mDB.getContext()); int newItemCount = 0; int feedNumber = 0; for (Feed feed : newFeeds) { if (feedId != 0 && feed.mId != feedId) { continue; } FeedStatus status = mDB.getFeedStatus(feed.mId); if (status == null) { status = new FeedStatus(); status.mFeedId = feed.mId; } ArrayList<FeedItem> feedItems = new ArrayList<FeedItem>(); ArrayList<Enclosure> enclosures = new ArrayList<Enclosure>(); String oldImageURL = feed.mImageURL; downloadFeed(feed, status, feedItems, enclosures); status.mLastHit.setTime(now.getTime()); mDB.updateFeedStatus(status); if (feed.mImageURL != null && !feed.mImageURL.equals(oldImageURL)) { boolean theResult = mDB.updateFeedImageURL(feed); Globals.LOG.d("changing feed image to " + feed.mImageURL + " result " + theResult); } updateFeedImage(feed); ArrayList<ShortFeedItem> dbItems = mDB.getShortFeedItems(feed.mId, null, true); // search for duplicates, add any new items for (FeedItem newItem : feedItems) { newItem.mFeedId = feed.mId; boolean needUpdate = false; boolean found = false; ShortFeedItem duplicate = findDuplicate(newItem, dbItems); if (duplicate != null) { found = true; newItem.mId = duplicate.mId; // if the new item is newer than the newest duplicate if (newItem.mPubDate.getTime() > duplicate.mPubDate) { needUpdate = true; } } if (found && needUpdate) { // TODO - why not this //if (db.updateFeedItem(newItem)) } else if (!found) { if (mLog.d()) mLog.d("hit a new feed item guid " + newItem.mGUID + " link " + newItem.mLink + " inserting into db"); // clean html description if (newItem.mCleanDescription.length() > 0) { newItem.mCleanDescription = cleanDescription(newItem.mCleanDescription); } else { newItem.mCleanDescription = cleanDescription(newItem.mDescription); } if (newItem.mPubDate.getTime() == 0) { newItem.mPubDate = new Date(); } newItem.mCleanTitle = cleanDescription(newItem.mTitle); newItem.mCleanDescription = removeTitleFromDescription(newItem.mCleanDescription, newItem.mCleanTitle); if (mDB.updateFeedItem(newItem)) { newItemCount += 1; dbItems.add(new ShortFeedItem(newItem.mId, newItem.mLink, newItem.mPubDate.getTime(), newItem.mTitle, newItem.mEnclosureURL, newItem.mGUID, newItem.mFlags)); if (newItem.mEnclosure != null) { newItem.mEnclosure.mItemId = newItem.mId; mDB.updateEnclosure(newItem.mEnclosure); } } updatedItems.add(newItem); } } if (!feedItems.isEmpty()) { // find everything in dbitems that is marked as deleted, and check against feed items // if it's not there, really delete it for (Iterator<ShortFeedItem> i = dbItems.iterator(); i.hasNext();) { ShortFeedItem item = i.next(); if ((item.mFlags & FeedItem.FLAG_DELETED) != 0) { if (findDuplicate(item, feedItems) == null) { if (mLog.d()) mLog.d("really deleting feed item " + item.mId + " from feed " + item.mFeedId); mDB.deleteFeedItem(item.mId); i.remove(); } } } } else { mLog.d("skipping delete items because we didn't get any feed items at all"); } FeedSettings feedSettings = getFeedSettings(feed.mId); if (feedSettings != null && feedSettings.mDisplayFullArticle) { for (ShortFeedItem feedItem : dbItems) { if (((feedItem.mFlags & FeedItem.FLAG_DELETED) == 0) && ((feedItem.mFlags & FeedItem.FLAG_READ) == 0)) { httpCache.seed(feedItem.mLink); } } } if (mFeedUpdateListener != null) { mFeedUpdateListener.feedUpdateProgress(feedNumber, newFeeds.size()); } feedNumber++; } httpCache.maintainCache(); mFeedConfig.addNewItemCount(newItemCount); // // figure out if there are any images to download // ArrayList<String> imageURLS = new ArrayList<String>(); // for(FeedItem item: updatedItems) // { // if( item.mImageURL.length() > 0 ) // { // imageURLS.add(item.mImageURL); // } // } // updatedItems.clear(); // // // then download them // for(String imageURL: imageURLS) // { // if (Globals.LOGGING) Log.d(Globals.LOG_TAG, "downloading an image " + imageURL); // // downloadImage(imageURL, false); // // if( mFeedUpdateListener != null ) // { // mFeedUpdateListener.feedUpdateProgress(feedNumber, newFeeds.size() + imageURLS.size() ); // } // feedNumber++; // } // // // mDB.expireImages(); // for now ignore the above and just delete any existing images, until we figure out how to cache them better mDB.deleteOlderImages(new Date().getTime()); return true; // updates were tried } private void updateFeedImage(Feed feed) { if (feed.mImageURL == null || feed.mImageURL.length() == 0) { return; } Globals.LOG.d("updateFeedImage - downloading image url " + feed.mImageURL + " for feed " + feed.mURL); downloadImage(feed.mImageURL, true); } private ShortFeedItem findDuplicate(FeedItem newItem, ArrayList<ShortFeedItem> items) { for (ShortFeedItem item : items) { if (newItem.mGUID.length() != 0 && newItem.mGUID.equals(item.mGUID)) { if (mLog.v()) mLog.v("direct hit"); // direct hit return item; } // next we check the pub date and title for an exact match if (newItem.mPubDate.getTime() == item.mPubDate && newItem.mTitle.equals(item.mTitle)) { // fuzzy match but it will do mLog.d("fuzzy match on title and pub date"); return item; } } return null; } private FeedItem findDuplicate(ShortFeedItem newItem, ArrayList<FeedItem> items) { for (FeedItem item : items) { if (newItem.mGUID.length() != 0 && newItem.mGUID.equals(item.mGUID)) { if (mLog.v()) mLog.v("direct hit"); // direct hit return item; } // next we check the pub date and title for an exact match if (newItem.mPubDate == item.mPubDate.getTime() && newItem.mTitle.equals(item.mTitle)) { // fuzzy match but it will do mLog.d("fuzzy match on title and pub date"); return item; } } return null; } protected Date calculateUpdateTime(FeedStatus feedStatus, int minIntervalMinutes) { int ttl = feedStatus.mTTL; if (ttl > 0) { if (ttl < minIntervalMinutes) { ttl = minIntervalMinutes; } // ignore ttls below 5 minutes if (ttl < 5) { ttl = 5; } // ignore ttls above 1 day else if (ttl > (24 * 60)) { ttl = 24 * 60; } } else { ttl = 60; } if (mLog.d()) mLog.d("set ttl from " + feedStatus.mTTL + " to " + ttl); Date updateTime = new Date(); updateTime.setTime(feedStatus.mLastHit.getTime() + ttl * 60000); return updateTime; } void downloadImage(String address, boolean persistant) { if (mDB.hasImage(address)) { mDB.updateImageTime(address, new Date().getTime()); return; } try { // use apache http client lib to set parameters from feedStatus DefaultHttpClient client = new DefaultHttpClient(); // set up proxy handler ProxySelectorRoutePlanner routePlanner = new ProxySelectorRoutePlanner( client.getConnectionManager().getSchemeRegistry(), ProxySelector.getDefault()); client.setRoutePlanner(routePlanner); HttpGet request = new HttpGet(address); request.setHeader("User-Agent", USER_AGENT); HttpResponse response = client.execute(request); StatusLine status = response.getStatusLine(); HttpEntity entity = response.getEntity(); if (entity != null && status.getStatusCode() == 200) { InputStream inputStream = entity.getContent(); // TODO - parse content-length here ByteArrayOutputStream data = new ByteArrayOutputStream(); byte bytes[] = new byte[512]; int count; while ((count = inputStream.read(bytes)) > 0) { data.write(bytes, 0, count); } if (data.size() > 0) { mDB.insertImage(address, new Date().getTime(), persistant, data.toByteArray()); } } } catch (IOException exc) { if (mLog.e()) mLog.e("error downloading image" + address, exc); } } void downloadFeed(Feed feed, FeedStatus feedStatus, ArrayList<FeedItem> feedItems, ArrayList<Enclosure> enclosures) { if (feed.mURL.startsWith("http")) { downloadFeedHttp(feed, feedStatus, feedItems, enclosures); } } void downloadFeedHttp(Feed feed, FeedStatus feedStatus, ArrayList<FeedItem> feedItems, ArrayList<Enclosure> enclosures) { try { // use apache http client lib to set parameters from feedStatus DefaultHttpClient client = new DefaultHttpClient(); // set up proxy handler ProxySelectorRoutePlanner routePlanner = new ProxySelectorRoutePlanner( client.getConnectionManager().getSchemeRegistry(), ProxySelector.getDefault()); client.setRoutePlanner(routePlanner); HttpGet request = new HttpGet(feed.mURL); HttpContext httpContext = new BasicHttpContext(); request.setHeader("User-Agent", USER_AGENT); // send etag if we have it if (feedStatus.mETag.length() > 0) { request.setHeader("If-None-Match", feedStatus.mETag); } // send If-Modified-Since if we have it if (feedStatus.mLastModified.getTime() > 0) { SimpleDateFormat dateFormat = new SimpleDateFormat("EEE', 'dd' 'MMM' 'yyyy' 'HH:mm:ss' GMT'", Locale.US); dateFormat.setTimeZone(TimeZone.getTimeZone("GMT")); String formattedTime = dateFormat.format(feedStatus.mLastModified); // If-Modified-Since: Sat, 29 Oct 1994 19:43:31 GMT request.setHeader("If-Modified-Since", formattedTime); } request.setHeader("Accept-Encoding", "gzip,deflate"); HttpResponse response = client.execute(request, httpContext); if (mLog.d()) mLog.d("http request: " + feed.mURL); if (mLog.d()) mLog.d("http response code: " + response.getStatusLine()); InputStream inputStream = null; StatusLine status = response.getStatusLine(); HttpEntity entity = response.getEntity(); if (entity != null) { inputStream = entity.getContent(); } try { if (entity != null && status.getStatusCode() == 200) { Header encodingHeader = entity.getContentEncoding(); if (encodingHeader != null) { if (encodingHeader.getValue().equalsIgnoreCase("gzip")) { inputStream = new GZIPInputStream(inputStream); } else if (encodingHeader.getValue().equalsIgnoreCase("deflate")) { inputStream = new InflaterInputStream(inputStream); } } // remove caching attributes to be replaced with new ones feedStatus.mETag = ""; feedStatus.mLastModified.setTime(0); feedStatus.mTTL = 0; boolean success = parseFeed(inputStream, feed, feedStatus, feedItems, enclosures); if (success) { // if the parse was ok, update these attributes // ETag: "6050003-78e5-4981d775e87c0" Header etagHeader = response.getFirstHeader("ETag"); if (etagHeader != null) { if (etagHeader.getValue().length() < MAX_ETAG_LENGTH) { feedStatus.mETag = etagHeader.getValue(); } else { mLog.e("etag length was too big: " + etagHeader.getValue().length()); } } // Last-Modified: Fri, 24 Dec 2010 00:57:11 GMT Header lastModifiedHeader = response.getFirstHeader("Last-Modified"); if (lastModifiedHeader != null) { try { feedStatus.mLastModified = parseRFC822Date(lastModifiedHeader.getValue()); } catch (ParseException exc) { mLog.e("unable to parse date", exc); } } HttpUriRequest currentReq = (HttpUriRequest) httpContext .getAttribute(ExecutionContext.HTTP_REQUEST); HttpHost currentHost = (HttpHost) httpContext .getAttribute(ExecutionContext.HTTP_TARGET_HOST); String currentUrl = currentHost.toURI() + currentReq.getURI(); mLog.w("loaded redirect from " + request.getURI().toString() + " to " + currentUrl); feedStatus.mLastURL = currentUrl; } } else { if (status.getStatusCode() == 304) { mLog.d("received 304 not modified"); } } } finally { if (inputStream != null) { inputStream.close(); } } } catch (IOException exc) { mLog.e("error downloading feed " + feed.mURL, exc); } } public static Date parseRFC822Date(String str) throws ParseException { for (SimpleDateFormat dateFormat : rfc822DateFormats) { try { return dateFormat.parse(str); } catch (ParseException exc) { } } throw new ParseException("unable to match any rfc822 date to:" + str, 0); } public static Date parseRFC3339Date(String datestring) throws ParseException { Date d = new Date(); //if there is no time zone, we don't need to do any special parsing. if (datestring.endsWith("Z")) { try { SimpleDateFormat s = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US);//spec for RFC3339 d = s.parse(datestring); } catch (ParseException pe) { //try again with optional decimals SimpleDateFormat s = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'", Locale.US);//spec for RFC3339 (with fractional seconds) s.setLenient(true); d = s.parse(datestring); } return d; } //step one, split off the timezone. int zoneMarker = Math.max(datestring.lastIndexOf('-'), datestring.lastIndexOf('+')); String firstpart = datestring.substring(0, zoneMarker); String secondpart = datestring.substring(zoneMarker); //step two, remove the colon from the timezone offset secondpart = secondpart.substring(0, secondpart.indexOf(':')) + secondpart.substring(secondpart.indexOf(':') + 1); datestring = firstpart + secondpart; SimpleDateFormat s = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.US);//spec for RFC3339 try { d = s.parse(datestring); } catch (java.text.ParseException pe) { //try again with optional decimals s = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSSZ", Locale.US);//spec for RFC3339 (with fractional seconds) s.setLenient(true); d = s.parse(datestring); } return d; } private boolean parseFeed(InputStream is, Feed feed, FeedStatus feedStatus, ArrayList<FeedItem> feedItems, ArrayList<Enclosure> enclosures) { try { DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance(); DocumentBuilder dBuilder = dbFactory.newDocumentBuilder(); Document doc = dBuilder.parse(is); parseAsRss(doc, feed, feedStatus, feedItems, enclosures); parseAsAtom(doc, feed, feedStatus, feedItems, enclosures); return true; } catch (ParserConfigurationException exc) { mLog.e("error parsing rss", exc); } catch (SAXException exc) { mLog.e("error parsing rss", exc); } catch (DOMException exc) { mLog.e("error parsing rss", exc); } catch (IOException exc) { mLog.e("error parsing rss", exc); } return false; } private void parseAsRss(Document doc, Feed feed, FeedStatus feedStatus, ArrayList<FeedItem> feedItems, ArrayList<Enclosure> enclosures) { // parse all 'item' elements NodeList nl = doc.getElementsByTagName("item"); for (int i = 0; i < nl.getLength(); i++) { FeedItem feedItem = new FeedItem(); Node node = nl.item(i); if (node.getNodeType() == Node.ELEMENT_NODE) { Element eElement = (Element) node; feedItem.mTitle = extractValue(eElement, "title"); feedItem.mLink = extractValue(eElement, "link"); feedItem.mGUID = extractValue(eElement, "guid"); feedItem.mAuthor = extractValue(eElement, "author"); feedItem.mDescription = extractValue(eElement, "description"); feedItem.mOriginalLink = extractValue(eElement, "feedburner:origLink"); feedItem.mImageURL = extractAttribute(eElement, "media:thumbnail", "url"); if (feedItem.mAuthor.length() == 0) { feedItem.mAuthor = extractValue(eElement, "dc:creator"); } Date pubDate = new Date(); pubDate.setTime(0); String pubDateString = extractValue(eElement, "pubDate"); try { pubDate = parseRFC822Date(pubDateString); } catch (ParseException exc) { mLog.e("unable to parse item pubdate:" + pubDateString); } feedItem.mPubDate = pubDate; NodeList enclosuresList = eElement.getElementsByTagName("enclosure"); if (enclosuresList != null && enclosuresList.getLength() > 0) { NamedNodeMap enclosureAttributes = enclosuresList.item(0).getAttributes(); if (enclosureAttributes != null) { Enclosure enclosure = new Enclosure(); Node enclosureURL = enclosureAttributes.getNamedItem("url"); Node enclosureLength = enclosureAttributes.getNamedItem("length"); Node enclosureType = enclosureAttributes.getNamedItem("type"); if (enclosureURL != null) { enclosure.mURL = enclosureURL.getNodeValue(); } if (enclosureLength != null) { try { enclosure.mLength = Long.parseLong(enclosureLength.getNodeValue()); } catch (NumberFormatException exc) { mLog.e("error parsing enclosure length", exc); } } if (enclosureType != null) { enclosure.mContentType = enclosureType.getNodeValue(); } String duration = extractValue(eElement, "itunes:duration"); if (duration != null && duration.length() > 0) { enclosure.mDuration = Utilities.parseDuration(duration) * 1000; } duration = extractAttribute(eElement, "media:content", "duration"); if (duration != null && duration.length() > 0) { enclosure.mDuration = Utilities.parseDuration(duration) * 1000; } duration = extractValue(eElement, "blip:runtime"); if (duration != null && duration.length() > 0) { enclosure.mDuration = Utilities.parseDuration(duration) * 1000; } // TODO - find a better way to do this // nuke image enclosures for now if (enclosure.mContentType.startsWith("image/")) { enclosure.mURL = ""; } if (enclosure.mURL.length() > 0) { try { URL url = new URL(enclosure.mURL); feedItem.mEnclosureURL = url.toExternalForm(); enclosure.mURL = url.toExternalForm(); feedItem.mEnclosure = enclosure; enclosures.add(enclosure); } catch (MalformedURLException exc) { mLog.e("error parsing enclosure url", exc); } } } } feedItems.add(feedItem); } } // parse all 'channel' elements nl = doc.getElementsByTagName("channel"); for (int i = 0; i < nl.getLength(); i++) { Node node = nl.item(i); if (node.getNodeType() == Node.ELEMENT_NODE) { // extract ttl Element eElement = (Element) node; String ttlString = extractValue(eElement, "ttl"); if (ttlString.length() > 0) { try { feedStatus.mTTL = Integer.parseInt(ttlString); } catch (NumberFormatException exc) { mLog.e("error parsing ttl: " + ttlString, exc); } } if (feed != null) { String title = extractValue(eElement, "title"); String link = extractValue(eElement, "link"); String description = extractValue(eElement, "description"); feed.mName = title; feed.mLink = link; feed.mDescription = description; Node imageNode = eElement.getElementsByTagName("image").item(0); if (imageNode != null && imageNode.getNodeType() == Node.ELEMENT_NODE) { String imageUrl = extractValue((Element) imageNode, "url"); feed.mImageURL = imageUrl; } } } } } private void parseAsAtom(Document doc, Feed feed, FeedStatus feedStatus, ArrayList<FeedItem> feedItems, ArrayList<Enclosure> enclosures) { // parse all 'item' elements NodeList nl = doc.getElementsByTagName("entry"); for (int i = 0; i < nl.getLength(); i++) { FeedItem feedItem = new FeedItem(); Node node = nl.item(i); if (node.getNodeType() == Node.ELEMENT_NODE) { Element eElement = (Element) node; feedItem.mTitle = extractValue(eElement, "title"); feedItem.mGUID = extractValue(eElement, "id"); feedItem.mCleanDescription = extractValue(eElement, "summary"); feedItem.mDescription = extractValue(eElement, "content"); feedItem.mOriginalLink = extractValue(eElement, "feedburner:origLink"); feedItem.mImageURL = extractAttribute(eElement, "media:thumbnail", "url"); NodeList authorNodes = eElement.getElementsByTagName("author"); for (int j = 0; j < authorNodes.getLength(); ++j) { if (authorNodes.item(j).getNodeType() == Node.ELEMENT_NODE) { feedItem.mAuthor = extractValue((Element) (authorNodes.item(j)), "name"); if (feedItem.mAuthor.length() > 0) { break; } } } Date pubDate = new Date(); pubDate.setTime(0); String pubDateString = extractValue(eElement, "updated"); try { pubDate = parseRFC3339Date(pubDateString); } catch (ParseException exc) { mLog.e("unable to parse item pubdate:" + pubDateString); } feedItem.mPubDate = pubDate; NodeList linksList = eElement.getElementsByTagName("link"); if (linksList != null) { for (int j = 0; j < linksList.getLength(); ++j) { NamedNodeMap linkAttributes = linksList.item(j).getAttributes(); Node relNode = linkAttributes.getNamedItem("rel"); if (relNode == null) { continue; } String rel = relNode.getNodeValue(); if (rel.equals("alternate") && feedItem.mLink.length() == 0) { feedItem.mLink = extractAttribute(eElement, "link", "href"); } else if (rel.equals("enclosure")) { Enclosure enclosure = new Enclosure(); Node enclosureURL = linkAttributes.getNamedItem("href"); Node enclosureLength = linkAttributes.getNamedItem("length"); Node enclosureType = linkAttributes.getNamedItem("type"); if (enclosureURL != null) { enclosure.mURL = enclosureURL.getNodeValue(); } if (enclosureLength != null) { try { enclosure.mLength = Long.parseLong(enclosureLength.getNodeValue()); } catch (NumberFormatException exc) { mLog.e("error parsing enclosure length", exc); } } if (enclosureType != null) { enclosure.mContentType = enclosureType.getNodeValue(); } // TODO - find a better way to do this // nuke image enclosures for now if (enclosure.mContentType.startsWith("image/")) { enclosure.mURL = ""; } if (enclosure.mURL.length() > 0) { try { URL url = new URL(enclosure.mURL); feedItem.mEnclosureURL = url.toExternalForm(); enclosure.mURL = url.toExternalForm(); feedItem.mEnclosure = enclosure; enclosures.add(enclosure); } catch (MalformedURLException exc) { mLog.e("error parsing enclosure url", exc); } } } } } if (feedItem.mLink.length() > 0) { feedItems.add(feedItem); } } } // proccess all entries // parse all 'feed' elements nl = doc.getElementsByTagName("feed"); for (int i = 0; i < nl.getLength(); i++) { Node node = nl.item(i); if (node.getNodeType() == Node.ELEMENT_NODE) { // extract ttl Element eElement = (Element) node; if (feed != null) { String title = extractValue(eElement, "title"); String link = extractAttribute(eElement, "link", "href"); String description = extractValue(eElement, "subtitle"); feed.mName = title; feed.mLink = link; feed.mDescription = description; feed.mImageURL = extractValue(eElement, "icon"); } } feedStatus.mTTL = 10; } } private String extractValue(Element eElement, String tag) { StringBuilder builder = new StringBuilder(); NodeList nlList = eElement.getElementsByTagName(tag); if (nlList.getLength() > 0) { nlList = nlList.item(0).getChildNodes(); for (int j = 0; j < nlList.getLength(); ++j) { Node nValue = (Node) nlList.item(j); builder.append(nValue.getNodeValue()); } deSpace(builder); } return builder.toString(); } private String extractAttribute(Element root, String tagName, String attributeName) { StringBuffer buffer = new StringBuffer(); NodeList nlList = root.getElementsByTagName(tagName); if (nlList.getLength() > 0) { NamedNodeMap attributes = nlList.item(0).getAttributes(); Node nValue = attributes.getNamedItem(attributeName); if (nValue != null) { buffer.append(nValue.getNodeValue()); } } return buffer.toString(); } private void deSpace(StringBuilder builder) { // remove whitespace from front while (builder.length() > 0) { if (Character.isWhitespace(builder.charAt(0))) { builder.deleteCharAt(0); } else { break; } } // remove whitespace from back while (builder.length() > 0) { int j = builder.length() - 1; if (Character.isWhitespace(builder.charAt(j))) { builder.deleteCharAt(j); } else { break; } } // remove duplicate spaces // TODO - detect any whitespace type boolean previousWasSpace = false; for (int j = 0; j < builder.length();) { char c = builder.charAt(j); if (previousWasSpace && c == ' ') { builder.deleteCharAt(j); } else { ++j; previousWasSpace = c == ' '; } } } public String cleanDescription(TagNode node) { final StringBuilder description = new StringBuilder(); node.traverse(new TagNodeVisitor() { @Override public boolean visit(TagNode tagNode, HtmlNode htmlNode) { if (htmlNode instanceof ContentNode) { ContentNode contentNode = (ContentNode) htmlNode; htmlUnescapeInto(contentNode.getContent(), description); } return true; } }); return description.toString().trim(); } public String cleanDescription(String input) { HtmlCleaner cleaner = new HtmlCleaner(); CleanerProperties props = cleaner.getProperties(); props.setOmitComments(true); TagNode node = cleaner.clean(input); return cleanDescription(node); } public String removeTitleFromDescription(String description, String title) { if (description.startsWith(title)) { description = description.substring(title.length(), description.length()).trim(); } return description; } public void loadConfig() { mPreviousPackageVersion = mFeedConfig.getPreviousPackageVersion(mPreviousPackageVersion); } public void setFeedUpdateListener(FeedUpdateListener listener) { mFeedUpdateListener = listener; } public boolean isFirstRun() { return mPreviousPackageVersion <= Globals.PREVIOUS_VERSION_CODE; } public void clearFirstRun() { if (mPreviousPackageVersion != mPackageVersion) { mPreviousPackageVersion = mPackageVersion; mFeedConfig.setPreviousPackageVersion(mPreviousPackageVersion); } } public byte[] getImage(String imageURL) { return mDB.getImage(imageURL); } public Feed getFeed(long feedId) { return mDB.getFeed(feedId); } public Feed getFeedByItemId(long itemId) { return mDB.getFeedByItemId(itemId); } public void htmlUnescapeInto(StringBuilder source, StringBuilder dest) { boolean inEntity = false; StringBuilder entity = new StringBuilder(10); int sourceLength = source.length(); for (int i = 0; i < sourceLength; ++i) { char c = source.charAt(i); if (inEntity) { if (c == ';') { // first see if this is a special sequence SpecialEntity special = org.htmlcleaner.SpecialEntity.getEntity(entity.toString()); if (special != null) { dest.append(special.getCharacter()); } // next try hex starting with #x else if (entity.length() > 1 && entity.charAt(0) == '#' && entity.charAt(1) == 'x') { int value = 0; for (int j = 2; j < entity.length(); ++j) { value *= 16; char e = entity.charAt(j); if (e >= '0' && e <= '9') { value += (int) (e - '0'); } else if (e >= 'a' && e <= 'f') { value += 10 + (int) (e - 'a'); } else if (e >= 'A' && e <= 'F') { value += 10 + (int) (e - 'A'); } else { value = 0; break; } } if (value != 0) { dest.append((char) value); } } // next try decimal starting with # else if (entity.length() > 0 && entity.charAt(0) == '#') { int value = 0; for (int j = 1; j < entity.length(); ++j) { value *= 10; char e = entity.charAt(j); if (e >= '0' && e <= '9') { value += (int) (e - '0'); } else { value = 0; break; } } if (value != 0) { dest.append((char) value); } } inEntity = false; entity.setLength(0); } else { entity.append(c); } } else { if (c == '&') { inEntity = true; } else { dest.append(c); } } } } public boolean addFeed(URL url, String name, int feedType) { boolean success = false; Feed feed = new Feed(feedType); feed.mURL = url.toExternalForm(); FeedStatus status = new FeedStatus(); ArrayList<FeedItem> items = new ArrayList<FeedItem>(); ArrayList<Enclosure> enclosures = new ArrayList<Enclosure>(); // TODO - this should be merged with the feed updater // TODO - this should actually check for success downloadFeed(feed, status, items, enclosures); if (name != null) { feed.mName = name; } if (items.size() > 0 && feed.mName.length() > 0) { mLog.w("feed downloaded, adding to db"); if (mDB.addFeed(feed)) { status.mFeedId = feed.mId; mDB.updateFeedStatus(status); mLog.d("status updated, adding " + items.size() + " items"); for (FeedItem item : items) { item.mFeedId = feed.mId; if (item.mCleanDescription.length() > 0) { item.mCleanDescription = cleanDescription(item.mCleanDescription); } else { item.mCleanDescription = cleanDescription(item.mDescription); } item.mCleanTitle = cleanDescription(item.mTitle); item.mCleanDescription = removeTitleFromDescription(item.mCleanDescription, item.mCleanTitle); if (mLog.d()) mLog.d("adding item " + item.mCleanTitle + " enclosure " + item.mEnclosureURL); mDB.updateFeedItem(item); if (item.mEnclosure != null) { item.mEnclosure.mItemId = item.mId; } } for (Enclosure enclosure : enclosures) { mDB.updateEnclosure(enclosure); } updateFeedImage(feed); success = true; } } return success; } public FeedDBAdaptor getDBAdaptor() { return mDB; } public Enclosure getEnclosure(String url) { return mDB.getEnclosure(url); } public Enclosure getEnclosure(FeedItem item) { Enclosure enclosure = mDB.getEnclosureFromItemId(item.mId); if (enclosure != null) { item.mEnclosure = enclosure; } return enclosure; } public boolean createFile(Enclosure enclosure) { try { // track down the feed it belongs to String feedName = null; FeedItem item = mDB.getFeedItem(enclosure.mItemId); if (item != null) { Feed feed = mDB.getFeed(item.mFeedId); if (feed != null) { feedName = feed.mName; } } if (feedName == null) { mLog.e("unabled to find feed for enclosure " + enclosure.mURL); return false; } URL url = new URL(enclosure.mURL); String[] parts = url.getPath().split("/"); String filename = url.getPath(); if (parts.length > 0) { filename = parts[parts.length - 1]; } // TODO - sanitise file name String destinationPath = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + "Podcasts" + File.separator + feedName + File.separator + filename; File destination = new File(destinationPath); for (int i = 1; i <= 100; ++i) { if (destination.exists()) { destination = new File(destinationPath + "." + i); } else { break; } } mLog.e("downloading " + url.toExternalForm() + " to " + destination); File parentFile = destination.getParentFile(); if (!parentFile.isDirectory()) { parentFile.mkdirs(); mLog.e("Tried to create parent directory " + parentFile); } boolean success = false; try { success = destination.createNewFile(); } catch (IOException exc) { mLog.e("Error creating destination file " + destination + " for download", exc); } if (success) { enclosure.mDownloadPath = destination.getAbsolutePath(); } return success; } catch (MalformedURLException exc) { mLog.e("beginDownload - error parsing url", exc); } return false; } public Enclosure getEnclosure(long enclosureId) { return mDB.getEnclosure(enclosureId); } public boolean updateEnclosure(Enclosure enclosure) { return mDB.updateEnclosure(enclosure); } public HashMap<Long, FeedEnclosureInfo> getFeedEnclosureInfo(String enclosureType) { return mDB.getFeedEnclosureInfo(enclosureType); } public HashMap<Long, FeedEnclosureInfo> getFeedsWithoutEnclosuresInfo() { return mDB.getFeedsWithoutEnclosuresInfo(); } public ArrayList<Feed> getFeeds() { return mDB.getFeeds(); } public ArrayList<FeedItemEnclosureInfo> getFeedItemEnclosureInfo(long feedId, String enclosureType) { return mDB.getFeedItemEnclosureInfo(feedId, enclosureType); } @SuppressWarnings("unchecked") public synchronized ArrayList<Download> getDownloads() { return (ArrayList<Download>) mDownloads.clone(); } public synchronized boolean isDownloading(Enclosure enclosure) { for (Download download : mDownloads) { if (download.mEnclosureId == enclosure.mId) { return true; } } return false; } public synchronized void addDownload(FeedItem item, Enclosure enclosure) { if (isDownloading(enclosure)) { return; } Download download = new Download(); download.mId = mDB.addDownload(enclosure.mId); download.mEnclosureId = enclosure.mId; download.mName = item.mCleanTitle; download.mSize = enclosure.mLength; download.mDownloaded = 0; download.mInProgress = false; download.mCancelled = false; mDownloads.add(download); } private synchronized void loadDownloads() { ArrayList<Download> downloads = mDB.getAllDownloads(); for (Download download : downloads) { Enclosure enclosure = mDB.getEnclosure(download.mEnclosureId); if (enclosure == null) { continue; } FeedItem item = mDB.getFeedItem(enclosure.mItemId); if (item == null) { continue; } download.mName = item.mCleanTitle; download.mSize = enclosure.mLength; download.mDownloaded = 0; download.mInProgress = false; mDownloads.add(download); } } public synchronized void downloadComplete(Download download) { mDB.deleteDownload(download.mId); mDownloads.remove(download); } public synchronized boolean deleteDownload(Download download, Enclosure enclosure) { File f = new File(enclosure.mDownloadPath); boolean deleted = f.delete(); mDB.deleteDownload(download.mId); mDownloads.remove(download); return deleted; } public void deleteFeed(Feed feed, boolean deleteDownloads) { if (feed == null) { return; } ArrayList<ShortFeedItem> items = mDB.getShortFeedItems(feed.mId, null, true); for (ShortFeedItem item : items) { Enclosure enclosure = mDB.getEnclosureFromItemId(item.mId); if (enclosure == null) { continue; } for (Download download : mDownloads) { if (download.mEnclosureId == enclosure.mId) { download.mCancelled = true; } } if (enclosure.mDownloadPath.length() > 0 && deleteDownloads) { File f = new File(enclosure.mDownloadPath); boolean deleted = f.delete(); if (!deleted) mLog.e("failed to delete downloaded enclosure " + enclosure.mDownloadPath); // attempt to delete enclosing folder - don't care about result f.getParentFile().delete(); } mDB.deleteDownloadByEnclosure(enclosure.mId); mDB.deleteEnclosure(enclosure.mId); } mDB.deleteFeed(feed.mId); } public boolean deleteFeedItem(FeedItem item) { item.mFlags = item.mFlags | FeedItem.FLAG_DELETED; item.mDescription = ""; item.mCleanDescription = ""; boolean success = mDB.updateFeedItem(item); Enclosure enclosure = getEnclosure(item); if (enclosure != null) { mDB.deleteEnclosure(enclosure.mId); mDB.deleteDownloadByEnclosure(enclosure.mId); if (enclosure.mDownloadPath.length() > 0) { File f = new File(enclosure.mDownloadPath); boolean deleted = f.delete(); if (!deleted) mLog.e("failed to delete downloaded enclosure " + enclosure.mDownloadPath); } } if (!success) mLog.e("failed to delete item id " + item.mId); return success; } public void deleteDownloadedEnclosure(Enclosure enclosure) { mDB.deleteDownloadByEnclosure(enclosure.mId); if (enclosure.mDownloadPath.length() > 0) { File f = new File(enclosure.mDownloadPath); boolean deleted = f.delete(); if (!deleted) mLog.e("failed to delete downloaded enclosure " + enclosure.mDownloadPath); } enclosure.mDownloadPath = ""; enclosure.mDownloadTime = 0; mDB.updateEnclosure(enclosure); } public void deleteFeedItemsRead(long feedId) { long startTime = System.currentTimeMillis(); ArrayList<ShortFeedItem> items = mDB.getShortFeedItems(feedId, null, false); mDB.mDb.beginTransaction(); try { for (ShortFeedItem item : items) { if ((item.mFlags & FeedItem.FLAG_DELETED) == 0 && (item.mFlags & FeedItem.FLAG_READ) != 0 && (item.mFlags & FeedItem.FLAG_STARRED) == 0) { FeedItem fullItem = getItemById(item.mId); deleteFeedItem(fullItem); } } mDB.mDb.setTransactionSuccessful(); } finally { mDB.mDb.endTransaction(); } if (mLog.d()) mLog.d("deleteFeedItemsRead " + items.size() + " took " + (System.currentTimeMillis() - startTime)); } public void setFeedItemsRead(long feedId) { mDB.setFeedItemsRead(feedId); } public void verifyEnclosure(Enclosure enclosure) { boolean changed = false; if (enclosure.mDownloadTime > 0) { File f = new File(enclosure.mDownloadPath); if (!f.exists()) { enclosure.mDownloadTime = 0; changed = true; } } if (changed) { updateEnclosure(enclosure); } } public FeedConfig getFeedConfig() { return mFeedConfig; } public Feed getLocalFeed() { Feed localFeed = mDB.getFeedByURL(Feed.SCHEME_LOCAL + ":/readlater"); if (localFeed == null) { localFeed = new Feed(Feed.TYPE_NEWS); localFeed.mName = "Local Bookmarks"; localFeed.mURL = Feed.SCHEME_LOCAL + ":/readlater"; if (mDB.addFeed(localFeed)) { FeedSettings feedSettings = new FeedSettings(); feedSettings.mFeedId = localFeed.mId; feedSettings.mCacheFullArticle = true; feedSettings.mDisplayFullArticle = true; feedSettings.mCacheImages = true; feedSettings.mTextify = true; feedSettings.mUpdateAutomatically = true; mDB.updateFeedSettings(feedSettings); } } return localFeed; } public void addLocalBookmark(String url) { Feed localFeed = getLocalFeed(); FeedItem item = new FeedItem(); item.mFeedId = localFeed.mId; item.mLink = url; item.mOriginalLink = url; item.mPubDate = new Date(); item.mCleanTitle = url; item.mTitle = url; // TODO - add this article to the download queue and flesh out some details mDB.updateFeedItem(item); } public FeedSettings getFeedSettings(long feedId) { FeedSettings feedSettings = mDB.getFeedSettings(feedId); if (feedSettings == null) { feedSettings = new FeedSettings(); feedSettings.mFeedId = feedId; feedSettings.mCacheFullArticle = true; feedSettings.mDisplayFullArticle = false; feedSettings.mCacheImages = true; feedSettings.mTextify = false; feedSettings.mUpdateAutomatically = true; } return feedSettings; } public void updateFeedSettings(FeedSettings mFeedSettings) { mDB.updateFeedSettings(mFeedSettings); } public String exportOPML() { try { ByteArrayOutputStream os = new ByteArrayOutputStream(16 * 1024); XmlPullParserFactory parserFactory = XmlPullParserFactory.newInstance(); XmlSerializer serializer = parserFactory.newSerializer(); serializer.setOutput(os, "UTF-8"); serializer.startDocument("UTF-8", true); serializer.startTag(null, "opml"); serializer.attribute(null, "version", "1.0"); serializer.startTag(null, "head"); serializer.startTag(null, "title"); serializer.text("FeedScribe Export"); serializer.endTag(null, "title"); serializer.endTag(null, "head"); serializer.startTag(null, "body"); ArrayList<Feed> feeds = mDB.getFeeds(); for (Feed feed : feeds) { serializer.startTag(null, "outline"); serializer.attribute(null, "text", feed.mName); serializer.attribute(null, "title", feed.mName); serializer.attribute(null, "type", "rss"); serializer.attribute(null, "xmlUrl", feed.mURL); if (feed.mLink != null && feed.mLink.length() > 0) { serializer.attribute(null, "htmlUrl", feed.mLink); } serializer.endTag(null, "outline"); } serializer.endTag(null, "body"); serializer.endTag(null, "opml"); serializer.endDocument(); return os.toString(); } catch (IOException exc) { mLog.e("FeedManager.exportOPML", exc); } catch (XmlPullParserException exc) { mLog.e("FeedManager.exportOPML", exc); } return null; } /** * @return -1 for failure, otherwise number of items imported */ public int importOPML(String data) { try { XmlPullParserFactory parserFactory = XmlPullParserFactory.newInstance(); XmlPullParser parser = parserFactory.newPullParser(); parser.setInput(new StringReader(data)); return importOPML(parser); } catch (XmlPullParserException e) { mLog.e("importOPML", e); } return -1; } /** * @return -1 for failure, otherwise number of items imported */ public int importOPML(Reader reader) { try { XmlPullParserFactory parserFactory = XmlPullParserFactory.newInstance(); XmlPullParser parser = parserFactory.newPullParser(); parser.setInput(reader); return importOPML(parser); } catch (XmlPullParserException e) { mLog.e("importOPML", e); } return -1; } /** * * @param parser * @return -1 for failure, otherwise number of items imported */ protected int importOPML(XmlPullParser parser) { try { int eventType = parser.getEventType(); boolean isOPML = false; boolean inBody = false; int numAdded = 0; while (eventType != XmlPullParser.END_DOCUMENT) { if (eventType == XmlPullParser.START_DOCUMENT) { } else if (eventType == XmlPullParser.START_TAG) { String tag = parser.getName(); if ("opml".equals(tag) && "1.0".equals(parser.getAttributeValue(null, "version"))) { isOPML = true; } else if ("body".equals(tag) && isOPML) { inBody = true; } else if ("outline".equals(tag) && inBody && "rss".equals(parser.getAttributeValue(null, "type"))) { String name = parser.getAttributeValue(null, "title"); if (name == null) { name = parser.getAttributeValue(null, "text"); } String url = parser.getAttributeValue(null, "xmlUrl"); if (name != null && url != null) { url = url.toLowerCase(Locale.US); mLog.w("checking for existing feed name " + name + " url " + url); // make sure we don't duplicate any urls boolean found = false; ArrayList<Feed> feeds = getFeeds(); for (Feed feed : feeds) { if (feed.mURL.equals(url)) { found = true; } } if (!found) { try { mLog.w("adding feed name " + name + " url " + url); URL realURL = new URL(url); addFeed(realURL, name, Feed.TYPE_PODCAST); numAdded += 1; } catch (MalformedURLException exc) { mLog.e("importOPML", exc); } } } } } else if (eventType == XmlPullParser.END_TAG) { String tag = parser.getName(); if ("body".equals(tag)) { inBody = false; } } eventType = parser.next(); } if (isOPML) { return numAdded; } else { return -1; } } catch (XmlPullParserException e) { mLog.e("importOPML", e); } catch (IOException e) { mLog.e("importOPML", e); } return -1; } public boolean setFeedName(long feedId, String newName) { return mDB.setFeedName(feedId, newName); } }