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