Search code examples
androidandroid-serviceandroidhttpclient

Create a better Download Service


I'm new with services (and downloading for that matter)... I have this download service that downloads an XML feed and parses it, but it takes a while.

I'm downloading from 14 different URLs, this service downloads from a url, parses the xml, then sends a message back to the activity that tells the service to start over with the next URL.

I'm sure there's a better way to accomplish this and am looking for some advice. Here's the code from the download service, if you need additional code (like the handlemessage in the main activity), just let me know and I'll put it up.

Because I was advised that I need to have a specific question: is there a more efficient way to download from multiple URLs other than receiving a response from this download service and restarting it in a loop until all urls are downloaded?

    package prs.psesto.rotorss;

import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;

import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlPullParserFactory;

import android.app.Activity;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
import android.os.Messenger;
import android.os.RemoteException;
import android.util.Log;

public class DownloadService extends Service {

    // XML node keys
    private final String KEY_ITEM = "item"; // parent node
    private final String KEY_GUID = "guid";
    private final String KEY_LINK = "link";
    private final String KEY_TITLE = "title";
    private final String KEY_DESCRIPTION = "description";
    private final String KEY_UPDATED = "a10:updated";

    public static final String nameSpace = null;

    private static String[] sportArray = { "nfl", "mlb", "nba", "nhl", "bpl",
            "cfb", "gol", "nas" };

    private final int PLAYER_NEWS = 0;
    private final int ARTICLES = 1;

    private final String THREAD_PLAYER_NEWS = "THREAD_PLAYER_NEWS";
    private final String THREAD_ARTICLES = "THREAD_ARTICLES";

    public static enum DownloadType {
        PLAYER_NEWS, ARTICLES
    };

    /**
     * Looper associated with the HandlerThread.
     */
    private volatile Looper mServiceLooper;

    /**
     * Processes Messages sent to it from onStartCommnand() that
     */
    private volatile ServiceHandler mServiceHandler;

    /**
     * Hook method called when DownloadService is first launched by the Android
     * ActivityManager.
     */
    public void onCreate() {
        super.onCreate();
        Log.d("DownloadService", "onCreate");

        // Create and start a background HandlerThread since by
        // default a Service runs in the UI Thread, which we don't
        // want to block.
        HandlerThread thread = new HandlerThread("DownloadService");
        thread.start();

        // Get the HandlerThread's Looper and use it for our Handler.
        mServiceLooper = thread.getLooper();
        mServiceHandler = new ServiceHandler(mServiceLooper);
    }

    /**
     * Hook method called each time a Started Service is sent an Intent via
     * startService().
     */
    public int onStartCommand(Intent intent, int flags, int startId) {
        Log.d("DownloadService", "onStartCommand");

        // Create a Message that will be sent to ServiceHandler to
        // retrieve animagebased on the URI in the Intent.
        Message message = mServiceHandler.makeDownloadMessage(intent, startId);

        // Send the Message to ServiceHandler to retrieve an image
        // based on contents of the Intent.
        mServiceHandler.sendMessage(message);

        // Don't restart the DownloadService automatically if its
        // process is killed while it's running.
        return Service.START_NOT_STICKY;
    }

    /**
     * Helper method that returns sport if download succeeded.
     */
    public static int getSportIndex(Message message) {
        Log.d("DownloadService", "getSportIndex");

        // Extract the data from Message, which is in the form
        // of a Bundle that can be passed across processes.
        Bundle data = message.getData();

        // Extract the pathname from the Bundle.
        int sport = data.getInt("SPORTCODE");
        Log.d("DownloadService", "sport = " + String.valueOf(sport));

        // Check to see if the download succeeded.
        if (message.arg1 == 9)
            return 9;
        else
            return sport;
    }

    /**
     * Helper method that returns sport if download succeeded.
     */
    public static int getTypeIndex(Message message) {
        Log.d("DownloadService", "getTypeIndex");

        // Extract the data from Message, which is in the form
        // of a Bundle that can be passed across processes.
        Bundle data = message.getData();

        // Extract the pathname from the Bundle.
        int type = data.getInt("TYPECODE");
        Log.d("DownloadService", "type = " + String.valueOf(type));

        // Check to see if the download succeeded.
        if (message.arg1 == 9)
            return 9;
        else
            return type;
    }

    /**
     * Factory method to make the desired Intent.
     */
    public static Intent makeIntent(Context context, int type, int sport,
            Handler downloadHandler) {
        Log.d("DownloadService", "makeIntent");
        // Create the Intent that's associated to the DownloadService
        // class.
        Intent intent = new Intent(context, DownloadService.class);

        // Pathname for the downloaded image.
        intent.putExtra("TYPECODE", type);
        intent.putExtra("SPORTCODE", sport);

        // Create and pass a Messenger as an "extra" so the
        // DownloadService can send back the pathname.
        intent.putExtra("MESSENGER", new Messenger(downloadHandler));
        return intent;
    }

    /**
     * @class ServiceHandler
     *
     * @brief An inner class that inherits from Handler and uses its
     *        handleMessage() hook method to process Messages sent to it from
     *        onStartCommnand() that indicate which url to download.
     */
    private final class ServiceHandler extends Handler {
        /**
         * Class constructor initializes the Looper.
         * 
         * @param Looper
         *            The Looper that we borrow from HandlerThread.
         */
        public ServiceHandler(Looper looper) {
            super(looper);
            log("public ServiceHandler(Looper looper)");

        }

        /**
         * A factory method that creates a Message to return to the
         * DownloadActivity with the downloaded sport and type.
         */
        private Message makeReplyMessage(int type, int sport, int result) {
            log("makeReplyMessage");

            Message message = Message.obtain();

            Bundle data = new Bundle();

            // Pathname for the downloaded image.
            data.putInt("TYPECODE", type);
            data.putInt("SPORTCODE", sport);
            message.arg1 = result;

            message.setData(data);
            return message;
        }

        /**
         * A factory method that creates a Message that contains information on
         * how to stop the Service.
         */
        private Message makeDownloadMessage(Intent intent, int startId) {
            log("makeDownloadMessage");

            Message message = Message.obtain();
            // Include Intent & startId in Message to indicate which URI
            // to retrieve and which request is being stopped when
            // download completes.
            message.obj = intent;
            message.arg1 = startId;
            return message;
        }

        /**
         * Retrieves the download response and sport/type and replies to the
         * DownloadActivity via the Messenger sent with the Intent.
         */
        private void downloadAndReply(Intent intent) {
            log("downloadAndReply");

            // Download the requested image.
            int sport = intent.getExtras().getInt("SPORTCODE");
            int type = intent.getExtras().getInt("TYPECODE");

            int result = downloadAndParse(type, sport);

            // Extract the Messenger.
            Messenger messenger = (Messenger) intent.getExtras().get(
                    "MESSENGER");

            // Send the pathname via the messenger.
            sendResult(messenger, type, sport, result);
        }

        /**
         * Send the result back to the DownloadActivity via the messenger.
         */
        private void sendResult(Messenger messenger, int type, int sport,
                int result) {
            log("sendResult");

            // Call factory method to create Message.
            Message message = makeReplyMessage(type, sport, result);

            try {
                // Send pathname to back to the DownloadActivity.
                messenger.send(message);
            } catch (RemoteException e) {
                Log.e(getClass().getName(), "Exception while sending.", e);
            }
        }

        public int downloadAndParse(int typeIndex, int sportIndex) {
            log("downloadAndParse");

            long id = 0;
            String link = null;
            int linkType = ARTICLES;
            String title = null;
            String description = null;
            String updated = null;

            String sport = sportArray[sportIndex];

            try {
                InputStream stream = null;

                try {
                    stream = downloadUrl(getUrl(getDlType(typeIndex), sport));

                    XmlPullParserFactory factory = XmlPullParserFactory
                            .newInstance();
                    factory.setNamespaceAware(false);
                    XmlPullParser xpp = factory.newPullParser();
                    xpp.setInput(stream, null);
                    boolean insideItem = false;

                    // Returns the type of current event: START_TAG,
                    // END_TAG,
                    // etc..
                    int eventType = xpp.getEventType();
                    while (eventType != XmlPullParser.END_DOCUMENT) {
                        if (eventType == XmlPullParser.START_TAG) {

                            if (xpp.getName().equalsIgnoreCase(KEY_ITEM)) {
                                insideItem = true;
                            } else if (xpp.getName().equalsIgnoreCase(KEY_GUID)) {
                                if (insideItem) {
                                    id = Long.valueOf(String.valueOf(
                                            xpp.nextText()).replace(" ", ""));
                                }
                            } else if (xpp.getName().equalsIgnoreCase(KEY_LINK)) {
                                if (insideItem) {
                                    link = xpp.nextText();

                                    if (link.contains("player")) {
                                        linkType = PLAYER_NEWS;
                                    }
                                }
                            } else if (xpp.getName()
                                    .equalsIgnoreCase(KEY_TITLE)) {
                                if (insideItem) {
                                    title = xpp.nextText();
                                }
                            } else if (xpp.getName().equalsIgnoreCase(
                                    KEY_DESCRIPTION)) {
                                if (insideItem) {
                                    description = xpp.nextText();
                                }
                            } else if (xpp.getName().equalsIgnoreCase(
                                    KEY_UPDATED)) {
                                if (insideItem) {
                                    updated = xpp.nextText();
                                }
                            }

                        } else if (eventType == XmlPullParser.END_TAG
                                && xpp.getName().equalsIgnoreCase("item")) {
                            insideItem = false;
                        }

                        eventType = xpp.next(); // / move to next element
                        RotoItem rItem = DatabaseManager.sInstance.newRotoItem(
                                id, sport, link, linkType, title, description,
                                updated);

                        DatabaseManager.sInstance.addRotoItem(rItem);
                    }

                    // Makes sure that the InputStream is closed after the
                    // class
                    // is
                    // finished using it.
                } finally {

                    if (stream != null) {
                        stream.close();
                    }
                }
            } catch (IOException e) {
                Log.e("DownloadXml - performDownload", "Connection Error");
                Log.e("IOException", e.toString());
                return 9;
            } catch (XmlPullParserException e) {
                Log.e("DownloadXml - performDownload", "Error in data set");
                Log.e("XmlPullParserException", e.toString());
                return 9;
            }
            Log.d("DownloadData", "sportIndex = " + String.valueOf(sportIndex));

            return sportIndex;

        }

        public DownloadType getDlType(int typeIndex) {
            if (typeIndex == PLAYER_NEWS) {
                return DownloadType.PLAYER_NEWS;
            } else {
                return DownloadType.ARTICLES;
            }
        }

        public String getUrl(DownloadType downloadType, String sport) {

            if (downloadType == DownloadType.PLAYER_NEWS) {
                return "http://www.rotoworld.com/rss/feed.aspx?sport=" + sport
                        + "&ftype=news&count=12&format=rss";
            } else {
                return "http://www.rotoworld.com/rss/feed.aspx?sport=" + sport
                        + "&ftype=article&count=12&format=rss";
            }
        }

        // Given a string representation of a URL, sets up a connection and gets
        // an input stream.
        private InputStream downloadUrl(String urlString) throws IOException {

            URL url = new URL(urlString);
            HttpURLConnection conn = (HttpURLConnection) url.openConnection();
            conn.setReadTimeout(10000 /* milliseconds */);
            conn.setConnectTimeout(15000 /* milliseconds */);
            conn.setRequestMethod("GET");
            conn.setDoInput(true);
            // Starts the query
            conn.connect();
            return conn.getInputStream();
        }

        public void handleMessage(Message message) {
            log("handleMessage");

            // Download the designated image and reply to the
            // DownloadActivity via the Messenger sent with the
            // Intent.
            downloadAndReply((Intent) message.obj);

            // Stop the Service using the startId, so it doesn't stop
            // in the middle of handling another download request.
            stopSelf(message.arg1);
        }
    }

    /**
     * Hook method called back to shutdown the Looper.
     */
    public void onDestroy() {
        log("onDestroy");

        mServiceLooper.quit();
    }

    /**
     * This hook method is a no-op since we're a Start Service.
     */
    public IBinder onBind(Intent arg0) {
        log("IBinder");
        return null;
    }

    public void log(String message) {
        Log.d("DownloadService", message);
    }
}

Solution

  • is there a more efficient way to download from multiple URLs other than receiving a response from this download service and restarting until all urls are downloaded?

    Use a ThreadPoolExecutor and run a few threads in parallel. Dump your Messenger-and-HandlerThread stuff and have onStartCommand() just hand each job over to the ThreadPoolExecutor. Use a packaged event bus (e.g., greenrobot's EventBus, LocalBroadcastManager) to let your UI layer know about the work that was done.

    The ThreadPoolExecutor logic would then be fairly standard Java. The only Android-isms would be onStartCommand() being the recipient of the work to do and using the event bus to relay results.