content: /* This file is part of Aard Dictionary for Android <http://aarddict.org>.
   *
   * This program is free software: you can redistribute it and/or modify
   * it under the terms of the GNU General Public License version 3
   * as published by the Free Software Foundation.
   *
   * This program is distributed in the hope that it will be useful,
   * but WITHOUT ANY WARRANTY; without even the implied warranty of
   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   * GNU General Public License <http://www.gnu.org/licenses/gpl-3.0.txt>
   * for more details.
   *
   * Copyright (C) 2010 Igor Tkach
   */
  
  package aarddict.android;
  
  import java.io.IOException;
  import java.io.InputStream;
  import java.io.InputStreamReader;
  import java.io.Reader;
  import java.lang.reflect.Method;
  import java.util.ArrayList;
  import java.util.Collections;
  import java.util.HashMap;
  import java.util.Iterator;
  import java.util.LinkedList;
  import java.util.List;
  import java.util.Map;
  import java.util.Timer;
  import java.util.TimerTask;
  
  import aarddict.Article;
  import aarddict.ArticleNotFound;
  import aarddict.Entry;
  import aarddict.LookupWord;
  import aarddict.RedirectTooManyLevels;
  import aarddict.Volume;
  import android.annotation.TargetApi;
  import android.app.AlertDialog;
  import android.app.SearchManager;
  import android.content.DialogInterface;
  import android.content.DialogInterface.OnClickListener;
  import android.content.Intent;
  import android.content.SharedPreferences;
  import android.content.SharedPreferences.Editor;
  import android.net.Uri;
  import android.os.Build;
  import android.os.Bundle;
  import android.support.v4.view.MenuItemCompat;
  import android.util.Log;
  import android.view.ActionMode;
  import android.view.KeyEvent;
  import android.view.Menu;
  import android.view.MenuItem;
  import android.view.MotionEvent;
  import android.view.Surface;
  import android.view.View;
  import android.view.Window;
  import android.view.animation.AlphaAnimation;
  import android.view.animation.Animation;
  import android.view.animation.Animation.AnimationListener;
  import android.webkit.JsResult;
  import android.webkit.WebChromeClient;
  import android.webkit.WebView;
  import android.webkit.WebViewClient;
  import android.widget.Button;
  import android.widget.Toast;
  
  public class ArticleViewActivity extends BaseDictionaryActivity {
  
      private static final String BASE_URL = "aard://A/";
  
      private final static String    TAG                 = ArticleViewActivity.class
                                                                 .getName();
  
      public static final int        NOOK_KEY_PREV_LEFT  = 92;
      public static final int        NOOK_KEY_NEXT_LEFT  = 93;
  
      public static final int        NOOK_KEY_PREV_RIGHT = 94;
      public static final int        NOOK_KEY_NEXT_RIGHT = 95;
  
      private ArticleView            articleView;
      private String                 sharedCSS;
      private String                 mediawikiSharedCSS;
      private String                 mediawikiMonobookCSS;
      private String                 js;
  
      private List<HistoryItem>      backItems;
      private Timer                  timer;
      private TimerTask              currentTask;
      private TimerTask              currentHideNextButtonTask;
      private AlphaAnimation         fadeOutAnimation;
      private boolean                useAnimation        = false;
  
      private Map<Article, ScrollXY> scrollPositionsH;
      private Map<Article, ScrollXY> scrollPositionsV;
      private boolean                saveScrollPos       = true;
  
      static class AnimationAdapter implements AnimationListener {
          public void onAnimationEnd(Animation animation) {
          }
  
          public void onAnimationRepeat(Animation animation) {
          }
  
          public void onAnimationStart(Animation animation) {
          }
      }
  
      @Override
      void initUI() {
          this.scrollPositionsH = Collections
                  .synchronizedMap(new HashMap<Article, ScrollXY>());
          this.scrollPositionsV = Collections
                  .synchronizedMap(new HashMap<Article, ScrollXY>());
          loadAssets();
  
          if (DeviceInfo.EINK_SCREEN) {
              useAnimation = false;
              setContentView(R.layout.eink_article_view);
              articleView = (ArticleView) findViewById(R.id.EinkArticleView);
              N2EpdController.n2MainActivity = this;
              EinkScreen.ResetController(2, articleView); // force full screen
                                                          // refresh when changing
                                                          // articles
          }
          // Setup animations only on non-eink screens
          else {
              useAnimation = true;
              fadeOutAnimation = new AlphaAnimation(1f, 0f);
              fadeOutAnimation.setDuration(600);
              fadeOutAnimation.setAnimationListener(new AnimationAdapter() {
                  public void onAnimationEnd(Animation animation) {
                      Button nextButton = (Button) findViewById(R.id.NextButton);
                      nextButton.setVisibility(Button.GONE);
                  }
              });
  
              getWindow().requestFeature(Window.FEATURE_PROGRESS);
              setContentView(R.layout.article_view);
              articleView = (ArticleView) findViewById(R.id.ArticleView);
          }
  
          timer = new Timer();
  
          backItems = Collections.synchronizedList(new LinkedList<HistoryItem>());
  
          if (android.os.Build.VERSION.SDK_INT >= 11) {
              try {
                  showFindDialogMethod = articleView.getClass().getMethod(
                          "showFindDialog", String.class, boolean.class);
              } catch (NoSuchMethodException e1) {
                  Log.d(TAG, "showFindDialog method not found");
              }
  
          }
  
          articleView.setOnScrollListener(new ArticleView.ScrollListener() {
              public void onScroll(int l, int t, int oldl, int oldt) {
                  saveScrollPos(l, t);
              }
          });
  
          articleView.getSettings().setJavaScriptEnabled(true);
          articleView.addJavascriptInterface(new SectionMatcher(), "matcher");
          articleView.addJavascriptInterface(articleView, "scrollControl");
  
          articleView.setWebChromeClient(new WebChromeClient() {
  
              @Override
              public boolean onJsAlert(WebView view, String url, String message,
                      JsResult result) {
                  Log.d(TAG + ".js", String.format("[%s]: %s", url, message));
                  result.cancel();
                  return true;
              }
  
              public void onProgressChanged(WebView view, int newProgress) {
                  Log.d(TAG, "Progress: " + newProgress);
                  setProgress(5000 + newProgress * 50);
              }
          });
  
          articleView.setWebViewClient(new WebViewClient() {
  
              @Override
              public void onPageFinished(WebView view, String url) {
                  Log.d(TAG, "Page finished: " + url);
                  currentTask = null;
                  String section = null;
  
                  if (url.contains("#")) {
                      LookupWord lookupWord = LookupWord.splitWord(url);
                      section = lookupWord.section;
                      if (backItems.size() > 0) {
                          HistoryItem currentHistoryItem = backItems
                                  .get(backItems.size() - 1);
                          HistoryItem h = new HistoryItem(currentHistoryItem);
                          h.article.section = section;
                          backItems.add(h);
                      }
                  } else if (backItems.size() > 0) {
                      Article current = backItems.get(backItems.size() - 1).article;
                      section = current.section;
                  }
                  if (!restoreScrollPos()) {
                      goToSection(section);
                  }
                  updateNextButtonVisibility();
                  articleView.armScroll();
              }
  
              @Override
              public boolean shouldOverrideUrlLoading(WebView view, String origUrl) {
                  Log.d(TAG, "URL clicked: " + origUrl);
                  final String url;
                  if (origUrl.startsWith(BASE_URL)) {
                      url = origUrl.substring(BASE_URL.length());
                      if (currentTask == null) {
                          currentTask = new TimerTask() {
                              public void run() {
                                  try {
                                      Article currentArticle = backItems
                                              .get(backItems.size() - 1).article;
                                      try {
                                          Iterator<Entry> currentIterator = dictionaryService
                                                  .followLink(url,
                                                          currentArticle.volumeId);
                                          List<Entry> result = new ArrayList<Entry>();
                                          while (currentIterator.hasNext()
                                                  && result.size() < 20) {
                                              result.add(currentIterator.next());
                                          }
                                          showNext(new HistoryItem(result));
                                      } catch (ArticleNotFound e) {
                                          showMessage(getString(
                                                  R.string.msgArticleNotFound,
                                                  e.word.toString()));
                                      }
                                  } catch (Exception e) {
                                      StringBuilder msgBuilder = new StringBuilder(
                                              "There was an error following link ")
                                              .append("\"").append(url)
                                              .append("\"");
                                      if (e.getMessage() != null) {
                                          msgBuilder.append(": ").append(
                                                  e.getMessage());
                                      }
                                      final String msg = msgBuilder.toString();
                                      Log.e(TAG, msg, e);
                                      showError(msg);
                                  }
                              }
                          };
                          try {
                              timer.schedule(currentTask, 0);
                          } catch (Exception e) {
                              Log.d(TAG, "Failed to schedule task", e);
                          }
                      }
                  }
                  else {
                      try {
                          Uri uri = Uri.parse(origUrl);
                          Intent browserIntent = new Intent(Intent.ACTION_VIEW, uri);
                          startActivity(browserIntent);
                      }
                      catch (Exception e) {
                          Toast.makeText(ArticleViewActivity.this, String.format("Failed to parse URL %s", origUrl),
                                  Toast.LENGTH_SHORT).show();
                      }
                  }
                  return true;
              }
          });
          final Button nextButton = (Button) findViewById(R.id.NextButton);
          nextButton.getBackground().setAlpha(180);
          nextButton.setOnClickListener(new View.OnClickListener() {
              public void onClick(View v) {
                  if (nextButton.getVisibility() == View.VISIBLE) {
                      updateNextButtonVisibility();
                      nextArticle();
                      updateNextButtonVisibility();
                  }
              }
          });
          articleView.setOnTouchListener(new View.OnTouchListener() {
              public boolean onTouch(View v, MotionEvent event) {
                  updateNextButtonVisibility();
                  return false;
              }
          });
  
          setProgressBarVisibility(true);
      }
  
      @TargetApi(Build.VERSION_CODES.HONEYCOMB)
      @Override
      public void onActionModeStarted(ActionMode mode) {
          super.onActionModeStarted(mode);
          articleView.mustBeArmedToScroll = false;
      }
  
      @TargetApi(Build.VERSION_CODES.HONEYCOMB)
      @Override
      public void onActionModeFinished(ActionMode mode) {
          super.onActionModeFinished(mode);
          articleView.mustBeArmedToScroll = true;
      }
  
      private void scrollTo(ScrollXY s) {
          scrollTo(s.x, s.y);
      }
  
      private void scrollTo(int x, int y) {
          saveScrollPos = false;
          Log.d(TAG, "Scroll to " + x + ", " + y);
          articleView.realScrollTo(x, y);
          saveScrollPos = true;
      }
  
      private void goToSection(String section) {
          Log.d(TAG, "Go to section " + section);
          if (section == null || section.trim().equals("")) {
              scrollTo(0, 0);
          } else {
              articleView.loadUrl(String.format(
                      "javascript:scrollToMatch(\"%s\")", section));
          }
      }
  
      @Override
      public boolean onKeyDown(int keyCode, KeyEvent event) {
          switch (keyCode) {
          case KeyEvent.KEYCODE_BACK:
              goBack();
              break;
          case NOOK_KEY_PREV_LEFT:
          case NOOK_KEY_PREV_RIGHT:
          case KeyEvent.KEYCODE_VOLUME_UP:
              if (!articleView.pageUp(false)) {
                  goBack();
              }
              break;
          case KeyEvent.KEYCODE_VOLUME_DOWN:
          case NOOK_KEY_NEXT_LEFT:
          case NOOK_KEY_NEXT_RIGHT:
              if (!articleView.pageDown(false)) {
                  nextArticle();
              };
              break;
          default:
              return super.onKeyDown(keyCode, event);
          }
          return true;
      }
  
      @Override
      public boolean onKeyUp(int keyCode, KeyEvent event) {
          // eat key ups corresponding to key downs so that volume keys don't beep
          switch (keyCode) {
          case KeyEvent.KEYCODE_BACK:
          case KeyEvent.KEYCODE_VOLUME_UP:
          case KeyEvent.KEYCODE_VOLUME_DOWN:
              break;
          default:
              return super.onKeyDown(keyCode, event);
          }
          return true;
      }
  
      private boolean zoomIn() {
          if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
              return textZoomIn();
          }
          else {
              return articleView.zoomIn();
          }
      }
  
      @TargetApi(android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH)
      private boolean textZoomIn() {
          int newZoom = articleView.getSettings().getTextZoom() + 20;
          if (newZoom <= 200) {
              articleView.getSettings().setTextZoom(newZoom);
              return true;
          }
          else {
              return false;
          }
      }
  
      private boolean zoomOut() {
          if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
              return textZoomOut();
          }
          else {
              return articleView.zoomOut();
          }
      }
  
      @TargetApi(android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH)
      private boolean textZoomOut() {
          int newZoom = articleView.getSettings().getTextZoom() - 20;
          if (newZoom >= 40) {
              articleView.getSettings().setTextZoom(newZoom);
              return true;
          }
          else {
              return false;
          }
      }
  
      private void goBack() {
          if (backItems.size() == 1) {
              finish();
          }
          if (currentTask != null) {
              return;
          }
          if (backItems.size() > 1) {
              HistoryItem current = backItems.remove(backItems.size() - 1);
              HistoryItem prev = backItems.get(backItems.size() - 1);
  
              Article prevArticle = prev.article;
              if (prevArticle.equalsIgnoreSection(current.article)) {
                  resetTitleToCurrent();
                  if (!prevArticle.sectionEquals(current.article)
                          && !restoreScrollPos()) {
                      goToSection(prevArticle.section);
                  }
              } else {
                  showCurrentArticle();
              }
          }
      }
  
      private void nextArticle() {
          HistoryItem current = backItems.get(backItems.size() - 1);
          if (current.hasNext()) {
              showNext(current);
          }
      }
  
      @Override
      public boolean onSearchRequested() {
          Intent intent = getIntent();
          String action = intent == null ? null : intent.getAction();
          if (action != null) {
              String word = null;
              if (action.equals(Intent.ACTION_SEARCH)) {
                  word = intent.getStringExtra("query");
              } else if (action.equals(Intent.ACTION_SEND)) {
                  word = intent.getStringExtra(Intent.EXTRA_TEXT);
              }
              if (word != null) {
                  Intent next = new Intent();
                  next.setClass(this, LookupActivity.class);
                  next.setAction(Intent.ACTION_SEARCH);
                  next.putExtra(SearchManager.QUERY, word);
                  startActivity(next);
              }
          }
          finish();
          return true;
      }
  
      public boolean onKeyLongPress(int keyCode, KeyEvent event) {
          if (keyCode == KeyEvent.KEYCODE_SEARCH) {
              finish();
              return true;
          }
          return super.onKeyLongPress(keyCode, event);
      }
  
      final static int MENU_VIEW_ONLINE  = 1;
      final static int MENU_NEW_LOOKUP   = 2;
      final static int MENU_ZOOM_IN      = 3;
      final static int MENU_ZOOM_OUT     = 4;
      final static int MENU_NEXT         = 5;
      final static int MENU_FIND_IN_PAGE = 6;
  
      private MenuItem miViewOnline;
  
      private MenuItem miNextArticle;
  
      private Method   showFindDialogMethod;
  
      @Override
      public boolean onCreateOptionsMenu(Menu menu) {
          if (showFindDialogMethod != null) {
              MenuItem miFindInPage = menu.add(0, MENU_FIND_IN_PAGE, 0,
                      R.string.mnFindInPage).setIcon(
                      android.R.drawable.ic_menu_search);
              MenuItemCompat.setShowAsAction(miFindInPage,
                      MenuItemCompat.SHOW_AS_ACTION_ALWAYS);
          }
          miViewOnline = menu.add(0, MENU_VIEW_ONLINE, 0, R.string.mnViewOnline)
                  .setIcon(android.R.drawable.ic_menu_view);
          menu.add(0, MENU_NEW_LOOKUP, 0, R.string.mnNewLookup).setIcon(
                  android.R.drawable.ic_menu_search);
          menu.add(0, MENU_ZOOM_OUT, 0, R.string.mnZoomOut).setIcon(
                  R.drawable.ic_menu_zoom_out);
          menu.add(0, MENU_ZOOM_IN, 0, R.string.mnZoomIn).setIcon(
                  R.drawable.ic_menu_zoom_in);
          miNextArticle = menu.add(0, MENU_NEXT, 0, R.string.mnNext).setIcon(
                  android.R.drawable.ic_media_next);
          MenuItemCompat.setShowAsAction(miNextArticle,
                  MenuItemCompat.SHOW_AS_ACTION_ALWAYS);
          return true;
      }
  
      @Override
      public boolean onPrepareOptionsMenu(Menu menu) {
          boolean enableViewOnline = false;
          boolean hasNextArticle = false;
          if (this.backItems.size() > 0) {
              HistoryItem historyItem = backItems.get(backItems.size() - 1);
              Article current = historyItem.article;
              Volume d = dictionaryService.getVolume(current.volumeId);
              enableViewOnline = d != null && d.getArticleURLTemplate() != null;
              hasNextArticle = historyItem.hasNext();
          }
          miViewOnline.setEnabled(enableViewOnline);
          miNextArticle.setVisible(hasNextArticle);
          return true;
      }
  
      private void showFindDialog() {
          if (showFindDialogMethod != null) {
              try {
                  showFindDialogMethod.invoke(articleView, "", true);
              } catch (Exception e) {
                  Log.e(TAG, "", e);
              }
          }
      }
  
      @Override
      public boolean onOptionsItemSelected(MenuItem item) {
          switch (item.getItemId()) {
          case MENU_VIEW_ONLINE:
              viewOnline();
              break;
          case android.R.id.home:
          case MENU_NEW_LOOKUP:
              onSearchRequested();
              break;
          case MENU_ZOOM_IN:
              zoomIn();
              break;
          case MENU_ZOOM_OUT:
              zoomOut();
              break;
          case MENU_NEXT:
              nextArticle();
              break;
          case MENU_FIND_IN_PAGE:
              showFindDialog();
              break;
          default:
              return super.onOptionsItemSelected(item);
          }
          return true;
      }
  
      private void viewOnline() {
          if (this.backItems.size() > 0) {
              Article current = this.backItems.get(this.backItems.size() - 1).article;
              Volume d = dictionaryService.getVolume(current.volumeId);
              String url = d == null ? null : d.getArticleURL(current.title);
              if (url != null) {
                  Intent browserIntent = new Intent(Intent.ACTION_VIEW,
                          Uri.parse(url));
                  startActivity(browserIntent);
              }
          }
      }
  
      private void showArticle(String volumeId, long articlePointer, String word,
              String section) {
          Log.d(TAG, "word: " + word);
          Log.d(TAG, "dictionaryId: " + volumeId);
          Log.d(TAG, "articlePointer: " + articlePointer);
          Log.d(TAG, "section: " + section);
          Volume d = dictionaryService.getVolume(volumeId);
          Entry entry = new Entry(d.getId(), word, articlePointer);
          entry.section = section;
          this.showArticle(entry);
      }
  
      private void showArticle(Entry entry) {
          List<Entry> result = new ArrayList<Entry>();
          result.add(entry);
  
          try {
              Iterator<Entry> currentIterator = dictionaryService.followLink(
                      entry.title, entry.volumeId);
              while (currentIterator.hasNext() && result.size() < 20) {
                  Entry next = currentIterator.next();
                  if (!next.equals(entry)) {
                      result.add(next);
                  }
              }
          } catch (ArticleNotFound e) {
              Log.d(TAG, String.format("Article \"%s\" not found - unexpected",
                      e.word));
          }
          showNext(new HistoryItem(result));
      }
  
      private Map<Article, ScrollXY> getScrollPositions() {
          int orientation = getWindowManager().getDefaultDisplay()
                  .getOrientation();
          switch (orientation) {
          case Surface.ROTATION_0:
          case Surface.ROTATION_180:
              return scrollPositionsV;
          default:
              return scrollPositionsH;
          }
      }
  
      private void saveScrollPos(int x, int y) {
          if (!saveScrollPos) {
              // Log.d(TAG, "Not saving scroll position (disabled)");
              return;
          }
          if (backItems.size() > 0) {
              Article a = backItems.get(backItems.size() - 1).article;
              Map<Article, ScrollXY> positions = getScrollPositions();
              ScrollXY s = positions.get(a);
              if (s == null) {
                  s = new ScrollXY(x, y);
                  positions.put(a, s);
              } else {
                  s.x = x;
                  s.y = y;
              }
              // Log.d(TAG, String.format("Saving scroll position %s for %s", s,
              // a.title));
              getScrollPositions().put(a, s);
          }
      }
  
      private boolean restoreScrollPos() {
          if (backItems.size() > 0) {
              Article a = backItems.get(backItems.size() - 1).article;
              ScrollXY s = getScrollPositions().get(a);
              if (s == null) {
                  return false;
              }
              scrollTo(s);
              return true;
          }
          return false;
      }
  
      private void showNext(HistoryItem item_) {
          final HistoryItem item = new HistoryItem(item_);
          final Entry entry = item.next();
          runOnUiThread(new Runnable() {
              public void run() {
                  setTitle(item);
                  setProgress(1000);
              }
          });
          currentTask = new TimerTask() {
              public void run() {
                  try {
                      Article a = dictionaryService.getArticle(entry);
                      try {
                          a = dictionaryService.redirect(a);
                          item.article = new Article(a);
                      } catch (ArticleNotFound e) {
                          showMessage(getString(R.string.msgRedirectNotFound,
                                  e.word.toString()));
                          return;
                      } catch (RedirectTooManyLevels e) {
                          showMessage(getString(R.string.msgTooManyRedirects,
                                  a.getRedirect()));
                          return;
                      } catch (Exception e) {
                          Log.e(TAG, "Redirect failed", e);
                          showError(getString(R.string.msgErrorLoadingArticle,
                                  a.title));
                          return;
                      }
  
                      HistoryItem oldCurrent = null;
                      if (!backItems.isEmpty())
                          oldCurrent = backItems.get(backItems.size() - 1);
  
                      backItems.add(item);
  
                      if (oldCurrent != null) {
                          HistoryItem newCurrent = item;
                          if (newCurrent.article
                                  .equalsIgnoreSection(oldCurrent.article)) {
  
                              final String section = oldCurrent.article
                                      .sectionEquals(newCurrent.article) ? null
                                      : newCurrent.article.section;
  
                              runOnUiThread(new Runnable() {
                                  public void run() {
                                      resetTitleToCurrent();
                                      if (section != null) {
                                          goToSection(section);
                                      }
                                      setProgress(10000);
                                      currentTask = null;
                                  }
                              });
                          } else {
                              showCurrentArticle();
                          }
                      } else {
                          showCurrentArticle();
                      }
                  } catch (Exception e) {
                      String msg = getString(R.string.msgErrorLoadingArticle,
                              entry.title);
                      Log.e(TAG, msg, e);
                      showError(msg);
                  }
              }
          };
          try {
              timer.schedule(currentTask, 0);
          } catch (Exception e) {
              Log.d(TAG, "Failed to schedule task", e);
          }
      }
  
      private void showCurrentArticle() {
          runOnUiThread(new Runnable() {
              public void run() {
                  setProgress(5000);
                  resetTitleToCurrent();
                  Article a = backItems.get(backItems.size() - 1).article;
                  Log.d(TAG, "Show article: " + a.text);
                  articleView.loadDataWithBaseURL(BASE_URL, wrap(a.text), "text/html",
                          "utf-8", null);
              }
          });
      }
  
      private void updateNextButtonVisibility() {
          if (android.os.Build.VERSION.SDK_INT >= 11) {
              return;
          }
          if (currentHideNextButtonTask != null) {
              currentHideNextButtonTask.cancel();
              currentHideNextButtonTask = null;
          }
          boolean hasNextArticle = false;
          if (backItems.size() > 0) {
              HistoryItem historyItem = backItems.get(backItems.size() - 1);
              hasNextArticle = historyItem.hasNext();
          }
          final Button nextButton = (Button) findViewById(R.id.NextButton);
          if (hasNextArticle) {
              if (nextButton.getVisibility() == View.GONE) {
                  nextButton.setVisibility(View.VISIBLE);
              }
              currentHideNextButtonTask = new TimerTask() {
                  @Override
                  public void run() {
                      runOnUiThread(new Runnable() {
                          public void run() {
                              if (useAnimation) {
                                  nextButton.startAnimation(fadeOutAnimation);
                              } else {
                                  nextButton.setVisibility(View.GONE);
                              }
                              currentHideNextButtonTask = null;
                          }
                      });
                  }
              };
              try {
                  timer.schedule(currentHideNextButtonTask, 1800);
              } catch (IllegalStateException e) {
                  // this may happen if orientation changes while users touches
                  // screen
                  Log.d(TAG, "Failed to schedule \"Next\" button hide", e);
              }
          } else {
              nextButton.setVisibility(View.GONE);
          }
      }
  
      private void showMessage(final String message) {
          runOnUiThread(new Runnable() {
              public void run() {
                  currentTask = null;
                  setProgress(10000);
                  resetTitleToCurrent();
                  Toast.makeText(ArticleViewActivity.this, message,
                          Toast.LENGTH_LONG).show();
                  if (backItems.isEmpty()) {
                      finish();
                  }
              }
          });
      }
  
      private void showError(final String message) {
          runOnUiThread(new Runnable() {
              public void run() {
                  currentTask = null;
                  setProgress(10000);
                  resetTitleToCurrent();
                  AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(
                          ArticleViewActivity.this);
                  dialogBuilder
                          .setTitle(R.string.titleError)
                          .setMessage(message)
                          .setNeutralButton(R.string.btnDismiss,
                                  new OnClickListener() {
                                      public void onClick(DialogInterface dialog,
                                              int which) {
                                          dialog.dismiss();
                                          if (backItems.isEmpty()) {
                                              finish();
                                          }
                                      }
                                  });
                  dialogBuilder.show();
              }
          });
      }
  
      private void setTitle(CharSequence articleTitle, CharSequence dictTitle) {
          setTitle(getString(R.string.titleArticleViewActivity, articleTitle,
                  dictTitle));
      }
  
      private void resetTitleToCurrent() {
          if (!backItems.isEmpty()) {
              HistoryItem current = backItems.get(backItems.size() - 1);
              setTitle(current);
          }
      }
  
      private void setTitle(HistoryItem item) {
          StringBuilder title = new StringBuilder();
          boolean hasNextArticle = false;
          if (item.entries.size() > 1) {
              title.append(item.entryIndex + 1).append("/")
                      .append(item.entries.size()).append(" ");
              hasNextArticle = item.hasNext();
          }
          if (miNextArticle != null) {
              miNextArticle.setVisible(hasNextArticle);
          }
          Entry entry = item.current();
          title.append(entry.title);
          setTitle(title, dictionaryService.getDisplayTitle(entry.volumeId));
      }
  
      private String wrap(String articleText) {
          StringBuilder sb = new StringBuilder("<html>").append("<head>");
          if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
              sb.append("<meta name='viewport' content='width=device-width, initial-scale=1.0, user-scalable=0'/>");
          }
          return sb.append(this.sharedCSS).append(this.mediawikiSharedCSS)
                  .append(this.mediawikiMonobookCSS).append(this.js)
                  .append("</head>").append("<body>")
                  .append("<div id=\"globalWrapper\">").append(articleText)
                  .append("</div>").append("</body>").append("</html>")
                  .toString();
      }
  
      private String wrapCSS(String css) {
          return String.format("<style type=\"text/css\">%s</style>", css);
      }
  
      private String wrapJS(String js) {
          return String
                  .format("<script type=\"text/javascript\">%s</script>", js);
      }
  
      private void loadAssets() {
          try {
              this.sharedCSS = wrapCSS(readFile("shared.css"));
              this.mediawikiSharedCSS = wrapCSS(readFile("mediawiki_shared.css"));
              this.mediawikiMonobookCSS = wrapCSS(readFile("mediawiki_monobook.css"));
              this.js = wrapJS(readFile("aar.js"));
          } catch (IOException e) {
              Log.e(TAG, "Failed to load assets", e);
          }
      }
  
      private String readFile(String name) throws IOException {
          final char[] buffer = new char[0x1000];
          StringBuilder out = new StringBuilder();
          InputStream is = getResources().getAssets().open(name);
          Reader in = new InputStreamReader(is, "UTF-8");
          int read;
          do {
              read = in.read(buffer, 0, buffer.length);
              if (read > 0) {
                  out.append(buffer, 0, read);
              }
          } while (read >= 0);
          return out.toString();
      }
  
      @Override
      protected void onPause() {
          super.onPause();
          if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
              saveTextZoomPref();
          }
          else {
              SharedPreferences prefs = getPreferences(MODE_PRIVATE);
              Editor e = prefs.edit();
              e.putFloat("articleView.scale", articleView.getScale());
              boolean success = e.commit();
              if (!success) {
                  Log.w(TAG, "Failed to save article view scale pref");
              }
          }
      }
  
      @Override
      protected void onCreate(Bundle savedInstanceState) {
          super.onCreate(savedInstanceState);
          if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
              applyTextZoomPref();
          }
          else {
              SharedPreferences prefs = getPreferences(MODE_PRIVATE);
              float scale = prefs.getFloat("articleView.scale", 1.0f);
              int initialScale = Math.round(scale * 100);
              Log.d(TAG, "Setting initial article view scale to " + initialScale);
              articleView.setInitialScale(initialScale);
          }
          if (android.os.Build.VERSION.SDK_INT >= 11) {
              try {
                  Method getActionBar = getClass().getMethod("getActionBar");
                  Object actionBar = getActionBar.invoke(this);
                  Method setDisplayHomeAsUpEnabled = actionBar.getClass()
                          .getMethod("setDisplayHomeAsUpEnabled", boolean.class);
                  setDisplayHomeAsUpEnabled.invoke(actionBar, true);
              } catch (Exception e) {
              }
          }
      }
  
      @TargetApi(android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH)
      private void applyTextZoomPref() {
          SharedPreferences prefs = getPreferences(MODE_PRIVATE);
          int textZoom = prefs.getInt("articleView.textZoom", 100);
          articleView.getSettings().setTextZoom(textZoom);
      }
  
      @TargetApi(android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH)
      private void saveTextZoomPref() {
          SharedPreferences prefs = getPreferences(MODE_PRIVATE);
          int textZoom = articleView.getSettings().getTextZoom();
          Editor e = prefs.edit();
          e.putInt("articleView.textZoom", textZoom);
          boolean success = e.commit();
          if (!success) {
              Log.w(TAG, "Failed to save article view text zoom pref");
          }
      }
  
      @Override
      protected void onDestroy() {
          super.onDestroy();
          timer.cancel();
          scrollPositionsH.clear();
          scrollPositionsV.clear();
          backItems.clear();
      }
  
      @Override
      void onDictionaryServiceReady() {
          if (this.backItems.isEmpty()) {
              final Intent intent = getIntent();
              if (intent != null && intent.getAction() != null) {
                  String action = intent.getAction();
                  String _word = null;
                  if (action.equals(Intent.ACTION_SEARCH)) {
                      _word = intent.getStringExtra("query");
                  } else if (action.equals(Intent.ACTION_SEND)) {
                      _word = intent.getStringExtra(Intent.EXTRA_TEXT);
                  }
  
                  final String word = _word;
  
                  if (word != null) {
  
                      if (currentTask != null) {
                          currentTask.cancel();
                      }
  
                      currentTask = new TimerTask() {
                          @Override
                          public void run() {
                              setProgress(500);
                              String currentWord = word;
                              Log.d(TAG,
                                      "intent.getDataString(): "
                                              + intent.getDataString());
                              while (currentWord.length() > 0) {
                                  Iterator<Entry> results = dictionaryService
                                          .lookup(currentWord);
                                  Log.d(TAG, "Looked up " + word);
                                  if (results.hasNext()) {
                                      currentTask = null;
                                      Entry entry = results.next();
                                      showArticle(entry);
                                      break;
                                  } else {
                                      currentWord = currentWord.substring(0,
                                              currentWord.length() - 1);
                                  }
                              }
                              if (currentWord.length() == 0) {
                                  onSearchRequested();
                              }
                          }
                      };
  
                      try {
                          timer.schedule(currentTask, 0);
                      } catch (Exception e) {
                          Log.d(TAG, "Failed to schedule task", e);
                          showError(getString(R.string.msgErrorLoadingArticle,
                                  word));
                      }
                  }
              } else {
                  String word = intent.getStringExtra("word");
                  String section = intent.getStringExtra("section");
                  String volumeId = intent.getStringExtra("volumeId");
                  long articlePointer = intent.getLongExtra("articlePointer", -1);
                  dictionaryService.setPreferred(volumeId);
                  showArticle(volumeId, articlePointer, word, section);
              }
          } else {
              showCurrentArticle();
          }
      }
  
      @SuppressWarnings({ "unchecked", "rawtypes" })
      @Override
      protected void onSaveInstanceState(Bundle outState) {
          super.onSaveInstanceState(outState);
          outState.putSerializable("backItems", new LinkedList(backItems));
          outState.putSerializable("scrollPositionsH", new HashMap(
                  scrollPositionsH));
          outState.putSerializable("scrollPositionsV", new HashMap(
                  scrollPositionsV));
      }
  
      @SuppressWarnings({ "unchecked", "rawtypes" })
      @Override
      protected void onRestoreInstanceState(Bundle savedInstanceState) {
          super.onRestoreInstanceState(savedInstanceState);
          backItems = Collections.synchronizedList((List) savedInstanceState
                  .getSerializable("backItems"));
          scrollPositionsH = Collections.synchronizedMap((Map) savedInstanceState
                  .getSerializable("scrollPositionsH"));
          scrollPositionsV = Collections.synchronizedMap((Map) savedInstanceState
                  .getSerializable("scrollPositionsV"));
      }
  }
  