Home | History | Annotate | Download | only in rssreader
      1 /*
      2  * Copyright (C) 2007 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.example.android.rssreader;
     18 
     19 import org.xmlpull.v1.XmlPullParser;
     20 import org.xmlpull.v1.XmlPullParserException;
     21 
     22 import android.app.ListActivity;
     23 import android.content.Context;
     24 import android.content.Intent;
     25 import android.net.Uri;
     26 import android.os.Bundle;
     27 import android.os.Handler;
     28 import android.view.Menu;
     29 import android.view.MenuItem;
     30 import android.view.View;
     31 import android.view.View.OnClickListener;
     32 import android.view.ViewGroup;
     33 import android.view.LayoutInflater;
     34 import android.widget.ArrayAdapter;
     35 import android.widget.Button;
     36 import android.widget.EditText;
     37 import android.widget.ListView;
     38 import android.widget.TextView;
     39 import android.widget.TwoLineListItem;
     40 import android.util.Xml;
     41 
     42 import java.io.IOException;
     43 import java.io.InputStream;
     44 import java.net.URL;
     45 import java.net.URLConnection;
     46 import java.util.ArrayList;
     47 import java.util.List;
     48 
     49 /**
     50  * The RssReader example demonstrates forking off a thread to download
     51  * rss data in the background and post the results to a ListView in the UI.
     52  * It also shows how to display custom data in a ListView
     53  * with a ArrayAdapter subclass.
     54  *
     55  * <ul>
     56  * <li>We own a ListView
     57  * <li>The ListView uses our custom RSSListAdapter which
     58  * <ul>
     59  * <li>The adapter feeds data to the ListView
     60  * <li>Override of getView() in the adapter provides the display view
     61  * used for selected list items
     62  * </ul>
     63  * <li>Override of onListItemClick() creates an intent to open the url for that
     64  * RssItem in the browser.
     65  * <li>Download = fork off a worker thread
     66  * <li>The worker thread opens a network connection for the rss data
     67  * <li>Uses XmlPullParser to extract the rss item data
     68  * <li>Uses mHandler.post() to send new RssItems to the UI
     69  * <li>Supports onSaveInstanceState()/onRestoreInstanceState() to save list/selection state on app
     70  * pause, so can resume seamlessly
     71  * </ul>
     72  */
     73 public class RssReader extends ListActivity {
     74     /**
     75      * Custom list adapter that fits our rss data into the list.
     76      */
     77     private RSSListAdapter mAdapter;
     78 
     79     /**
     80      * Url edit text field.
     81      */
     82     private EditText mUrlText;
     83 
     84     /**
     85      * Status text field.
     86      */
     87     private TextView mStatusText;
     88 
     89     /**
     90      * Handler used to post runnables to the UI thread.
     91      */
     92     private Handler mHandler;
     93 
     94     /**
     95      * Currently running background network thread.
     96      */
     97     private RSSWorker mWorker;
     98 
     99     // Take this many chars from the front of the description.
    100     public static final int SNIPPET_LENGTH = 90;
    101 
    102 
    103     // Keys used for data in the onSaveInstanceState() Map.
    104     public static final String STRINGS_KEY = "strings";
    105 
    106     public static final String SELECTION_KEY = "selection";
    107 
    108     public static final String URL_KEY = "url";
    109 
    110     public static final String STATUS_KEY = "status";
    111 
    112     /**
    113      * Called when the activity starts up. Do activity initialization
    114      * here, not in a constructor.
    115      *
    116      * @see Activity#onCreate
    117      */
    118     @Override
    119     protected void onCreate(Bundle savedInstanceState) {
    120         super.onCreate(savedInstanceState);
    121 
    122         setContentView(R.layout.rss_layout);
    123         // The above layout contains a list id "android:list"
    124         // which ListActivity adopts as its list -- we can
    125         // access it with getListView().
    126 
    127         // Install our custom RSSListAdapter.
    128         List<RssItem> items = new ArrayList<RssItem>();
    129         mAdapter = new RSSListAdapter(this, items);
    130         getListView().setAdapter(mAdapter);
    131 
    132         // Get pointers to the UI elements in the rss_layout
    133         mUrlText = (EditText)findViewById(R.id.urltext);
    134         mStatusText = (TextView)findViewById(R.id.statustext);
    135 
    136         Button download = (Button)findViewById(R.id.download);
    137         download.setOnClickListener(new OnClickListener() {
    138             public void onClick(View v) {
    139                 doRSS(mUrlText.getText());
    140             }
    141         });
    142 
    143         // Need one of these to post things back to the UI thread.
    144         mHandler = new Handler();
    145 
    146         // NOTE: this could use the icicle as done in
    147         // onRestoreInstanceState().
    148     }
    149 
    150     /**
    151      * ArrayAdapter encapsulates a java.util.List of T, for presentation in a
    152      * ListView. This subclass specializes it to hold RssItems and display
    153      * their title/description data in a TwoLineListItem.
    154      */
    155     private class RSSListAdapter extends ArrayAdapter<RssItem> {
    156         private LayoutInflater mInflater;
    157 
    158         public RSSListAdapter(Context context, List<RssItem> objects) {
    159             super(context, 0, objects);
    160 
    161             mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    162         }
    163 
    164         /**
    165          * This is called to render a particular item for the on screen list.
    166          * Uses an off-the-shelf TwoLineListItem view, which contains text1 and
    167          * text2 TextViews. We pull data from the RssItem and set it into the
    168          * view. The convertView is the view from a previous getView(), so
    169          * we can re-use it.
    170          *
    171          * @see ArrayAdapter#getView
    172          */
    173         @Override
    174         public View getView(int position, View convertView, ViewGroup parent) {
    175             TwoLineListItem view;
    176 
    177             // Here view may be passed in for re-use, or we make a new one.
    178             if (convertView == null) {
    179                 view = (TwoLineListItem) mInflater.inflate(android.R.layout.simple_list_item_2,
    180                         null);
    181             } else {
    182                 view = (TwoLineListItem) convertView;
    183             }
    184 
    185             RssItem item = this.getItem(position);
    186 
    187             // Set the item title and description into the view.
    188             // This example does not render real HTML, so as a hack to make
    189             // the description look better, we strip out the
    190             // tags and take just the first SNIPPET_LENGTH chars.
    191             view.getText1().setText(item.getTitle());
    192             String descr = item.getDescription().toString();
    193             descr = removeTags(descr);
    194             view.getText2().setText(descr.substring(0, Math.min(descr.length(), SNIPPET_LENGTH)));
    195             return view;
    196         }
    197 
    198     }
    199 
    200     /**
    201      * Simple code to strip out <tag>s -- primitive way to sortof display HTML as
    202      * plain text.
    203      */
    204     public String removeTags(String str) {
    205         str = str.replaceAll("<.*?>", " ");
    206         str = str.replaceAll("\\s+", " ");
    207         return str;
    208     }
    209 
    210     /**
    211      * Called when user clicks an item in the list. Starts an activity to
    212      * open the url for that item.
    213      */
    214     @Override
    215     protected void onListItemClick(ListView l, View v, int position, long id) {
    216         RssItem item = mAdapter.getItem(position);
    217 
    218         // Creates and starts an intent to open the item.link url.
    219         Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(item.getLink().toString()));
    220         startActivity(intent);
    221     }
    222 
    223     /**
    224      * Resets the output UI -- list and status text empty.
    225      */
    226     public void resetUI() {
    227         // Reset the list to be empty.
    228         List<RssItem> items = new ArrayList<RssItem>();
    229         mAdapter = new RSSListAdapter(this, items);
    230         getListView().setAdapter(mAdapter);
    231 
    232         mStatusText.setText("");
    233         mUrlText.requestFocus();
    234     }
    235 
    236     /**
    237      * Sets the currently active running worker. Interrupts any earlier worker,
    238      * so we only have one at a time.
    239      *
    240      * @param worker the new worker
    241      */
    242     public synchronized void setCurrentWorker(RSSWorker worker) {
    243         if (mWorker != null) mWorker.interrupt();
    244         mWorker = worker;
    245     }
    246 
    247     /**
    248      * Is the given worker the currently active one.
    249      *
    250      * @param worker
    251      * @return
    252      */
    253     public synchronized boolean isCurrentWorker(RSSWorker worker) {
    254         return (mWorker == worker);
    255     }
    256 
    257     /**
    258      * Given an rss url string, starts the rss-download-thread going.
    259      *
    260      * @param rssUrl
    261      */
    262     private void doRSS(CharSequence rssUrl) {
    263         RSSWorker worker = new RSSWorker(rssUrl);
    264         setCurrentWorker(worker);
    265 
    266         resetUI();
    267         mStatusText.setText("Downloading\u2026");
    268 
    269         worker.start();
    270     }
    271 
    272     /**
    273      * Runnable that the worker thread uses to post RssItems to the
    274      * UI via mHandler.post
    275      */
    276     private class ItemAdder implements Runnable {
    277         RssItem mItem;
    278 
    279         ItemAdder(RssItem item) {
    280             mItem = item;
    281         }
    282 
    283         public void run() {
    284             mAdapter.add(mItem);
    285         }
    286 
    287         // NOTE: Performance idea -- would be more efficient to have he option
    288         // to add multiple items at once, so you get less "update storm" in the UI
    289         // compared to adding things one at a time.
    290     }
    291 
    292     /**
    293      * Worker thread takes in an rss url string, downloads its data, parses
    294      * out the rss items, and communicates them back to the UI as they are read.
    295      */
    296     private class RSSWorker extends Thread {
    297         private CharSequence mUrl;
    298 
    299         public RSSWorker(CharSequence url) {
    300             mUrl = url;
    301         }
    302 
    303         @Override
    304         public void run() {
    305             String status = "";
    306             try {
    307                 // Standard code to make an HTTP connection.
    308                 URL url = new URL(mUrl.toString());
    309                 URLConnection connection = url.openConnection();
    310                 connection.setConnectTimeout(10000);
    311 
    312                 connection.connect();
    313                 InputStream in = connection.getInputStream();
    314 
    315                 parseRSS(in, mAdapter);
    316                 status = "done";
    317             } catch (Exception e) {
    318                 status = "failed:" + e.getMessage();
    319             }
    320 
    321             // Send status to UI (unless a newer worker has started)
    322             // To communicate back to the UI from a worker thread,
    323             // pass a Runnable to handler.post().
    324             final String temp = status;
    325             if (isCurrentWorker(this)) {
    326                 mHandler.post(new Runnable() {
    327                     public void run() {
    328                         mStatusText.setText(temp);
    329                     }
    330                 });
    331             }
    332         }
    333     }
    334 
    335     /**
    336      * Populates the menu.
    337      */
    338     @Override
    339     public boolean onCreateOptionsMenu(Menu menu) {
    340         super.onCreateOptionsMenu(menu);
    341 
    342         menu.add(0, 0, 0, "Slashdot")
    343             .setOnMenuItemClickListener(new RSSMenu("http://rss.slashdot.org/Slashdot/slashdot"));
    344 
    345         menu.add(0, 0, 0, "Google News")
    346             .setOnMenuItemClickListener(new RSSMenu("http://news.google.com/?output=rss"));
    347 
    348         menu.add(0, 0, 0, "News.com")
    349             .setOnMenuItemClickListener(new RSSMenu("http://news.com.com/2547-1_3-0-20.xml"));
    350 
    351         menu.add(0, 0, 0, "Bad Url")
    352             .setOnMenuItemClickListener(new RSSMenu("http://nifty.stanford.edu:8080"));
    353 
    354         menu.add(0, 0, 0, "Reset")
    355                 .setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
    356             public boolean onMenuItemClick(MenuItem item) {
    357                 resetUI();
    358                 return true;
    359             }
    360         });
    361 
    362         return true;
    363     }
    364 
    365     /**
    366      * Puts text in the url text field and gives it focus. Used to make a Runnable
    367      * for each menu item. This way, one inner class works for all items vs. an
    368      * anonymous inner class for each menu item.
    369      */
    370     private class RSSMenu implements MenuItem.OnMenuItemClickListener {
    371         private CharSequence mUrl;
    372 
    373         RSSMenu(CharSequence url) {
    374             mUrl = url;
    375         }
    376 
    377         public boolean onMenuItemClick(MenuItem item) {
    378             mUrlText.setText(mUrl);
    379             mUrlText.requestFocus();
    380             return true;
    381         }
    382     }
    383 
    384 
    385     /**
    386      * Called for us to save out our current state before we are paused,
    387      * such a for example if the user switches to another app and memory
    388      * gets scarce. The given outState is a Bundle to which we can save
    389      * objects, such as Strings, Integers or lists of Strings. In this case, we
    390      * save out the list of currently downloaded rss data, (so we don't have to
    391      * re-do all the networking just because the user goes back and forth
    392      * between aps) which item is currently selected, and the data for the text views.
    393      * In onRestoreInstanceState() we look at the map to reconstruct the run-state of the
    394      * application, so returning to the activity looks seamlessly correct.
    395      * TODO: the Activity javadoc should give more detail about what sort of
    396      * data can go in the outState map.
    397      *
    398      * @see android.app.Activity#onSaveInstanceState
    399      */
    400     @SuppressWarnings("unchecked")
    401     @Override
    402     protected void onSaveInstanceState(Bundle outState) {
    403         super.onSaveInstanceState(outState);
    404 
    405         // Make a List of all the RssItem data for saving
    406         // NOTE: there may be a way to save the RSSItems directly,
    407         // rather than their string data.
    408         int count = mAdapter.getCount();
    409 
    410         // Save out the items as a flat list of CharSequence objects --
    411         // title0, link0, descr0, title1, link1, ...
    412         ArrayList<CharSequence> strings = new ArrayList<CharSequence>();
    413         for (int i = 0; i < count; i++) {
    414             RssItem item = mAdapter.getItem(i);
    415             strings.add(item.getTitle());
    416             strings.add(item.getLink());
    417             strings.add(item.getDescription());
    418         }
    419         outState.putSerializable(STRINGS_KEY, strings);
    420 
    421         // Save current selection index (if focussed)
    422         if (getListView().hasFocus()) {
    423             outState.putInt(SELECTION_KEY, Integer.valueOf(getListView().getSelectedItemPosition()));
    424         }
    425 
    426         // Save url
    427         outState.putString(URL_KEY, mUrlText.getText().toString());
    428 
    429         // Save status
    430         outState.putCharSequence(STATUS_KEY, mStatusText.getText());
    431     }
    432 
    433     /**
    434      * Called to "thaw" re-animate the app from a previous onSaveInstanceState().
    435      *
    436      * @see android.app.Activity#onRestoreInstanceState
    437      */
    438     @SuppressWarnings("unchecked")
    439     @Override
    440     protected void onRestoreInstanceState(Bundle state) {
    441         super.onRestoreInstanceState(state);
    442 
    443         // Note: null is a legal value for onRestoreInstanceState.
    444         if (state == null) return;
    445 
    446         // Restore items from the big list of CharSequence objects
    447         List<CharSequence> strings = (ArrayList<CharSequence>)state.getSerializable(STRINGS_KEY);
    448         List<RssItem> items = new ArrayList<RssItem>();
    449         for (int i = 0; i < strings.size(); i += 3) {
    450             items.add(new RssItem(strings.get(i), strings.get(i + 1), strings.get(i + 2)));
    451         }
    452 
    453         // Reset the list view to show this data.
    454         mAdapter = new RSSListAdapter(this, items);
    455         getListView().setAdapter(mAdapter);
    456 
    457         // Restore selection
    458         if (state.containsKey(SELECTION_KEY)) {
    459             getListView().requestFocus(View.FOCUS_FORWARD);
    460             // todo: is above right? needed it to work
    461             getListView().setSelection(state.getInt(SELECTION_KEY));
    462         }
    463 
    464         // Restore url
    465         mUrlText.setText(state.getCharSequence(URL_KEY));
    466 
    467         // Restore status
    468         mStatusText.setText(state.getCharSequence(STATUS_KEY));
    469     }
    470 
    471 
    472 
    473     /**
    474      * Does rudimentary RSS parsing on the given stream and posts rss items to
    475      * the UI as they are found. Uses Android's XmlPullParser facility. This is
    476      * not a production quality RSS parser -- it just does a basic job of it.
    477      *
    478      * @param in stream to read
    479      * @param adapter adapter for ui events
    480      */
    481     void parseRSS(InputStream in, RSSListAdapter adapter) throws IOException,
    482             XmlPullParserException {
    483         // TODO: switch to sax
    484 
    485         XmlPullParser xpp = Xml.newPullParser();
    486         xpp.setInput(in, null);  // null = default to UTF-8
    487 
    488         int eventType;
    489         String title = "";
    490         String link = "";
    491         String description = "";
    492         eventType = xpp.getEventType();
    493         while (eventType != XmlPullParser.END_DOCUMENT) {
    494             if (eventType == XmlPullParser.START_TAG) {
    495                 String tag = xpp.getName();
    496                 if (tag.equals("item")) {
    497                     title = link = description = "";
    498                 } else if (tag.equals("title")) {
    499                     xpp.next(); // Skip to next element -- assume text is directly inside the tag
    500                     title = xpp.getText();
    501                 } else if (tag.equals("link")) {
    502                     xpp.next();
    503                     link = xpp.getText();
    504                 } else if (tag.equals("description")) {
    505                     xpp.next();
    506                     description = xpp.getText();
    507                 }
    508             } else if (eventType == XmlPullParser.END_TAG) {
    509                 // We have a comlete item -- post it back to the UI
    510                 // using the mHandler (necessary because we are not
    511                 // running on the UI thread).
    512                 String tag = xpp.getName();
    513                 if (tag.equals("item")) {
    514                     RssItem item = new RssItem(title, link, description);
    515                     mHandler.post(new ItemAdder(item));
    516                 }
    517             }
    518             eventType = xpp.next();
    519         }
    520     }
    521 
    522     // SAX version of the code to do the parsing.
    523     /*
    524     private class RSSHandler extends DefaultHandler {
    525         RSSListAdapter mAdapter;
    526 
    527         String mTitle;
    528         String mLink;
    529         String mDescription;
    530 
    531         StringBuilder mBuff;
    532 
    533         boolean mInItem;
    534 
    535         public RSSHandler(RSSListAdapter adapter) {
    536             mAdapter = adapter;
    537             mInItem = false;
    538             mBuff = new StringBuilder();
    539         }
    540 
    541         public void startElement(String uri,
    542                 String localName,
    543                 String qName,
    544                 Attributes atts)
    545                 throws SAXException {
    546             String tag = localName;
    547             if (tag.equals("")) tag = qName;
    548 
    549             // If inside <item>, clear out buff on each tag start
    550             if (mInItem) {
    551                 mBuff.delete(0, mBuff.length());
    552             }
    553 
    554             if (tag.equals("item")) {
    555                 mTitle = mLink = mDescription = "";
    556                 mInItem = true;
    557             }
    558         }
    559 
    560         public void characters(char[] ch,
    561                       int start,
    562                       int length)
    563                       throws SAXException {
    564             // Buffer up all the chars when inside <item>
    565             if (mInItem) mBuff.append(ch, start, length);
    566         }
    567 
    568         public void endElement(String uri,
    569                       String localName,
    570                       String qName)
    571                       throws SAXException {
    572             String tag = localName;
    573             if (tag.equals("")) tag = qName;
    574 
    575             // For each tag, copy buff chars to right variable
    576             if (tag.equals("title")) mTitle = mBuff.toString();
    577             else if (tag.equals("link")) mLink = mBuff.toString();
    578             if (tag.equals("description")) mDescription = mBuff.toString();
    579 
    580             // Have all the data at this point .... post it to the UI.
    581             if (tag.equals("item")) {
    582                 RssItem item = new RssItem(mTitle, mLink, mDescription);
    583                 mHandler.post(new ItemAdder(item));
    584                 mInItem = false;
    585             }
    586         }
    587     }
    588     */
    589 
    590     /*
    591     public void parseRSS2(InputStream in, RSSListAdapter adapter) throws IOException {
    592             SAXParser parser = SAXParserFactory.newInstance().newSAXParser();
    593             DefaultHandler handler = new RSSHandler(adapter);
    594 
    595             parser.parse(in, handler);
    596             // TODO: does the parser figure out the encoding right on its own?
    597     }
    598     */
    599 }
    600