/* * Copyright (C) 2007 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.example.android.rssreader; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import android.app.ListActivity; import android.content.Context; import android.content.Intent; import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; import android.view.LayoutInflater; import android.widget.ArrayAdapter; import android.widget.Button; import android.widget.EditText; import android.widget.ListView; import android.widget.TextView; import android.widget.TwoLineListItem; import android.util.Xml; import java.io.IOException; import java.io.InputStream; import java.net.URL; import java.net.URLConnection; import java.util.ArrayList; import java.util.List; /** * The RssReader example demonstrates forking off a thread to download * rss data in the background and post the results to a ListView in the UI. * It also shows how to display custom data in a ListView * with a ArrayAdapter subclass. * * <ul> * <li>We own a ListView * <li>The ListView uses our custom RSSListAdapter which * <ul> * <li>The adapter feeds data to the ListView * <li>Override of getView() in the adapter provides the display view * used for selected list items * </ul> * <li>Override of onListItemClick() creates an intent to open the url for that * RssItem in the browser. * <li>Download = fork off a worker thread * <li>The worker thread opens a network connection for the rss data * <li>Uses XmlPullParser to extract the rss item data * <li>Uses mHandler.post() to send new RssItems to the UI * <li>Supports onSaveInstanceState()/onRestoreInstanceState() to save list/selection state on app * pause, so can resume seamlessly * </ul> */ public class RssReader extends ListActivity { /** * Custom list adapter that fits our rss data into the list. */ private RSSListAdapter mAdapter; /** * Url edit text field. */ private EditText mUrlText; /** * Status text field. */ private TextView mStatusText; /** * Handler used to post runnables to the UI thread. */ private Handler mHandler; /** * Currently running background network thread. */ private RSSWorker mWorker; // Take this many chars from the front of the description. public static final int SNIPPET_LENGTH = 90; // Keys used for data in the onSaveInstanceState() Map. public static final String STRINGS_KEY = "strings"; public static final String SELECTION_KEY = "selection"; public static final String URL_KEY = "url"; public static final String STATUS_KEY = "status"; /** * Called when the activity starts up. Do activity initialization * here, not in a constructor. * * @see Activity#onCreate */ @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.rss_layout); // The above layout contains a list id "android:list" // which ListActivity adopts as its list -- we can // access it with getListView(). // Install our custom RSSListAdapter. List<RssItem> items = new ArrayList<RssItem>(); mAdapter = new RSSListAdapter(this, items); getListView().setAdapter(mAdapter); // Get pointers to the UI elements in the rss_layout mUrlText = (EditText)findViewById(R.id.urltext); mStatusText = (TextView)findViewById(R.id.statustext); Button download = (Button)findViewById(R.id.download); download.setOnClickListener(new OnClickListener() { public void onClick(View v) { doRSS(mUrlText.getText()); } }); // Need one of these to post things back to the UI thread. mHandler = new Handler(); // NOTE: this could use the icicle as done in // onRestoreInstanceState(). } /** * ArrayAdapter encapsulates a java.util.List of T, for presentation in a * ListView. This subclass specializes it to hold RssItems and display * their title/description data in a TwoLineListItem. */ private class RSSListAdapter extends ArrayAdapter<RssItem> { private LayoutInflater mInflater; public RSSListAdapter(Context context, List<RssItem> objects) { super(context, 0, objects); mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); } /** * This is called to render a particular item for the on screen list. * Uses an off-the-shelf TwoLineListItem view, which contains text1 and * text2 TextViews. We pull data from the RssItem and set it into the * view. The convertView is the view from a previous getView(), so * we can re-use it. * * @see ArrayAdapter#getView */ @Override public View getView(int position, View convertView, ViewGroup parent) { TwoLineListItem view; // Here view may be passed in for re-use, or we make a new one. if (convertView == null) { view = (TwoLineListItem) mInflater.inflate(android.R.layout.simple_list_item_2, null); } else { view = (TwoLineListItem) convertView; } RssItem item = this.getItem(position); // Set the item title and description into the view. // This example does not render real HTML, so as a hack to make // the description look better, we strip out the // tags and take just the first SNIPPET_LENGTH chars. view.getText1().setText(item.getTitle()); String descr = item.getDescription().toString(); descr = removeTags(descr); view.getText2().setText(descr.substring(0, Math.min(descr.length(), SNIPPET_LENGTH))); return view; } } /** * Simple code to strip out <tag>s -- primitive way to sortof display HTML as * plain text. */ public String removeTags(String str) { str = str.replaceAll("<.*?>", " "); str = str.replaceAll("\\s+", " "); return str; } /** * Called when user clicks an item in the list. Starts an activity to * open the url for that item. */ @Override protected void onListItemClick(ListView l, View v, int position, long id) { RssItem item = mAdapter.getItem(position); // Creates and starts an intent to open the item.link url. Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(item.getLink().toString())); startActivity(intent); } /** * Resets the output UI -- list and status text empty. */ public void resetUI() { // Reset the list to be empty. List<RssItem> items = new ArrayList<RssItem>(); mAdapter = new RSSListAdapter(this, items); getListView().setAdapter(mAdapter); mStatusText.setText(""); mUrlText.requestFocus(); } /** * Sets the currently active running worker. Interrupts any earlier worker, * so we only have one at a time. * * @param worker the new worker */ public synchronized void setCurrentWorker(RSSWorker worker) { if (mWorker != null) mWorker.interrupt(); mWorker = worker; } /** * Is the given worker the currently active one. * * @param worker * @return */ public synchronized boolean isCurrentWorker(RSSWorker worker) { return (mWorker == worker); } /** * Given an rss url string, starts the rss-download-thread going. * * @param rssUrl */ private void doRSS(CharSequence rssUrl) { RSSWorker worker = new RSSWorker(rssUrl); setCurrentWorker(worker); resetUI(); mStatusText.setText("Downloading\u2026"); worker.start(); } /** * Runnable that the worker thread uses to post RssItems to the * UI via mHandler.post */ private class ItemAdder implements Runnable { RssItem mItem; ItemAdder(RssItem item) { mItem = item; } public void run() { mAdapter.add(mItem); } // NOTE: Performance idea -- would be more efficient to have he option // to add multiple items at once, so you get less "update storm" in the UI // compared to adding things one at a time. } /** * Worker thread takes in an rss url string, downloads its data, parses * out the rss items, and communicates them back to the UI as they are read. */ private class RSSWorker extends Thread { private CharSequence mUrl; public RSSWorker(CharSequence url) { mUrl = url; } @Override public void run() { String status = ""; try { // Standard code to make an HTTP connection. URL url = new URL(mUrl.toString()); URLConnection connection = url.openConnection(); connection.setConnectTimeout(10000); connection.connect(); InputStream in = connection.getInputStream(); parseRSS(in, mAdapter); status = "done"; } catch (Exception e) { status = "failed:" + e.getMessage(); } // Send status to UI (unless a newer worker has started) // To communicate back to the UI from a worker thread, // pass a Runnable to handler.post(). final String temp = status; if (isCurrentWorker(this)) { mHandler.post(new Runnable() { public void run() { mStatusText.setText(temp); } }); } } } /** * Populates the menu. */ @Override public boolean onCreateOptionsMenu(Menu menu) { super.onCreateOptionsMenu(menu); menu.add(0, 0, 0, "Slashdot") .setOnMenuItemClickListener(new RSSMenu("http://rss.slashdot.org/Slashdot/slashdot")); menu.add(0, 0, 0, "Google News") .setOnMenuItemClickListener(new RSSMenu("http://news.google.com/?output=rss")); menu.add(0, 0, 0, "News.com") .setOnMenuItemClickListener(new RSSMenu("http://news.com.com/2547-1_3-0-20.xml")); menu.add(0, 0, 0, "Bad Url") .setOnMenuItemClickListener(new RSSMenu("http://nifty.stanford.edu:8080")); menu.add(0, 0, 0, "Reset") .setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() { public boolean onMenuItemClick(MenuItem item) { resetUI(); return true; } }); return true; } /** * Puts text in the url text field and gives it focus. Used to make a Runnable * for each menu item. This way, one inner class works for all items vs. an * anonymous inner class for each menu item. */ private class RSSMenu implements MenuItem.OnMenuItemClickListener { private CharSequence mUrl; RSSMenu(CharSequence url) { mUrl = url; } public boolean onMenuItemClick(MenuItem item) { mUrlText.setText(mUrl); mUrlText.requestFocus(); return true; } } /** * Called for us to save out our current state before we are paused, * such a for example if the user switches to another app and memory * gets scarce. The given outState is a Bundle to which we can save * objects, such as Strings, Integers or lists of Strings. In this case, we * save out the list of currently downloaded rss data, (so we don't have to * re-do all the networking just because the user goes back and forth * between aps) which item is currently selected, and the data for the text views. * In onRestoreInstanceState() we look at the map to reconstruct the run-state of the * application, so returning to the activity looks seamlessly correct. * TODO: the Activity javadoc should give more detail about what sort of * data can go in the outState map. * * @see android.app.Activity#onSaveInstanceState */ @SuppressWarnings("unchecked") @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); // Make a List of all the RssItem data for saving // NOTE: there may be a way to save the RSSItems directly, // rather than their string data. int count = mAdapter.getCount(); // Save out the items as a flat list of CharSequence objects -- // title0, link0, descr0, title1, link1, ... ArrayList<CharSequence> strings = new ArrayList<CharSequence>(); for (int i = 0; i < count; i++) { RssItem item = mAdapter.getItem(i); strings.add(item.getTitle()); strings.add(item.getLink()); strings.add(item.getDescription()); } outState.putSerializable(STRINGS_KEY, strings); // Save current selection index (if focussed) if (getListView().hasFocus()) { outState.putInt(SELECTION_KEY, Integer.valueOf(getListView().getSelectedItemPosition())); } // Save url outState.putString(URL_KEY, mUrlText.getText().toString()); // Save status outState.putCharSequence(STATUS_KEY, mStatusText.getText()); } /** * Called to "thaw" re-animate the app from a previous onSaveInstanceState(). * * @see android.app.Activity#onRestoreInstanceState */ @SuppressWarnings("unchecked") @Override protected void onRestoreInstanceState(Bundle state) { super.onRestoreInstanceState(state); // Note: null is a legal value for onRestoreInstanceState. if (state == null) return; // Restore items from the big list of CharSequence objects List<CharSequence> strings = (ArrayList<CharSequence>)state.getSerializable(STRINGS_KEY); List<RssItem> items = new ArrayList<RssItem>(); for (int i = 0; i < strings.size(); i += 3) { items.add(new RssItem(strings.get(i), strings.get(i + 1), strings.get(i + 2))); } // Reset the list view to show this data. mAdapter = new RSSListAdapter(this, items); getListView().setAdapter(mAdapter); // Restore selection if (state.containsKey(SELECTION_KEY)) { getListView().requestFocus(View.FOCUS_FORWARD); // todo: is above right? needed it to work getListView().setSelection(state.getInt(SELECTION_KEY)); } // Restore url mUrlText.setText(state.getCharSequence(URL_KEY)); // Restore status mStatusText.setText(state.getCharSequence(STATUS_KEY)); } /** * Does rudimentary RSS parsing on the given stream and posts rss items to * the UI as they are found. Uses Android's XmlPullParser facility. This is * not a production quality RSS parser -- it just does a basic job of it. * * @param in stream to read * @param adapter adapter for ui events */ void parseRSS(InputStream in, RSSListAdapter adapter) throws IOException, XmlPullParserException { // TODO: switch to sax XmlPullParser xpp = Xml.newPullParser(); xpp.setInput(in, null); // null = parser figures out encoding int eventType; String title = ""; String link = ""; String description = ""; eventType = xpp.getEventType(); while (eventType != XmlPullParser.END_DOCUMENT) { if (eventType == XmlPullParser.START_TAG) { String tag = xpp.getName(); if (tag.equals("item")) { title = link = description = ""; } else if (tag.equals("title")) { xpp.next(); // Skip to next element -- assume text is directly inside the tag title = xpp.getText(); } else if (tag.equals("link")) { xpp.next(); link = xpp.getText(); } else if (tag.equals("description")) { xpp.next(); description = xpp.getText(); } } else if (eventType == XmlPullParser.END_TAG) { // We have a comlete item -- post it back to the UI // using the mHandler (necessary because we are not // running on the UI thread). String tag = xpp.getName(); if (tag.equals("item")) { RssItem item = new RssItem(title, link, description); mHandler.post(new ItemAdder(item)); } } eventType = xpp.next(); } } // SAX version of the code to do the parsing. /* private class RSSHandler extends DefaultHandler { RSSListAdapter mAdapter; String mTitle; String mLink; String mDescription; StringBuilder mBuff; boolean mInItem; public RSSHandler(RSSListAdapter adapter) { mAdapter = adapter; mInItem = false; mBuff = new StringBuilder(); } public void startElement(String uri, String localName, String qName, Attributes atts) throws SAXException { String tag = localName; if (tag.equals("")) tag = qName; // If inside <item>, clear out buff on each tag start if (mInItem) { mBuff.delete(0, mBuff.length()); } if (tag.equals("item")) { mTitle = mLink = mDescription = ""; mInItem = true; } } public void characters(char[] ch, int start, int length) throws SAXException { // Buffer up all the chars when inside <item> if (mInItem) mBuff.append(ch, start, length); } public void endElement(String uri, String localName, String qName) throws SAXException { String tag = localName; if (tag.equals("")) tag = qName; // For each tag, copy buff chars to right variable if (tag.equals("title")) mTitle = mBuff.toString(); else if (tag.equals("link")) mLink = mBuff.toString(); if (tag.equals("description")) mDescription = mBuff.toString(); // Have all the data at this point .... post it to the UI. if (tag.equals("item")) { RssItem item = new RssItem(mTitle, mLink, mDescription); mHandler.post(new ItemAdder(item)); mInItem = false; } } } */ /* public void parseRSS2(InputStream in, RSSListAdapter adapter) throws IOException { SAXParser parser = SAXParserFactory.newInstance().newSAXParser(); DefaultHandler handler = new RSSHandler(adapter); parser.parse(in, handler); // TODO: does the parser figure out the encoding right on its own? } */ }