Android Services Part 2
Join the DZone community and get the full member experience.
Join For FreeIntroduction
In the first part of this series we examined how to create a basic Android service. We examined the Service class and how to properly extend that class to provide the functionality we needed for our application. Along the way we also discovered how to start and stop services from within an Android application. In this second part of the series we will start fleshing out the RSS Serice a little bit more and start adding functionaity. I'll show you some more techniques along the way which you can use in your Android applications, service or otherwise.
Extending the Service
We need to extend our basic service even more to provide some meaningful implementation. Before we do that we need to reate some supporting classes. These classes will encapsulate the RSSFeed as well as abstract the actual reading and parsing of the feed. We'll start with the parsing classes. It makes sense to utilize the interpreter pattern to parse the feed. Let's look at the interface which exposes that functionality.
package com.demo.service; import java.util.List; public interface RSSFeedParser { List<RSSMessage> parse(); }
This simple interface implements a classic interpreter pattern via the parse method. This method will return a list of RSSMessage objects which were found in the feed. Let's take a look at that class now.
package com.demo.service; import java.net.MalformedURLException; import java.net.URL; import java.util.Date; import java.text.ParseException; import java.text.SimpleDateFormat; import android.util.Log; public class RSSMessage implements Comparable<RSSMessage> { static SimpleDateFormat DateFormatter = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z"); static String TAG = "RSSMessage"; private String title; private URL link; private String description; private Date date; @Override public int compareTo(RSSMessage another) { if( another == null ) return 1; // sort descending, most recent first return another.date.compareTo(this.date); } public void setTitle(String title) { this.title = title; } public String getTitle() { return title; } public String getUrl() { return this.link.toString(); } public void setLink(String link) { try { this.link = new URL(link); } catch (MalformedURLException e) { Log.e(TAG, e.getMessage()); throw new RuntimeException(e); } } public void setDescription(String description) { this.description = description; } public String getDescription() { return description; } public void setDate(String date) { // pad the date if necessary while(!date.endsWith("00")) { date += "0"; } try { this.date = DateFormatter.parse(date.trim()); } catch (ParseException e) { Log.e(TAG, e.getLocalizedMessage()); throw new RuntimeException(e); } } public String getDate() { return DateFormatter.format(this.date); } }
This class is a simple pojo which encapsulates the elements of an RSSFeed.
Now let's take a look at a basic implementation of our RSSFeedParser interface. In this example I am utilizing a base class to provide basic information and functionality of a feed parser but does not do the actual parse. It's up to an extending class to provide that function. Let's take a look at the base class.
package com.demo.service; import java.io.IOException; import java.io.InputStream; import java.net.MalformedURLException; import java.net.URL; import android.util.Log; public abstract class BaseFeedParser implements RSSFeedParser { static String TAG = "BaseFeedParser"; // names of the XML tags static final String CHANNEL = "channel"; static final String PUB_DATE = "pubDate"; static final String DESCRIPTION = "description"; static final String LINK = "link"; static final String TITLE = "title"; static final String ITEM = "item"; final URL feedUrl; public BaseFeedParser(String feedUrl) { try { this.feedUrl = new URL(feedUrl); } catch (MalformedURLException e) { Log.e(TAG, e.getLocalizedMessage()); throw new RuntimeException(e); } } protected InputStream getInputStream() { try { return feedUrl.openConnection().getInputStream(); } catch (IOException e) { Log.e(TAG, e.getLocalizedMessage()); throw new RuntimeException(e); } } }
The class begins with implementing the RSSFeedParser interface we looked at earlier. The class is abstract since it doesn't implement the parse method. The BaseFeedParser contains definitions of the RSS field names we are interested. The constructer accepts a String parameter which specifies the url of the feed. The class also provides a simple function to encapsulate reading the input stream from the socket connection to the url specified in the base constructor.
Let's take a look at a concrete implementaion of this class now.
package com.demo.service; import java.util.ArrayList; import java.util.List; import org.xmlpull.v1.XmlPullParser; import android.util.Xml; public class XmlPullFeedParser extends BaseFeedParser { public XmlPullFeedParser(String feedUrl) { super(feedUrl); } public List<RSSMessage> parse() { List<RSSMessage> messages = null; XmlPullParser parser = Xml.newPullParser(); try { // auto-detect the encoding from the stream parser.setInput(this.getInputStream(), null); int eventType = parser.getEventType(); RSSMessage currentMessage = null; boolean done = false; while (eventType != XmlPullParser.END_DOCUMENT && !done){ String name = null; switch (eventType){ case XmlPullParser.START_DOCUMENT: messages = new ArrayList<RSSMessage>(); break; case XmlPullParser.START_TAG: name = parser.getName(); if (name.equalsIgnoreCase(ITEM)){ currentMessage = new RSSMessage(); } else if (currentMessage != null){ if (name.equalsIgnoreCase(LINK)){ currentMessage.setLink(parser.nextText()); } else if (name.equalsIgnoreCase(DESCRIPTION)){ currentMessage.setDescription(parser.nextText()); } else if (name.equalsIgnoreCase(PUB_DATE)){ currentMessage.setDate(parser.nextText()); } else if (name.equalsIgnoreCase(TITLE)){ currentMessage.setTitle(parser.nextText()); } } break; case XmlPullParser.END_TAG: name = parser.getName(); if (name.equalsIgnoreCase(ITEM) && currentMessage != null){ messages.add(currentMessage); } else if (name.equalsIgnoreCase(CHANNEL)){ done = true; } break; } eventType = parser.next(); } } catch (Exception e) { throw new RuntimeException(e); } return messages; } }
This is an important class so let's examine it closely. This class provides parsing functionality by utilizing an XmlPullParser. Pull parsers differ from DOM and push parsers in that it's up to the api user to control the parsing of the xml stream, hence the word pull. In push parsers the parsing is left out of the API users control. Push parsers utilize callbacks. When any kind of state is required push parsers can be very difficult to work with.
DOM parsers suffer from memory bloat. With smaller XML sets this isn't such an issue but as the XML size grows DOM parsers become less and less efficient making them not very scaleable in terms of growing your XML. Push parsers are a happy medium since they allow you to control parsing, thereby eliminating any kind of complex state management since the state is always known, and they don't suffer from the memory bloat of DOM parsers.
In this example you can see that this class uses the base class constant values to determine which node it's operating on. Here you can see that when the start of the document is detected the function creates a new list of RSSMessages. When a start tag is found the name is compared to the Item node name. If an Item node is found a new RSSMessage is created. This is the current RSSMessage. If any other valid node name is found it's assumed to be an RSSMessage field and added to the currently active message. When an end tag is detected the name is compared to the Item node name and to the Channel node name. If it's the Item node then if a RSSMessage was started previously it's added to the message list. If the end tag name is the name of the Channel node the function signals that parsing is complete and the main loop is then exited.
Now that we have a functional parser it's time to re-examine the service we created in the first article. That was a simple service which created an update timer which fired a function called refreshFeed. Here is that code to for a refresher.
package com.demo.service; import java.util.Date; import java.util.List; import java.util.Timer; import java.util.TimerTask; import android.app.Service; import android.content.Intent; import android.os.IBinder; import android.widget.Toast; public class RSSService extends Service { private Timer updateTimer; private Date lastRead = new Date(0L); @Override public IBinder onBind(Intent arg0) { return null; } @Override public void onCreate() { updateTimer = new Timer("RSSServiceUpdateTimer"); } @Override public void onStart(Intent intent, int startId) { Toast.makeText(this, "RSSService Started", Toast.LENGTH_LONG).show(); // TODO: Read from user preferences int period = 10; // cancel the current timer updateTimer.cancel(); // create a new timer updateTimer = new Timer("RSSServiceUpdateTimer"); updateTimer.scheduleAtFixedRate( new TimerTask() { @Override public void run() { RSSService.this.refreshFeed(); } }, 0, period*60*1000); } protected void refreshFeed() { } private void announceNewFeed(RSSMessage feed) { } @Override public void onDestroy() { super.onDestroy(); Toast.makeText(this, "RSSService Stopped", Toast.LENGTH_LONG).show(); } }
The function we are interested in is refreshFeed. Let's go ahead and flesh that out now that we have a feed parser and can actually do something meaningful with the RSS feed. Here is that code.
protected void refreshFeed() { // perform http lookup for new feeds RSSFeedParser parser = new XmlPullFeedParser("http://rss.news.yahoo.com/rss/topstories"); Date maxMessageDate = new Date(0L); List<RSSMessage> messages = parser.parse(); // anything older than lastRead we announce for(int i=0; i<messages.size(); i++) { String dateString = messages.get(i).getDate(); Date messageDate = new Date(dateString); if(messageDate.compareTo(lastRead) > 0) { if( maxMessageDate.compareTo(messageDate) < 0 ) { maxMessageDate = messageDate; } announceNewFeed(messages.get(i)); } } lastRead = maxMessageDate; }
In this function you can see the familiar XmlPullFeedParser as well as our original interface RSSFeedParser which implements the interpreter pattern. In this case I am using a hardcoded url but in a real application you would most likely supply this from a users input or a settings/preference file. I am using the top stories feed from yahoo since that feed changes quite often and it has lot's of different media types which you can use to create a rich user experience.
The first thing the function does is parse the feed into a list of RSSMessage classes. It then iterates through each RSSMessage in the list. In this case we want to only announce new feeds so we compare to the date we last refreshed the feed. If the date is greater than the last refresh date then we call announceNewFeed with that message. In this example announceNewFeed does nothing, but in a later article we will utilize that to hook up to a data provider which we can then consume in our applications. After all the messages are processed the lastRead field is assigned to the largest date read from the feed.
Conclusion
In this article we extended our service example from the first article. We created a pojo to represent an RSS feed message and wrote an XML pull parser to parse the feed using the interpreter pattern. Finally we wired all that up to our refreshFeed function which is called from the update timer we examined in the first article. We provided a hook to announce new feeds from the refreshFeed method and will be looking at that in a later article. By extending a basic framework we were able to create a rich data service for RSS feeds. There is no limit to what you can do with Android services as this article demonstrates.
Opinions expressed by DZone contributors are their own.
Comments