diff --git a/samples/Wiktionary/Android.mk b/samples/Wiktionary/Android.mk new file mode 100644 index 000000000..d6ce1f16c --- /dev/null +++ b/samples/Wiktionary/Android.mk @@ -0,0 +1,16 @@ +LOCAL_PATH:= $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_MODULE_TAGS := samples + +# Only compile source java files in this apk. +LOCAL_SRC_FILES := $(call all-java-files-under, src) + +LOCAL_PACKAGE_NAME := Wiktionary + +LOCAL_SDK_VERSION := current + +include $(BUILD_PACKAGE) + +# Use the following include to make our test apk. +include $(call all-makefiles-under,$(LOCAL_PATH)) diff --git a/samples/Wiktionary/AndroidManifest.xml b/samples/Wiktionary/AndroidManifest.xml new file mode 100644 index 000000000..1641a8b0a --- /dev/null +++ b/samples/Wiktionary/AndroidManifest.xml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/Wiktionary/_index.html b/samples/Wiktionary/_index.html new file mode 100644 index 000000000..eb7cb96bb --- /dev/null +++ b/samples/Wiktionary/_index.html @@ -0,0 +1,10 @@ +

A sample application that demonstrates how to create an interactive widget +for display on the Android home screen.

+ +

When installed, this adds a "Wiktionary" option to the widget installation +menu. The word of the day is downloaded from Wiktionary and displayed in a +frame. Touching the widget will open a custom WebView to render the +definition.

+ + + diff --git a/samples/Wiktionary/res/anim/slide_in.xml b/samples/Wiktionary/res/anim/slide_in.xml new file mode 100644 index 000000000..3da074e09 --- /dev/null +++ b/samples/Wiktionary/res/anim/slide_in.xml @@ -0,0 +1,22 @@ + + + + + + diff --git a/samples/Wiktionary/res/anim/slide_out.xml b/samples/Wiktionary/res/anim/slide_out.xml new file mode 100644 index 000000000..ec21f521e --- /dev/null +++ b/samples/Wiktionary/res/anim/slide_out.xml @@ -0,0 +1,22 @@ + + + + + + diff --git a/samples/Wiktionary/res/drawable/app_icon.png b/samples/Wiktionary/res/drawable/app_icon.png new file mode 100644 index 000000000..2b1417aad Binary files /dev/null and b/samples/Wiktionary/res/drawable/app_icon.png differ diff --git a/samples/Wiktionary/res/drawable/ic_menu_shuffle.png b/samples/Wiktionary/res/drawable/ic_menu_shuffle.png new file mode 100755 index 000000000..cb7009dea Binary files /dev/null and b/samples/Wiktionary/res/drawable/ic_menu_shuffle.png differ diff --git a/samples/Wiktionary/res/drawable/logo_overlay.9.png b/samples/Wiktionary/res/drawable/logo_overlay.9.png new file mode 100644 index 000000000..851ceb15b Binary files /dev/null and b/samples/Wiktionary/res/drawable/logo_overlay.9.png differ diff --git a/samples/Wiktionary/res/drawable/lookup_bg.xml b/samples/Wiktionary/res/drawable/lookup_bg.xml new file mode 100644 index 000000000..46d76eb9b --- /dev/null +++ b/samples/Wiktionary/res/drawable/lookup_bg.xml @@ -0,0 +1,20 @@ + + + + + + + diff --git a/samples/Wiktionary/res/drawable/progress_spin.xml b/samples/Wiktionary/res/drawable/progress_spin.xml new file mode 100644 index 000000000..4594a181b --- /dev/null +++ b/samples/Wiktionary/res/drawable/progress_spin.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + diff --git a/samples/Wiktionary/res/drawable/star_logo.png b/samples/Wiktionary/res/drawable/star_logo.png new file mode 100644 index 000000000..b32d1756c Binary files /dev/null and b/samples/Wiktionary/res/drawable/star_logo.png differ diff --git a/samples/Wiktionary/res/drawable/widget_bg.xml b/samples/Wiktionary/res/drawable/widget_bg.xml new file mode 100644 index 000000000..c2b846277 --- /dev/null +++ b/samples/Wiktionary/res/drawable/widget_bg.xml @@ -0,0 +1,22 @@ + + + + + + + + + diff --git a/samples/Wiktionary/res/drawable/widget_bg_normal.9.png b/samples/Wiktionary/res/drawable/widget_bg_normal.9.png new file mode 100644 index 000000000..314eb8ef9 Binary files /dev/null and b/samples/Wiktionary/res/drawable/widget_bg_normal.9.png differ diff --git a/samples/Wiktionary/res/drawable/widget_bg_pressed.9.png b/samples/Wiktionary/res/drawable/widget_bg_pressed.9.png new file mode 100644 index 000000000..cc23e787b Binary files /dev/null and b/samples/Wiktionary/res/drawable/widget_bg_pressed.9.png differ diff --git a/samples/Wiktionary/res/drawable/widget_bg_selected.9.png b/samples/Wiktionary/res/drawable/widget_bg_selected.9.png new file mode 100644 index 000000000..ef0cdc066 Binary files /dev/null and b/samples/Wiktionary/res/drawable/widget_bg_selected.9.png differ diff --git a/samples/Wiktionary/res/layout/about.xml b/samples/Wiktionary/res/layout/about.xml new file mode 100644 index 000000000..3b25b3274 --- /dev/null +++ b/samples/Wiktionary/res/layout/about.xml @@ -0,0 +1,40 @@ + + + + + + + + + + diff --git a/samples/Wiktionary/res/layout/lookup.xml b/samples/Wiktionary/res/layout/lookup.xml new file mode 100644 index 000000000..43cffaa1a --- /dev/null +++ b/samples/Wiktionary/res/layout/lookup.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + diff --git a/samples/Wiktionary/res/layout/widget_message.xml b/samples/Wiktionary/res/layout/widget_message.xml new file mode 100644 index 000000000..ba9471447 --- /dev/null +++ b/samples/Wiktionary/res/layout/widget_message.xml @@ -0,0 +1,34 @@ + + + + + + + + diff --git a/samples/Wiktionary/res/layout/widget_word.xml b/samples/Wiktionary/res/layout/widget_word.xml new file mode 100644 index 000000000..0e76f0b80 --- /dev/null +++ b/samples/Wiktionary/res/layout/widget_word.xml @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + diff --git a/samples/Wiktionary/res/menu/lookup.xml b/samples/Wiktionary/res/menu/lookup.xml new file mode 100644 index 000000000..741ca9ab2 --- /dev/null +++ b/samples/Wiktionary/res/menu/lookup.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + diff --git a/samples/Wiktionary/res/values/strings.xml b/samples/Wiktionary/res/values/strings.xml new file mode 100644 index 000000000..38d993768 --- /dev/null +++ b/samples/Wiktionary/res/values/strings.xml @@ -0,0 +1,56 @@ + + + + + Wiktionary example + Example of a fast Wiktionary browser and Word-of-day widget + "All dictionary content provided by Wiktionary under a GFDL license. http://en.wiktionary.org\n\nIcon derived from Tango Desktop Project under a public domain license. http://tango.freedesktop.org" + + "%s/%s (Linux; Android)" + "Wiktionary:Word of the day/%s %s" + "http://en.wiktionary.org/wiki/%s" + + Wiktionary + + "Loading word\nof day\u2026" + No word of day found + + + January + February + March + April + May + June + July + August + September + October + November + December + + + + Wiktionary search + Define word + + Search + Random + About + + No entry found for this word, or problem reading data. + + diff --git a/samples/Wiktionary/res/values/styles.xml b/samples/Wiktionary/res/values/styles.xml new file mode 100644 index 000000000..45fc8f5a4 --- /dev/null +++ b/samples/Wiktionary/res/values/styles.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/Wiktionary/res/values/themes.xml b/samples/Wiktionary/res/values/themes.xml new file mode 100644 index 000000000..c4d7630f6 --- /dev/null +++ b/samples/Wiktionary/res/values/themes.xml @@ -0,0 +1,21 @@ + + + + + + diff --git a/samples/Wiktionary/res/xml/searchable.xml b/samples/Wiktionary/res/xml/searchable.xml new file mode 100644 index 000000000..02ee31fc5 --- /dev/null +++ b/samples/Wiktionary/res/xml/searchable.xml @@ -0,0 +1,19 @@ + + + + diff --git a/samples/Wiktionary/res/xml/widget_word.xml b/samples/Wiktionary/res/xml/widget_word.xml new file mode 100644 index 000000000..46d31c321 --- /dev/null +++ b/samples/Wiktionary/res/xml/widget_word.xml @@ -0,0 +1,21 @@ + + + + diff --git a/samples/Wiktionary/src/com/example/android/wiktionary/ExtendedWikiHelper.java b/samples/Wiktionary/src/com/example/android/wiktionary/ExtendedWikiHelper.java new file mode 100644 index 000000000..3a3917248 --- /dev/null +++ b/samples/Wiktionary/src/com/example/android/wiktionary/ExtendedWikiHelper.java @@ -0,0 +1,278 @@ +/* + * Copyright (C) 2009 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.wiktionary; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import android.net.Uri; +import android.text.TextUtils; +import android.webkit.WebView; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Extended version of {@link SimpleWikiHelper}. This version adds methods to + * pick a random word, and to format generic wiki-style text into HTML. + */ +public class ExtendedWikiHelper extends SimpleWikiHelper { + /** + * HTML style sheet to include with any {@link #formatWikiText(String)} HTML + * results. It formats nicely for a mobile screen, and hides some content + * boxes to keep things tidy. + */ + private static final String STYLE_SHEET = ""; + + /** + * Pattern of section titles we're interested in showing. This trims out + * extra sections that can clutter things up on a mobile screen. + */ + private static final Pattern sValidSections = + Pattern.compile("(verb|noun|adjective|pronoun|interjection)", Pattern.CASE_INSENSITIVE); + + /** + * Pattern that can be used to split a returned wiki page into its various + * sections. Doesn't treat children sections differently. + */ + private static final Pattern sSectionSplit = + Pattern.compile("^=+(.+?)=+.+?(?=^=)", Pattern.MULTILINE | Pattern.DOTALL); + + /** + * When picking random words in {@link #getRandomWord()}, we sometimes + * encounter special articles or templates. This pattern ignores any words + * like those, usually because they have ":" or other punctuation. + */ + private static final Pattern sInvalidWord = Pattern.compile("[^A-Za-z0-9 ]"); + + /** + * {@link Uri} authority to use when creating internal links. + */ + public static final String WIKI_AUTHORITY = "wiktionary"; + + /** + * {@link Uri} host to use when creating internal links. + */ + public static final String WIKI_LOOKUP_HOST = "lookup"; + + /** + * Mime-type to use when showing parsed results in a {@link WebView}. + */ + public static final String MIME_TYPE = "text/html"; + + /** + * Encoding to use when showing parsed results in a {@link WebView}. + */ + public static final String ENCODING = "utf-8"; + + /** + * {@link Uri} to use when requesting a random page. + */ + private static final String WIKTIONARY_RANDOM = + "http://en.wiktionary.org/w/api.php?action=query&list=random&format=json"; + + /** + * Fake section to insert at the bottom of a wiki response before parsing. + * This ensures that {@link #sSectionSplit} will always catch the last + * section, as it uses section headers in its searching. + */ + private static final String STUB_SECTION = "\n=Stub section="; + + /** + * Number of times to try finding a random word in {@link #getRandomWord()}. + * These failures are usually when the found word fails the + * {@link #sInvalidWord} test, or when a network error happens. + */ + private static final int RANDOM_TRIES = 3; + + /** + * Internal class to hold a wiki formatting rule. It's mostly a wrapper to + * simplify {@link Matcher#replaceAll(String)}. + */ + private static class FormatRule { + private Pattern mPattern; + private String mReplaceWith; + + /** + * Create a wiki formatting rule. + * + * @param pattern Search string to be compiled into a {@link Pattern}. + * @param replaceWith String to replace any found occurances with. This + * string can also include back-references into the given + * pattern. + * @param flags Any flags to compile the {@link Pattern} with. + */ + public FormatRule(String pattern, String replaceWith, int flags) { + mPattern = Pattern.compile(pattern, flags); + mReplaceWith = replaceWith; + } + + /** + * Create a wiki formatting rule. + * + * @param pattern Search string to be compiled into a {@link Pattern}. + * @param replaceWith String to replace any found occurances with. This + * string can also include back-references into the given + * pattern. + */ + public FormatRule(String pattern, String replaceWith) { + this(pattern, replaceWith, 0); + } + + /** + * Apply this formatting rule to the given input string, and return the + * resulting new string. + */ + public String apply(String input) { + Matcher m = mPattern.matcher(input); + return m.replaceAll(mReplaceWith); + } + + } + + /** + * List of internal formatting rules to apply when parsing wiki text. These + * include indenting various bullets, apply italic and bold styles, and + * adding internal linking. + */ + private static final List sFormatRules = new ArrayList(); + + static { + // Format header blocks and wrap outside content in ordered list + sFormatRules.add(new FormatRule("^=+(.+?)=+", "

$1

    ", + Pattern.MULTILINE)); + + // Indent quoted blocks, handle ordered and bullet lists + sFormatRules.add(new FormatRule("^#+\\*?:(.+?)$", "
    $1
    ", + Pattern.MULTILINE)); + sFormatRules.add(new FormatRule("^#+:?\\*(.+?)$", "
    • $1
    ", + Pattern.MULTILINE)); + sFormatRules.add(new FormatRule("^#+(.+?)$", "
  1. $1
  2. ", + Pattern.MULTILINE)); + + // Add internal links + sFormatRules.add(new FormatRule("\\[\\[([^:\\|\\]]+)\\]\\]", + String.format("$1", WIKI_AUTHORITY, WIKI_LOOKUP_HOST))); + sFormatRules.add(new FormatRule("\\[\\[([^:\\|\\]]+)\\|([^\\]]+)\\]\\]", + String.format("$2", WIKI_AUTHORITY, WIKI_LOOKUP_HOST))); + + // Add bold and italic formatting + sFormatRules.add(new FormatRule("'''(.+?)'''", "$1")); + sFormatRules.add(new FormatRule("([^'])''([^'].*?[^'])''([^'])", "$1$2$3")); + + // Remove odd category links and convert remaining links into flat text + sFormatRules.add(new FormatRule("(\\{+.+?\\}+|\\[\\[[^:]+:[^\\\\|\\]]+\\]\\]|" + + "\\[http.+?\\]|\\[\\[Category:.+?\\]\\])", "", Pattern.MULTILINE | Pattern.DOTALL)); + sFormatRules.add(new FormatRule("\\[\\[([^\\|\\]]+\\|)?(.+?)\\]\\]", "$2", + Pattern.MULTILINE)); + + } + + /** + * Query the Wiktionary API to pick a random dictionary word. Will try + * multiple times to find a valid word before giving up. + * + * @return Random dictionary word, or null if no valid word was found. + * @throws ApiException If any connection or server error occurs. + * @throws ParseException If there are problems parsing the response. + */ + public static String getRandomWord() throws ApiException, ParseException { + // Keep trying a few times until we find a valid word + int tries = 0; + while (tries++ < RANDOM_TRIES) { + // Query the API for a random word + String content = getUrlContent(WIKTIONARY_RANDOM); + try { + // Drill into the JSON response to find the returned word + JSONObject response = new JSONObject(content); + JSONObject query = response.getJSONObject("query"); + JSONArray random = query.getJSONArray("random"); + JSONObject word = random.getJSONObject(0); + String foundWord = word.getString("title"); + + // If we found an actual word, and it wasn't rejected by our invalid + // filter, then accept and return it. + if (foundWord != null && + !sInvalidWord.matcher(foundWord).find()) { + return foundWord; + } + } catch (JSONException e) { + throw new ParseException("Problem parsing API response", e); + } + } + + // No valid word found in number of tries, so return null + return null; + } + + /** + * Format the given wiki-style text into formatted HTML content. This will + * create headers, lists, internal links, and style formatting for any wiki + * markup found. + * + * @param wikiText The raw text to format, with wiki-markup included. + * @return HTML formatted content, ready for display in {@link WebView}. + */ + public static String formatWikiText(String wikiText) { + if (wikiText == null) { + return null; + } + + // Insert a fake last section into the document so our section splitter + // can correctly catch the last section. + wikiText = wikiText.concat(STUB_SECTION); + + // Read through all sections, keeping only those matching our filter, + // and only including the first entry for each title. + HashSet foundSections = new HashSet(); + StringBuilder builder = new StringBuilder(); + + Matcher sectionMatcher = sSectionSplit.matcher(wikiText); + while (sectionMatcher.find()) { + String title = sectionMatcher.group(1); + if (!foundSections.contains(title) && + sValidSections.matcher(title).matches()) { + String sectionContent = sectionMatcher.group(); + foundSections.add(title); + builder.append(sectionContent); + } + } + + // Our new wiki text is the selected sections only + wikiText = builder.toString(); + + // Apply all formatting rules, in order, to the wiki text + for (FormatRule rule : sFormatRules) { + wikiText = rule.apply(wikiText); + } + + // Return the resulting HTML with style sheet, if we have content left + if (!TextUtils.isEmpty(wikiText)) { + return STYLE_SHEET + wikiText; + } else { + return null; + } + } + +} diff --git a/samples/Wiktionary/src/com/example/android/wiktionary/LookupActivity.java b/samples/Wiktionary/src/com/example/android/wiktionary/LookupActivity.java new file mode 100644 index 000000000..6cc231b8c --- /dev/null +++ b/samples/Wiktionary/src/com/example/android/wiktionary/LookupActivity.java @@ -0,0 +1,344 @@ +/* + * Copyright (C) 2009 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.wiktionary; + +import com.example.android.wiktionary.SimpleWikiHelper.ApiException; +import com.example.android.wiktionary.SimpleWikiHelper.ParseException; + +import android.app.Activity; +import android.app.AlertDialog; +import android.app.SearchManager; +import android.content.Intent; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Bundle; +import android.os.SystemClock; +import android.text.TextUtils; +import android.text.format.DateUtils; +import android.util.Log; +import android.view.KeyEvent; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.animation.Animation; +import android.view.animation.AnimationUtils; +import android.view.animation.Animation.AnimationListener; +import android.webkit.WebView; +import android.widget.ProgressBar; +import android.widget.TextView; + +import java.util.Stack; + +/** + * Activity that lets users browse through Wiktionary content. This is just the + * user interface, and all API communication and parsing is handled in + * {@link ExtendedWikiHelper}. + */ +public class LookupActivity extends Activity implements AnimationListener { + private static final String TAG = "LookupActivity"; + + private View mTitleBar; + private TextView mTitle; + private ProgressBar mProgress; + private WebView mWebView; + + private Animation mSlideIn; + private Animation mSlideOut; + + /** + * History stack of previous words browsed in this session. This is + * referenced when the user taps the "back" key, to possibly intercept and + * show the last-visited entry, instead of closing the activity. + */ + private Stack mHistory = new Stack(); + + private String mEntryTitle; + + /** + * Keep track of last time user tapped "back" hard key. When pressed more + * than once within {@link #BACK_THRESHOLD}, we treat let the back key fall + * through and close the app. + */ + private long mLastPress = -1; + + private static final long BACK_THRESHOLD = DateUtils.SECOND_IN_MILLIS / 2; + + /** + * {@inheritDoc} + */ + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setContentView(R.layout.lookup); + + // Load animations used to show/hide progress bar + mSlideIn = AnimationUtils.loadAnimation(this, R.anim.slide_in); + mSlideOut = AnimationUtils.loadAnimation(this, R.anim.slide_out); + + // Listen for the "in" animation so we make the progress bar visible + // only after the sliding has finished. + mSlideIn.setAnimationListener(this); + + mTitleBar = findViewById(R.id.title_bar); + mTitle = (TextView) findViewById(R.id.title); + mProgress = (ProgressBar) findViewById(R.id.progress); + mWebView = (WebView) findViewById(R.id.webview); + + // Make the view transparent to show background + mWebView.setBackgroundColor(0); + + // Prepare User-Agent string for wiki actions + ExtendedWikiHelper.prepareUserAgent(this); + + // Handle incoming intents as possible searches or links + onNewIntent(getIntent()); + } + + /** + * Intercept the back-key to try walking backwards along our word history + * stack. If we don't have any remaining history, the key behaves normally + * and closes this activity. + */ + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + // Handle back key as long we have a history stack + if (keyCode == KeyEvent.KEYCODE_BACK && !mHistory.empty()) { + + // Compare against last pressed time, and if user hit multiple times + // in quick succession, we should consider bailing out early. + long currentPress = SystemClock.uptimeMillis(); + if (currentPress - mLastPress < BACK_THRESHOLD) { + return super.onKeyDown(keyCode, event); + } + mLastPress = currentPress; + + // Pop last entry off stack and start loading + String lastEntry = mHistory.pop(); + startNavigating(lastEntry, false); + + return true; + } + + // Otherwise fall through to parent + return super.onKeyDown(keyCode, event); + } + + /** + * Start navigating to the given word, pushing any current word onto the + * history stack if requested. The navigation happens on a background thread + * and updates the GUI when finished. + * + * @param word The dictionary word to navigate to. + * @param pushHistory If true, push the current word onto history stack. + */ + private void startNavigating(String word, boolean pushHistory) { + // Push any current word onto the history stack + if (!TextUtils.isEmpty(mEntryTitle) && pushHistory) { + mHistory.add(mEntryTitle); + } + + // Start lookup for new word in background + new LookupTask().execute(word); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean onCreateOptionsMenu(Menu menu) { + MenuInflater inflater = getMenuInflater(); + inflater.inflate(R.menu.lookup, menu); + return true; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.lookup_search: { + onSearchRequested(); + return true; + } + case R.id.lookup_random: { + startNavigating(null, true); + return true; + } + case R.id.lookup_about: { + showAbout(); + return true; + } + } + return false; + } + + /** + * Show an about dialog that cites data sources. + */ + protected void showAbout() { + // Inflate the about message contents + View messageView = getLayoutInflater().inflate(R.layout.about, null, false); + + // When linking text, force to always use default color. This works + // around a pressed color state bug. + TextView textView = (TextView) messageView.findViewById(R.id.about_credits); + int defaultColor = textView.getTextColors().getDefaultColor(); + textView.setTextColor(defaultColor); + + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setIcon(R.drawable.app_icon); + builder.setTitle(R.string.app_name); + builder.setView(messageView); + builder.create(); + builder.show(); + } + + /** + * Because we're singleTop, we handle our own new intents. These usually + * come from the {@link SearchManager} when a search is requested, or from + * internal links the user clicks on. + */ + @Override + public void onNewIntent(Intent intent) { + final String action = intent.getAction(); + if (Intent.ACTION_SEARCH.equals(action)) { + // Start query for incoming search request + String query = intent.getStringExtra(SearchManager.QUERY); + startNavigating(query, true); + + } else if (Intent.ACTION_VIEW.equals(action)) { + // Treat as internal link only if valid Uri and host matches + Uri data = intent.getData(); + if (data != null && ExtendedWikiHelper.WIKI_LOOKUP_HOST + .equals(data.getHost())) { + String query = data.getPathSegments().get(0); + startNavigating(query, true); + } + + } else { + // If not recognized, then start showing random word + startNavigating(null, true); + } + } + + /** + * Set the title for the current entry. + */ + protected void setEntryTitle(String entryText) { + mEntryTitle = entryText; + mTitle.setText(mEntryTitle); + } + + /** + * Set the content for the current entry. This will update our + * {@link WebView} to show the requested content. + */ + protected void setEntryContent(String entryContent) { + mWebView.loadDataWithBaseURL(ExtendedWikiHelper.WIKI_AUTHORITY, entryContent, + ExtendedWikiHelper.MIME_TYPE, ExtendedWikiHelper.ENCODING, null); + } + + /** + * Background task to handle Wiktionary lookups. This correctly shows and + * hides the loading animation from the GUI thread before starting a + * background query to the Wiktionary API. When finished, it transitions + * back to the GUI thread where it updates with the newly-found entry. + */ + private class LookupTask extends AsyncTask { + /** + * Before jumping into background thread, start sliding in the + * {@link ProgressBar}. We'll only show it once the animation finishes. + */ + @Override + protected void onPreExecute() { + mTitleBar.startAnimation(mSlideIn); + } + + /** + * Perform the background query using {@link ExtendedWikiHelper}, which + * may return an error message as the result. + */ + @Override + protected String doInBackground(String... args) { + String query = args[0]; + String parsedText = null; + + try { + // If query word is null, assume request for random word + if (query == null) { + query = ExtendedWikiHelper.getRandomWord(); + } + + if (query != null) { + // Push our requested word to the title bar + publishProgress(query); + String wikiText = ExtendedWikiHelper.getPageContent(query, true); + parsedText = ExtendedWikiHelper.formatWikiText(wikiText); + } + } catch (ApiException e) { + Log.e(TAG, "Problem making wiktionary request", e); + } catch (ParseException e) { + Log.e(TAG, "Problem making wiktionary request", e); + } + + if (parsedText == null) { + parsedText = getString(R.string.empty_result); + } + + return parsedText; + } + + /** + * Our progress update pushes a title bar update. + */ + @Override + protected void onProgressUpdate(String... args) { + String searchWord = args[0]; + setEntryTitle(searchWord); + } + + /** + * When finished, push the newly-found entry content into our + * {@link WebView} and hide the {@link ProgressBar}. + */ + @Override + protected void onPostExecute(String parsedText) { + mTitleBar.startAnimation(mSlideOut); + mProgress.setVisibility(View.INVISIBLE); + + setEntryContent(parsedText); + } + } + + /** + * Make the {@link ProgressBar} visible when our in-animation finishes. + */ + public void onAnimationEnd(Animation animation) { + mProgress.setVisibility(View.VISIBLE); + } + + public void onAnimationRepeat(Animation animation) { + // Not interested if the animation repeats + } + + public void onAnimationStart(Animation animation) { + // Not interested when the animation starts + } +} diff --git a/samples/Wiktionary/src/com/example/android/wiktionary/SimpleWikiHelper.java b/samples/Wiktionary/src/com/example/android/wiktionary/SimpleWikiHelper.java new file mode 100644 index 000000000..1c71d7e8b --- /dev/null +++ b/samples/Wiktionary/src/com/example/android/wiktionary/SimpleWikiHelper.java @@ -0,0 +1,208 @@ +/* + * Copyright (C) 2009 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.wiktionary; + +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.StatusLine; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.DefaultHttpClient; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import android.content.Context; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.net.Uri; +import android.util.Log; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * Helper methods to simplify talking with and parsing responses from a + * lightweight Wiktionary API. Before making any requests, you should call + * {@link #prepareUserAgent(Context)} to generate a User-Agent string based on + * your application package name and version. + */ +public class SimpleWikiHelper { + private static final String TAG = "SimpleWikiHelper"; + + /** + * Partial URL to use when requesting the detailed entry for a specific + * Wiktionary page. Use {@link String#format(String, Object...)} to insert + * the desired page title after escaping it as needed. + */ + private static final String WIKTIONARY_PAGE = + "http://en.wiktionary.org/w/api.php?action=query&prop=revisions&titles=%s&" + + "rvprop=content&format=json%s"; + + /** + * Partial URL to append to {@link #WIKTIONARY_PAGE} when you want to expand + * any templates found on the requested page. This is useful when browsing + * full entries, but may use more network bandwidth. + */ + private static final String WIKTIONARY_EXPAND_TEMPLATES = + "&rvexpandtemplates=true"; + + /** + * {@link StatusLine} HTTP status code when no server error has occurred. + */ + private static final int HTTP_STATUS_OK = 200; + + /** + * Shared buffer used by {@link #getUrlContent(String)} when reading results + * from an API request. + */ + private static byte[] sBuffer = new byte[512]; + + /** + * User-agent string to use when making requests. Should be filled using + * {@link #prepareUserAgent(Context)} before making any other calls. + */ + private static String sUserAgent = null; + + /** + * Thrown when there were problems contacting the remote API server, either + * because of a network error, or the server returned a bad status code. + */ + public static class ApiException extends Exception { + public ApiException(String detailMessage, Throwable throwable) { + super(detailMessage, throwable); + } + + public ApiException(String detailMessage) { + super(detailMessage); + } + } + + /** + * Thrown when there were problems parsing the response to an API call, + * either because the response was empty, or it was malformed. + */ + public static class ParseException extends Exception { + public ParseException(String detailMessage, Throwable throwable) { + super(detailMessage, throwable); + } + } + + /** + * Prepare the internal User-Agent string for use. This requires a + * {@link Context} to pull the package name and version number for this + * application. + */ + public static void prepareUserAgent(Context context) { + try { + // Read package name and version number from manifest + PackageManager manager = context.getPackageManager(); + PackageInfo info = manager.getPackageInfo(context.getPackageName(), 0); + sUserAgent = String.format(context.getString(R.string.template_user_agent), + info.packageName, info.versionName); + + } catch(NameNotFoundException e) { + Log.e(TAG, "Couldn't find package information in PackageManager", e); + } + } + + /** + * Read and return the content for a specific Wiktionary page. This makes a + * lightweight API call, and trims out just the page content returned. + * Because this call blocks until results are available, it should not be + * run from a UI thread. + * + * @param title The exact title of the Wiktionary page requested. + * @param expandTemplates If true, expand any wiki templates found. + * @return Exact content of page. + * @throws ApiException If any connection or server error occurs. + * @throws ParseException If there are problems parsing the response. + */ + public static String getPageContent(String title, boolean expandTemplates) + throws ApiException, ParseException { + // Encode page title and expand templates if requested + String encodedTitle = Uri.encode(title); + String expandClause = expandTemplates ? WIKTIONARY_EXPAND_TEMPLATES : ""; + + // Query the API for content + String content = getUrlContent(String.format(WIKTIONARY_PAGE, + encodedTitle, expandClause)); + try { + // Drill into the JSON response to find the content body + JSONObject response = new JSONObject(content); + JSONObject query = response.getJSONObject("query"); + JSONObject pages = query.getJSONObject("pages"); + JSONObject page = pages.getJSONObject((String) pages.keys().next()); + JSONArray revisions = page.getJSONArray("revisions"); + JSONObject revision = revisions.getJSONObject(0); + return revision.getString("*"); + } catch (JSONException e) { + throw new ParseException("Problem parsing API response", e); + } + } + + /** + * Pull the raw text content of the given URL. This call blocks until the + * operation has completed, and is synchronized because it uses a shared + * buffer {@link #sBuffer}. + * + * @param url The exact URL to request. + * @return The raw content returned by the server. + * @throws ApiException If any connection or server error occurs. + */ + protected static synchronized String getUrlContent(String url) throws ApiException { + if (sUserAgent == null) { + throw new ApiException("User-Agent string must be prepared"); + } + + // Create client and set our specific user-agent string + HttpClient client = new DefaultHttpClient(); + HttpGet request = new HttpGet(url); + request.setHeader("User-Agent", sUserAgent); + + try { + HttpResponse response = client.execute(request); + + // Check if server response is valid + StatusLine status = response.getStatusLine(); + if (status.getStatusCode() != HTTP_STATUS_OK) { + throw new ApiException("Invalid response from server: " + + status.toString()); + } + + // Pull content stream from response + HttpEntity entity = response.getEntity(); + InputStream inputStream = entity.getContent(); + + ByteArrayOutputStream content = new ByteArrayOutputStream(); + + // Read response into a buffered stream + int readBytes = 0; + while ((readBytes = inputStream.read(sBuffer)) != -1) { + content.write(sBuffer, 0, readBytes); + } + + // Return result from buffered stream + return new String(content.toByteArray()); + } catch (IOException e) { + throw new ApiException("Problem communicating with API", e); + } + } + +} diff --git a/samples/Wiktionary/src/com/example/android/wiktionary/WordWidget.java b/samples/Wiktionary/src/com/example/android/wiktionary/WordWidget.java new file mode 100644 index 000000000..e80eaf92a --- /dev/null +++ b/samples/Wiktionary/src/com/example/android/wiktionary/WordWidget.java @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2009 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.wiktionary; + +import com.example.android.wiktionary.SimpleWikiHelper.ApiException; +import com.example.android.wiktionary.SimpleWikiHelper.ParseException; + +import android.app.PendingIntent; +import android.app.Service; +import android.appwidget.AppWidgetManager; +import android.appwidget.AppWidgetProvider; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.res.Resources; +import android.net.Uri; +import android.os.IBinder; +import android.text.format.Time; +import android.util.Log; +import android.widget.RemoteViews; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Define a simple widget that shows the Wiktionary "Word of the day." To build + * an update we spawn a background {@link Service} to perform the API queries. + */ +public class WordWidget extends AppWidgetProvider { + @Override + public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { + // To prevent any ANR timeouts, we perform the update in a service + context.startService(new Intent(context, UpdateService.class)); + } + + public static class UpdateService extends Service { + @Override + public void onStart(Intent intent, int startId) { + // Build the widget update for today + RemoteViews updateViews = buildUpdate(this); + + // Push update for this widget to the home screen + ComponentName thisWidget = new ComponentName(this, WordWidget.class); + AppWidgetManager manager = AppWidgetManager.getInstance(this); + manager.updateAppWidget(thisWidget, updateViews); + } + + @Override + public IBinder onBind(Intent intent) { + return null; + } + + /** + * Regular expression that splits "Word of the day" entry into word + * name, word type, and the first description bullet point. + */ + private static final String WOTD_PATTERN = + "(?s)\\{\\{wotd\\|(.+?)\\|(.+?)\\|([^#\\|]+).*?\\}\\}"; + + /** + * Build a widget update to show the current Wiktionary + * "Word of the day." Will block until the online API returns. + */ + public RemoteViews buildUpdate(Context context) { + // Pick out month names from resources + Resources res = context.getResources(); + String[] monthNames = res.getStringArray(R.array.month_names); + + // Find current month and day + Time today = new Time(); + today.setToNow(); + + // Build the page title for today, such as "March 21" + String pageName = res.getString(R.string.template_wotd_title, + monthNames[today.month], today.monthDay); + String pageContent = null; + + try { + // Try querying the Wiktionary API for today's word + SimpleWikiHelper.prepareUserAgent(context); + pageContent = SimpleWikiHelper.getPageContent(pageName, false); + } catch (ApiException e) { + Log.e("WordWidget", "Couldn't contact API", e); + } catch (ParseException e) { + Log.e("WordWidget", "Couldn't parse API response", e); + } + + RemoteViews views = null; + Matcher matcher = Pattern.compile(WOTD_PATTERN).matcher(pageContent); + if (matcher.find()) { + // Build an update that holds the updated widget contents + views = new RemoteViews(context.getPackageName(), R.layout.widget_word); + + String wordTitle = matcher.group(1); + views.setTextViewText(R.id.word_title, wordTitle); + views.setTextViewText(R.id.word_type, matcher.group(2)); + views.setTextViewText(R.id.definition, matcher.group(3).trim()); + + // When user clicks on widget, launch to Wiktionary definition page + String definePage = String.format("%s://%s/%s", ExtendedWikiHelper.WIKI_AUTHORITY, + ExtendedWikiHelper.WIKI_LOOKUP_HOST, wordTitle); + Intent defineIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(definePage)); + PendingIntent pendingIntent = PendingIntent.getActivity(context, + 0 /* no requestCode */, defineIntent, 0 /* no flags */); + views.setOnClickPendingIntent(R.id.widget, pendingIntent); + + } else { + // Didn't find word of day, so show error message + views = new RemoteViews(context.getPackageName(), R.layout.widget_message); + views.setTextViewText(R.id.message, context.getString(R.string.widget_error)); + } + return views; + } + } +} diff --git a/samples/WiktionarySimple/Android.mk b/samples/WiktionarySimple/Android.mk new file mode 100644 index 000000000..a5a1423e0 --- /dev/null +++ b/samples/WiktionarySimple/Android.mk @@ -0,0 +1,16 @@ +LOCAL_PATH:= $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_MODULE_TAGS := samples + +# Only compile source java files in this apk. +LOCAL_SRC_FILES := $(call all-java-files-under, src) + +LOCAL_PACKAGE_NAME := WiktionarySimple + +LOCAL_SDK_VERSION := current + +include $(BUILD_PACKAGE) + +# Use the following include to make our test apk. +include $(call all-makefiles-under,$(LOCAL_PATH)) diff --git a/samples/WiktionarySimple/AndroidManifest.xml b/samples/WiktionarySimple/AndroidManifest.xml new file mode 100644 index 000000000..c6b872486 --- /dev/null +++ b/samples/WiktionarySimple/AndroidManifest.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/WiktionarySimple/_index.html b/samples/WiktionarySimple/_index.html new file mode 100644 index 000000000..cf8142e89 --- /dev/null +++ b/samples/WiktionarySimple/_index.html @@ -0,0 +1,10 @@ +

    A sample application that demonstrates how to create an interactive widget for display on the Android home screen.

    + +

    When installed, this adds a "Wiktionary simple" option to the widget +installation menu. The word of the day is downloaded from Wiktionary and +displayed in a frame. Touching the widget will open a new browser session and +load the word's Wiktionary entry.

    + +

    A more advanced version of this sample is available in the Wiktionary directory.

    + + diff --git a/samples/WiktionarySimple/res/drawable/app_icon.png b/samples/WiktionarySimple/res/drawable/app_icon.png new file mode 100644 index 000000000..2b1417aad Binary files /dev/null and b/samples/WiktionarySimple/res/drawable/app_icon.png differ diff --git a/samples/WiktionarySimple/res/drawable/star_logo.png b/samples/WiktionarySimple/res/drawable/star_logo.png new file mode 100644 index 000000000..b32d1756c Binary files /dev/null and b/samples/WiktionarySimple/res/drawable/star_logo.png differ diff --git a/samples/WiktionarySimple/res/drawable/widget_bg.xml b/samples/WiktionarySimple/res/drawable/widget_bg.xml new file mode 100644 index 000000000..692a13d84 --- /dev/null +++ b/samples/WiktionarySimple/res/drawable/widget_bg.xml @@ -0,0 +1,23 @@ + + + + + + + + + + diff --git a/samples/WiktionarySimple/res/drawable/widget_bg_normal.9.png b/samples/WiktionarySimple/res/drawable/widget_bg_normal.9.png new file mode 100644 index 000000000..314eb8ef9 Binary files /dev/null and b/samples/WiktionarySimple/res/drawable/widget_bg_normal.9.png differ diff --git a/samples/WiktionarySimple/res/drawable/widget_bg_pressed.9.png b/samples/WiktionarySimple/res/drawable/widget_bg_pressed.9.png new file mode 100644 index 000000000..cc23e787b Binary files /dev/null and b/samples/WiktionarySimple/res/drawable/widget_bg_pressed.9.png differ diff --git a/samples/WiktionarySimple/res/drawable/widget_bg_selected.9.png b/samples/WiktionarySimple/res/drawable/widget_bg_selected.9.png new file mode 100644 index 000000000..ef0cdc066 Binary files /dev/null and b/samples/WiktionarySimple/res/drawable/widget_bg_selected.9.png differ diff --git a/samples/WiktionarySimple/res/layout/widget_message.xml b/samples/WiktionarySimple/res/layout/widget_message.xml new file mode 100644 index 000000000..ba9471447 --- /dev/null +++ b/samples/WiktionarySimple/res/layout/widget_message.xml @@ -0,0 +1,34 @@ + + + + + + + + diff --git a/samples/WiktionarySimple/res/layout/widget_word.xml b/samples/WiktionarySimple/res/layout/widget_word.xml new file mode 100644 index 000000000..0e76f0b80 --- /dev/null +++ b/samples/WiktionarySimple/res/layout/widget_word.xml @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + diff --git a/samples/WiktionarySimple/res/values/strings.xml b/samples/WiktionarySimple/res/values/strings.xml new file mode 100644 index 000000000..65e44cb86 --- /dev/null +++ b/samples/WiktionarySimple/res/values/strings.xml @@ -0,0 +1,44 @@ + + + + + Wiktionary simple example + + "%s/%s (Linux; Android)" + "Wiktionary:Word of the day/%s %s" + "http://en.wiktionary.org/wiki/%s" + + Wiktionary simple + + Loading word\nof day\u2026 + No word of\nday found + + + January + February + March + April + May + June + July + August + September + October + November + December + + + diff --git a/samples/WiktionarySimple/res/values/styles.xml b/samples/WiktionarySimple/res/values/styles.xml new file mode 100644 index 000000000..42d679c56 --- /dev/null +++ b/samples/WiktionarySimple/res/values/styles.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + diff --git a/samples/WiktionarySimple/res/xml/widget_word.xml b/samples/WiktionarySimple/res/xml/widget_word.xml new file mode 100644 index 000000000..46d31c321 --- /dev/null +++ b/samples/WiktionarySimple/res/xml/widget_word.xml @@ -0,0 +1,21 @@ + + + + diff --git a/samples/WiktionarySimple/src/com/example/android/simplewiktionary/SimpleWikiHelper.java b/samples/WiktionarySimple/src/com/example/android/simplewiktionary/SimpleWikiHelper.java new file mode 100644 index 000000000..bb39d7bd7 --- /dev/null +++ b/samples/WiktionarySimple/src/com/example/android/simplewiktionary/SimpleWikiHelper.java @@ -0,0 +1,214 @@ +/* + * Copyright (C) 2009 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.simplewiktionary; + +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.StatusLine; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.DefaultHttpClient; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import android.content.Context; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.net.Uri; +import android.util.Log; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * Helper methods to simplify talking with and parsing responses from a + * lightweight Wiktionary API. Before making any requests, you should call + * {@link #prepareUserAgent(Context)} to generate a User-Agent string based on + * your application package name and version. + */ +public class SimpleWikiHelper { + private static final String TAG = "SimpleWikiHelper"; + + /** + * Regular expression that splits "Word of the day" entry into word + * name, word type, and the first description bullet point. + */ + public static final String WORD_OF_DAY_REGEX = + "(?s)\\{\\{wotd\\|(.+?)\\|(.+?)\\|([^#\\|]+).*?\\}\\}"; + + /** + * Partial URL to use when requesting the detailed entry for a specific + * Wiktionary page. Use {@link String#format(String, Object...)} to insert + * the desired page title after escaping it as needed. + */ + private static final String WIKTIONARY_PAGE = + "http://en.wiktionary.org/w/api.php?action=query&prop=revisions&titles=%s&" + + "rvprop=content&format=json%s"; + + /** + * Partial URL to append to {@link #WIKTIONARY_PAGE} when you want to expand + * any templates found on the requested page. This is useful when browsing + * full entries, but may use more network bandwidth. + */ + private static final String WIKTIONARY_EXPAND_TEMPLATES = + "&rvexpandtemplates=true"; + + /** + * {@link StatusLine} HTTP status code when no server error has occurred. + */ + private static final int HTTP_STATUS_OK = 200; + + /** + * Shared buffer used by {@link #getUrlContent(String)} when reading results + * from an API request. + */ + private static byte[] sBuffer = new byte[512]; + + /** + * User-agent string to use when making requests. Should be filled using + * {@link #prepareUserAgent(Context)} before making any other calls. + */ + private static String sUserAgent = null; + + /** + * Thrown when there were problems contacting the remote API server, either + * because of a network error, or the server returned a bad status code. + */ + public static class ApiException extends Exception { + public ApiException(String detailMessage, Throwable throwable) { + super(detailMessage, throwable); + } + + public ApiException(String detailMessage) { + super(detailMessage); + } + } + + /** + * Thrown when there were problems parsing the response to an API call, + * either because the response was empty, or it was malformed. + */ + public static class ParseException extends Exception { + public ParseException(String detailMessage, Throwable throwable) { + super(detailMessage, throwable); + } + } + + /** + * Prepare the internal User-Agent string for use. This requires a + * {@link Context} to pull the package name and version number for this + * application. + */ + public static void prepareUserAgent(Context context) { + try { + // Read package name and version number from manifest + PackageManager manager = context.getPackageManager(); + PackageInfo info = manager.getPackageInfo(context.getPackageName(), 0); + sUserAgent = String.format(context.getString(R.string.template_user_agent), + info.packageName, info.versionName); + + } catch(NameNotFoundException e) { + Log.e(TAG, "Couldn't find package information in PackageManager", e); + } + } + + /** + * Read and return the content for a specific Wiktionary page. This makes a + * lightweight API call, and trims out just the page content returned. + * Because this call blocks until results are available, it should not be + * run from a UI thread. + * + * @param title The exact title of the Wiktionary page requested. + * @param expandTemplates If true, expand any wiki templates found. + * @return Exact content of page. + * @throws ApiException If any connection or server error occurs. + * @throws ParseException If there are problems parsing the response. + */ + public static String getPageContent(String title, boolean expandTemplates) + throws ApiException, ParseException { + // Encode page title and expand templates if requested + String encodedTitle = Uri.encode(title); + String expandClause = expandTemplates ? WIKTIONARY_EXPAND_TEMPLATES : ""; + + // Query the API for content + String content = getUrlContent(String.format(WIKTIONARY_PAGE, + encodedTitle, expandClause)); + try { + // Drill into the JSON response to find the content body + JSONObject response = new JSONObject(content); + JSONObject query = response.getJSONObject("query"); + JSONObject pages = query.getJSONObject("pages"); + JSONObject page = pages.getJSONObject((String) pages.keys().next()); + JSONArray revisions = page.getJSONArray("revisions"); + JSONObject revision = revisions.getJSONObject(0); + return revision.getString("*"); + } catch (JSONException e) { + throw new ParseException("Problem parsing API response", e); + } + } + + /** + * Pull the raw text content of the given URL. This call blocks until the + * operation has completed, and is synchronized because it uses a shared + * buffer {@link #sBuffer}. + * + * @param url The exact URL to request. + * @return The raw content returned by the server. + * @throws ApiException If any connection or server error occurs. + */ + protected static synchronized String getUrlContent(String url) throws ApiException { + if (sUserAgent == null) { + throw new ApiException("User-Agent string must be prepared"); + } + + // Create client and set our specific user-agent string + HttpClient client = new DefaultHttpClient(); + HttpGet request = new HttpGet(url); + request.setHeader("User-Agent", sUserAgent); + + try { + HttpResponse response = client.execute(request); + + // Check if server response is valid + StatusLine status = response.getStatusLine(); + if (status.getStatusCode() != HTTP_STATUS_OK) { + throw new ApiException("Invalid response from server: " + + status.toString()); + } + + // Pull content stream from response + HttpEntity entity = response.getEntity(); + InputStream inputStream = entity.getContent(); + + ByteArrayOutputStream content = new ByteArrayOutputStream(); + + // Read response into a buffered stream + int readBytes = 0; + while ((readBytes = inputStream.read(sBuffer)) != -1) { + content.write(sBuffer, 0, readBytes); + } + + // Return result from buffered stream + return new String(content.toByteArray()); + } catch (IOException e) { + throw new ApiException("Problem communicating with API", e); + } + } +} diff --git a/samples/WiktionarySimple/src/com/example/android/simplewiktionary/WordWidget.java b/samples/WiktionarySimple/src/com/example/android/simplewiktionary/WordWidget.java new file mode 100644 index 000000000..d005faa4d --- /dev/null +++ b/samples/WiktionarySimple/src/com/example/android/simplewiktionary/WordWidget.java @@ -0,0 +1,127 @@ +/* + * Copyright (C) 2009 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.simplewiktionary; + +import com.example.android.simplewiktionary.SimpleWikiHelper.ApiException; +import com.example.android.simplewiktionary.SimpleWikiHelper.ParseException; + +import android.app.PendingIntent; +import android.app.Service; +import android.appwidget.AppWidgetManager; +import android.appwidget.AppWidgetProvider; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.res.Resources; +import android.net.Uri; +import android.os.IBinder; +import android.text.format.Time; +import android.util.Log; +import android.widget.RemoteViews; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Define a simple widget that shows the Wiktionary "Word of the day." To build + * an update we spawn a background {@link Service} to perform the API queries. + */ +public class WordWidget extends AppWidgetProvider { + @Override + public void onUpdate(Context context, AppWidgetManager appWidgetManager, + int[] appWidgetIds) { + // To prevent any ANR timeouts, we perform the update in a service + context.startService(new Intent(context, UpdateService.class)); + } + + public static class UpdateService extends Service { + @Override + public void onStart(Intent intent, int startId) { + // Build the widget update for today + RemoteViews updateViews = buildUpdate(this); + + // Push update for this widget to the home screen + ComponentName thisWidget = new ComponentName(this, WordWidget.class); + AppWidgetManager manager = AppWidgetManager.getInstance(this); + manager.updateAppWidget(thisWidget, updateViews); + } + + /** + * Build a widget update to show the current Wiktionary + * "Word of the day." Will block until the online API returns. + */ + public RemoteViews buildUpdate(Context context) { + // Pick out month names from resources + Resources res = context.getResources(); + String[] monthNames = res.getStringArray(R.array.month_names); + + // Find current month and day + Time today = new Time(); + today.setToNow(); + + // Build today's page title, like "Wiktionary:Word of the day/March 21" + String pageName = res.getString(R.string.template_wotd_title, + monthNames[today.month], today.monthDay); + RemoteViews updateViews = null; + String pageContent = ""; + + try { + // Try querying the Wiktionary API for today's word + SimpleWikiHelper.prepareUserAgent(context); + pageContent = SimpleWikiHelper.getPageContent(pageName, false); + } catch (ApiException e) { + Log.e("WordWidget", "Couldn't contact API", e); + } catch (ParseException e) { + Log.e("WordWidget", "Couldn't parse API response", e); + } + + // Use a regular expression to parse out the word and its definition + Pattern pattern = Pattern.compile(SimpleWikiHelper.WORD_OF_DAY_REGEX); + Matcher matcher = pattern.matcher(pageContent); + if (matcher.find()) { + // Build an update that holds the updated widget contents + updateViews = new RemoteViews(context.getPackageName(), R.layout.widget_word); + + String wordTitle = matcher.group(1); + updateViews.setTextViewText(R.id.word_title, wordTitle); + updateViews.setTextViewText(R.id.word_type, matcher.group(2)); + updateViews.setTextViewText(R.id.definition, matcher.group(3).trim()); + + // When user clicks on widget, launch to Wiktionary definition page + String definePage = res.getString(R.string.template_define_url, + Uri.encode(wordTitle)); + Intent defineIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(definePage)); + PendingIntent pendingIntent = PendingIntent.getActivity(context, + 0 /* no requestCode */, defineIntent, 0 /* no flags */); + updateViews.setOnClickPendingIntent(R.id.widget, pendingIntent); + + } else { + // Didn't find word of day, so show error message + updateViews = new RemoteViews(context.getPackageName(), R.layout.widget_message); + CharSequence errorMessage = context.getText(R.string.widget_error); + updateViews.setTextViewText(R.id.message, errorMessage); + } + return updateViews; + } + + @Override + public IBinder onBind(Intent intent) { + // We don't need to bind to this service + return null; + } + } +}