| 1 | /* This file is part of Aard Dictionary for Android <http://aarddict.org>. |
| 2 | * |
| 3 | * This program is free software: you can redistribute it and/or modify |
| 4 | * it under the terms of the GNU General Public License version 3 |
| 5 | * as published by the Free Software Foundation. |
| 6 | * |
| 7 | * This program is distributed in the hope that it will be useful, |
| 8 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 9 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 10 | * GNU General Public License <http://www.gnu.org/licenses/gpl-3.0.txt> |
| 11 | * for more details. |
| 12 | * |
| 13 | * Copyright (C) 2010 Igor Tkach |
| 14 | */ |
| 15 | |
| 16 | package aarddict.android; |
| 17 | |
| 18 | import java.io.IOException; |
| 19 | import java.io.InputStream; |
| 20 | import java.io.InputStreamReader; |
| 21 | import java.io.Reader; |
| 22 | import java.util.ArrayList; |
| 23 | import java.util.Collections; |
| 24 | import java.util.HashMap; |
| 25 | import java.util.Iterator; |
| 26 | import java.util.LinkedList; |
| 27 | import java.util.List; |
| 28 | import java.util.Map; |
| 29 | import java.util.Timer; |
| 30 | import java.util.TimerTask; |
| 31 | |
| 32 | import aarddict.Article; |
| 33 | import aarddict.ArticleNotFound; |
| 34 | import aarddict.Entry; |
| 35 | import aarddict.LookupWord; |
| 36 | import aarddict.RedirectTooManyLevels; |
| 37 | import aarddict.Volume; |
| 38 | import android.app.AlertDialog; |
| 39 | import android.app.SearchManager; |
| 40 | import android.content.DialogInterface; |
| 41 | import android.content.Intent; |
| 42 | import android.content.SharedPreferences; |
| 43 | import android.content.DialogInterface.OnClickListener; |
| 44 | import android.content.SharedPreferences.Editor; |
| 45 | import android.net.Uri; |
| 46 | import android.os.Build; |
| 47 | import android.os.Bundle; |
| 48 | import android.util.Log; |
| 49 | import android.view.KeyEvent; |
| 50 | import android.view.Menu; |
| 51 | import android.view.MenuItem; |
| 52 | import android.view.MotionEvent; |
| 53 | import android.view.Surface; |
| 54 | import android.view.View; |
| 55 | import android.view.Window; |
| 56 | import android.view.animation.AlphaAnimation; |
| 57 | import android.view.animation.Animation; |
| 58 | import android.view.animation.Animation.AnimationListener; |
| 59 | import android.webkit.JsResult; |
| 60 | import android.webkit.WebChromeClient; |
| 61 | import android.webkit.WebView; |
| 62 | import android.webkit.WebViewClient; |
| 63 | import android.widget.Button; |
| 64 | import android.widget.Toast; |
| 65 | |
| 66 | |
| 67 | public class ArticleViewActivity extends BaseDictionaryActivity { |
| 68 | |
| 69 | private final static String TAG = ArticleViewActivity.class.getName(); |
| 70 | |
| 71 | public static final int NOOK_KEY_PREV_LEFT = 92; |
| 72 | public static final int NOOK_KEY_NEXT_LEFT = 93; |
| 73 | |
| 74 | public static final int NOOK_KEY_PREV_RIGHT = 94; |
| 75 | public static final int NOOK_KEY_NEXT_RIGHT = 95; |
| 76 | |
| 77 | private ArticleView articleView; |
| 78 | private String sharedCSS; |
| 79 | private String mediawikiSharedCSS; |
| 80 | private String mediawikiMonobookCSS; |
| 81 | private String js; |
| 82 | |
| 83 | private List<HistoryItem> backItems; |
| 84 | private Timer timer; |
| 85 | private TimerTask currentTask; |
| 86 | private TimerTask currentHideNextButtonTask; |
| 87 | private AlphaAnimation fadeOutAnimation; |
| 88 | private boolean useAnimation = false; |
| 89 | |
| 90 | private Map<Article, ScrollXY> scrollPositionsH; |
| 91 | private Map<Article, ScrollXY> scrollPositionsV; |
| 92 | private boolean saveScrollPos = true; |
| 93 | |
| 94 | |
| 95 | static class AnimationAdapter implements AnimationListener { |
| 96 | public void onAnimationEnd(Animation animation) {} |
| 97 | public void onAnimationRepeat(Animation animation) {} |
| 98 | public void onAnimationStart(Animation animation) {} |
| 99 | } |
| 100 | |
| 101 | @Override |
| 102 | void initUI() { |
| 103 | this.scrollPositionsH = Collections.synchronizedMap(new HashMap<Article, ScrollXY>()); |
| 104 | this.scrollPositionsV = Collections.synchronizedMap(new HashMap<Article, ScrollXY>()); |
| 105 | loadAssets(); |
| 106 | |
| 107 | if (DeviceInfo.EINK_SCREEN) { |
| 108 | useAnimation = false; |
| 109 | N2EpdController.setGL16Mode(2); // force full screen refresh when changing articles |
| 110 | |
| 111 | setContentView(R.layout.eink_article_view); |
| 112 | articleView = (ArticleView)findViewById(R.id.EinkArticleView); |
| 113 | } |
| 114 | // Setup animations only on non-eink screens |
| 115 | else |
| 116 | { |
| 117 | //Animation is broken before 2.1 - animation listener notified, |
| 118 | //only sometimes so we can't use it |
| 119 | try { |
| 120 | useAnimation = Integer.parseInt(Build.VERSION.SDK) > 6; |
| 121 | } |
| 122 | catch (Exception e) { |
| 123 | Log.w(TAG, "Failed to parse SDK version string as int: " + Build.VERSION.SDK); |
| 124 | } |
| 125 | |
| 126 | fadeOutAnimation = new AlphaAnimation(1f, 0f); |
| 127 | fadeOutAnimation.setDuration(600); |
| 128 | fadeOutAnimation.setAnimationListener(new AnimationAdapter() { |
| 129 | public void onAnimationEnd(Animation animation) { |
| 130 | Button nextButton = (Button)findViewById(R.id.NextButton); |
| 131 | nextButton.setVisibility(Button.GONE); |
| 132 | } |
| 133 | }); |
| 134 | |
| 135 | getWindow().requestFeature(Window.FEATURE_PROGRESS); |
| 136 | setContentView(R.layout.article_view); |
| 137 | articleView = (ArticleView)findViewById(R.id.ArticleView); |
| 138 | } |
| 139 | |
| 140 | Log.d(TAG, "Build.VERSION.SDK: " + Build.VERSION.SDK); |
| 141 | Log.d(TAG, "use animation? " + useAnimation); |
| 142 | |
| 143 | timer = new Timer(); |
| 144 | |
| 145 | backItems = Collections.synchronizedList(new LinkedList<HistoryItem>()); |
| 146 | |
| 147 | articleView.setOnScrollListener(new ArticleView.ScrollListener(){ |
| 148 | public void onScroll(int l, int t, int oldl, int oldt) { |
| 149 | saveScrollPos(l, t); |
| 150 | } |
| 151 | }); |
| 152 | |
| 153 | articleView.getSettings().setJavaScriptEnabled(true); |
| 154 | |
| 155 | articleView.addJavascriptInterface(new SectionMatcher(), "matcher"); |
| 156 | |
| 157 | articleView.setWebChromeClient(new WebChromeClient(){ |
| 158 | |
| 159 | @Override |
| 160 | public boolean onJsAlert(WebView view, String url, String message, |
| 161 | JsResult result) { |
| 162 | Log.d(TAG + ".js", String.format("[%s]: %s", url, message)); |
| 163 | result.cancel(); |
| 164 | return true; |
| 165 | } |
| 166 | |
| 167 | public void onProgressChanged(WebView view, int newProgress) { |
| 168 | Log.d(TAG, "Progress: " + newProgress); |
| 169 | setProgress(5000 + newProgress * 50); |
| 170 | } |
| 171 | }); |
| 172 | |
| 173 | articleView.setWebViewClient(new WebViewClient() { |
| 174 | |
| 175 | @Override |
| 176 | public void onPageFinished(WebView view, String url) { |
| 177 | Log.d(TAG, "Page finished: " + url); |
| 178 | currentTask = null; |
| 179 | String section = null; |
| 180 | |
| 181 | if (url.contains("#")) { |
| 182 | LookupWord lookupWord = LookupWord.splitWord(url); |
| 183 | section = lookupWord.section; |
| 184 | if (backItems.size() > 0) { |
| 185 | HistoryItem currentHistoryItem = backItems.get(backItems.size() - 1); |
| 186 | HistoryItem h = new HistoryItem(currentHistoryItem); |
| 187 | h.article.section = section; |
| 188 | backItems.add(h); |
| 189 | } |
| 190 | } |
| 191 | else if (backItems.size() > 0) { |
| 192 | Article current = backItems.get(backItems.size() - 1).article; |
| 193 | section = current.section; |
| 194 | } |
| 195 | if (!restoreScrollPos()) { |
| 196 | goToSection(section); |
| 197 | } |
| 198 | } |
| 199 | |
| 200 | @Override |
| 201 | public boolean shouldOverrideUrlLoading(WebView view, final String url) { |
| 202 | Log.d(TAG, "URL clicked: " + url); |
| 203 | String urlLower = url.toLowerCase(); |
| 204 | if (urlLower.startsWith("http://") || |
| 205 | urlLower.startsWith("https://") || |
| 206 | urlLower.startsWith("ftp://") || |
| 207 | urlLower.startsWith("sftp://") || |
| 208 | urlLower.startsWith("mailto:")) { |
| 209 | Intent browserIntent = new Intent(Intent.ACTION_VIEW, |
| 210 | Uri.parse(url)); |
| 211 | startActivity(browserIntent); |
| 212 | } |
| 213 | else { |
| 214 | if (currentTask == null) { |
| 215 | currentTask = new TimerTask() { |
| 216 | public void run() { |
| 217 | try { |
| 218 | Article currentArticle = backItems.get(backItems.size() - 1).article; |
| 219 | try { |
| 220 | Iterator<Entry> currentIterator = dictionaryService.followLink(url, currentArticle.volumeId); |
| 221 | List<Entry> result = new ArrayList<Entry>(); |
| 222 | while (currentIterator.hasNext() && result.size() < 20) { |
| 223 | result.add(currentIterator.next()); |
| 224 | } |
| 225 | showNext(new HistoryItem(result)); |
| 226 | } |
| 227 | catch (ArticleNotFound e) { |
| 228 | showMessage(getString(R.string.msgArticleNotFound, e.word.toString())); |
| 229 | } |
| 230 | } |
| 231 | catch (Exception e) { |
| 232 | StringBuilder msgBuilder = new StringBuilder("There was an error following link ") |
| 233 | .append("\"").append(url).append("\""); |
| 234 | if (e.getMessage() != null) { |
| 235 | msgBuilder.append(": ").append(e.getMessage()); |
| 236 | } |
| 237 | final String msg = msgBuilder.toString(); |
| 238 | Log.e(TAG, msg, e); |
| 239 | showError(msg); |
| 240 | } |
| 241 | } |
| 242 | }; |
| 243 | try { |
| 244 | timer.schedule(currentTask, 0); |
| 245 | } |
| 246 | catch (Exception e) { |
| 247 | Log.d(TAG, "Failed to schedule task", e); |
| 248 | } |
| 249 | } |
| 250 | } |
| 251 | return true; |
| 252 | } |
| 253 | }); |
| 254 | final Button nextButton = (Button)findViewById(R.id.NextButton); |
| 255 | nextButton.getBackground().setAlpha(180); |
| 256 | nextButton.setOnClickListener(new View.OnClickListener() { |
| 257 | public void onClick(View v) { |
| 258 | if (nextButton.getVisibility() == View.VISIBLE) { |
| 259 | updateNextButtonVisibility(); |
| 260 | nextArticle(); |
| 261 | updateNextButtonVisibility(); |
| 262 | } |
| 263 | } |
| 264 | }); |
| 265 | articleView.setOnTouchListener( |
| 266 | new View.OnTouchListener() { |
| 267 | public boolean onTouch(View v, MotionEvent event) { |
| 268 | updateNextButtonVisibility(); |
| 269 | return false; |
| 270 | } |
| 271 | } |
| 272 | ); |
| 273 | setProgressBarVisibility(true); |
| 274 | } |
| 275 | |
| 276 | private void scrollTo(ScrollXY s) { |
| 277 | scrollTo(s.x, s.y); |
| 278 | } |
| 279 | |
| 280 | private void scrollTo(int x, int y) { |
| 281 | saveScrollPos = false; |
| 282 | Log.d(TAG, "Scroll to " + x + ", " + y); |
| 283 | articleView.scrollTo(x, y); |
| 284 | saveScrollPos = true; |
| 285 | } |
| 286 | |
| 287 | private void goToSection(String section) { |
| 288 | Log.d(TAG, "Go to section " + section); |
| 289 | if (section == null || section.trim().equals("")) { |
| 290 | scrollTo(0, 0); |
| 291 | } |
| 292 | else { |
| 293 | articleView.loadUrl(String.format("javascript:scrollToMatch(\"%s\")", section)); |
| 294 | } |
| 295 | } |
| 296 | |
| 297 | @Override |
| 298 | public boolean onKeyDown(int keyCode, KeyEvent event) { |
| 299 | switch (keyCode) { |
| 300 | case KeyEvent.KEYCODE_BACK: |
| 301 | goBack(); |
| 302 | break; |
| 303 | case NOOK_KEY_PREV_LEFT: |
| 304 | case NOOK_KEY_PREV_RIGHT: |
| 305 | case KeyEvent.KEYCODE_VOLUME_UP: |
| 306 | if (!articleView.pageUp(false)) { |
| 307 | goBack(); |
| 308 | } |
| 309 | break; |
| 310 | case KeyEvent.KEYCODE_VOLUME_DOWN: |
| 311 | case NOOK_KEY_NEXT_LEFT: |
| 312 | case NOOK_KEY_NEXT_RIGHT: |
| 313 | if (!articleView.pageDown(false)) { |
| 314 | nextArticle(); |
| 315 | }; |
| 316 | break; |
| 317 | default: |
| 318 | return super.onKeyDown(keyCode, event); |
| 319 | } |
| 320 | return true; |
| 321 | } |
| 322 | |
| 323 | @Override |
| 324 | public boolean onKeyUp(int keyCode, KeyEvent event) { |
| 325 | //eat key ups corresponding to key downs so that volume keys don't beep |
| 326 | switch (keyCode) { |
| 327 | case KeyEvent.KEYCODE_BACK: |
| 328 | case KeyEvent.KEYCODE_VOLUME_UP: |
| 329 | case KeyEvent.KEYCODE_VOLUME_DOWN: |
| 330 | break; |
| 331 | default: |
| 332 | return super.onKeyDown(keyCode, event); |
| 333 | } |
| 334 | return true; |
| 335 | } |
| 336 | |
| 337 | private boolean zoomIn() { |
| 338 | boolean zoomed = articleView.zoomIn(); |
| 339 | float scale = articleView.getScale(); |
| 340 | articleView.setInitialScale(Math.round(scale*100)); |
| 341 | return zoomed; |
| 342 | } |
| 343 | |
| 344 | private boolean zoomOut() { |
| 345 | boolean zoomed = articleView.zoomOut(); |
| 346 | float scale = articleView.getScale(); |
| 347 | articleView.setInitialScale(Math.round(scale*100)); |
| 348 | return zoomed; |
| 349 | } |
| 350 | |
| 351 | private void goBack() { |
| 352 | if (backItems.size() == 1) { |
| 353 | finish(); |
| 354 | } |
| 355 | if (currentTask != null) { |
| 356 | return; |
| 357 | } |
| 358 | if (backItems.size() > 1) { |
| 359 | HistoryItem current = backItems.remove(backItems.size() - 1); |
| 360 | HistoryItem prev = backItems.get(backItems.size() - 1); |
| 361 | |
| 362 | Article prevArticle = prev.article; |
| 363 | if (prevArticle.equalsIgnoreSection(current.article)) { |
| 364 | resetTitleToCurrent(); |
| 365 | if (!prevArticle.sectionEquals(current.article) && !restoreScrollPos()) { |
| 366 | goToSection(prevArticle.section); |
| 367 | } |
| 368 | } |
| 369 | else { |
| 370 | showCurrentArticle(); |
| 371 | } |
| 372 | } |
| 373 | } |
| 374 | |
| 375 | private void nextArticle() { |
| 376 | HistoryItem current = backItems.get(backItems.size() - 1); |
| 377 | if (current.hasNext()) { |
| 378 | showNext(current); |
| 379 | } |
| 380 | } |
| 381 | |
| 382 | @Override |
| 383 | public boolean onSearchRequested() { |
| 384 | Intent intent = getIntent(); |
| 385 | if (intent != null && intent.getAction() != null && intent.getAction().equals(Intent.ACTION_SEARCH)) { |
| 386 | Intent next = new Intent(); |
| 387 | next.setClass(this, LookupActivity.class); |
| 388 | next.setAction(Intent.ACTION_SEARCH); |
| 389 | next.putExtra(SearchManager.QUERY, intent.getStringExtra("query")); |
| 390 | startActivity(next); |
| 391 | } |
| 392 | finish(); |
| 393 | return true; |
| 394 | } |
| 395 | |
| 396 | public boolean onKeyLongPress(int keyCode, KeyEvent event) { |
| 397 | if (keyCode == KeyEvent.KEYCODE_SEARCH){ |
| 398 | finish(); |
| 399 | return true; |
| 400 | } |
| 401 | return super.onKeyLongPress(keyCode, event); |
| 402 | } |
| 403 | |
| 404 | |
| 405 | final static int MENU_VIEW_ONLINE = 1; |
| 406 | final static int MENU_NEW_LOOKUP = 2; |
| 407 | final static int MENU_ZOOM_IN = 3; |
| 408 | final static int MENU_ZOOM_OUT = 4; |
| 409 | |
| 410 | private MenuItem miViewOnline; |
| 411 | |
| 412 | @Override |
| 413 | public boolean onCreateOptionsMenu(Menu menu) { |
| 414 | miViewOnline = menu.add(0, MENU_VIEW_ONLINE, 0, R.string.mnViewOnline).setIcon(android.R.drawable.ic_menu_view); |
| 415 | menu.add(0, MENU_NEW_LOOKUP, 0, R.string.mnNewLookup).setIcon(android.R.drawable.ic_menu_search); |
| 416 | menu.add(0, MENU_ZOOM_OUT, 0, R.string.mnZoomOut).setIcon(R.drawable.ic_menu_zoom_out); |
| 417 | menu.add(0, MENU_ZOOM_IN, 0, R.string.mnZoomIn).setIcon(R.drawable.ic_menu_zoom_in); |
| 418 | return true; |
| 419 | } |
| 420 | |
| 421 | @Override |
| 422 | public boolean onPrepareOptionsMenu(Menu menu) { |
| 423 | boolean enableViewOnline = false; |
| 424 | if (this.backItems.size() > 0) { |
| 425 | HistoryItem historyItem = backItems.get(backItems.size() - 1); |
| 426 | Article current = historyItem.article; |
| 427 | Volume d = dictionaryService.getVolume(current.volumeId); |
| 428 | enableViewOnline = d.getArticleURLTemplate() != null; |
| 429 | } |
| 430 | miViewOnline.setEnabled(enableViewOnline); |
| 431 | return true; |
| 432 | } |
| 433 | |
| 434 | @Override |
| 435 | public boolean onOptionsItemSelected(MenuItem item) { |
| 436 | switch (item.getItemId()) { |
| 437 | case MENU_VIEW_ONLINE: |
| 438 | viewOnline(); |
| 439 | break; |
| 440 | case MENU_NEW_LOOKUP: |
| 441 | onSearchRequested(); |
| 442 | break; |
| 443 | case MENU_ZOOM_IN: |
| 444 | zoomIn(); |
| 445 | break; |
| 446 | case MENU_ZOOM_OUT: |
| 447 | zoomOut(); |
| 448 | break; |
| 449 | default: |
| 450 | return super.onOptionsItemSelected(item); |
| 451 | } |
| 452 | return true; |
| 453 | } |
| 454 | |
| 455 | private void viewOnline() { |
| 456 | if (this.backItems.size() > 0) { |
| 457 | Article current = this.backItems.get(this.backItems.size() - 1).article; |
| 458 | Volume d = dictionaryService.getVolume(current.volumeId); |
| 459 | String url = d == null ? null : d.getArticleURL(current.title); |
| 460 | if (url != null) { |
| 461 | Intent browserIntent = new Intent(Intent.ACTION_VIEW, |
| 462 | Uri.parse(url)); |
| 463 | startActivity(browserIntent); |
| 464 | } |
| 465 | } |
| 466 | } |
| 467 | |
| 468 | private void showArticle(String volumeId, long articlePointer, String word, String section) { |
| 469 | Log.d(TAG, "word: " + word); |
| 470 | Log.d(TAG, "dictionaryId: " + volumeId); |
| 471 | Log.d(TAG, "articlePointer: " + articlePointer); |
| 472 | Log.d(TAG, "section: " + section); |
| 473 | Volume d = dictionaryService.getVolume(volumeId); |
| 474 | Entry entry = new Entry(d.getId(), word, articlePointer); |
| 475 | entry.section = section; |
| 476 | this.showArticle(entry); |
| 477 | } |
| 478 | |
| 479 | private void showArticle(Entry entry) { |
| 480 | List<Entry> result = new ArrayList<Entry>(); |
| 481 | result.add(entry); |
| 482 | |
| 483 | try { |
| 484 | Iterator<Entry> currentIterator = dictionaryService.followLink(entry.title, entry.volumeId); |
| 485 | while (currentIterator.hasNext() && result.size() < 20) { |
| 486 | Entry next = currentIterator.next(); |
| 487 | if (!next.equals(entry)) { |
| 488 | result.add(next); |
| 489 | } |
| 490 | } |
| 491 | } |
| 492 | catch (ArticleNotFound e) { |
| 493 | Log.d(TAG, String.format("Article \"%s\" not found - unexpected", e.word)); |
| 494 | } |
| 495 | showNext(new HistoryItem(result)); |
| 496 | } |
| 497 | |
| 498 | private Map<Article, ScrollXY> getScrollPositions() { |
| 499 | int orientation = getWindowManager().getDefaultDisplay().getOrientation(); |
| 500 | switch (orientation) { |
| 501 | case Surface.ROTATION_0: |
| 502 | case Surface.ROTATION_180: |
| 503 | return scrollPositionsV; |
| 504 | default: |
| 505 | return scrollPositionsH; |
| 506 | } |
| 507 | } |
| 508 | |
| 509 | private void saveScrollPos(int x, int y) { |
| 510 | if (!saveScrollPos) { |
| 511 | //Log.d(TAG, "Not saving scroll position (disabled)"); |
| 512 | return; |
| 513 | } |
| 514 | if (backItems.size() > 0) { |
| 515 | Article a = backItems.get(backItems.size() - 1).article; |
| 516 | Map<Article, ScrollXY> positions = getScrollPositions(); |
| 517 | ScrollXY s = positions.get(a); |
| 518 | if (s == null) { |
| 519 | s = new ScrollXY(x, y); |
| 520 | positions.put(a, s); |
| 521 | } |
| 522 | else { |
| 523 | s.x = x; |
| 524 | s.y = y; |
| 525 | } |
| 526 | //Log.d(TAG, String.format("Saving scroll position %s for %s", s, a.title)); |
| 527 | getScrollPositions().put(a, s); |
| 528 | } |
| 529 | } |
| 530 | |
| 531 | private boolean restoreScrollPos() { |
| 532 | if (backItems.size() > 0) { |
| 533 | Article a = backItems.get(backItems.size() - 1).article; |
| 534 | ScrollXY s = getScrollPositions().get(a); |
| 535 | if (s == null) { |
| 536 | return false; |
| 537 | } |
| 538 | scrollTo(s); |
| 539 | return true; |
| 540 | } |
| 541 | return false; |
| 542 | } |
| 543 | |
| 544 | private void showNext(HistoryItem item_) { |
| 545 | final HistoryItem item = new HistoryItem(item_); |
| 546 | final Entry entry = item.next(); |
| 547 | runOnUiThread(new Runnable() { |
| 548 | public void run() { |
| 549 | setTitle(item); |
| 550 | setProgress(1000); |
| 551 | } |
| 552 | }); |
| 553 | currentTask = new TimerTask() { |
| 554 | public void run() { |
| 555 | try { |
| 556 | Article a = dictionaryService.getArticle(entry); |
| 557 | try { |
| 558 | a = dictionaryService.redirect(a); |
| 559 | item.article = new Article(a); |
| 560 | } |
| 561 | catch (ArticleNotFound e) { |
| 562 | showMessage(getString(R.string.msgRedirectNotFound, e.word.toString())); |
| 563 | return; |
| 564 | } |
| 565 | catch (RedirectTooManyLevels e) { |
| 566 | showMessage(getString(R.string.msgTooManyRedirects, a.getRedirect())); |
| 567 | return; |
| 568 | } |
| 569 | catch (Exception e) { |
| 570 | Log.e(TAG, "Redirect failed", e); |
| 571 | showError(getString(R.string.msgErrorLoadingArticle, a.title)); |
| 572 | return; |
| 573 | } |
| 574 | |
| 575 | HistoryItem oldCurrent = null; |
| 576 | if (!backItems.isEmpty()) |
| 577 | oldCurrent = backItems.get(backItems.size() - 1); |
| 578 | |
| 579 | backItems.add(item); |
| 580 | |
| 581 | if (oldCurrent != null) { |
| 582 | HistoryItem newCurrent = item; |
| 583 | if (newCurrent.article.equalsIgnoreSection(oldCurrent.article)) { |
| 584 | |
| 585 | final String section = oldCurrent.article.sectionEquals(newCurrent.article) ? null : newCurrent.article.section; |
| 586 | |
| 587 | runOnUiThread(new Runnable() { |
| 588 | public void run() { |
| 589 | resetTitleToCurrent(); |
| 590 | if (section != null) { |
| 591 | goToSection(section); |
| 592 | } |
| 593 | setProgress(10000); |
| 594 | currentTask = null; |
| 595 | } |
| 596 | }); |
| 597 | } |
| 598 | else { |
| 599 | showCurrentArticle(); |
| 600 | } |
| 601 | } |
| 602 | else { |
| 603 | showCurrentArticle(); |
| 604 | } |
| 605 | } |
| 606 | catch (Exception e) { |
| 607 | String msg = getString(R.string.msgErrorLoadingArticle, entry.title); |
| 608 | Log.e(TAG, msg, e); |
| 609 | showError(msg); |
| 610 | } |
| 611 | } |
| 612 | }; |
| 613 | try { |
| 614 | timer.schedule(currentTask, 0); |
| 615 | } |
| 616 | catch (Exception e) { |
| 617 | Log.d(TAG, "Failed to schedule task", e); |
| 618 | } |
| 619 | } |
| 620 | |
| 621 | private void showCurrentArticle() { |
| 622 | runOnUiThread(new Runnable() { |
| 623 | public void run() { |
| 624 | setProgress(5000); |
| 625 | resetTitleToCurrent(); |
| 626 | Article a = backItems.get(backItems.size() - 1).article; |
| 627 | Log.d(TAG, "Show article: " + a.text); |
| 628 | articleView.loadDataWithBaseURL("", wrap(a.text), "text/html", "utf-8", null); |
| 629 | } |
| 630 | }); |
| 631 | } |
| 632 | |
| 633 | private void updateNextButtonVisibility() { |
| 634 | if (currentHideNextButtonTask != null) { |
| 635 | currentHideNextButtonTask.cancel(); |
| 636 | currentHideNextButtonTask = null; |
| 637 | } |
| 638 | boolean hasNextArticle = false; |
| 639 | if (backItems.size() > 0) { |
| 640 | HistoryItem historyItem = backItems.get(backItems.size() - 1); |
| 641 | hasNextArticle = historyItem.hasNext(); |
| 642 | } |
| 643 | final Button nextButton = (Button)findViewById(R.id.NextButton); |
| 644 | if (hasNextArticle) { |
| 645 | if (nextButton.getVisibility() == View.GONE){ |
| 646 | nextButton.setVisibility(View.VISIBLE); |
| 647 | } |
| 648 | currentHideNextButtonTask = new TimerTask() { |
| 649 | @Override |
| 650 | public void run() { |
| 651 | runOnUiThread(new Runnable() { |
| 652 | public void run() { |
| 653 | if (useAnimation) { |
| 654 | nextButton.startAnimation(fadeOutAnimation); |
| 655 | } |
| 656 | else { |
| 657 | nextButton.setVisibility(View.GONE); |
| 658 | } |
| 659 | currentHideNextButtonTask = null; |
| 660 | } |
| 661 | }); |
| 662 | } |
| 663 | }; |
| 664 | try { |
| 665 | timer.schedule(currentHideNextButtonTask, 1800); |
| 666 | } |
| 667 | catch (IllegalStateException e) { |
| 668 | //this may happen if orientation changes while users touches screen |
| 669 | Log.d(TAG, "Failed to schedule \"Next\" button hide", e); |
| 670 | } |
| 671 | } |
| 672 | else { |
| 673 | nextButton.setVisibility(View.GONE); |
| 674 | } |
| 675 | } |
| 676 | |
| 677 | private void showMessage(final String message) { |
| 678 | runOnUiThread(new Runnable() { |
| 679 | public void run() { |
| 680 | currentTask = null; |
| 681 | setProgress(10000); |
| 682 | resetTitleToCurrent(); |
| 683 | Toast.makeText(ArticleViewActivity.this, message, Toast.LENGTH_LONG).show(); |
| 684 | if (backItems.isEmpty()) { |
| 685 | finish(); |
| 686 | } |
| 687 | } |
| 688 | }); |
| 689 | } |
| 690 | |
| 691 | private void showError(final String message) { |
| 692 | runOnUiThread(new Runnable() { |
| 693 | public void run() { |
| 694 | currentTask = null; |
| 695 | setProgress(10000); |
| 696 | resetTitleToCurrent(); |
| 697 | AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(ArticleViewActivity.this); |
| 698 | dialogBuilder.setTitle(R.string.titleError).setMessage(message).setNeutralButton(R.string.btnDismiss, new OnClickListener() { |
| 699 | public void onClick(DialogInterface dialog, int which) { |
| 700 | dialog.dismiss(); |
| 701 | if (backItems.isEmpty()) { |
| 702 | finish(); |
| 703 | } |
| 704 | } |
| 705 | }); |
| 706 | dialogBuilder.show(); |
| 707 | } |
| 708 | }); |
| 709 | } |
| 710 | |
| 711 | |
| 712 | private void setTitle(CharSequence articleTitle, CharSequence dictTitle) { |
| 713 | setTitle(getString(R.string.titleArticleViewActivity, articleTitle, dictTitle)); |
| 714 | } |
| 715 | |
| 716 | private void resetTitleToCurrent() { |
| 717 | if (!backItems.isEmpty()) { |
| 718 | HistoryItem current = backItems.get(backItems.size() - 1); |
| 719 | setTitle(current); |
| 720 | } |
| 721 | } |
| 722 | |
| 723 | private void setTitle(HistoryItem item) { |
| 724 | StringBuilder title = new StringBuilder(); |
| 725 | if (item.entries.size() > 1) { |
| 726 | title |
| 727 | .append(item.entryIndex + 1) |
| 728 | .append("/") |
| 729 | .append(item.entries.size()) |
| 730 | .append(" "); |
| 731 | } |
| 732 | Entry entry = item.current(); |
| 733 | title.append(entry.title); |
| 734 | setTitle(title, dictionaryService.getDisplayTitle(entry.volumeId)); |
| 735 | } |
| 736 | |
| 737 | private String wrap(String articleText) { |
| 738 | return new StringBuilder("<html>") |
| 739 | .append("<head>") |
| 740 | .append(this.sharedCSS) |
| 741 | .append(this.mediawikiSharedCSS) |
| 742 | .append(this.mediawikiMonobookCSS) |
| 743 | .append(this.js) |
| 744 | .append("</head>") |
| 745 | .append("<body>") |
| 746 | .append("<div id=\"globalWrapper\">") |
| 747 | .append(articleText) |
| 748 | .append("</div>") |
| 749 | .append("</body>") |
| 750 | .append("</html>") |
| 751 | .toString(); |
| 752 | } |
| 753 | |
| 754 | private String wrapCSS(String css) { |
| 755 | return String.format("<style type=\"text/css\">%s</style>", css); |
| 756 | } |
| 757 | |
| 758 | private String wrapJS(String js) { |
| 759 | return String.format("<script type=\"text/javascript\">%s</script>", js); |
| 760 | } |
| 761 | |
| 762 | private void loadAssets() { |
| 763 | try { |
| 764 | this.sharedCSS = wrapCSS(readFile("shared.css")); |
| 765 | this.mediawikiSharedCSS = wrapCSS(readFile("mediawiki_shared.css")); |
| 766 | this.mediawikiMonobookCSS = wrapCSS(readFile("mediawiki_monobook.css")); |
| 767 | this.js = wrapJS(readFile("aar.js")); |
| 768 | } |
| 769 | catch (IOException e) { |
| 770 | Log.e(TAG, "Failed to load assets", e); |
| 771 | } |
| 772 | } |
| 773 | |
| 774 | private String readFile(String name) throws IOException { |
| 775 | final char[] buffer = new char[0x1000]; |
| 776 | StringBuilder out = new StringBuilder(); |
| 777 | InputStream is = getResources().getAssets().open(name); |
| 778 | Reader in = new InputStreamReader(is, "UTF-8"); |
| 779 | int read; |
| 780 | do { |
| 781 | read = in.read(buffer, 0, buffer.length); |
| 782 | if (read>0) { |
| 783 | out.append(buffer, 0, read); |
| 784 | } |
| 785 | } while (read>=0); |
| 786 | return out.toString(); |
| 787 | } |
| 788 | |
| 789 | |
| 790 | @Override |
| 791 | protected void onPause() { |
| 792 | super.onPause(); |
| 793 | SharedPreferences prefs = getPreferences(MODE_PRIVATE); |
| 794 | Editor e = prefs.edit(); |
| 795 | e.putFloat("articleView.scale", articleView.getScale()); |
| 796 | boolean success = e.commit(); |
| 797 | if (!success) { |
| 798 | Log.w(TAG, "Failed to save article view scale pref"); |
| 799 | } |
| 800 | } |
| 801 | |
| 802 | @Override |
| 803 | protected void onCreate(Bundle savedInstanceState) { |
| 804 | super.onCreate(savedInstanceState); |
| 805 | SharedPreferences prefs = getPreferences(MODE_PRIVATE); |
| 806 | float scale = prefs.getFloat("articleView.scale", 1.0f); |
| 807 | int initialScale = Math.round(scale*100); |
| 808 | Log.d(TAG, "Setting initial article view scale to " + initialScale); |
| 809 | articleView.setInitialScale(initialScale); |
| 810 | } |
| 811 | |
| 812 | @Override |
| 813 | protected void onDestroy() { |
| 814 | super.onDestroy(); |
| 815 | timer.cancel(); |
| 816 | scrollPositionsH.clear(); |
| 817 | scrollPositionsV.clear(); |
| 818 | backItems.clear(); |
| 819 | } |
| 820 | |
| 821 | @Override |
| 822 | void onDictionaryServiceReady() { |
| 823 | if (this.backItems.isEmpty()) { |
| 824 | final Intent intent = getIntent(); |
| 825 | if (intent != null && intent.getAction() != null && intent.getAction().equals(Intent.ACTION_SEARCH)) { |
| 826 | final String word = intent.getStringExtra("query"); |
| 827 | |
| 828 | if (currentTask != null) { |
| 829 | currentTask.cancel(); |
| 830 | } |
| 831 | |
| 832 | currentTask = new TimerTask() { |
| 833 | @Override |
| 834 | public void run() { |
| 835 | setProgress(500); |
| 836 | Log.d(TAG, "intent.getDataString(): " + intent.getDataString()); |
| 837 | Iterator<Entry> results = dictionaryService.lookup(word); |
| 838 | Log.d(TAG, "Looked up " + word ); |
| 839 | if (results.hasNext()) { |
| 840 | currentTask = null; |
| 841 | Entry entry = results.next(); |
| 842 | showArticle(entry); |
| 843 | } |
| 844 | else { |
| 845 | onSearchRequested(); |
| 846 | } |
| 847 | } |
| 848 | }; |
| 849 | |
| 850 | try { |
| 851 | timer.schedule(currentTask, 0); |
| 852 | } |
| 853 | catch (Exception e) { |
| 854 | Log.d(TAG, "Failed to schedule task", e); |
| 855 | showError(getString(R.string.msgErrorLoadingArticle, word)); |
| 856 | } |
| 857 | } |
| 858 | else { |
| 859 | String word = intent.getStringExtra("word"); |
| 860 | String section = intent.getStringExtra("section"); |
| 861 | String volumeId = intent.getStringExtra("volumeId"); |
| 862 | long articlePointer = intent.getLongExtra("articlePointer", -1); |
| 863 | dictionaryService.setPreferred(volumeId); |
| 864 | showArticle(volumeId, articlePointer, word, section); |
| 865 | } |
| 866 | } |
| 867 | else { |
| 868 | showCurrentArticle(); |
| 869 | } |
| 870 | } |
| 871 | |
| 872 | @SuppressWarnings("unchecked") |
| 873 | @Override |
| 874 | protected void onSaveInstanceState(Bundle outState) { |
| 875 | super.onSaveInstanceState(outState); |
| 876 | outState.putSerializable("backItems", new LinkedList(backItems)); |
| 877 | outState.putSerializable("scrollPositionsH", new HashMap(scrollPositionsH)); |
| 878 | outState.putSerializable("scrollPositionsV", new HashMap(scrollPositionsV)); |
| 879 | } |
| 880 | |
| 881 | @SuppressWarnings("unchecked") |
| 882 | @Override |
| 883 | protected void onRestoreInstanceState(Bundle savedInstanceState) { |
| 884 | super.onRestoreInstanceState(savedInstanceState); |
| 885 | backItems = Collections.synchronizedList((List)savedInstanceState.getSerializable("backItems")); |
| 886 | scrollPositionsH = Collections.synchronizedMap((Map)savedInstanceState.getSerializable("scrollPositionsH")); |
| 887 | scrollPositionsV = Collections.synchronizedMap((Map)savedInstanceState.getSerializable("scrollPositionsV")); |
| 888 | } |
| 889 | } |