1 package com.hughes.android.dictionary;
4 import java.io.FileWriter;
5 import java.io.IOException;
6 import java.io.PrintWriter;
7 import java.io.RandomAccessFile;
8 import java.text.SimpleDateFormat;
9 import java.util.Arrays;
10 import java.util.Date;
11 import java.util.concurrent.Executor;
12 import java.util.concurrent.Executors;
13 import java.util.concurrent.atomic.AtomicBoolean;
15 import android.app.ListActivity;
16 import android.content.Context;
17 import android.content.Intent;
18 import android.content.SharedPreferences;
19 import android.content.SharedPreferences.Editor;
20 import android.graphics.Typeface;
21 import android.os.Bundle;
22 import android.os.Handler;
23 import android.preference.PreferenceManager;
24 import android.text.ClipboardManager;
25 import android.text.Editable;
26 import android.text.Spannable;
27 import android.text.TextWatcher;
28 import android.text.style.StyleSpan;
29 import android.util.Log;
30 import android.view.ContextMenu;
31 import android.view.KeyEvent;
32 import android.view.Menu;
33 import android.view.MenuItem;
34 import android.view.View;
35 import android.view.ViewGroup;
36 import android.view.ContextMenu.ContextMenuInfo;
37 import android.view.MenuItem.OnMenuItemClickListener;
38 import android.view.View.OnClickListener;
39 import android.view.inputmethod.InputMethodManager;
40 import android.widget.AdapterView;
41 import android.widget.BaseAdapter;
42 import android.widget.Button;
43 import android.widget.EditText;
44 import android.widget.ListView;
45 import android.widget.TableLayout;
46 import android.widget.TableRow;
47 import android.widget.TextView;
48 import android.widget.Toast;
50 import com.hughes.android.dictionary.Dictionary.IndexEntry;
51 import com.hughes.android.dictionary.Dictionary.LanguageData;
52 import com.hughes.android.dictionary.Dictionary.Row;
53 import com.hughes.android.dictionary.engine.Language;
54 import com.ibm.icu.text.Collator;
56 public class DictionaryActivity extends ListActivity {
59 // * Easy reverse lookup.
60 // * Download latest dicts.
61 // * http://ftp.tu-chemnitz.de/pub/Local/urz/ding/de-en-devel/
62 // * http://www1.dict.cc/translation_file_request.php?l=e
63 // * Compress all the strings everywhere, put compression table in file.
65 // * Only one way to way for current search to end. (won't do).
67 static final String LOG = "QuickDic";
68 static final String PREF_DICT_ACTIVE_LANG = "DICT_DIR_PREF";
69 static final String PREF_ACTIVE_SEARCH_TEXT = "ACTIVE_WORD_PREF";
72 final Handler uiHandler = new Handler();
73 private final Executor searchExecutor = Executors.newSingleThreadExecutor();
77 int lastSelectedRow = 0; // TODO: I'm evil.
79 private boolean prefsMightHaveChanged = true;
82 private File wordList;
83 private RandomAccessFile dictRaf = null;
84 private Dictionary dictionary = null;
85 private boolean saveOnlyFirstSubentry = false;
87 // Visible for testing.
88 LanguageListAdapter languageList = null;
89 private SearchOperation searchOperation = null;
91 public DictionaryActivity() {
93 searchExecutor.execute(new Runnable() {
95 final long startMillis = System.currentTimeMillis();
96 for (final String lang : Arrays.asList("EN", "DE")) {
97 Language.lookup(lang).getFindCollator();
98 final Collator c = Language.lookup(lang).getSortCollator();
99 if (c.compare("pre-print", "preppy") >= 0) {
100 Log.e(LOG, c.getClass() + " is buggy, lookups may not work properly.");
103 Log.d(LOG, "Loading collators took:" + (System.currentTimeMillis() - startMillis));
109 /** Called when the activity is first created. */
111 public void onCreate(Bundle savedInstanceState) {
112 super.onCreate(savedInstanceState);
113 Log.d(LOG, "onCreate:" + this);
116 initDictionaryAndPrefs();
117 } catch (Exception e) {
123 setContentView(R.layout.dictionary_activity);
124 searchText = (EditText) findViewById(R.id.SearchText);
125 langButton = (Button) findViewById(R.id.LangButton);
127 Log.d(LOG, "adding text changed listener");
128 searchText.addTextChangedListener(new SearchTextWatcher());
130 getListView().setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
131 public void onItemSelected(AdapterView<?> arg0, View arg1, int row,
135 public void onNothingSelected(AdapterView<?> arg0) {
139 getListView().setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() {
140 public boolean onItemLongClick(AdapterView<?> arg0, View arg1, int row,
147 final Button clearSearchTextButton = (Button) findViewById(R.id.ClearSearchTextButton);
148 clearSearchTextButton.setOnClickListener(new OnClickListener() {
149 public void onClick(View v) {
150 onClearSearchTextButton(clearSearchTextButton);
153 clearSearchTextButton.setVisibility(PreferenceManager.getDefaultSharedPreferences(this).getBoolean(
154 getString(R.string.showClearSearchTextButtonKey), true) ? View.VISIBLE
157 final Button langButton = (Button) findViewById(R.id.LangButton);
158 langButton.setOnClickListener(new OnClickListener() {
159 public void onClick(View v) {
164 final Button upButton = (Button) findViewById(R.id.UpButton);
165 upButton.setOnClickListener(new OnClickListener() {
166 public void onClick(View v) {
170 final Button downButton = (Button) findViewById(R.id.DownButton);
171 downButton.setOnClickListener(new OnClickListener() {
172 public void onClick(View v) {
178 registerForContextMenu(getListView());
183 private void initDictionaryAndPrefs() throws Exception {
184 if (!prefsMightHaveChanged) {
187 closeCurrentDictionary();
189 final SharedPreferences prefs = PreferenceManager
190 .getDefaultSharedPreferences(this);
191 wordList = new File(prefs.getString(getString(R.string.wordListFileKey),
192 getString(R.string.wordListFileDefault)));
193 Log.d(LOG, "wordList=" + wordList);
195 saveOnlyFirstSubentry = prefs.getBoolean(getString(R.string.saveOnlyFirstSubentryKey), false);
197 final File dictFile = new File(prefs.getString(getString(R.string.dictFileKey),
198 getString(R.string.dictFileDefault)));
199 Log.d(LOG, "dictFile=" + dictFile);
202 if (!dictFile.canRead()) {
203 throw new IOException("Unable to read dictionary file.");
206 dictRaf = new RandomAccessFile(dictFile, "r");
207 final long startMillis = System.currentTimeMillis();
208 dictionary = new Dictionary(dictRaf);
209 Log.d(LOG, "Read dictionary millis: " + (System.currentTimeMillis() - startMillis));
210 } catch (IOException e) {
211 Log.e(LOG, "Couldn't open dictionary.", e);
212 this.startActivity(new Intent(this, NoDictionaryActivity.class));
214 throw new Exception(e);
217 final byte lang = prefs.getInt(PREF_DICT_ACTIVE_LANG, SimpleEntry.LANG1) == SimpleEntry.LANG1 ? SimpleEntry.LANG1
220 languageList = new LanguageListAdapter(dictionary.languageDatas[lang]);
221 setListAdapter(languageList);
222 prefsMightHaveChanged = false;
226 public void onResume() {
228 Log.d(LOG, "onResume:" + this);
231 initDictionaryAndPrefs();
232 } catch (Exception e) {
236 final SharedPreferences prefs = PreferenceManager
237 .getDefaultSharedPreferences(this);
238 final String searchTextString = prefs
239 .getString(PREF_ACTIVE_SEARCH_TEXT, "");
240 searchText.setText(searchTextString);
241 getListView().requestFocus();
242 onSearchTextChange(searchTextString);
246 public void onPause() {
248 Log.d(LOG, "onPause:" + this);
249 final Editor prefs = PreferenceManager.getDefaultSharedPreferences(this)
251 prefs.putInt(PREF_DICT_ACTIVE_LANG, languageList.languageData.lang);
252 prefs.putString(PREF_ACTIVE_SEARCH_TEXT, searchText.getText().toString());
257 public void onStop() {
259 Log.d(LOG, "onStop:" + this);
261 Log.i(LOG, "isFinishing()==true, closing dictionary.");
262 closeCurrentDictionary();
266 private void closeCurrentDictionary() {
267 Log.i(LOG, "closeCurrentDictionary");
268 if (dictionary == null) {
273 setListAdapter(null);
274 Log.d(LOG, "setListAdapter finished.");
277 if (dictRaf != null) {
280 } catch (IOException e) {
281 throw new RuntimeException(e);
286 public String getSelectedRowRawText(final boolean onlyFirstSubentry) {
287 final Row row = languageList.languageData.rows.get(getSelectedRow());
288 return languageList.languageData.rowToString(row, onlyFirstSubentry);
291 // ----------------------------------------------------------------
293 // ----------------------------------------------------------------
295 private MenuItem switchLanguageMenuItem = null;
298 public boolean onCreateOptionsMenu(final Menu menu) {
299 switchLanguageMenuItem = menu.add(getString(R.string.switchToLanguage));
300 switchLanguageMenuItem
301 .setOnMenuItemClickListener(new OnMenuItemClickListener() {
302 public boolean onMenuItemClick(final MenuItem menuItem) {
308 final MenuItem preferences = menu.add(getString(R.string.preferences));
309 preferences.setOnMenuItemClickListener(new OnMenuItemClickListener() {
310 public boolean onMenuItemClick(final MenuItem menuItem) {
311 prefsMightHaveChanged = true;
312 startActivity(new Intent(DictionaryActivity.this,
313 PreferenceActivity.class));
318 final MenuItem about = menu.add(getString(R.string.about));
319 about.setOnMenuItemClickListener(new OnMenuItemClickListener() {
320 public boolean onMenuItemClick(final MenuItem menuItem) {
321 final Intent intent = new Intent().setClassName(AboutActivity.class
322 .getPackage().getName(), AboutActivity.class.getCanonicalName());
323 final String currentDictInfo;
324 if (dictionary == null) {
325 currentDictInfo = getString(R.string.noDictLoaded);
327 final LanguageData lang0 = dictionary.languageDatas[0];
328 final LanguageData lang1 = dictionary.languageDatas[1];
329 currentDictInfo = getString(R.string.aboutText, dictionary.dictionaryInfo, dictionary.entries.size(),
330 lang0.language.symbol, lang0.sortedIndex.size(), lang0.rows.size(),
331 lang1.language.symbol, lang1.sortedIndex.size(), lang1.rows.size());
333 intent.putExtra(AboutActivity.CURRENT_DICT_INFO, currentDictInfo
335 startActivity(intent);
340 final MenuItem download = menu.add(getString(R.string.downloadDictionary));
341 download.setOnMenuItemClickListener(new OnMenuItemClickListener() {
342 public boolean onMenuItemClick(final MenuItem menuItem) {
343 prefsMightHaveChanged = true;
344 startDownloadDictActivity(DictionaryActivity.this);
353 public boolean onPrepareOptionsMenu(final Menu menu) {
354 switchLanguageMenuItem.setTitle(getString(R.string.switchToLanguage,
355 dictionary.languageDatas[SimpleEntry
356 .otherLang(languageList.languageData.lang)].language.symbol));
357 return super.onPrepareOptionsMenu(menu);
360 void updateLangButton() {
361 langButton.setText(languageList.languageData.language.symbol);
364 // ----------------------------------------------------------------
366 // ----------------------------------------------------------------
368 void onLanguageButton() {
370 languageList = new LanguageListAdapter(
371 dictionary.languageDatas[(languageList.languageData == dictionary.languageDatas[0]) ? 1
373 Log.d(LOG, "onLanguageButton, newLang=" + languageList.languageData.language.symbol);
374 setListAdapter(languageList);
376 onSearchTextChange(searchText.getText().toString());
380 final int destRowIndex = languageList.languageData.getPrevTokenRow(getSelectedRow());
381 Log.d(LOG, "onUpButton, destRowIndex=" + destRowIndex);
382 jumpToRow(languageList, destRowIndex);
385 void onDownButton() {
386 final int destRowIndex = languageList.languageData.getNextTokenRow(getSelectedRow());
387 Log.d(LOG, "onDownButton, destRowIndex=" + destRowIndex);
388 jumpToRow(languageList, destRowIndex);
391 void onAppendToWordList() {
392 final int row = getSelectedRow();
396 final StringBuilder rawText = new StringBuilder();
397 final String word = languageList.languageData.getIndexEntryForRow(row).word;
399 new SimpleDateFormat("yyyy.MM.dd HH:mm:ss").format(new Date()))
401 rawText.append(word).append("\t");
402 rawText.append(getSelectedRowRawText(saveOnlyFirstSubentry));
403 Log.d(LOG, "Writing : " + rawText);
405 wordList.getParentFile().mkdirs();
406 final PrintWriter out = new PrintWriter(
407 new FileWriter(wordList, true));
408 out.println(rawText.toString());
410 } catch (IOException e) {
411 Log.e(LOG, "Unable to append to " + wordList.getAbsolutePath(), e);
412 Toast.makeText(this, getString(R.string.failedAddingToWordList, wordList.getAbsolutePath()), Toast.LENGTH_LONG);
418 final int row = getSelectedRow();
422 Log.d(LOG, "Copy." + DictionaryActivity.this.getSelectedRow());
423 final StringBuilder result = new StringBuilder();
424 result.append(getSelectedRowRawText(false));
425 final ClipboardManager clipboardManager = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
426 clipboardManager.setText(result.toString());
427 Log.d(LOG, "Copied: " + result);
431 public boolean onKeyDown(int keyCode, KeyEvent event) {
432 if (event.getUnicodeChar() != 0) {
433 if (!searchText.hasFocus()) {
434 searchText.setText("" + (char) event.getUnicodeChar());
435 onSearchTextChange(searchText.getText().toString());
436 searchText.requestFocus();
440 return super.onKeyDown(keyCode, event);
444 protected void onListItemClick(ListView l, View v, int row, long id) {
446 openContextMenu(getListView());
449 void onSearchTextChange(final String searchText) {
450 Log.d(LOG, "onSearchTextChange: " + searchText);
451 synchronized (this) {
452 searchOperation = new SearchOperation(languageList, searchText.trim(), searchOperation);
453 searchExecutor.execute(searchOperation);
457 private void onClearSearchTextButton(final Button clearSearchTextButton) {
458 clearSearchTextButton.requestFocus();
459 searchText.setText("");
460 searchText.requestFocus();
461 Log.d(LOG, "Trying to show soft keyboard.");
462 final InputMethodManager manager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
463 manager.showSoftInput(searchText, InputMethodManager.SHOW_IMPLICIT);
466 // ----------------------------------------------------------------
468 // ----------------------------------------------------------------
471 public void onCreateContextMenu(ContextMenu menu, View v,
472 ContextMenuInfo menuInfo) {
473 final int row = getSelectedRow();
478 final MenuItem addToWordlist = menu.add(getString(R.string.addToWordList, wordList.getName()));
479 addToWordlist.setOnMenuItemClickListener(new OnMenuItemClickListener() {
480 public boolean onMenuItemClick(MenuItem item) {
481 onAppendToWordList();
486 final MenuItem copy = menu.add(android.R.string.copy);
487 copy.setOnMenuItemClickListener(new OnMenuItemClickListener() {
488 public boolean onMenuItemClick(MenuItem item) {
496 private void jumpToRow(final LanguageListAdapter dictionaryListAdapter,
497 final int rowIndex) {
498 Log.d(LOG, "jumpToRow: " + rowIndex);
499 if (dictionaryListAdapter != this.languageList) {
500 Log.w(LOG, "skipping jumpToRow for old list adapter: " + rowIndex);
503 setSelection(rowIndex);
504 setSelectedRow(rowIndex);
505 getListView().setSelected(true);
508 // TODO: delete me somehow.
509 private int getSelectedRow() {
510 return lastSelectedRow;
512 private void setSelectedRow(final int row) {
513 lastSelectedRow = row;
514 Log.d(LOG, "Selected: " + getSelectedRowRawText(true));
518 private void updateSearchText() {
519 Log.d(LOG, "updateSearchText");
520 final int selectedRowIndex = getSelectedRow();
521 if (!searchText.hasFocus()) {
522 if (selectedRowIndex >= 0) {
523 final String word = languageList.languageData
524 .getIndexEntryForRow(selectedRowIndex).word;
525 if (!word.equals(searchText.getText().toString())) {
526 Log.d(LOG, "updateSearchText: setText: " + word);
527 searchText.setText(word);
530 Log.w(LOG, "updateSearchText: nothing selected.");
535 static void startDownloadDictActivity(final Context context) {
536 final Intent intent = new Intent(context, DownloadActivity.class);
537 final SharedPreferences prefs = PreferenceManager
538 .getDefaultSharedPreferences(context);
539 final String dictFetchUrl = prefs.getString(context
540 .getString(R.string.dictFetchUrlKey), context
541 .getString(R.string.dictFetchUrlDefault));
542 final String dictFileName = prefs.getString(context
543 .getString(R.string.dictFileKey), context
544 .getString(R.string.dictFileDefault));
545 intent.putExtra(DownloadActivity.SOURCE, dictFetchUrl);
546 intent.putExtra(DownloadActivity.DEST, dictFileName);
547 context.startActivity(intent);
550 class LanguageListAdapter extends BaseAdapter {
552 // Visible for testing.
553 final LanguageData languageData;
555 LanguageListAdapter(final LanguageData languageData) {
556 this.languageData = languageData;
559 public int getCount() {
560 return languageData.rows.size();
563 public Dictionary.Row getItem(int rowIndex) {
564 assert rowIndex < languageData.rows.size();
565 return languageData.rows.get(rowIndex);
568 public long getItemId(int rowIndex) {
572 public View getView(final int rowIndex, final View convertView,
573 final ViewGroup parent) {
574 final Row row = getItem(rowIndex);
578 TextView result = null;
579 if (convertView instanceof TextView) {
580 result = (TextView) convertView;
582 result = new TextView(parent.getContext());
587 result.setText(languageData.rowToString(row, false));
588 result.setTextAppearance(parent.getContext(),
589 android.R.style.TextAppearance_Large);
590 result.setClickable(false);
595 final TableLayout result = new TableLayout(parent.getContext());
597 final SimpleEntry entry = new SimpleEntry(null, null);//.entries.get(row.getIndex());
598 final int rowCount = entry.getRowCount();
599 for (int r = 0; r < rowCount; ++r) {
600 final TableRow tableRow = new TableRow(result.getContext());
602 TextView column1 = new TextView(tableRow.getContext());
603 TextView column2 = new TextView(tableRow.getContext());
604 final TableRow.LayoutParams layoutParams = new TableRow.LayoutParams();
605 layoutParams.weight = 0.5f;
608 final TextView spacer = new TextView(tableRow.getContext());
609 spacer.setText(r == 0 ? "� " : " � ");
610 tableRow.addView(spacer);
612 tableRow.addView(column1, layoutParams);
614 final TextView spacer = new TextView(tableRow.getContext());
615 spacer.setText(r == 0 ? "� " : " � ");
616 tableRow.addView(spacer);
618 tableRow.addView(column2, layoutParams);
622 // column1.setTextAppearance(parent.getContext(), android.R.style.Text);
624 // TODO: color words by gender
625 final String col1Text = entry.getAllText(languageData.lang)[r];
626 column1.setText(col1Text, TextView.BufferType.SPANNABLE);
627 final Spannable col1Spannable = (Spannable) column1.getText();
629 final String token = languageData.getIndexEntryForRow(rowIndex).word;
630 while ((startPos = col1Text.indexOf(token, startPos)) != -1) {
631 col1Spannable.setSpan(new StyleSpan(Typeface.BOLD), startPos,
632 startPos + token.length(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
633 startPos += token.length();
637 entry.getAllText(SimpleEntry.otherLang(languageData.lang))[r],
638 TextView.BufferType.NORMAL);
640 result.addView(tableRow);
646 } // DictionaryListAdapter
648 private final class SearchOperation implements Runnable {
649 SearchOperation previousSearchOperation;
651 final LanguageListAdapter listAdapter;
652 final LanguageData languageData;
653 final String searchText;
654 final AtomicBoolean interrupted = new AtomicBoolean(false);
655 boolean searchFinished = false;
657 SearchOperation(final LanguageListAdapter listAdapter,
658 final String searchText, final SearchOperation previousSearchOperation) {
659 this.listAdapter = listAdapter;
660 this.languageData = listAdapter.languageData;
661 this.searchText = searchText;
662 this.previousSearchOperation = previousSearchOperation;
666 if (previousSearchOperation != null) {
667 previousSearchOperation.stopAndWait();
669 previousSearchOperation = null;
671 Log.d(LOG, "SearchOperation: " + searchText);
672 final int indexLocation = languageData.lookup(searchText, interrupted);
673 if (!interrupted.get()) {
674 final IndexEntry indexEntry = languageData.sortedIndex.get(indexLocation);
676 Log.d(LOG, "SearchOperation completed: " + indexEntry.toString());
677 uiHandler.post(new Runnable() {
679 // Check is just a performance operation.
680 if (!interrupted.get()) {
681 // This is safe, because it checks that the listAdapter hasn't changed.
682 jumpToRow(listAdapter, indexEntry.startRow);
684 synchronized (DictionaryActivity.this) {
685 searchOperation = null;
686 DictionaryActivity.this.notifyAll();
691 synchronized (this) {
692 searchFinished = true;
697 private void stopAndWait() {
698 interrupted.set(true);
699 synchronized (this) {
700 while (!searchFinished) {
701 Log.d(LOG, "stopAndWait: " + searchText);
704 } catch (InterruptedException e) {
705 Log.e(LOG, "Interrupted", e);
712 void waitForSearchEnd() {
713 synchronized (this) {
714 while (searchOperation != null) {
715 Log.d(LOG, "waitForSearchEnd");
718 } catch (InterruptedException e) {
719 Log.e(LOG, "Interrupted.", e);
725 private class SearchTextWatcher implements TextWatcher {
726 public void afterTextChanged(final Editable searchTextEditable) {
727 Log.d(LOG, "Search text changed: " + searchText.getText().toString());
728 if (searchText.hasFocus()) {
729 // If they were typing to cause the change, update the UI.
730 onSearchTextChange(searchText.getText().toString());
734 public void beforeTextChanged(CharSequence arg0, int arg1, int arg2,
738 public void onTextChanged(CharSequence arg0, int arg1, int arg2, int arg3) {