Browse through Wiktionary content
//
//src\com\example\android\wiktionary\ExtendedWikiHelper.java
/*
* 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 = "<style>h2 {font-size:1.2em;font-weight:normal;} " +
"a {color:#6688cc;} ol {padding-left:1.5em;} blockquote {margin-left:0em;} " +
".interProject, .noprint {display:none;} " +
"li, blockquote {margin-top:0.5em;margin-bottom:0.5em;}</style>";
/**
* 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<FormatRule> sFormatRules = new ArrayList<FormatRule>();
static {
// Format header blocks and wrap outside content in ordered list
sFormatRules.add(new FormatRule("^=+(.+?)=+", "</ol><h2>$1</h2><ol>",
Pattern.MULTILINE));
// Indent quoted blocks, handle ordered and bullet lists
sFormatRules.add(new FormatRule("^#+\\*?:(.+?)$", "<blockquote>$1</blockquote>",
Pattern.MULTILINE));
sFormatRules.add(new FormatRule("^#+:?\\*(.+?)$", "<ul><li>$1</li></ul>",
Pattern.MULTILINE));
sFormatRules.add(new FormatRule("^#+(.+?)$", "<li>$1</li>",
Pattern.MULTILINE));
// Add internal links
sFormatRules.add(new FormatRule("\\[\\[([^:\\|\\]]+)\\]\\]",
String.format("<a href=\"%s://%s/$1\">$1</a>", WIKI_AUTHORITY, WIKI_LOOKUP_HOST)));
sFormatRules.add(new FormatRule("\\[\\[([^:\\|\\]]+)\\|([^\\]]+)\\]\\]",
String.format("<a href=\"%s://%s/$1\">$2</a>", WIKI_AUTHORITY, WIKI_LOOKUP_HOST)));
// Add bold and italic formatting
sFormatRules.add(new FormatRule("'''(.+?)'''", "<b>$1</b>"));
sFormatRules.add(new FormatRule("([^'])''([^'].*?[^'])''([^'])", "$1<i>$2</i>$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<String> foundSections = new HashSet<String>();
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;
}
}
}
//src\com\example\android\wiktionary\LookupActivity.java
/*
* 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<String> mHistory = new Stack<String>();
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<String, String, String> {
/**
* 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
}
}
//src\com\example\android\wiktionary\SimpleWikiHelper.java
/*
* 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);
}
}
}
//src\com\example\android\wiktionary\WordWidget.java
/*
* 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;
}
}
}
//
//res\anim\slide_in.xml
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<set xmlns:android="http://schemas.android.com/apk/res/android" android:fillAfter="true">
<translate
android:fromXDelta="-26"
android:toXDelta="0"
android:duration="400" />
</set>
//res\anim\slide_out.xml
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<set xmlns:android="http://schemas.android.com/apk/res/android" android:fillAfter="true">
<translate
android:fromXDelta="0"
android:toXDelta="-26"
android:duration="400" />
</set>
//
//res\layout\about.xml
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="20dip">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="16sp"
android:text="@string/app_descrip"
android:textColor="?android:attr/textColorPrimaryInverse" />
<TextView
android:id="@+id/about_credits"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="20dip"
android:textSize="16sp"
android:text="@string/app_credits"
android:autoLink="web"
android:textColor="?android:attr/textColorPrimaryInverse" />
</LinearLayout>
//res\layout\lookup.xml
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<LinearLayout
android:id="@+id/title_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical">
<ProgressBar
android:id="@+id/progress"
android:layout_width="18dip"
android:layout_height="18dip"
android:layout_marginLeft="10dip"
android:visibility="invisible"
android:indeterminateOnly="true"
android:indeterminateDrawable="@drawable/progress_spin"
android:indeterminateBehavior="repeat"
android:indeterminateDuration="3500" />
<TextView
android:id="@+id/title"
android:layout_width="0dip"
android:layout_height="wrap_content"
android:layout_weight="1"
android:padding="10dip"
style="@style/LookupTitle" />
</LinearLayout>
<WebView
android:id="@+id/webview"
android:layout_width="match_parent"
android:layout_height="0dip"
android:layout_weight="1" />
</LinearLayout>
//res\layout\widget_message.xml
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/widget"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
style="@style/WidgetBackground">
<TextView
android:id="@+id/message"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dip"
android:padding="10dip"
android:gravity="center"
android:text="@string/widget_loading"
style="@style/Text.Loading" />
</LinearLayout>
//res\layout\widget_word.xml
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/widget"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:focusable="true"
style="@style/WidgetBackground">
<ImageView
android:id="@+id/icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:src="@drawable/star_logo" />
<TextView
android:id="@+id/word_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="14dip"
android:layout_marginBottom="1dip"
android:includeFontPadding="false"
android:singleLine="true"
android:ellipsize="end"
style="@style/Text.WordTitle" />
<TextView
android:id="@+id/word_type"
android:layout_width="0dip"
android:layout_height="wrap_content"
android:layout_toRightOf="@id/word_title"
android:layout_toLeftOf="@id/icon"
android:layout_alignBaseline="@id/word_title"
android:paddingLeft="4dip"
android:includeFontPadding="false"
android:singleLine="true"
android:ellipsize="end"
style="@style/Text.WordType" />
<TextView
android:id="@+id/bullet"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/word_title"
android:paddingRight="4dip"
android:includeFontPadding="false"
android:singleLine="true"
style="@style/BulletPoint" />
<TextView
android:id="@+id/definition"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/word_title"
android:layout_toRightOf="@id/bullet"
android:paddingRight="5dip"
android:paddingBottom="4dip"
android:includeFontPadding="false"
android:lineSpacingMultiplier="0.9"
android:maxLines="4"
android:fadingEdge="vertical"
style="@style/Text.Definition" />
</RelativeLayout>
//
//res\menu\lookup.xml
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/lookup_search"
android:title="@string/lookup_search"
android:icon="@android:drawable/ic_menu_search" />
<item
android:id="@+id/lookup_random"
android:title="@string/lookup_random"
android:icon="@drawable/ic_menu_shuffle" />
<item
android:id="@+id/lookup_about"
android:title="@string/lookup_about"
android:icon="@android:drawable/ic_menu_help" />
</menu>
//
//res\values\strings.xml
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<resources>
<string name="app_name">Wiktionary example</string>
<string name="app_descrip">Example of a fast Wiktionary browser and Word-of-day widget</string>
<string name="app_credits">"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"</string>
<string name="template_user_agent" translatable="false">"%1$s/%2$s (Linux; Android)"</string>
<string name="template_wotd_title">"Wiktionary:Word of the day/%1$s %2$s"</string>
<string name="template_define_url" translatable="false">"http://en.wiktionary.org/wiki/%s"</string>
<string name="widget_name">Wiktionary</string>
<string name="widget_loading">"Loading word\nof day\u2026"</string>
<string name="widget_error">No word of day found</string>
<string-array name="month_names">
<item>January</item>
<item>February</item>
<item>March</item>
<item>April</item>
<item>May</item>
<item>June</item>
<item>July</item>
<item>August</item>
<item>September</item>
<item>October</item>
<item>November</item>
<item>December</item>
</string-array>
<string name="search_label">Wiktionary search</string>
<string name="search_hint">Define word</string>
<string name="lookup_search">Search</string>
<string name="lookup_random">Random</string>
<string name="lookup_about">About</string>
<string name="empty_result">No entry found for this word, or problem reading data.</string>
</resources>
//res\values\styles.xml
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<resources>
<style name="WidgetBackground">
<item name="android:background">@drawable/widget_bg</item>
</style>
<style name="BulletPoint">
<item name="android:textSize">13sp</item>
<item name="android:textColor">@android:color/black</item>
</style>
<style name="Text" />
<style name="Text.Loading">
<item name="android:textSize">14sp</item>
<item name="android:textColor">@android:color/black</item>
</style>
<style name="Text.WordTitle">
<item name="android:textSize">16sp</item>
<item name="android:textStyle">bold</item>
<item name="android:textColor">@android:color/black</item>
</style>
<style name="Text.WordType">
<item name="android:textSize">14sp</item>
<item name="android:textStyle">italic</item>
<item name="android:textColor">@android:color/black</item>
</style>
<style name="Text.Definition">
<item name="android:textSize">13sp</item>
<item name="android:textColor">@android:color/black</item>
</style>
<style name="LookupProgress">
<item name="android:indeterminateOnly">true</item>
<item name="android:indeterminateDrawable">@drawable/progress_spin</item>
<item name="android:indeterminateBehavior">repeat</item>
<item name="android:indeterminateDuration">3500</item>
</style>
<style name="LookupTitle">
<item name="android:textSize">30sp</item>
<item name="android:textStyle">bold</item>
<item name="android:textColor">?android:attr/textColorPrimary</item>
</style>
</resources>
//res\values\themes.xml
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<resources>
<style name="LookupTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">@drawable/lookup_bg</item>
</style>
</resources>
//
//res\xml\searchable.xml
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<searchable xmlns:android="http://schemas.android.com/apk/res/android"
android:label="@string/search_label"
android:hint="@string/search_hint" />
//res\xml\widget_word.xml
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:minWidth="146dip"
android:minHeight="72dip"
android:updatePeriodMillis="86400000"
android:initialLayout="@layout/widget_message" />
Related examples in the same category