1 /*
2  * Copyright (C) 2009 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.example.android.wiktionary;
18 
19 import com.example.android.wiktionary.SimpleWikiHelper.ApiException;
20 import com.example.android.wiktionary.SimpleWikiHelper.ParseException;
21 
22 import android.app.Activity;
23 import android.app.AlertDialog;
24 import android.app.SearchManager;
25 import android.content.Intent;
26 import android.net.Uri;
27 import android.os.AsyncTask;
28 import android.os.Bundle;
29 import android.os.SystemClock;
30 import android.text.TextUtils;
31 import android.text.format.DateUtils;
32 import android.util.Log;
33 import android.view.KeyEvent;
34 import android.view.Menu;
35 import android.view.MenuInflater;
36 import android.view.MenuItem;
37 import android.view.View;
38 import android.view.animation.Animation;
39 import android.view.animation.AnimationUtils;
40 import android.view.animation.Animation.AnimationListener;
41 import android.webkit.WebView;
42 import android.widget.ProgressBar;
43 import android.widget.TextView;
44 
45 import java.util.Stack;
46 
47 /**
48  * Activity that lets users browse through Wiktionary content. This is just the
49  * user interface, and all API communication and parsing is handled in
50  * {@link ExtendedWikiHelper}.
51  */
52 public class LookupActivity extends Activity implements AnimationListener {
53     private static final String TAG = "LookupActivity";
54 
55     private View mTitleBar;
56     private TextView mTitle;
57     private ProgressBar mProgress;
58     private WebView mWebView;
59 
60     private Animation mSlideIn;
61     private Animation mSlideOut;
62 
63     /**
64      * History stack of previous words browsed in this session. This is
65      * referenced when the user taps the "back" key, to possibly intercept and
66      * show the last-visited entry, instead of closing the activity.
67      */
68     private Stack<String> mHistory = new Stack<String>();
69 
70     private String mEntryTitle;
71 
72     /**
73      * Keep track of last time user tapped "back" hard key. When pressed more
74      * than once within {@link #BACK_THRESHOLD}, we treat let the back key fall
75      * through and close the app.
76      */
77     private long mLastPress = -1;
78 
79     private static final long BACK_THRESHOLD = DateUtils.SECOND_IN_MILLIS / 2;
80 
81     /**
82      * {@inheritDoc}
83      */
84     @Override
onCreate(Bundle savedInstanceState)85     public void onCreate(Bundle savedInstanceState) {
86         super.onCreate(savedInstanceState);
87 
88         setContentView(R.layout.lookup);
89 
90         // Load animations used to show/hide progress bar
91         mSlideIn = AnimationUtils.loadAnimation(this, R.anim.slide_in);
92         mSlideOut = AnimationUtils.loadAnimation(this, R.anim.slide_out);
93 
94         // Listen for the "in" animation so we make the progress bar visible
95         // only after the sliding has finished.
96         mSlideIn.setAnimationListener(this);
97 
98         mTitleBar = findViewById(R.id.title_bar);
99         mTitle = (TextView) findViewById(R.id.title);
100         mProgress = (ProgressBar) findViewById(R.id.progress);
101         mWebView = (WebView) findViewById(R.id.webview);
102 
103         // Make the view transparent to show background
104         mWebView.setBackgroundColor(0);
105 
106         // Prepare User-Agent string for wiki actions
107         ExtendedWikiHelper.prepareUserAgent(this);
108 
109         // Handle incoming intents as possible searches or links
110         onNewIntent(getIntent());
111     }
112 
113     /**
114      * Intercept the back-key to try walking backwards along our word history
115      * stack. If we don't have any remaining history, the key behaves normally
116      * and closes this activity.
117      */
118     @Override
onKeyDown(int keyCode, KeyEvent event)119     public boolean onKeyDown(int keyCode, KeyEvent event) {
120         // Handle back key as long we have a history stack
121         if (keyCode == KeyEvent.KEYCODE_BACK && !mHistory.empty()) {
122 
123             // Compare against last pressed time, and if user hit multiple times
124             // in quick succession, we should consider bailing out early.
125             long currentPress = SystemClock.uptimeMillis();
126             if (currentPress - mLastPress < BACK_THRESHOLD) {
127                 return super.onKeyDown(keyCode, event);
128             }
129             mLastPress = currentPress;
130 
131             // Pop last entry off stack and start loading
132             String lastEntry = mHistory.pop();
133             startNavigating(lastEntry, false);
134 
135             return true;
136         }
137 
138         // Otherwise fall through to parent
139         return super.onKeyDown(keyCode, event);
140     }
141 
142     /**
143      * Start navigating to the given word, pushing any current word onto the
144      * history stack if requested. The navigation happens on a background thread
145      * and updates the GUI when finished.
146      *
147      * @param word The dictionary word to navigate to.
148      * @param pushHistory If true, push the current word onto history stack.
149      */
startNavigating(String word, boolean pushHistory)150     private void startNavigating(String word, boolean pushHistory) {
151         // Push any current word onto the history stack
152         if (!TextUtils.isEmpty(mEntryTitle) && pushHistory) {
153             mHistory.add(mEntryTitle);
154         }
155 
156         // Start lookup for new word in background
157         new LookupTask().execute(word);
158     }
159 
160     /**
161      * {@inheritDoc}
162      */
163     @Override
onCreateOptionsMenu(Menu menu)164     public boolean onCreateOptionsMenu(Menu menu) {
165         MenuInflater inflater = getMenuInflater();
166         inflater.inflate(R.menu.lookup, menu);
167         return true;
168     }
169 
170     /**
171      * {@inheritDoc}
172      */
173     @Override
onOptionsItemSelected(MenuItem item)174     public boolean onOptionsItemSelected(MenuItem item) {
175         switch (item.getItemId()) {
176             case R.id.lookup_search: {
177                 onSearchRequested();
178                 return true;
179             }
180             case R.id.lookup_random: {
181                 startNavigating(null, true);
182                 return true;
183             }
184             case R.id.lookup_about: {
185                 showAbout();
186                 return true;
187             }
188         }
189         return false;
190     }
191 
192     /**
193      * Show an about dialog that cites data sources.
194      */
showAbout()195     protected void showAbout() {
196         // Inflate the about message contents
197         View messageView = getLayoutInflater().inflate(R.layout.about, null, false);
198 
199         // When linking text, force to always use default color. This works
200         // around a pressed color state bug.
201         TextView textView = (TextView) messageView.findViewById(R.id.about_credits);
202         int defaultColor = textView.getTextColors().getDefaultColor();
203         textView.setTextColor(defaultColor);
204 
205         AlertDialog.Builder builder = new AlertDialog.Builder(this);
206         builder.setIcon(R.drawable.app_icon);
207         builder.setTitle(R.string.app_name);
208         builder.setView(messageView);
209         builder.create();
210         builder.show();
211     }
212 
213     /**
214      * Because we're singleTop, we handle our own new intents. These usually
215      * come from the {@link SearchManager} when a search is requested, or from
216      * internal links the user clicks on.
217      */
218     @Override
onNewIntent(Intent intent)219     public void onNewIntent(Intent intent) {
220         final String action = intent.getAction();
221         if (Intent.ACTION_SEARCH.equals(action)) {
222             // Start query for incoming search request
223             String query = intent.getStringExtra(SearchManager.QUERY);
224             startNavigating(query, true);
225 
226         } else if (Intent.ACTION_VIEW.equals(action)) {
227             // Treat as internal link only if valid Uri and host matches
228             Uri data = intent.getData();
229             if (data != null && ExtendedWikiHelper.WIKI_LOOKUP_HOST
230                     .equals(data.getHost())) {
231                 String query = data.getPathSegments().get(0);
232                 startNavigating(query, true);
233             }
234 
235         } else {
236             // If not recognized, then start showing random word
237             startNavigating(null, true);
238         }
239     }
240 
241     /**
242      * Set the title for the current entry.
243      */
setEntryTitle(String entryText)244     protected void setEntryTitle(String entryText) {
245         mEntryTitle = entryText;
246         mTitle.setText(mEntryTitle);
247     }
248 
249     /**
250      * Set the content for the current entry. This will update our
251      * {@link WebView} to show the requested content.
252      */
setEntryContent(String entryContent)253     protected void setEntryContent(String entryContent) {
254         mWebView.loadDataWithBaseURL(ExtendedWikiHelper.WIKI_AUTHORITY, entryContent,
255                 ExtendedWikiHelper.MIME_TYPE, ExtendedWikiHelper.ENCODING, null);
256     }
257 
258     /**
259      * Background task to handle Wiktionary lookups. This correctly shows and
260      * hides the loading animation from the GUI thread before starting a
261      * background query to the Wiktionary API. When finished, it transitions
262      * back to the GUI thread where it updates with the newly-found entry.
263      */
264     private class LookupTask extends AsyncTask<String, String, String> {
265         /**
266          * Before jumping into background thread, start sliding in the
267          * {@link ProgressBar}. We'll only show it once the animation finishes.
268          */
269         @Override
onPreExecute()270         protected void onPreExecute() {
271             mTitleBar.startAnimation(mSlideIn);
272         }
273 
274         /**
275          * Perform the background query using {@link ExtendedWikiHelper}, which
276          * may return an error message as the result.
277          */
278         @Override
doInBackground(String... args)279         protected String doInBackground(String... args) {
280             String query = args[0];
281             String parsedText = null;
282 
283             try {
284                 // If query word is null, assume request for random word
285                 if (query == null) {
286                     query = ExtendedWikiHelper.getRandomWord();
287                 }
288 
289                 if (query != null) {
290                     // Push our requested word to the title bar
291                     publishProgress(query);
292                     String wikiText = ExtendedWikiHelper.getPageContent(query, true);
293                     parsedText = ExtendedWikiHelper.formatWikiText(wikiText);
294                 }
295             } catch (ApiException e) {
296                 Log.e(TAG, "Problem making wiktionary request", e);
297             } catch (ParseException e) {
298                 Log.e(TAG, "Problem making wiktionary request", e);
299             }
300 
301             if (parsedText == null) {
302                 parsedText = getString(R.string.empty_result);
303             }
304 
305             return parsedText;
306         }
307 
308         /**
309          * Our progress update pushes a title bar update.
310          */
311         @Override
onProgressUpdate(String... args)312         protected void onProgressUpdate(String... args) {
313             String searchWord = args[0];
314             setEntryTitle(searchWord);
315         }
316 
317         /**
318          * When finished, push the newly-found entry content into our
319          * {@link WebView} and hide the {@link ProgressBar}.
320          */
321         @Override
onPostExecute(String parsedText)322         protected void onPostExecute(String parsedText) {
323             mTitleBar.startAnimation(mSlideOut);
324             mProgress.setVisibility(View.INVISIBLE);
325 
326             setEntryContent(parsedText);
327         }
328     }
329 
330     /**
331      * Make the {@link ProgressBar} visible when our in-animation finishes.
332      */
onAnimationEnd(Animation animation)333     public void onAnimationEnd(Animation animation) {
334         mProgress.setVisibility(View.VISIBLE);
335     }
336 
onAnimationRepeat(Animation animation)337     public void onAnimationRepeat(Animation animation) {
338         // Not interested if the animation repeats
339     }
340 
onAnimationStart(Animation animation)341     public void onAnimationStart(Animation animation) {
342         // Not interested when the animation starts
343     }
344 }
345