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.widget.AdapterView;
40 import android.widget.BaseAdapter;
41 import android.widget.Button;
42 import android.widget.EditText;
43 import android.widget.ListView;
44 import android.widget.TableLayout;
45 import android.widget.TableRow;
46 import android.widget.TextView;
47 import android.widget.Toast;
49 import com.hughes.android.dictionary.Dictionary.IndexEntry;
50 import com.hughes.android.dictionary.Dictionary.LanguageData;
51 import com.hughes.android.dictionary.Dictionary.Row;
52 import com.ibm.icu.text.Collator;
54 public class DictionaryActivity extends ListActivity {
57 // * Download latest dicts.
58 // * http://ftp.tu-chemnitz.de/pub/Local/urz/ding/de-en-devel/
59 // * http://www1.dict.cc/translation_file_request.php?l=e
60 // * Compress all the strings everywhere, put compression table in file.
62 // * Only one way to way for current search to end. (won't do).
64 static final String LOG = "QuickDic";
65 static final String PREF_DICT_ACTIVE_LANG = "DICT_DIR_PREF";
66 static final String PREF_ACTIVE_SEARCH_TEXT = "ACTIVE_WORD_PREF";
69 final Handler uiHandler = new Handler();
70 private final Executor searchExecutor = Executors.newSingleThreadExecutor();
74 int lastSelectedRow = 0; // TODO: I'm evil.
76 private boolean prefsMightHaveChanged = true;
79 private File wordList;
80 private RandomAccessFile dictRaf = null;
81 private Dictionary dictionary = null;
82 private boolean saveOnlyFirstSubentry = false;
84 // Visible for testing.
85 LanguageListAdapter languageList = null;
86 private SearchOperation searchOperation = null;
88 public DictionaryActivity() {
90 searchExecutor.execute(new Runnable() {
92 final long startMillis = System.currentTimeMillis();
93 for (final String lang : Arrays.asList("EN", "DE")) {
94 Language.lookup(lang).getFindCollator();
95 final Collator c = Language.lookup(lang).getSortCollator();
96 if (c.compare("pre-print", "preppy") >= 0) {
97 Log.e(LOG, c.getClass() + " is buggy, lookups may not work properly.");
100 Log.d(LOG, "Loading collators took:" + (System.currentTimeMillis() - startMillis));
106 /** Called when the activity is first created. */
108 public void onCreate(Bundle savedInstanceState) {
109 super.onCreate(savedInstanceState);
110 Log.d(LOG, "onCreate:" + this);
113 initDictionaryAndPrefs();
114 } catch (Exception e) {
120 setContentView(R.layout.main);
121 searchText = (EditText) findViewById(R.id.SearchText);
122 langButton = (Button) findViewById(R.id.LangButton);
124 Log.d(LOG, "adding text changed listener");
125 searchText.addTextChangedListener(new SearchTextWatcher());
127 getListView().setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
128 public void onItemSelected(AdapterView<?> arg0, View arg1, int row,
132 public void onNothingSelected(AdapterView<?> arg0) {
136 getListView().setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() {
137 public boolean onItemLongClick(AdapterView<?> arg0, View arg1, int row,
144 final Button clearSearchTextButton = (Button) findViewById(R.id.ClearSearchTextButton);
145 clearSearchTextButton.setOnClickListener(new OnClickListener() {
146 public void onClick(View v) {
147 clearSearchTextButton.requestFocus();
148 searchText.setText("");
149 searchText.requestFocus();
152 clearSearchTextButton.setVisibility(PreferenceManager.getDefaultSharedPreferences(this).getBoolean(
153 getString(R.string.showClearSearchTextButtonKey), true) ? View.VISIBLE
156 final Button langButton = (Button) findViewById(R.id.LangButton);
157 langButton.setOnClickListener(new OnClickListener() {
158 public void onClick(View v) {
163 final Button upButton = (Button) findViewById(R.id.UpButton);
164 upButton.setOnClickListener(new OnClickListener() {
165 public void onClick(View v) {
169 final Button downButton = (Button) findViewById(R.id.DownButton);
170 downButton.setOnClickListener(new OnClickListener() {
171 public void onClick(View v) {
177 registerForContextMenu(getListView());
182 private void initDictionaryAndPrefs() throws Exception {
183 if (!prefsMightHaveChanged) {
186 closeCurrentDictionary();
188 final SharedPreferences prefs = PreferenceManager
189 .getDefaultSharedPreferences(this);
190 wordList = new File(prefs.getString(getString(R.string.wordListFileKey),
191 getString(R.string.wordListFileDefault)));
192 Log.d(LOG, "wordList=" + wordList);
194 saveOnlyFirstSubentry = prefs.getBoolean(getString(R.string.saveOnlyFirstSubentryKey), false);
196 final File dictFile = new File(prefs.getString(getString(R.string.dictFileKey),
197 getString(R.string.dictFileDefault)));
198 Log.d(LOG, "dictFile=" + dictFile);
201 if (!dictFile.canRead()) {
202 throw new IOException("Unable to read dictionary file.");
205 dictRaf = new RandomAccessFile(dictFile, "r");
206 final long startMillis = System.currentTimeMillis();
207 dictionary = new Dictionary(dictRaf);
208 Log.d(LOG, "Read dictionary millis: " + (System.currentTimeMillis() - startMillis));
209 } catch (IOException e) {
210 Log.e(LOG, "Couldn't open dictionary.", e);
211 this.startActivity(new Intent(this, NoDictionaryActivity.class));
213 throw new Exception(e);
216 final byte lang = prefs.getInt(PREF_DICT_ACTIVE_LANG, Entry.LANG1) == Entry.LANG1 ? Entry.LANG1
219 languageList = new LanguageListAdapter(dictionary.languageDatas[lang]);
220 setListAdapter(languageList);
221 prefsMightHaveChanged = false;
225 public void onResume() {
227 Log.d(LOG, "onResume:" + this);
230 initDictionaryAndPrefs();
231 } catch (Exception e) {
235 final SharedPreferences prefs = PreferenceManager
236 .getDefaultSharedPreferences(this);
237 final String searchTextString = prefs
238 .getString(PREF_ACTIVE_SEARCH_TEXT, "");
239 searchText.setText(searchTextString);
240 getListView().requestFocus();
241 onSearchTextChange(searchTextString);
245 public void onPause() {
247 Log.d(LOG, "onPause:" + this);
248 final Editor prefs = PreferenceManager.getDefaultSharedPreferences(this)
250 prefs.putInt(PREF_DICT_ACTIVE_LANG, languageList.languageData.lang);
251 prefs.putString(PREF_ACTIVE_SEARCH_TEXT, searchText.getText().toString());
256 public void onStop() {
258 Log.d(LOG, "onStop:" + this);
260 Log.i(LOG, "isFinishing()==true, closing dictionary.");
261 closeCurrentDictionary();
265 private void closeCurrentDictionary() {
266 Log.i(LOG, "closeCurrentDictionary");
267 if (dictionary == null) {
272 setListAdapter(null);
273 Log.d(LOG, "setListAdapter finished.");
276 if (dictRaf != null) {
279 } catch (IOException e) {
280 throw new RuntimeException(e);
285 public String getSelectedRowRawText(final boolean onlyFirstSubentry) {
286 final Row row = languageList.languageData.rows.get(getSelectedRow());
287 return languageList.languageData.rowToString(row, onlyFirstSubentry);
290 // ----------------------------------------------------------------
292 // ----------------------------------------------------------------
294 private MenuItem switchLanguageMenuItem = null;
297 public boolean onCreateOptionsMenu(final Menu menu) {
298 switchLanguageMenuItem = menu.add(getString(R.string.switchToLanguage));
299 switchLanguageMenuItem
300 .setOnMenuItemClickListener(new OnMenuItemClickListener() {
301 public boolean onMenuItemClick(final MenuItem menuItem) {
307 final MenuItem preferences = menu.add(getString(R.string.preferences));
308 preferences.setOnMenuItemClickListener(new OnMenuItemClickListener() {
309 public boolean onMenuItemClick(final MenuItem menuItem) {
310 prefsMightHaveChanged = true;
311 startActivity(new Intent(DictionaryActivity.this,
312 PreferenceActivity.class));
317 final MenuItem about = menu.add(getString(R.string.about));
318 about.setOnMenuItemClickListener(new OnMenuItemClickListener() {
319 public boolean onMenuItemClick(final MenuItem menuItem) {
320 final Intent intent = new Intent().setClassName(AboutActivity.class
321 .getPackage().getName(), AboutActivity.class.getCanonicalName());
322 final String currentDictInfo;
323 if (dictionary == null) {
324 currentDictInfo = getString(R.string.noDictLoaded);
326 final LanguageData lang0 = dictionary.languageDatas[0];
327 final LanguageData lang1 = dictionary.languageDatas[1];
328 currentDictInfo = getString(R.string.aboutText, dictionary.dictionaryInfo, dictionary.entries.size(),
329 lang0.language.symbol, lang0.sortedIndex.size(), lang0.rows.size(),
330 lang1.language.symbol, lang1.sortedIndex.size(), lang1.rows.size());
332 intent.putExtra(AboutActivity.CURRENT_DICT_INFO, currentDictInfo
334 startActivity(intent);
339 final MenuItem download = menu.add(getString(R.string.downloadDictionary));
340 download.setOnMenuItemClickListener(new OnMenuItemClickListener() {
341 public boolean onMenuItemClick(final MenuItem menuItem) {
342 prefsMightHaveChanged = true;
343 startDownloadDictActivity(DictionaryActivity.this);
352 public boolean onPrepareOptionsMenu(final Menu menu) {
353 switchLanguageMenuItem.setTitle(getString(R.string.switchToLanguage,
354 dictionary.languageDatas[Entry
355 .otherLang(languageList.languageData.lang)].language.symbol));
356 return super.onPrepareOptionsMenu(menu);
359 void updateLangButton() {
360 langButton.setText(languageList.languageData.language.symbol);
363 // ----------------------------------------------------------------
365 // ----------------------------------------------------------------
367 void onLanguageButton() {
369 languageList = new LanguageListAdapter(
370 dictionary.languageDatas[(languageList.languageData == dictionary.languageDatas[0]) ? 1
372 Log.d(LOG, "onLanguageButton, newLang=" + languageList.languageData.language.symbol);
373 setListAdapter(languageList);
375 onSearchTextChange(searchText.getText().toString());
379 final int destRowIndex = languageList.languageData.getPrevTokenRow(getSelectedRow());
380 Log.d(LOG, "onUpButton, destRowIndex=" + destRowIndex);
381 jumpToRow(languageList, destRowIndex);
384 void onDownButton() {
385 final int destRowIndex = languageList.languageData.getNextTokenRow(getSelectedRow());
386 Log.d(LOG, "onDownButton, destRowIndex=" + destRowIndex);
387 jumpToRow(languageList, destRowIndex);
390 void onAppendToWordList() {
391 final int row = getSelectedRow();
395 final StringBuilder rawText = new StringBuilder();
396 final String word = languageList.languageData.getIndexEntryForRow(row).word;
398 new SimpleDateFormat("yyyy.MM.dd HH:mm:ss").format(new Date()))
400 rawText.append(word).append("\t");
401 rawText.append(getSelectedRowRawText(saveOnlyFirstSubentry));
402 Log.d(LOG, "Writing : " + rawText);
404 wordList.getParentFile().mkdirs();
405 final PrintWriter out = new PrintWriter(
406 new FileWriter(wordList, true));
407 out.println(rawText.toString());
409 } catch (IOException e) {
410 Log.e(LOG, "Unable to append to " + wordList.getAbsolutePath(), e);
411 Toast.makeText(this, getString(R.string.failedAddingToWordList, wordList.getAbsolutePath()), Toast.LENGTH_LONG);
417 final int row = getSelectedRow();
421 Log.d(LOG, "Copy." + DictionaryActivity.this.getSelectedRow());
422 final StringBuilder result = new StringBuilder();
423 result.append(getSelectedRowRawText(false));
424 final ClipboardManager clipboardManager = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
425 clipboardManager.setText(result.toString());
426 Log.d(LOG, "Copied: " + result);
430 public boolean onKeyDown(int keyCode, KeyEvent event) {
431 if (event.getUnicodeChar() != 0) {
432 if (!searchText.hasFocus()) {
433 searchText.setText("" + (char) event.getUnicodeChar());
434 onSearchTextChange(searchText.getText().toString());
435 searchText.requestFocus();
439 return super.onKeyDown(keyCode, event);
443 protected void onListItemClick(ListView l, View v, int row, long id) {
445 openContextMenu(getListView());
448 void onSearchTextChange(final String searchText) {
449 Log.d(LOG, "onSearchTextChange: " + searchText);
450 synchronized (this) {
451 searchOperation = new SearchOperation(languageList, searchText, searchOperation);
452 searchExecutor.execute(searchOperation);
458 // ----------------------------------------------------------------
460 // ----------------------------------------------------------------
463 public void onCreateContextMenu(ContextMenu menu, View v,
464 ContextMenuInfo menuInfo) {
465 final int row = getSelectedRow();
470 final MenuItem addToWordlist = menu.add(getString(R.string.addToWordList, wordList.getName()));
471 addToWordlist.setOnMenuItemClickListener(new OnMenuItemClickListener() {
472 public boolean onMenuItemClick(MenuItem item) {
473 onAppendToWordList();
478 final MenuItem copy = menu.add(android.R.string.copy);
479 copy.setOnMenuItemClickListener(new OnMenuItemClickListener() {
480 public boolean onMenuItemClick(MenuItem item) {
488 private void jumpToRow(final LanguageListAdapter dictionaryListAdapter,
489 final int rowIndex) {
490 Log.d(LOG, "jumpToRow: " + rowIndex);
491 if (dictionaryListAdapter != this.languageList) {
492 Log.w(LOG, "skipping jumpToRow for old list adapter: " + rowIndex);
495 setSelection(rowIndex);
496 setSelectedRow(rowIndex);
497 getListView().setSelected(true);
500 // TODO: delete me somehow.
501 private int getSelectedRow() {
502 return lastSelectedRow;
504 private void setSelectedRow(final int row) {
505 lastSelectedRow = row;
506 Log.d(LOG, "Selected: " + getSelectedRowRawText(true));
510 private void updateSearchText() {
511 Log.d(LOG, "updateSearchText");
512 final int selectedRowIndex = getSelectedRow();
513 if (!searchText.hasFocus()) {
514 if (selectedRowIndex >= 0) {
515 final String word = languageList.languageData
516 .getIndexEntryForRow(selectedRowIndex).word;
517 if (!word.equals(searchText.getText().toString())) {
518 Log.d(LOG, "updateSearchText: setText: " + word);
519 searchText.setText(word);
522 Log.w(LOG, "updateSearchText: nothing selected.");
527 static void startDownloadDictActivity(final Context context) {
528 final Intent intent = new Intent(context, DownloadActivity.class);
529 final SharedPreferences prefs = PreferenceManager
530 .getDefaultSharedPreferences(context);
531 final String dictFetchUrl = prefs.getString(context
532 .getString(R.string.dictFetchUrlKey), context
533 .getString(R.string.dictFetchUrlDefault));
534 final String dictFileName = prefs.getString(context
535 .getString(R.string.dictFileKey), context
536 .getString(R.string.dictFileDefault));
537 intent.putExtra(DownloadActivity.SOURCE, dictFetchUrl);
538 intent.putExtra(DownloadActivity.DEST, dictFileName);
539 context.startActivity(intent);
542 class LanguageListAdapter extends BaseAdapter {
544 // Visible for testing.
545 final LanguageData languageData;
547 LanguageListAdapter(final LanguageData languageData) {
548 this.languageData = languageData;
551 public int getCount() {
552 return languageData.rows.size();
555 public Dictionary.Row getItem(int rowIndex) {
556 assert rowIndex < languageData.rows.size();
557 return languageData.rows.get(rowIndex);
560 public long getItemId(int rowIndex) {
564 public View getView(final int rowIndex, final View convertView,
565 final ViewGroup parent) {
566 final Row row = getItem(rowIndex);
570 TextView result = null;
571 if (convertView instanceof TextView) {
572 result = (TextView) convertView;
574 result = new TextView(parent.getContext());
579 result.setText(languageData.rowToString(row, false));
580 result.setTextAppearance(parent.getContext(),
581 android.R.style.TextAppearance_Large);
582 result.setClickable(false);
587 final TableLayout result = new TableLayout(parent.getContext());
589 final Entry entry = dictionary.entries.get(row.getIndex());
590 final int rowCount = entry.getRowCount();
591 for (int r = 0; r < rowCount; ++r) {
592 final TableRow tableRow = new TableRow(result.getContext());
594 TextView column1 = new TextView(tableRow.getContext());
595 TextView column2 = new TextView(tableRow.getContext());
596 final TableRow.LayoutParams layoutParams = new TableRow.LayoutParams();
597 layoutParams.weight = 0.5f;
600 final TextView spacer = new TextView(tableRow.getContext());
601 spacer.setText(r == 0 ? "
\95 " : "
\95 ");
602 tableRow.addView(spacer);
604 tableRow.addView(column1, layoutParams);
606 final TextView spacer = new TextView(tableRow.getContext());
607 spacer.setText(r == 0 ? "
\95 " : "
\95 ");
608 tableRow.addView(spacer);
610 tableRow.addView(column2, layoutParams);
614 // column1.setTextAppearance(parent.getContext(), android.R.style.Text);
616 // TODO: color words by gender
617 final String col1Text = entry.getAllText(languageData.lang)[r];
618 column1.setText(col1Text, TextView.BufferType.SPANNABLE);
619 final Spannable col1Spannable = (Spannable) column1.getText();
621 final String token = languageData.getIndexEntryForRow(rowIndex).word;
622 while ((startPos = col1Text.indexOf(token, startPos)) != -1) {
623 col1Spannable.setSpan(new StyleSpan(Typeface.BOLD), startPos,
624 startPos + token.length(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
625 startPos += token.length();
629 entry.getAllText(Entry.otherLang(languageData.lang))[r],
630 TextView.BufferType.NORMAL);
632 result.addView(tableRow);
638 } // DictionaryListAdapter
640 private final class SearchOperation implements Runnable {
641 SearchOperation previousSearchOperation;
643 final LanguageListAdapter listAdapter;
644 final LanguageData languageData;
645 final String searchText;
646 final AtomicBoolean interrupted = new AtomicBoolean(false);
647 boolean searchFinished = false;
649 SearchOperation(final LanguageListAdapter listAdapter,
650 final String searchText, final SearchOperation previousSearchOperation) {
651 this.listAdapter = listAdapter;
652 this.languageData = listAdapter.languageData;
653 this.searchText = searchText;
654 this.previousSearchOperation = previousSearchOperation;
658 if (previousSearchOperation != null) {
659 previousSearchOperation.stopAndWait();
661 previousSearchOperation = null;
663 Log.d(LOG, "SearchOperation: " + searchText);
664 final int indexLocation = languageData.lookup(searchText, interrupted);
665 if (!interrupted.get()) {
666 final IndexEntry indexEntry = languageData.sortedIndex.get(indexLocation);
668 Log.d(LOG, "SearchOperation completed: " + indexEntry.toString());
669 uiHandler.post(new Runnable() {
671 // Check is just a performance operation.
672 if (!interrupted.get()) {
673 // This is safe, because it checks that the listAdapter hasn't changed.
674 jumpToRow(listAdapter, indexEntry.startRow);
676 synchronized (DictionaryActivity.this) {
677 searchOperation = null;
678 DictionaryActivity.this.notifyAll();
683 synchronized (this) {
684 searchFinished = true;
689 private void stopAndWait() {
690 interrupted.set(true);
691 synchronized (this) {
692 while (!searchFinished) {
693 Log.d(LOG, "stopAndWait: " + searchText);
696 } catch (InterruptedException e) {
697 Log.e(LOG, "Interrupted", e);
704 void waitForSearchEnd() {
705 synchronized (this) {
706 while (searchOperation != null) {
707 Log.d(LOG, "waitForSearchEnd");
710 } catch (InterruptedException e) {
711 Log.e(LOG, "Interrupted.", e);
717 private class SearchTextWatcher implements TextWatcher {
718 public void afterTextChanged(final Editable searchTextEditable) {
719 Log.d(LOG, "Search text changed: " + searchText.getText().toString());
720 if (searchText.hasFocus()) {
721 // If they were typing to cause the change, update the UI.
722 onSearchTextChange(searchText.getText().toString());
726 public void beforeTextChanged(CharSequence arg0, int arg1, int arg2,
730 public void onTextChanged(CharSequence arg0, int arg1, int arg2, int arg3) {