From 807dc15b095139d806eab5c9d4de4fbf692ed447 Mon Sep 17 00:00:00 2001 From: Trevor Johns Date: Mon, 7 Dec 2009 17:04:53 -0800 Subject: [PATCH] Importing Wikitionary/SimpleWiktionary sample code from http://code.google.com/p/wiktionary-android/. Original code by Jeffrey Sharkey . Modifications: - Remove .classpath, .project, and default.properties. - Remove generated files (/gen). - Create Android.mk and _index.html. - Cleaned up whitespace and converted to match AOSP style guide. - Renamed SimpleWiktionary to WiktionarySimple to keep both samples next to each other in the directory listing. - Removed the android:text attribute in the BulletPoint due to localization issues. --- samples/Wiktionary/Android.mk | 16 + samples/Wiktionary/AndroidManifest.xml | 70 ++++ samples/Wiktionary/_index.html | 10 + samples/Wiktionary/res/anim/slide_in.xml | 22 ++ samples/Wiktionary/res/anim/slide_out.xml | 22 ++ samples/Wiktionary/res/drawable/app_icon.png | Bin 0 -> 1985 bytes .../res/drawable/ic_menu_shuffle.png | Bin 0 -> 2384 bytes .../res/drawable/logo_overlay.9.png | Bin 0 -> 16911 bytes samples/Wiktionary/res/drawable/lookup_bg.xml | 20 + .../Wiktionary/res/drawable/progress_spin.xml | 43 +++ samples/Wiktionary/res/drawable/star_logo.png | Bin 0 -> 2747 bytes samples/Wiktionary/res/drawable/widget_bg.xml | 22 ++ .../res/drawable/widget_bg_normal.9.png | Bin 0 -> 268 bytes .../res/drawable/widget_bg_pressed.9.png | Bin 0 -> 275 bytes .../res/drawable/widget_bg_selected.9.png | Bin 0 -> 276 bytes samples/Wiktionary/res/layout/about.xml | 40 ++ samples/Wiktionary/res/layout/lookup.xml | 56 +++ .../Wiktionary/res/layout/widget_message.xml | 34 ++ samples/Wiktionary/res/layout/widget_word.xml | 79 ++++ samples/Wiktionary/res/menu/lookup.xml | 34 ++ samples/Wiktionary/res/values/strings.xml | 56 +++ samples/Wiktionary/res/values/styles.xml | 66 ++++ samples/Wiktionary/res/values/themes.xml | 21 ++ samples/Wiktionary/res/xml/searchable.xml | 19 + samples/Wiktionary/res/xml/widget_word.xml | 21 ++ .../wiktionary/ExtendedWikiHelper.java | 278 ++++++++++++++ .../android/wiktionary/LookupActivity.java | 344 ++++++++++++++++++ .../android/wiktionary/SimpleWikiHelper.java | 208 +++++++++++ .../android/wiktionary/WordWidget.java | 129 +++++++ samples/WiktionarySimple/Android.mk | 16 + samples/WiktionarySimple/AndroidManifest.xml | 41 +++ samples/WiktionarySimple/_index.html | 10 + .../res/drawable/app_icon.png | Bin 0 -> 1985 bytes .../res/drawable/star_logo.png | Bin 0 -> 2747 bytes .../res/drawable/widget_bg.xml | 23 ++ .../res/drawable/widget_bg_normal.9.png | Bin 0 -> 268 bytes .../res/drawable/widget_bg_pressed.9.png | Bin 0 -> 275 bytes .../res/drawable/widget_bg_selected.9.png | Bin 0 -> 276 bytes .../res/layout/widget_message.xml | 34 ++ .../res/layout/widget_word.xml | 79 ++++ .../WiktionarySimple/res/values/strings.xml | 44 +++ .../WiktionarySimple/res/values/styles.xml | 52 +++ .../WiktionarySimple/res/xml/widget_word.xml | 21 ++ .../simplewiktionary/SimpleWikiHelper.java | 214 +++++++++++ .../android/simplewiktionary/WordWidget.java | 127 +++++++ 45 files changed, 2271 insertions(+) create mode 100644 samples/Wiktionary/Android.mk create mode 100644 samples/Wiktionary/AndroidManifest.xml create mode 100644 samples/Wiktionary/_index.html create mode 100644 samples/Wiktionary/res/anim/slide_in.xml create mode 100644 samples/Wiktionary/res/anim/slide_out.xml create mode 100644 samples/Wiktionary/res/drawable/app_icon.png create mode 100755 samples/Wiktionary/res/drawable/ic_menu_shuffle.png create mode 100644 samples/Wiktionary/res/drawable/logo_overlay.9.png create mode 100644 samples/Wiktionary/res/drawable/lookup_bg.xml create mode 100644 samples/Wiktionary/res/drawable/progress_spin.xml create mode 100644 samples/Wiktionary/res/drawable/star_logo.png create mode 100644 samples/Wiktionary/res/drawable/widget_bg.xml create mode 100644 samples/Wiktionary/res/drawable/widget_bg_normal.9.png create mode 100644 samples/Wiktionary/res/drawable/widget_bg_pressed.9.png create mode 100644 samples/Wiktionary/res/drawable/widget_bg_selected.9.png create mode 100644 samples/Wiktionary/res/layout/about.xml create mode 100644 samples/Wiktionary/res/layout/lookup.xml create mode 100644 samples/Wiktionary/res/layout/widget_message.xml create mode 100644 samples/Wiktionary/res/layout/widget_word.xml create mode 100644 samples/Wiktionary/res/menu/lookup.xml create mode 100644 samples/Wiktionary/res/values/strings.xml create mode 100644 samples/Wiktionary/res/values/styles.xml create mode 100644 samples/Wiktionary/res/values/themes.xml create mode 100644 samples/Wiktionary/res/xml/searchable.xml create mode 100644 samples/Wiktionary/res/xml/widget_word.xml create mode 100644 samples/Wiktionary/src/com/example/android/wiktionary/ExtendedWikiHelper.java create mode 100644 samples/Wiktionary/src/com/example/android/wiktionary/LookupActivity.java create mode 100644 samples/Wiktionary/src/com/example/android/wiktionary/SimpleWikiHelper.java create mode 100644 samples/Wiktionary/src/com/example/android/wiktionary/WordWidget.java create mode 100644 samples/WiktionarySimple/Android.mk create mode 100644 samples/WiktionarySimple/AndroidManifest.xml create mode 100644 samples/WiktionarySimple/_index.html create mode 100644 samples/WiktionarySimple/res/drawable/app_icon.png create mode 100644 samples/WiktionarySimple/res/drawable/star_logo.png create mode 100644 samples/WiktionarySimple/res/drawable/widget_bg.xml create mode 100644 samples/WiktionarySimple/res/drawable/widget_bg_normal.9.png create mode 100644 samples/WiktionarySimple/res/drawable/widget_bg_pressed.9.png create mode 100644 samples/WiktionarySimple/res/drawable/widget_bg_selected.9.png create mode 100644 samples/WiktionarySimple/res/layout/widget_message.xml create mode 100644 samples/WiktionarySimple/res/layout/widget_word.xml create mode 100644 samples/WiktionarySimple/res/values/strings.xml create mode 100644 samples/WiktionarySimple/res/values/styles.xml create mode 100644 samples/WiktionarySimple/res/xml/widget_word.xml create mode 100644 samples/WiktionarySimple/src/com/example/android/simplewiktionary/SimpleWikiHelper.java create mode 100644 samples/WiktionarySimple/src/com/example/android/simplewiktionary/WordWidget.java 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 0000000000000000000000000000000000000000..2b1417aad99e1ecf3376d795c9f76eb61d83202b GIT binary patch literal 1985 zcmV;y2R`_TP)`Kj+^0xjVb|#&+VwNkbC?MHPiYr~s)& zq7|wvQC_GJFGxi~JRlX7S6-SI5Pt)&2&6m!QY8uk0S%Q(A!-#;D}(?|l_pK%R8|}( z-rc!#4-d1u-u3>l5+!SKEKck7rli$&)9G9q%*F0>_VkH?(^j-!#U4e|Dz&XjaPvuN3N01XUGL zmCkKCI&J9&(7m%r2haib9&SWC6Urkc&^f2?U%uWJW%-R)fBff31jbsx?wxz-V@H=x zeB<{-FcCWqq(gK5F z{DF$|o`r=50p{)rAk>mdVtc7br*DDWdFJNofrq;%ASzj!M7FEH_a18u(=3~ESJ0TP zg{mBwgd0sjsHJIS1{Se3Otb9Eou@vVS!3m)Nx0DjkVcY3J%=wXV0%ez8j?HDOjgAp z6SMJ{H6XJ|67?=sJ=PkgxQ1!Ff?93oY&^yS%nT6l9&0T+WeZd*F)Hkvgc}!yX3{w9 zU8*Y9TBa9+%4}Sz#HeS_B;05Msg67@1x%*Oj*O%+EKB;_uD^icjM>97?G$o84&OwVkJ0z8f!&!weC~3zt=@$w1N-cFzKpG(uJ9 z#$kpC7^AgnW$=X_VXALqUCEs%jsk)xNRBPP7>~jX5%At*l}a+8tr4>;amcIUy)wUN zwmq-ak)d>mfEdXtX*4)4yR!zMTnGF1HMH5feR$OGLj+WPR!LLSH?S*NxGQZ}Fh4hA z#21G}B-;M_FacN0CP`@f)sE+2n2zp zUY|h~>XnE%v{YO`o`s2e(90X#Z4Kzm* z076yjX%bmM2(97rjhmc3`!3g3*J!nJ6vb;ftGJW}NT+??iN)p7baw)} zp`vqYWMI1i5n;2LbN$8|)`lMeLLiz%z*<8ii9!%Kefk2w`Td(byfnj;k1w$}pWUqo z+_Rj2_e0)Zxyt3sYpmW_%ZKjH>#P1Q6SZlbF>TH~HNk{?0@D>pb`L$HzXX zyBl*iveTSPAFT80YySpdV`Ed-*Vo=&y>#X$fTdiw1{g^|j8G2)%R4IzF z)>>>7b~_W@iM^P(oq&4vYpXYIvbe8o2An>7i7$Tck$YC=UgkXWB0{tXL6S#8zPgk><#c%)e*RTET(~o^LN|Pwq zY_@xVG9zp6_@Kpm1r^P*MK(6B=0LN*tQjDns$N9$fBo$juP+{X;!A~V{U`|Hrx5YR zm;&|E7r_97_V??`eQ(ii1LbLV0hVK180me{+c@uHML;QB^OcQ_tLs3k^Vyocy+{)g z3sixWGIfWCjy<&$MAf>8YEg(Hg^GAz6rxb5c&{K@NTDDqMd3vR=L$s%FNjh&EyOE1 zia2$M;tDU~6u`N{mo#(DbLT}wRomk3Y&Ki1^-ap`yRNG3i3B<%A|UA!7y?XBgwp48 z;Et+p4gtoM$s!U`60l6QvA(53=U4zORc(y~O{()d|FsdYI|QIyDn+;M872P%4!Y{- TZnFlg00000NkvXXu0mjfX5y*S literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..cb7009deaa2c13f10987565fad4c9e1ebe66c5aa GIT binary patch literal 2384 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 UT?P;EZVeEaqT2In1&h%C2lWtEaR2}S literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..b32d1756ccde3f84a26d989b45a8af70a7b60d04 GIT binary patch literal 2747 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 + + + + + + + + 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 0000000000000000000000000000000000000000..314eb8ef96947d191ce4920c3d312da92fc65767 GIT binary patch literal 268 zcmeAS@N?(olHy`uVBq!ia0vp^LO?9a!3HF~Pw#sVq!^2X+?^QKos)S90RNH+H(seOwv7tK_w>@#H4kzg|Ch3)w>#|ZKJJHmFDQ8$`^3*!ZN$uN z^50;Q_OFM(-!cd*yZpHS|38mUPY=)kfB(}T+&J^p!JsD~sd3`O#!k+xfA(w~zvbs9 z^$N~rxvl2ME_5bQ<_Txf`Xi>t7|R8f_qDX~9g3OyfBmi6>4M!B%nU!cm2Iu7dYysJ OWAJqKb6Mw<&;$T6n`1}- literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..ef0cdc066a526eeeb7ba760d907115a2d1c0c26a GIT binary patch literal 276 zcmeAS@N?(olHy`uVBq!ia0vp^LO?9a!3HF~Pw#sVq!^2X+?^QKos)S9q?zevUW;JPv PbRUDKtDnm{r-UW|1UqF4 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..2b1417aad99e1ecf3376d795c9f76eb61d83202b GIT binary patch literal 1985 zcmV;y2R`_TP)`Kj+^0xjVb|#&+VwNkbC?MHPiYr~s)& zq7|wvQC_GJFGxi~JRlX7S6-SI5Pt)&2&6m!QY8uk0S%Q(A!-#;D}(?|l_pK%R8|}( z-rc!#4-d1u-u3>l5+!SKEKck7rli$&)9G9q%*F0>_VkH?(^j-!#U4e|Dz&XjaPvuN3N01XUGL zmCkKCI&J9&(7m%r2haib9&SWC6Urkc&^f2?U%uWJW%-R)fBff31jbsx?wxz-V@H=x zeB<{-FcCWqq(gK5F z{DF$|o`r=50p{)rAk>mdVtc7br*DDWdFJNofrq;%ASzj!M7FEH_a18u(=3~ESJ0TP zg{mBwgd0sjsHJIS1{Se3Otb9Eou@vVS!3m)Nx0DjkVcY3J%=wXV0%ez8j?HDOjgAp z6SMJ{H6XJ|67?=sJ=PkgxQ1!Ff?93oY&^yS%nT6l9&0T+WeZd*F)Hkvgc}!yX3{w9 zU8*Y9TBa9+%4}Sz#HeS_B;05Msg67@1x%*Oj*O%+EKB;_uD^icjM>97?G$o84&OwVkJ0z8f!&!weC~3zt=@$w1N-cFzKpG(uJ9 z#$kpC7^AgnW$=X_VXALqUCEs%jsk)xNRBPP7>~jX5%At*l}a+8tr4>;amcIUy)wUN zwmq-ak)d>mfEdXtX*4)4yR!zMTnGF1HMH5feR$OGLj+WPR!LLSH?S*NxGQZ}Fh4hA z#21G}B-;M_FacN0CP`@f)sE+2n2zp zUY|h~>XnE%v{YO`o`s2e(90X#Z4Kzm* z076yjX%bmM2(97rjhmc3`!3g3*J!nJ6vb;ftGJW}NT+??iN)p7baw)} zp`vqYWMI1i5n;2LbN$8|)`lMeLLiz%z*<8ii9!%Kefk2w`Td(byfnj;k1w$}pWUqo z+_Rj2_e0)Zxyt3sYpmW_%ZKjH>#P1Q6SZlbF>TH~HNk{?0@D>pb`L$HzXX zyBl*iveTSPAFT80YySpdV`Ed-*Vo=&y>#X$fTdiw1{g^|j8G2)%R4IzF z)>>>7b~_W@iM^P(oq&4vYpXYIvbe8o2An>7i7$Tck$YC=UgkXWB0{tXL6S#8zPgk><#c%)e*RTET(~o^LN|Pwq zY_@xVG9zp6_@Kpm1r^P*MK(6B=0LN*tQjDns$N9$fBo$juP+{X;!A~V{U`|Hrx5YR zm;&|E7r_97_V??`eQ(ii1LbLV0hVK180me{+c@uHML;QB^OcQ_tLs3k^Vyocy+{)g z3sixWGIfWCjy<&$MAf>8YEg(Hg^GAz6rxb5c&{K@NTDDqMd3vR=L$s%FNjh&EyOE1 zia2$M;tDU~6u`N{mo#(DbLT}wRomk3Y&Ki1^-ap`yRNG3i3B<%A|UA!7y?XBgwp48 z;Et+p4gtoM$s!U`60l6QvA(53=U4zORc(y~O{()d|FsdYI|QIyDn+;M872P%4!Y{- TZnFlg00000NkvXXu0mjfX5y*S literal 0 HcmV?d00001 diff --git a/samples/WiktionarySimple/res/drawable/star_logo.png b/samples/WiktionarySimple/res/drawable/star_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..b32d1756ccde3f84a26d989b45a8af70a7b60d04 GIT binary patch literal 2747 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 + + + + + + + + + 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 0000000000000000000000000000000000000000..314eb8ef96947d191ce4920c3d312da92fc65767 GIT binary patch literal 268 zcmeAS@N?(olHy`uVBq!ia0vp^LO?9a!3HF~Pw#sVq!^2X+?^QKos)S90RNH+H(seOwv7tK_w>@#H4kzg|Ch3)w>#|ZKJJHmFDQ8$`^3*!ZN$uN z^50;Q_OFM(-!cd*yZpHS|38mUPY=)kfB(}T+&J^p!JsD~sd3`O#!k+xfA(w~zvbs9 z^$N~rxvl2ME_5bQ<_Txf`Xi>t7|R8f_qDX~9g3OyfBmi6>4M!B%nU!cm2Iu7dYysJ OWAJqKb6Mw<&;$T6n`1}- literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..ef0cdc066a526eeeb7ba760d907115a2d1c0c26a GIT binary patch literal 276 zcmeAS@N?(olHy`uVBq!ia0vp^LO?9a!3HF~Pw#sVq!^2X+?^QKos)S9q?zevUW;JPv PbRUDKtDnm{r-UW|1UqF4 literal 0 HcmV?d00001 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; + } + } +}