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 the 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