ca.luniv.afr.service.FeedRetrieverService.java Source code

Java tutorial

Introduction

Here is the source code for ca.luniv.afr.service.FeedRetrieverService.java

Source

/*
 * $Id$
 *
 * Copyright (C) 2007 James Gilbertson <azurite@telusplanet.net>
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the
 * Free Software Foundation; either version 2, or (at your option) any
 * later version.
 *
 * This program 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.
 */
package ca.luniv.afr.service;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.SocketTimeoutException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.zip.GZIPInputStream;

import org.apache.commons.httpclient.Header;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.http.HttpStatus;

import android.app.Notification;
import android.app.NotificationManager;
import android.app.Service;
import android.content.ContentValues;
import android.database.Cursor;
import android.os.DeadObjectException;
import android.os.IBinder;
import android.util.Log;
import ca.luniv.afr.R;
import ca.luniv.afr.provider.Afr;
import ca.luniv.afr.provider.dao.Entry;
import ca.luniv.afr.provider.dao.Feed;

import com.sun.syndication.feed.synd.SyndContent;
import com.sun.syndication.feed.synd.SyndEntry;
import com.sun.syndication.feed.synd.SyndFeed;
import com.sun.syndication.feed.synd.SyndPerson;
import com.sun.syndication.io.SyndFeedInput;
import com.sun.syndication.io.XmlReader;

public class FeedRetrieverService extends Service {
    private BlockingQueue<Feed> queue;
    private NotificationManager notificationManager;
    private Thread thread;

    private final IFeedRetrieverService.Stub binder = new IFeedRetrieverService.Stub() {
        public void retrieveURI(String uri) throws DeadObjectException {
            Feed feed = new Feed(getContentResolver());
            try {
                feed.setUri(new URI(uri));
            } catch (URISyntaxException e) {
                Log.e("AFR", "IFeedRetrieverService.Stub.retrieveFeed(String): failed to parse '" + uri + "'", e);
                displayErrorNotification(R.string.feed_retriever_err_unspecified,
                        feed.getName() != null ? feed.getName() : feed.getUri().toString());
                return;
            }

            queue.add(feed);
        }

        public void retrieveFeed(long id) throws DeadObjectException {
            Feed feed = new Feed(getContentResolver(), id);
            if (!feed.load()) {
                Log.e("AFR", "IFeedRetrieverService.Stub.retrieveFeed(long): failed to load feed with id=" + id);
                displayErrorNotification(R.string.feed_retriever_err_unspecified_no_details);
                return;
            }

            queue.add(feed);
        }

        public void retrieveAllFeeds() throws DeadObjectException {
            Cursor c = getContentResolver().query(Afr.Feeds.CONTENT_URI, new String[] { Afr.Feeds._ID }, null, null,
                    Afr.Feeds.DEFAULT_SORT_ORDER);
            while (c.next()) {
                retrieveFeed(c.getLong(0));
            }
            c.close();
        }
    };

    private final Runnable retriever = new Runnable() {
        Feed feed;
        File file;
        String contentType;
        SyndFeed parsedFeed;

        public void run() {
            try {
                while (true) {
                    feed = queue.take();

                    // show a notification
                    Notification notification = displayStatusNotification(feed);

                    if (!download(notification) || !parse(notification)) {
                        notificationManager.cancel(R.string.feed_retriever_notification);
                        continue;
                    }

                    save(notification);
                    notificationManager.cancel(R.string.feed_retriever_notification);
                }
            } catch (InterruptedException e) {
                // if we are interrupted, we are going away, so do nothing
            }
        }

        boolean download(Notification notification) {
            // open a HTTP connection and get the headers
            System.setProperty("httpclient.useragent", getString(R.string.app_user_agent_string));
            HttpClient client = new HttpClient();
            // set a 10 second timeout
            client.getParams().setParameter("http.socket.timeout", new Integer(10000));
            GetMethod method = new GetMethod(feed.getUri().toString());

            Log.i("AFR",
                    "FeedRetrieverService.retriever.download(): retrieving feed from: " + feed.getUri().toString());

            try {
                method.setFollowRedirects(true);
                method.addRequestHeader("Accept-Encoding", "gzip");

                // if we have the data for conditional GET, use it
                if (feed.getEtag() != null) {
                    method.addRequestHeader("If-None-Match", feed.getEtag());
                }
                if (feed.getLastModified() != null) {
                    method.addRequestHeader("If-Modified-Since", feed.getLastModified());
                }

                // make the GET request
                if (!handleHttpStatus(client.executeMethod(method))) {
                    return false;
                }

                // get the headers we are interested in
                Header header = method.getResponseHeader("Last-Modified");
                if (header != null) {
                    feed.setLastModified(header.getValue());
                }
                header = method.getResponseHeader("ETag");
                if (header != null) {
                    feed.setEtag(header.getValue());
                }
                header = method.getResponseHeader("Content-Type");
                if (header != null) {
                    contentType = header.getValue();
                }

                // get the response stream
                InputStream in = null;
                if (method.getResponseHeader("Content-Encoding") != null
                        && "gzip".equalsIgnoreCase(method.getResponseHeader("Content-Encoding").getValue())) {
                    in = new GZIPInputStream(method.getResponseBodyAsStream());
                } else {
                    in = method.getResponseBodyAsStream();
                }

                // download the feed
                file = File.createTempFile("afr-feed-", ".xml");
                FileOutputStream out = new FileOutputStream(file);

                long length = method.getResponseContentLength();

                if (length != -1) {
                    String details = getText(R.string.feed_retriever_notification_dl).toString();
                    Log.i("AFR", "FeedRetrieverService.retriever.download(): Content-Length = " + length);

                    // use a 1 KiB buffer so we can update progress
                    byte[] buffer = new byte[1024];
                    float total = (float) length, readSoFar = 0f;
                    for (int read = in.read(buffer); read != -1; read = in.read(buffer)) {
                        out.write(buffer, 0, read);
                        readSoFar += read;
                        notification.statusBarBalloonText = String
                                .format(details, feed.getName() != null ? feed.getName() : feed.getUri().toString(),
                                        readSoFar / total * 100f)
                                .toString();
                    }
                } else {
                    Log.i("AFR", "FeedRetrieverService.retriever.download(): Content-Length not given");
                    // use a 8 KiB buffer
                    byte[] buffer = new byte[1024 * 8];
                    for (int read = in.read(buffer); read != -1; read = in.read(buffer)) {
                        out.write(buffer, 0, read);
                    }
                }

                in.close();
                out.close();
            } catch (UnknownHostException e) {
                Log.e("AFR", "FeedRetrieverService.retriever.download(): failed to retrieve feed: host not found",
                        e);
                String detail = feed.getUri().getScheme() + "://" + feed.getUri().getHost();
                displayErrorNotification(R.string.feed_retriever_err_unknown_host, detail);
                return false;
            } catch (SocketTimeoutException e) {
                Log.e("AFR", "FeedRetrieverService.retriever.download(): failed to retrieve feed: timed out", e);
                String detail = feed.getUri().getScheme() + "://" + feed.getUri().getHost();
                displayErrorNotification(R.string.feed_retriever_err_timed_out, detail);
                return false;
            } catch (IOException e) {
                Log.e("AFR", "FeedRetrieverService.retriever.download(): failed to retrieve feed: I/O error", e);
                displayErrorNotification(R.string.feed_retriever_err_unspecified,
                        feed.getName() != null ? feed.getName() : feed.getUri().toString());
                return false;
            } finally {
                method.releaseConnection();
            }

            return true;
        }

        boolean handleHttpStatus(int statusCode) {
            String detail = feed.getName() != null ? feed.getName() : feed.getUri().toString();
            int error = 0;

            if (statusCode == HttpStatus.SC_NOT_MODIFIED) {
                Log.i("AFR", "FeedRetrieverService.retriever.download(): [" + statusCode + "] feed unchanged");
                // update the feed and quit
                feed.setLastChecked(new Date(System.currentTimeMillis()));
                feed.update();
                return false;
            } else if (statusCode == HttpStatus.SC_FORBIDDEN) {
                Log.e("AFR",
                        "FeedRetrieverService.retriever.download(): [" + statusCode + "] authorization required");
                error = R.string.feed_retriever_err_auth_req;
                // TODO: figure out a way to ask for credentials & retry
            } else if (statusCode == HttpStatus.SC_NOT_FOUND || statusCode == HttpStatus.SC_GONE) {
                Log.e("AFR", "FeedRetrieverService.retriever.download(): [" + statusCode + "] not found");
                error = R.string.feed_retriever_err_not_found;
            } else if (statusCode >= 400 && statusCode < 500) {
                Log.e("AFR", "FeedRetrieverService.retriever.download(): [" + statusCode + "] client error");
                error = R.string.feed_retriever_err_client;
            } else if (statusCode >= 500 && statusCode < 600) {
                Log.e("AFR", "FeedRetrieverService.retriever.download(): [" + statusCode + "] server error");
                detail = feed.getUri().getScheme() + "://" + feed.getUri().getHost();
                error = R.string.feed_retriever_err_server;
            }

            if (error != 0) {
                displayErrorNotification(error, detail);
                return false;
            }

            Log.i("AFR", "FeedRetrieverService.retriever.download(): [" + statusCode + "] continuing");
            return true;
        }

        boolean parse(Notification notification) {
            try {
                FileInputStream stream = new FileInputStream(file);
                parsedFeed = new SyndFeedInput().build(new XmlReader(stream, contentType, true));
            } catch (Exception e) {
                Log.i("AFR", "FeedRetrieverService.retriever.parse(): error parsing feed", e);
                displayErrorNotification(R.string.feed_retriever_err_parse,
                        feed.getName() != null ? feed.getName() : feed.getUri());
                return false;
            } finally {
                file.delete();
            }

            return true;
        }

        @SuppressWarnings("unchecked")
        void save(Notification notification) {
            feed.setName(parsedFeed.getTitle());
            try {
                feed.setLink(new URI(parsedFeed.getLink()));
            } catch (URISyntaxException e) {
                Log.e("AFR",
                        "FeedRetrieverService.retriever.save(): failed to parse '" + parsedFeed.getLink() + "'", e);
                displayErrorNotification(R.string.feed_retriever_err_unspecified,
                        feed.getName() != null ? feed.getName() : feed.getUri().toString());
                return;
            }
            try {
                /* if the link and uri are the same, don't update the uri, because it
                 * might or might not be the URI we use to retrieve the feed. 
                 */
                if (parsedFeed.getUri() != null && !parsedFeed.getUri().equals(parsedFeed.getLink())) {
                    feed.setUri(new URI(parsedFeed.getUri()));
                }
            } catch (URISyntaxException e) {
                Log.e("AFR", "FeedRetrieverService.retriever.save(): failed to parse '" + parsedFeed.getUri() + "'",
                        e);
                displayErrorNotification(R.string.feed_retriever_err_unspecified,
                        feed.getName() != null ? feed.getName() : feed.getUri().toString());
                return;
            }
            feed.setLastChecked(new Date(System.currentTimeMillis()));

            feed.saveOrUpdate();

            ArrayList<ContentValues> entries = new ArrayList<ContentValues>(parsedFeed.getEntries().size());
            for (SyndEntry entry : (List<SyndEntry>) parsedFeed.getEntries()) {
                ContentValues values = processEntry(entry);
                if (values != null) {
                    entries.add(values);
                }
            }

            ContentValues[] bulkValues = entries.toArray(new ContentValues[entries.size()]);
            getContentResolver().bulkInsert(Afr.Entries.CONTENT_URI, bulkValues);
        }

        @SuppressWarnings("unchecked")
        ContentValues processEntry(SyndEntry parsedEntry) {
            Entry entry = new Entry(getContentResolver());

            // check if this item has already been retrieved (and stop if it has)
            entry.setUri(parsedEntry.getUri());
            if (entry.loadByUri()) {
                return null;
            }

            entry.setFeed(feed);
            // get the author (item author > feed author > item contributers > feed contributers > feed title)
            String author = parsedEntry.getAuthor();
            if (author == null || author.length() == 0) {
                if (parsedFeed.getAuthor() != null && parsedFeed.getAuthor().length() != 0) {
                    author = parsedFeed.getAuthor();
                } else if (parsedEntry.getContributors() != null && !parsedEntry.getContributors().isEmpty()) {
                    author = ((SyndPerson) parsedEntry.getContributors().get(0)).getName();
                } else if (parsedFeed.getContributors() != null && !parsedFeed.getContributors().isEmpty()) {
                    author = ((SyndPerson) parsedFeed.getContributors().get(0)).getName();
                } else {
                    author = parsedFeed.getTitle();
                }
            }
            entry.setAuthor(author);
            entry.setTitle(parsedEntry.getTitle());
            if (parsedEntry.getPublishedDate() != null) {
                entry.setDate(parsedEntry.getPublishedDate());
            } else {
                entry.setDate(new Date(System.currentTimeMillis()));
            }
            try {
                entry.setLink(new URI(parsedEntry.getLink()));
            } catch (URISyntaxException e) {
                Log.e("AFR", "FeedRetrieverService.retriever.saveItem(): failed to parse '" + parsedEntry.getLink()
                        + "'", e);
                displayErrorNotification(R.string.feed_retriever_err_unspecified,
                        feed.getName() != null ? feed.getName() : feed.getUri().toString());
                return null;
            }

            // get the content (prefer HTML over plain text)
            SyndContent bestContent = null;
            for (SyndContent content : (List<SyndContent>) parsedEntry.getContents()) {
                if (bestContent == null) {
                    bestContent = content;
                    continue;
                }

                if (content.getType() != null && bestContent.getType() == null) {
                    bestContent = content;
                    break;
                }
            }

            if (bestContent == null) {
                // no content found? get it from the description then
                bestContent = parsedEntry.getDescription();
            }

            if (bestContent == null) {
                // we're screwed. let's move on
                Log.e("AFR", "FeedRetrieverService.retriever.saveItem(): no item content found");
                displayErrorNotification(R.string.feed_retriever_err_unspecified,
                        feed.getName() != null ? feed.getName() : feed.getUri().toString());
                return null;
            }
            entry.setContent(bestContent.getValue());

            if (bestContent.getType() == "html") {
                entry.setType("text/html");
            } else {
                entry.setType("text/plain");
            }

            return entry.getContentValues();
        }
    };

    @Override
    public IBinder getBinder() {
        return binder;
    }

    @Override
    protected void onCreate() {
        notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
        queue = new LinkedBlockingQueue<Feed>();

        thread = new Thread(null, retriever, "FeedRetrieverService worker");
        thread.setDaemon(true);
        thread.start();

        super.onCreate();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
    }

    Notification displayStatusNotification(Feed feed) {
        String text = String.format(getString(R.string.feed_retriever_notification),
                feed.getName() != null ? feed.getName() : feed.getUri()).toString();
        Notification notification = new Notification(R.drawable.notification_afr, text, null, text, null);
        notificationManager.notify(R.string.feed_retriever_notification, notification);
        return notification;
    }

    void displayErrorNotification(int textResId, Object... params) {
        notificationManager.notifyWithText(textResId, String.format(getString(textResId), params).toString(),
                NotificationManager.LENGTH_SHORT, null);
    }
}