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 | } |