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
17package com.example.android.rssreader;
18
19import org.xmlpull.v1.XmlPullParser;
20import org.xmlpull.v1.XmlPullParserException;
21
22import android.app.ListActivity;
23import android.content.Context;
24import android.content.Intent;
25import android.net.Uri;
26import android.os.Bundle;
27import android.os.Handler;
28import android.view.Menu;
29import android.view.MenuItem;
30import android.view.View;
31import android.view.View.OnClickListener;
32import android.view.ViewGroup;
33import android.view.LayoutInflater;
34import android.widget.ArrayAdapter;
35import android.widget.Button;
36import android.widget.EditText;
37import android.widget.ListView;
38import android.widget.TextView;
39import android.widget.TwoLineListItem;
40import android.util.Xml;
41
42import java.io.IOException;
43import java.io.InputStream;
44import java.net.URL;
45import java.net.URLConnection;
46import java.util.ArrayList;
47import 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 */
73public 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