From 2f8cc17f5fbc2e05ac0889fbbddf4e530750087b Mon Sep 17 00:00:00 2001 From: Katie McCormick Date: Fri, 16 Mar 2012 17:47:55 -0700 Subject: cherrypick from ics-mr1 docs: source for nw app Change-Id: If50f407a0e56fa802fe9beedaa650e3a131872b2 Change-Id: I55d8668f4065129c844ada239f268a6621df4780 --- samples/training/network-usage/AndroidManifest.xml | 48 +++ samples/training/network-usage/README.txt | 14 + .../res/drawable-hdpi/ic_launcher.png | Bin 0 -> 4147 bytes .../res/drawable-ldpi/ic_launcher.png | Bin 0 -> 1723 bytes .../res/drawable-mdpi/ic_launcher.png | Bin 0 -> 2574 bytes samples/training/network-usage/res/layout/main.xml | 12 + .../training/network-usage/res/menu/mainmenu.xml | 24 ++ .../training/network-usage/res/values/arrays.xml | 28 ++ .../training/network-usage/res/values/strings.xml | 35 +++ .../training/network-usage/res/xml/preferences.xml | 33 +++ .../android/networkusage/NetworkActivity.java | 321 +++++++++++++++++++++ .../android/networkusage/SettingsActivity.java | 66 +++++ .../networkusage/StackOverflowXmlParser.java | 169 +++++++++++ 13 files changed, 750 insertions(+) create mode 100644 samples/training/network-usage/AndroidManifest.xml create mode 100644 samples/training/network-usage/README.txt create mode 100644 samples/training/network-usage/res/drawable-hdpi/ic_launcher.png create mode 100644 samples/training/network-usage/res/drawable-ldpi/ic_launcher.png create mode 100644 samples/training/network-usage/res/drawable-mdpi/ic_launcher.png create mode 100644 samples/training/network-usage/res/layout/main.xml create mode 100644 samples/training/network-usage/res/menu/mainmenu.xml create mode 100644 samples/training/network-usage/res/values/arrays.xml create mode 100644 samples/training/network-usage/res/values/strings.xml create mode 100644 samples/training/network-usage/res/xml/preferences.xml create mode 100644 samples/training/network-usage/src/com/example/android/networkusage/NetworkActivity.java create mode 100644 samples/training/network-usage/src/com/example/android/networkusage/SettingsActivity.java create mode 100644 samples/training/network-usage/src/com/example/android/networkusage/StackOverflowXmlParser.java (limited to 'samples') diff --git a/samples/training/network-usage/AndroidManifest.xml b/samples/training/network-usage/AndroidManifest.xml new file mode 100644 index 0000000..4b96d14 --- /dev/null +++ b/samples/training/network-usage/AndroidManifest.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/training/network-usage/README.txt b/samples/training/network-usage/README.txt new file mode 100644 index 0000000..cc9a7a0 --- /dev/null +++ b/samples/training/network-usage/README.txt @@ -0,0 +1,14 @@ +README +====== + +This Network Usage sample app does the following: + +-- Downloads an XML feed from StackOverflow.com for the most recent posts tagged "android". + +-- Parses the XML feed, combines feed elements with HTML markup, and displays the resulting HTML in the UI. + +-- Lets users control their network data usage through a settings UI. Users can choose to fetch the feed + when any network connection is available, or only when a Wi-Fi connection is available. + +-- Detects when there is a change in the device's connection status and responds accordingly. For example, if + the device loses its network connection, the app will not attempt to download the feed. diff --git a/samples/training/network-usage/res/drawable-hdpi/ic_launcher.png b/samples/training/network-usage/res/drawable-hdpi/ic_launcher.png new file mode 100644 index 0000000..8074c4c Binary files /dev/null and b/samples/training/network-usage/res/drawable-hdpi/ic_launcher.png differ diff --git a/samples/training/network-usage/res/drawable-ldpi/ic_launcher.png b/samples/training/network-usage/res/drawable-ldpi/ic_launcher.png new file mode 100644 index 0000000..1095584 Binary files /dev/null and b/samples/training/network-usage/res/drawable-ldpi/ic_launcher.png differ diff --git a/samples/training/network-usage/res/drawable-mdpi/ic_launcher.png b/samples/training/network-usage/res/drawable-mdpi/ic_launcher.png new file mode 100644 index 0000000..a07c69f Binary files /dev/null and b/samples/training/network-usage/res/drawable-mdpi/ic_launcher.png differ diff --git a/samples/training/network-usage/res/layout/main.xml b/samples/training/network-usage/res/layout/main.xml new file mode 100644 index 0000000..8498934 --- /dev/null +++ b/samples/training/network-usage/res/layout/main.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/samples/training/network-usage/res/menu/mainmenu.xml b/samples/training/network-usage/res/menu/mainmenu.xml new file mode 100644 index 0000000..17d44db --- /dev/null +++ b/samples/training/network-usage/res/menu/mainmenu.xml @@ -0,0 +1,24 @@ + + + + + + + + diff --git a/samples/training/network-usage/res/values/arrays.xml b/samples/training/network-usage/res/values/arrays.xml new file mode 100644 index 0000000..2e8b8a7 --- /dev/null +++ b/samples/training/network-usage/res/values/arrays.xml @@ -0,0 +1,28 @@ + + + + + + + Only when on Wi-Fi + On any network + + + Wi-Fi + Any + + diff --git a/samples/training/network-usage/res/values/strings.xml b/samples/training/network-usage/res/values/strings.xml new file mode 100644 index 0000000..d7c702f --- /dev/null +++ b/samples/training/network-usage/res/values/strings.xml @@ -0,0 +1,35 @@ + + + + + + + NetworkUsage + + + Settings + Refresh + + + Newest StackOverflow questions tagged \'android\' + Last updated: + Lost connection. + Wi-Fi reconnected. + Unable to load content. Check your network connection. + Error parsing XML. + + diff --git a/samples/training/network-usage/res/xml/preferences.xml b/samples/training/network-usage/res/xml/preferences.xml new file mode 100644 index 0000000..801ba79 --- /dev/null +++ b/samples/training/network-usage/res/xml/preferences.xml @@ -0,0 +1,33 @@ + + + + + + + + diff --git a/samples/training/network-usage/src/com/example/android/networkusage/NetworkActivity.java b/samples/training/network-usage/src/com/example/android/networkusage/NetworkActivity.java new file mode 100644 index 0000000..b7ed331 --- /dev/null +++ b/samples/training/network-usage/src/com/example/android/networkusage/NetworkActivity.java @@ -0,0 +1,321 @@ +/* + * Copyright (C) 2012 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.networkusage; + +import android.app.Activity; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.SharedPreferences; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.os.AsyncTask; +import android.os.Bundle; +import android.preference.PreferenceManager; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.webkit.WebView; +import android.widget.Toast; + +import com.example.android.networkusage.R; +import com.example.android.networkusage.StackOverflowXmlParser.Entry; + +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.List; + + +/** + * Main Activity for the sample application. + * + * This activity does the following: + * + * o Presents a WebView screen to users. This WebView has a list of HTML links to the latest + * questions tagged 'android' on stackoverflow.com. + * + * o Parses the StackOverflow XML feed using XMLPullParser. + * + * o Uses AsyncTask to download and process the XML feed. + * + * o Monitors preferences and the device's network connection to determine whether + * to refresh the WebView content. + */ +public class NetworkActivity extends Activity { + public static final String WIFI = "Wi-Fi"; + public static final String ANY = "Any"; + private static final String URL = + "http://stackoverflow.com/feeds/tag?tagnames=android&sort=newest"; + + // Whether there is a Wi-Fi connection. + private static boolean wifiConnected = false; + // Whether there is a mobile connection. + private static boolean mobileConnected = false; + // Whether the display should be refreshed. + public static boolean refreshDisplay = true; + + // The user's current network preference setting. + public static String sPref = null; + + // The BroadcastReceiver that tracks network connectivity changes. + private NetworkReceiver receiver = new NetworkReceiver(); + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // Register BroadcastReceiver to track connection changes. + IntentFilter filter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION); + receiver = new NetworkReceiver(); + this.registerReceiver(receiver, filter); + } + + // Refreshes the display if the network connection and the + // pref settings allow it. + @Override + public void onStart() { + super.onStart(); + + // Gets the user's network preference settings + SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this); + + // Retrieves a string value for the preferences. The second parameter + // is the default value to use if a preference value is not found. + sPref = sharedPrefs.getString("listPref", "Wi-Fi"); + + updateConnectedFlags(); + + // Only loads the page if refreshDisplay is true. Otherwise, keeps previous + // display. For example, if the user has set "Wi-Fi only" in prefs and the + // device loses its Wi-Fi connection midway through the user using the app, + // you don't want to refresh the display--this would force the display of + // an error page instead of stackoverflow.com content. + if (refreshDisplay) { + loadPage(); + } + } + + @Override + public void onDestroy() { + super.onDestroy(); + if (receiver != null) { + this.unregisterReceiver(receiver); + } + } + + // Checks the network connection and sets the wifiConnected and mobileConnected + // variables accordingly. + private void updateConnectedFlags() { + ConnectivityManager connMgr = + (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); + + NetworkInfo activeInfo = connMgr.getActiveNetworkInfo(); + if (activeInfo != null && activeInfo.isConnected()) { + wifiConnected = activeInfo.getType() == ConnectivityManager.TYPE_WIFI; + mobileConnected = activeInfo.getType() == ConnectivityManager.TYPE_MOBILE; + } else { + wifiConnected = false; + mobileConnected = false; + } + } + + // Uses AsyncTask subclass to download the XML feed from stackoverflow.com. + // This avoids UI lock up. To prevent network operations from + // causing a delay that results in a poor user experience, always perform + // network operations on a separate thread from the UI. + private void loadPage() { + if (((sPref.equals(ANY)) && (wifiConnected || mobileConnected)) + || ((sPref.equals(WIFI)) && (wifiConnected))) { + // AsyncTask subclass + new DownloadXmlTask().execute(URL); + } else { + showErrorPage(); + } + } + + // Displays an error if the app is unable to load content. + private void showErrorPage() { + setContentView(R.layout.main); + + // The specified network connection is not available. Displays error message. + WebView myWebView = (WebView) findViewById(R.id.webview); + myWebView.loadData(getResources().getString(R.string.connection_error), + "text/html", null); + } + + // Populates the activity's options menu. + @Override + public boolean onCreateOptionsMenu(Menu menu) { + MenuInflater inflater = getMenuInflater(); + inflater.inflate(R.menu.mainmenu, menu); + return true; + } + + // Handles the user's menu selection. + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.settings: + Intent settingsActivity = new Intent(getBaseContext(), SettingsActivity.class); + startActivity(settingsActivity); + return true; + case R.id.refresh: + loadPage(); + return true; + default: + return super.onOptionsItemSelected(item); + } + } + + // Implementation of AsyncTask used to download XML feed from stackoverflow.com. + private class DownloadXmlTask extends AsyncTask { + + @Override + protected String doInBackground(String... urls) { + try { + return loadXmlFromNetwork(urls[0]); + } catch (IOException e) { + return getResources().getString(R.string.connection_error); + } catch (XmlPullParserException e) { + return getResources().getString(R.string.xml_error); + } + } + + @Override + protected void onPostExecute(String result) { + setContentView(R.layout.main); + // Displays the HTML string in the UI via a WebView + WebView myWebView = (WebView) findViewById(R.id.webview); + myWebView.loadData(result, "text/html", null); + } + } + + // Uploads XML from stackoverflow.com, parses it, and combines it with + // HTML markup. Returns HTML string. + private String loadXmlFromNetwork(String urlString) throws XmlPullParserException, IOException { + InputStream stream = null; + StackOverflowXmlParser stackOverflowXmlParser = new StackOverflowXmlParser(); + List entries = null; + String title = null; + String url = null; + String summary = null; + Calendar rightNow = Calendar.getInstance(); + DateFormat formatter = new SimpleDateFormat("MMM dd h:mmaa"); + + // Checks whether the user set the preference to include summary text + SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this); + boolean pref = sharedPrefs.getBoolean("summaryPref", false); + + StringBuilder htmlString = new StringBuilder(); + htmlString.append("

" + getResources().getString(R.string.page_title) + "

"); + htmlString.append("" + getResources().getString(R.string.updated) + " " + + formatter.format(rightNow.getTime()) + ""); + + try { + stream = downloadUrl(urlString); + entries = stackOverflowXmlParser.parse(stream); + // Makes sure that the InputStream is closed after the app is + // finished using it. + } finally { + if (stream != null) { + stream.close(); + } + } + + // StackOverflowXmlParser returns a List (called "entries") of Entry objects. + // Each Entry object represents a single post in the XML feed. + // This section processes the entries list to combine each entry with HTML markup. + // Each entry is displayed in the UI as a link that optionally includes + // a text summary. + for (Entry entry : entries) { + htmlString.append("

" + entry.title + "

"); + // If the user set the preference to include summary text, + // adds it to the display. + if (pref) { + htmlString.append(entry.summary); + } + } + return htmlString.toString(); + } + + // Given a string representation of a URL, sets up a connection and gets + // an input stream. + private InputStream downloadUrl(String urlString) throws IOException { + URL url = new URL(urlString); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setReadTimeout(10000 /* milliseconds */); + conn.setConnectTimeout(15000 /* milliseconds */); + conn.setRequestMethod("GET"); + conn.setDoInput(true); + // Starts the query + conn.connect(); + InputStream stream = conn.getInputStream(); + return stream; + } + + /** + * + * This BroadcastReceiver intercepts the android.net.ConnectivityManager.CONNECTIVITY_ACTION, + * which indicates a connection change. It checks whether the type is TYPE_WIFI. + * If it is, it checks whether Wi-Fi is connected and sets the wifiConnected flag in the + * main activity accordingly. + * + */ + public class NetworkReceiver extends BroadcastReceiver { + + @Override + public void onReceive(Context context, Intent intent) { + ConnectivityManager connMgr = + (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo networkInfo = connMgr.getActiveNetworkInfo(); + + // Checks the user prefs and the network connection. Based on the result, decides + // whether + // to refresh the display or keep the current display. + // If the userpref is Wi-Fi only, checks to see if the device has a Wi-Fi connection. + if (WIFI.equals(sPref) && networkInfo != null + && networkInfo.getType() == ConnectivityManager.TYPE_WIFI) { + // If device has its Wi-Fi connection, sets refreshDisplay + // to true. This causes the display to be refreshed when the user + // returns to the app. + refreshDisplay = true; + Toast.makeText(context, R.string.wifi_connected, Toast.LENGTH_SHORT).show(); + + // If the setting is ANY network and there is a network connection + // (which by process of elimination would be mobile), sets refreshDisplay to true. + } else if (ANY.equals(sPref) && networkInfo != null) { + refreshDisplay = true; + + // Otherwise, the app can't download content--either because there is no network + // connection (mobile or Wi-Fi), or because the pref setting is WIFI, and there + // is no Wi-Fi connection. + // Sets refreshDisplay to false. + } else { + refreshDisplay = false; + Toast.makeText(context, R.string.lost_connection, Toast.LENGTH_SHORT).show(); + } + } + } +} diff --git a/samples/training/network-usage/src/com/example/android/networkusage/SettingsActivity.java b/samples/training/network-usage/src/com/example/android/networkusage/SettingsActivity.java new file mode 100644 index 0000000..73b72d2 --- /dev/null +++ b/samples/training/network-usage/src/com/example/android/networkusage/SettingsActivity.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2012 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.networkusage; + +import android.content.SharedPreferences; +import android.content.SharedPreferences.OnSharedPreferenceChangeListener; +import android.os.Bundle; +import android.preference.PreferenceActivity; +import com.example.android.networkusage.R; + +/** + * This preference activity has in its manifest declaration an intent filter for + * the ACTION_MANAGE_NETWORK_USAGE action. This activity provides a settings UI + * for users to specify network settings to control data usage. + */ +public class SettingsActivity extends PreferenceActivity + implements + OnSharedPreferenceChangeListener { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // Loads the XML preferences file. + addPreferencesFromResource(R.xml.preferences); + } + + @Override + protected void onResume() { + super.onResume(); + + // Registers a callback to be invoked whenever a user changes a preference. + getPreferenceScreen().getSharedPreferences().registerOnSharedPreferenceChangeListener(this); + } + + @Override + protected void onPause() { + super.onPause(); + + // Unregisters the listener set in onResume(). + // It's best practice to unregister listeners when your app isn't using them to cut down on + // unnecessary system overhead. You do this in onPause(). + getPreferenceScreen() + .getSharedPreferences().unregisterOnSharedPreferenceChangeListener(this); + } + + // Fires when the user changes a preference. + @Override + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { + // Sets refreshDisplay to true so that when the user returns to the main + // activity, the display refreshes to reflect the new settings. + NetworkActivity.refreshDisplay = true; + } +} diff --git a/samples/training/network-usage/src/com/example/android/networkusage/StackOverflowXmlParser.java b/samples/training/network-usage/src/com/example/android/networkusage/StackOverflowXmlParser.java new file mode 100644 index 0000000..6a01098 --- /dev/null +++ b/samples/training/network-usage/src/com/example/android/networkusage/StackOverflowXmlParser.java @@ -0,0 +1,169 @@ +/* + * Copyright (C) 2012 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.networkusage; + +import android.util.Xml; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +/** + * This class parses XML feeds from stackoverflow.com. + * Given an InputStream representation of a feed, it returns a List of entries, + * where each list element represents a single entry (post) in the XML feed. + */ +public class StackOverflowXmlParser { + private static final String ns = null; + + // We don't use namespaces + + public List parse(InputStream in) throws XmlPullParserException, IOException { + try { + XmlPullParser parser = Xml.newPullParser(); + parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false); + parser.setInput(in, null); + parser.nextTag(); + return readFeed(parser); + } finally { + in.close(); + } + } + + private List readFeed(XmlPullParser parser) throws XmlPullParserException, IOException { + List entries = new ArrayList(); + + parser.require(XmlPullParser.START_TAG, ns, "feed"); + while (parser.next() != XmlPullParser.END_TAG) { + if (parser.getEventType() != XmlPullParser.START_TAG) { + continue; + } + String name = parser.getName(); + // Starts by looking for the entry tag + if (name.equals("entry")) { + entries.add(readEntry(parser)); + } else { + skip(parser); + } + } + return entries; + } + + // This class represents a single entry (post) in the XML feed. + // It includes the data members "title," "link," and "summary." + public static class Entry { + public final String title; + public final String link; + public final String summary; + + private Entry(String title, String summary, String link) { + this.title = title; + this.summary = summary; + this.link = link; + } + } + + // Parses the contents of an entry. If it encounters a title, summary, or link tag, hands them + // off + // to their respective "read" methods for processing. Otherwise, skips the tag. + private Entry readEntry(XmlPullParser parser) throws XmlPullParserException, IOException { + parser.require(XmlPullParser.START_TAG, ns, "entry"); + String title = null; + String summary = null; + String link = null; + while (parser.next() != XmlPullParser.END_TAG) { + if (parser.getEventType() != XmlPullParser.START_TAG) { + continue; + } + String name = parser.getName(); + if (name.equals("title")) { + title = readTitle(parser); + } else if (name.equals("summary")) { + summary = readSummary(parser); + } else if (name.equals("link")) { + link = readLink(parser); + } else { + skip(parser); + } + } + return new Entry(title, summary, link); + } + + // Processes title tags in the feed. + private String readTitle(XmlPullParser parser) throws IOException, XmlPullParserException { + parser.require(XmlPullParser.START_TAG, ns, "title"); + String title = readText(parser); + parser.require(XmlPullParser.END_TAG, ns, "title"); + return title; + } + + // Processes link tags in the feed. + private String readLink(XmlPullParser parser) throws IOException, XmlPullParserException { + String link = ""; + parser.require(XmlPullParser.START_TAG, ns, "link"); + String tag = parser.getName(); + String relType = parser.getAttributeValue(null, "rel"); + if (tag.equals("link")) { + if (relType.equals("alternate")) { + link = parser.getAttributeValue(null, "href"); + parser.nextTag(); + } + } + parser.require(XmlPullParser.END_TAG, ns, "link"); + return link; + } + + // Processes summary tags in the feed. + private String readSummary(XmlPullParser parser) throws IOException, XmlPullParserException { + parser.require(XmlPullParser.START_TAG, ns, "summary"); + String summary = readText(parser); + parser.require(XmlPullParser.END_TAG, ns, "summary"); + return summary; + } + + // For the tags title and summary, extracts their text values. + private String readText(XmlPullParser parser) throws IOException, XmlPullParserException { + String result = ""; + if (parser.next() == XmlPullParser.TEXT) { + result = parser.getText(); + parser.nextTag(); + } + return result; + } + + // Skips tags the parser isn't interested in. Uses depth to handle nested tags. i.e., + // if the next tag after a START_TAG isn't a matching END_TAG, it keeps going until it + // finds the matching END_TAG (as indicated by the value of "depth" being 0). + private void skip(XmlPullParser parser) throws XmlPullParserException, IOException { + if (parser.getEventType() != XmlPullParser.START_TAG) { + throw new IllegalStateException(); + } + int depth = 1; + while (depth != 0) { + switch (parser.next()) { + case XmlPullParser.END_TAG: + depth--; + break; + case XmlPullParser.START_TAG: + depth++; + break; + } + } + } +} -- cgit v1.1