+import android.widget.TextView.BufferType;
+import android.widget.Toast;
+
+import com.hughes.android.dictionary.DictionaryInfo.IndexInfo;
+import com.hughes.android.dictionary.engine.Dictionary;
+import com.hughes.android.dictionary.engine.EntrySource;
+import com.hughes.android.dictionary.engine.HtmlEntry;
+import com.hughes.android.dictionary.engine.Index;
+import com.hughes.android.dictionary.engine.Index.IndexEntry;
+import com.hughes.android.dictionary.engine.Language.LanguageResources;
+import com.hughes.android.dictionary.engine.PairEntry;
+import com.hughes.android.dictionary.engine.PairEntry.Pair;
+import com.hughes.android.dictionary.engine.RowBase;
+import com.hughes.android.dictionary.engine.TokenRow;
+import com.hughes.android.dictionary.engine.TransliteratorManager;
+import com.hughes.android.util.IntentLauncher;
+import com.hughes.android.util.NonLinkClickableSpan;
+import com.hughes.util.StringUtil;
+
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.RandomAccessFile;
+import java.nio.channels.FileChannel;
+import java.text.SimpleDateFormat;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Random;
+import java.util.Set;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class DictionaryActivity extends ActionBarActivity {
+
+ static final String LOG = "QuickDic";
+
+ DictionaryApplication application;
+
+ File dictFile = null;
+ FileChannel dictRaf = null;
+ String dictFileTitleName = null;
+
+ Dictionary dictionary = null;
+
+ int indexIndex = 0;
+
+ Index index = null;
+
+ List<RowBase> rowsToShow = null; // if not null, just show these rows.
+
+ final Random rand = new Random();
+
+ final Handler uiHandler = new Handler();
+
+ private final ExecutorService searchExecutor = Executors.newSingleThreadExecutor(new ThreadFactory() {
+ @Override
+ public Thread newThread(Runnable r) {
+ return new Thread(r, "searchExecutor");
+ }
+ });
+
+ private SearchOperation currentSearchOperation = null;
+
+ TextToSpeech textToSpeech;
+ volatile boolean ttsReady;
+
+ Typeface typeface;
+ DictionaryApplication.Theme theme = DictionaryApplication.Theme.LIGHT;
+ int textColorFg = Color.BLACK;
+ int fontSizeSp;
+
+ private ListView listView;
+ private ListView getListView() {
+ if (listView == null) {
+ listView = (ListView)findViewById(android.R.id.list);
+ }
+ return listView;
+ }
+
+ private void setListAdapter(ListAdapter adapter) {
+ getListView().setAdapter(adapter);
+ }
+
+ private ListAdapter getListAdapter() {
+ return getListView().getAdapter();
+ }
+
+ SearchView searchView;
+ ImageButton languageButton;
+ SearchView.OnQueryTextListener onQueryTextListener;
+
+ MenuItem nextWordMenuItem, previousWordMenuItem, randomWordMenuItem;
+
+ // Never null.
+ private File wordList = null;
+ private boolean saveOnlyFirstSubentry = false;
+ private boolean clickOpensContextMenu = false;
+
+ // Visible for testing.
+ ListAdapter indexAdapter = null;
+
+ /**
+ * For some languages, loading the transliterators used in this search takes
+ * a long time, so we fire it up on a different thread, and don't invoke it
+ * from the main thread until it's already finished once.
+ */
+ private volatile boolean indexPrepFinished = false;
+
+ public DictionaryActivity() {
+ }
+
+ public static Intent getLaunchIntent(Context c, final File dictFile, final String indexShortName,
+ final String searchToken) {
+ final Intent intent = new Intent(c, DictionaryActivity.class);
+ intent.putExtra(C.DICT_FILE, dictFile.getPath());
+ intent.putExtra(C.INDEX_SHORT_NAME, indexShortName);
+ intent.putExtra(C.SEARCH_TOKEN, searchToken);
+ intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
+ return intent;
+ }
+
+ @Override
+ protected void onSaveInstanceState(final Bundle outState) {
+ super.onSaveInstanceState(outState);
+ Log.d(LOG, "onSaveInstanceState: " + searchView.getQuery().toString());
+ outState.putString(C.INDEX_SHORT_NAME, index.shortName);
+ outState.putString(C.SEARCH_TOKEN, searchView.getQuery().toString());
+ }
+
+ private int getMatchLen(String search, Index.IndexEntry e) {
+ if (e == null) return 0;
+ for (int i = 0; i < search.length(); ++i) {
+ String a = search.substring(0, i + 1);
+ String b = e.token.substring(0, i + 1);
+ if (!a.equalsIgnoreCase(b))
+ return i;
+ }
+ return search.length();
+ }
+
+ private void dictionaryOpenFail(Exception e) {
+ Log.e(LOG, "Unable to load dictionary.", e);
+ if (dictRaf != null) {
+ indexAdapter = null;
+ setListAdapter(null);
+ try {
+ dictRaf.close();
+ } catch (IOException e1) {
+ Log.e(LOG, "Unable to close dictRaf.", e1);
+ }
+ dictRaf = null;
+ }
+ Toast.makeText(this, getString(R.string.invalidDictionary, "", e.getMessage()),
+ Toast.LENGTH_LONG).show();
+ startActivity(DictionaryManagerActivity.getLaunchIntent(getApplicationContext()));
+ finish();
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ DictionaryApplication.INSTANCE.init(getApplicationContext());
+ application = DictionaryApplication.INSTANCE;
+ // This needs to be before super.onCreate, otherwise ActionbarSherlock
+ // doesn't makes the background of the actionbar white when you're
+ // in the dark theme.
+ setTheme(application.getSelectedTheme().themeId);
+
+ Log.d(LOG, "onCreate:" + this);
+ super.onCreate(savedInstanceState);
+
+ final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
+
+ // Don't auto-launch if this fails.
+ prefs.edit().remove(C.DICT_FILE).remove(C.INDEX_SHORT_NAME).commit();
+
+ setContentView(R.layout.dictionary_activity);
+
+ theme = application.getSelectedTheme();
+ textColorFg = getResources().getColor(theme.tokenRowFgColor);
+
+ if (dictRaf != null) {
+ try {
+ dictRaf.close();
+ } catch (IOException e) {
+ Log.e(LOG, "Failed to close dictionary", e);
+ }
+ dictRaf = null;
+ }
+
+ final Intent intent = getIntent();
+ String intentAction = intent.getAction();
+ /**
+ * @author Dominik Köppl Querying the Intent
+ * com.hughes.action.ACTION_SEARCH_DICT is the advanced query
+ * Arguments: SearchManager.QUERY -> the phrase to search from
+ * -> language in which the phrase is written to -> to which
+ * language shall be translated
+ */
+ if (intentAction != null && intentAction.equals("com.hughes.action.ACTION_SEARCH_DICT")) {
+ String query = intent.getStringExtra(SearchManager.QUERY);
+ String from = intent.getStringExtra("from");
+ if (from != null)
+ from = from.toLowerCase(Locale.US);
+ String to = intent.getStringExtra("to");
+ if (to != null)
+ to = to.toLowerCase(Locale.US);
+ if (query != null) {
+ getIntent().putExtra(C.SEARCH_TOKEN, query);
+ }
+ if (intent.getStringExtra(C.DICT_FILE) == null && (from != null || to != null)) {
+ Log.d(LOG, "DictSearch: from: " + from + " to " + to);
+ List<DictionaryInfo> dicts = application.getDictionariesOnDevice(null);
+ for (DictionaryInfo info : dicts) {
+ boolean hasFrom = from == null;
+ boolean hasTo = to == null;
+ for (IndexInfo index : info.indexInfos) {
+ if (!hasFrom && index.shortName.toLowerCase(Locale.US).equals(from))
+ hasFrom = true;
+ if (!hasTo && index.shortName.toLowerCase(Locale.US).equals(to))
+ hasTo = true;
+ }
+ if (hasFrom && hasTo) {
+ if (from != null) {
+ int which_index = 0;
+ for (; which_index < info.indexInfos.size(); ++which_index) {
+ if (info.indexInfos.get(which_index).shortName.toLowerCase(
+ Locale.US).equals(from))
+ break;
+ }
+ intent.putExtra(C.INDEX_SHORT_NAME,
+ info.indexInfos.get(which_index).shortName);
+
+ }
+ intent.putExtra(C.DICT_FILE, application.getPath(info.uncompressedFilename)
+ .toString());
+ break;
+ }
+ }
+
+ }
+ }
+ /**
+ * @author Dominik Köppl Querying the Intent Intent.ACTION_SEARCH is a
+ * simple query Arguments follow from android standard (see
+ * documentation)
+ */
+ if (intentAction != null && intentAction.equals(Intent.ACTION_SEARCH)) {
+ String query = intent.getStringExtra(SearchManager.QUERY);
+ if (query != null)
+ getIntent().putExtra(C.SEARCH_TOKEN, query);
+ }
+ if (intentAction != null && intentAction.equals(Intent.ACTION_SEND)) {
+ String query = intent.getStringExtra(Intent.EXTRA_TEXT);
+ if (query != null)
+ getIntent().putExtra(C.SEARCH_TOKEN, query);
+ }
+ /*
+ * This processes text on M+ devices where QuickDic shows up in the context menu.
+ */
+ if (intentAction != null && intentAction.equals(Intent.ACTION_PROCESS_TEXT)) {
+ String query = intent.getStringExtra(Intent.EXTRA_PROCESS_TEXT);
+ if (query != null) {
+ getIntent().putExtra(C.SEARCH_TOKEN, query);
+ }
+ }
+ // Support opening dictionary file directly
+ if (intentAction != null && intentAction.equals(Intent.ACTION_VIEW)) {
+ Uri uri = intent.getData();
+ intent.putExtra(C.DICT_FILE, uri.toString());
+ dictFileTitleName = uri.getLastPathSegment();
+ try {
+ dictRaf = getContentResolver().openAssetFileDescriptor(uri, "r").createInputStream().getChannel();
+ } catch (Exception e) {
+ dictionaryOpenFail(e);
+ return;
+ }
+ }
+ /**
+ * @author Dominik Köppl If no dictionary is chosen, use the default
+ * dictionary specified in the preferences If this step does
+ * fail (no default dictionary specified), show a toast and
+ * abort.
+ */
+ if (intent.getStringExtra(C.DICT_FILE) == null) {
+ String dictfile = prefs.getString(getString(R.string.defaultDicKey), null);
+ if (dictfile != null)
+ intent.putExtra(C.DICT_FILE, application.getPath(dictfile).toString());
+ }
+ String dictFilename = intent.getStringExtra(C.DICT_FILE);
+ if (dictFilename == null && intent.getStringExtra(C.SEARCH_TOKEN) != null) {
+ final List<DictionaryInfo> dics = application.getDictionariesOnDevice(null);
+ final String search = intent.getStringExtra(C.SEARCH_TOKEN);
+ String bestFname = null;
+ String bestIndex = null;
+ int bestMatchLen = 2; // ignore shorter matches
+ AtomicBoolean dummy = new AtomicBoolean();
+ for (int i = 0; dictFilename == null && i < dics.size(); ++i) {
+ try {
+ Log.d(LOG, "Checking dictionary " + dics.get(i).uncompressedFilename);
+ final File dictfile = application.getPath(dics.get(i).uncompressedFilename);
+ Dictionary dic = new Dictionary(new RandomAccessFile(dictfile, "r").getChannel());
+ for (int j = 0; j < dic.indices.size(); ++j) {
+ Index idx = dic.indices.get(j);
+ Log.d(LOG, "Checking index " + idx.shortName);
+ if (idx.findExact(search) != null) {
+ Log.d(LOG, "Found exact match");
+ dictFilename = dictfile.toString();
+ intent.putExtra(C.INDEX_SHORT_NAME, idx.shortName);
+ break;
+ }
+ int matchLen = getMatchLen(search, idx.findInsertionPoint(search, dummy));
+ Log.d(LOG, "Found partial match length " + matchLen);
+ if (matchLen > bestMatchLen) {
+ bestFname = dictfile.toString();
+ bestIndex = idx.shortName;
+ bestMatchLen = matchLen;
+ }
+ }
+ } catch (Exception e) {}
+ }
+ if (dictFilename == null && bestFname != null) {
+ dictFilename = bestFname;
+ intent.putExtra(C.INDEX_SHORT_NAME, bestIndex);
+ }
+ }
+
+ if (dictFilename == null) {
+ Toast.makeText(this, getString(R.string.no_dict_file), Toast.LENGTH_LONG).show();
+ startActivity(DictionaryManagerActivity.getLaunchIntent(getApplicationContext()));
+ finish();
+ return;
+ }
+ if (dictRaf == null && dictFilename != null)
+ dictFile = new File(dictFilename);
+
+ ttsReady = false;
+ textToSpeech = new TextToSpeech(getApplicationContext(), new OnInitListener() {
+ @Override
+ public void onInit(int status) {
+ ttsReady = true;
+ updateTTSLanguage(indexIndex);
+ }
+ });
+
+ try {
+ if (dictRaf == null) {
+ dictFileTitleName = application.getDictionaryName(dictFile.getName());
+ dictRaf = new RandomAccessFile(dictFile, "r").getChannel();
+ }
+ this.setTitle("QuickDic: " + dictFileTitleName);
+ dictionary = new Dictionary(dictRaf);
+ } catch (Exception e) {
+ dictionaryOpenFail(e);
+ return;
+ }
+ String targetIndex = intent.getStringExtra(C.INDEX_SHORT_NAME);
+ if (savedInstanceState != null && savedInstanceState.getString(C.INDEX_SHORT_NAME) != null) {
+ targetIndex = savedInstanceState.getString(C.INDEX_SHORT_NAME);
+ }
+ indexIndex = 0;
+ for (int i = 0; i < dictionary.indices.size(); ++i) {
+ if (dictionary.indices.get(i).shortName.equals(targetIndex)) {
+ indexIndex = i;
+ break;
+ }
+ }
+ Log.d(LOG, "Loading index " + indexIndex);
+ index = dictionary.indices.get(indexIndex);
+ getListView().setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS);
+ getListView().setEmptyView(findViewById(android.R.id.empty));
+ getListView().setOnItemClickListener(new OnItemClickListener() {
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int row, long id) {
+ onListItemClick(getListView(), view, row, id);
+ }
+ });
+
+ setListAdapter(new IndexAdapter(index));
+
+ // Pre-load the Transliterator (will spawn its own thread)
+ TransliteratorManager.init(new TransliteratorManager.Callback() {
+ @Override
+ public void onTransliteratorReady() {
+ uiHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ onSearchTextChange(searchView.getQuery().toString());
+ }
+ });
+ }
+ }, DictionaryApplication.threadBackground);
+
+ // Pre-load the collators.
+ new Thread(new Runnable() {
+ public void run() {
+ android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_LESS_FAVORABLE);
+ final long startMillis = System.currentTimeMillis();
+ try {
+ for (final Index index : dictionary.indices) {
+ final String searchToken = index.sortedIndexEntries.get(0).token;
+ final IndexEntry entry = index.findExact(searchToken);
+ if (entry == null || !searchToken.equals(entry.token)) {
+ Log.e(LOG, "Couldn't find token: " + searchToken + ", " + (entry == null ? "null" : entry.token));
+ }
+ }
+ indexPrepFinished = true;
+ } catch (Exception e) {
+ Log.w(LOG,
+ "Exception while prepping. This can happen if dictionary is closed while search is happening.");
+ }
+ Log.d(LOG, "Prepping indices took:" + (System.currentTimeMillis() - startMillis));
+ }
+ }).start();
+
+ String fontName = prefs.getString(getString(R.string.fontKey), "FreeSerif.otf.jpg");
+ if ("SYSTEM".equals(fontName)) {
+ typeface = Typeface.DEFAULT;
+ } else if ("SERIF".equals(fontName)) {
+ typeface = Typeface.SERIF;
+ } else if ("SANS_SERIF".equals(fontName)) {
+ typeface = Typeface.SANS_SERIF;
+ } else if ("MONOSPACE".equals(fontName)) {
+ typeface = Typeface.MONOSPACE;