| /* |
| * 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 = default to UTF-8 |
| |
| 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? |
| } |
| */ |
| } |