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
onCreate(Bundle savedInstanceState)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 
RSSListAdapter(Context context, List<RssItem> objects)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
getView(int position, View convertView, ViewGroup parent)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      */
removeTags(String str)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
onListItemClick(ListView l, View v, int position, long id)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      */
resetUI()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      */
setCurrentWorker(RSSWorker worker)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      */
isCurrentWorker(RSSWorker worker)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      */
doRSS(CharSequence rssUrl)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 
ItemAdder(RssItem item)279         ItemAdder(RssItem item) {
280             mItem = item;
281         }
282 
run()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 
RSSWorker(CharSequence url)299         public RSSWorker(CharSequence url) {
300             mUrl = url;
301         }
302 
303         @Override
run()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
onCreateOptionsMenu(Menu menu)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 
RSSMenu(CharSequence url)373         RSSMenu(CharSequence url) {
374             mUrl = url;
375         }
376 
onMenuItemClick(MenuItem item)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
onSaveInstanceState(Bundle outState)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
onRestoreInstanceState(Bundle state)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      */
parseRSS(InputStream in, RSSListAdapter adapter)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