|  | /* | 
|  | * 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? | 
|  | } | 
|  | */ | 
|  | } |