Initial Contribution
This commit is contained in:
@@ -0,0 +1,599 @@
|
||||
/*
|
||||
* 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 = parser figures out encoding
|
||||
|
||||
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?
|
||||
}
|
||||
*/
|
||||
}
|
||||
Reference in New Issue
Block a user