Development Android

//
//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 = "";
    /**
     * 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;
            }
        }
    }
    //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 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
        }
    }
    //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



                android:fromXDelta="-26"
            android:toXDelta="0"
            android:duration="400" />

    //res\anim\slide_out.xml



                android:fromXDelta="0"
            android:toXDelta="-26"
            android:duration="400" />

    //
    //res\layout\about.xml


        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        android:padding="20dip">
                android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:textSize="16sp"
            android:text="@string/app_descrip"
            android:textColor="?android:attr/textColorPrimaryInverse" />
                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" />

    //res\layout\lookup.xml


        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">
                android:id="@+id/title_bar"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal"
            android:gravity="center_vertical">
                        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" />
                        android:id="@+id/title"
                android:layout_width="0dip"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:padding="10dip"
                style="@style/LookupTitle" />
        
                android:id="@+id/webview"
            android:layout_width="match_parent"
            android:layout_height="0dip"
            android:layout_weight="1" />

    //res\layout\widget_message.xml


        android:id="@+id/widget"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        style="@style/WidgetBackground">
                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" />

    //res\layout\widget_word.xml


        android:id="@+id/widget"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:focusable="true"
        style="@style/WidgetBackground">
                android:id="@+id/icon"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentRight="true"
            android:src="@drawable/star_logo" />
                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" />
                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" />
                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" />
                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" />

    //
    //res\menu\lookup.xml



                android:id="@+id/lookup_search"
            android:title="@string/lookup_search"
            android:icon="@android:drawable/ic_menu_search" />
                android:id="@+id/lookup_random"
            android:title="@string/lookup_random"
            android:icon="@drawable/ic_menu_shuffle" />
                android:id="@+id/lookup_about"
            android:title="@string/lookup_about"
            android:icon="@android:drawable/ic_menu_help" />

    //
    //res\values\strings.xml



        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"

        "%1$s/%2$s (Linux; Android)"

        "Wiktionary:Word of the day/%1$s %2$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.

    //res\values\styles.xml



        
            @drawable/widget_bg
        
        
            13sp
            @android:color/black
        
        
        
            14sp
            @android:color/black
        
        
            16sp
            bold
            @android:color/black
        
        
            14sp
            italic
            @android:color/black
        
        
            13sp
            @android:color/black
        
        
            true
            @drawable/progress_spin
            repeat
            3500
        
        
            30sp
            bold
            ?android:attr/textColorPrimary
        

    //res\values\themes.xml



        
            @drawable/lookup_bg
        

    //
    //res\xml\searchable.xml


        android:label="@string/search_label"
        android:hint="@string/search_hint" />
    //res\xml\widget_word.xml


        android:minWidth="146dip"
        android:minHeight="72dip"
        android:updatePeriodMillis="86400000"
        android:initialLayout="@layout/widget_message" />